diff --git a/docs/cli/cli-supplemental-docs.md b/docs/cli/cli-supplemental-docs.md index 14f6bbe84..f2c2cf7f2 100644 --- a/docs/cli/cli-supplemental-docs.md +++ b/docs/cli/cli-supplemental-docs.md @@ -10,6 +10,8 @@ Supplemental files let you enrich any auto-generated CLI reference page with con **Validation is strict.** Any supplemental file whose name does not match a known namespace or command produces a build error, so renamed or removed commands can never leave orphaned docs behind silently. +**Frontmatter is preserved as metadata.** Add YAML frontmatter to set page metadata such as `description`, `applies_to`, or `navigation_title`. It is passed through to the generated page and is not rendered as supplemental description text. + ## File naming Two naming styles are supported and can coexist in the same folder. @@ -48,6 +50,24 @@ cli/ The heading structure of a supplemental file controls what it contributes to the generated page. +### Frontmatter + +Use frontmatter for page metadata: + +```markdown +--- +description: Use the Elastic CLI to call Elasticsearch REST APIs from the command line. +applies_to: + stack: preview +--- + +## Description + +The `elastic stack es` command group exposes Elasticsearch REST APIs as CLI commands. +``` + +The metadata remains metadata. The generated page uses the `## Description` section, or the schema description if the file only contains frontmatter. + ### No headings A file with no `##` headings replaces the auto-generated description entirely: diff --git a/src/Elastic.Markdown/Extensions/CliReference/CliMarkdownGenerator.cs b/src/Elastic.Markdown/Extensions/CliReference/CliMarkdownGenerator.cs index d4a616814..0a6c1df78 100644 --- a/src/Elastic.Markdown/Extensions/CliReference/CliMarkdownGenerator.cs +++ b/src/Elastic.Markdown/Extensions/CliReference/CliMarkdownGenerator.cs @@ -13,6 +13,7 @@ internal static partial class CliMarkdownGenerator public static string RootPage(CliSchema schema, CliSupplementalDoc? supplemental) { var sb = new StringBuilder(); + AppendFrontMatter(sb, supplemental); _ = sb.AppendLine($"# {schema.Name}"); _ = sb.AppendLine(); @@ -108,6 +109,7 @@ public static string NamespacePage( List? shortcuts = null) { var sb = new StringBuilder(); + AppendFrontMatter(sb, supplemental); var heading = fullPath is { Length: > 0 } ? string.Join(" ", fullPath) : ns.Segment; _ = sb.AppendLine($"# {heading} cli namespace"); _ = sb.AppendLine(); @@ -198,6 +200,7 @@ public static string CommandPage( List? shortcuts = null) { var sb = new StringBuilder(); + AppendFrontMatter(sb, supplemental); var heading = fullPath is { Length: > 0 } ? string.Join(" ", fullPath) : cmd.Name; _ = sb.AppendLine($"# {heading} cli command"); _ = sb.AppendLine(); @@ -322,6 +325,15 @@ public static string CommandPage( return sb.ToString(); } + private static void AppendFrontMatter(StringBuilder sb, CliSupplementalDoc? supplemental) + { + if (string.IsNullOrWhiteSpace(supplemental?.FrontMatter)) + return; + + _ = sb.AppendLine(supplemental.FrontMatter); + _ = sb.AppendLine(); + } + private static void AppendCommandModifiers(StringBuilder sb, CliCommandSchema cmd) { if (cmd.Deprecated is not null) diff --git a/src/Elastic.Markdown/Extensions/CliReference/CliSupplementalDoc.cs b/src/Elastic.Markdown/Extensions/CliReference/CliSupplementalDoc.cs index ca59d01dd..854ceab23 100644 --- a/src/Elastic.Markdown/Extensions/CliReference/CliSupplementalDoc.cs +++ b/src/Elastic.Markdown/Extensions/CliReference/CliSupplementalDoc.cs @@ -7,6 +7,7 @@ namespace Elastic.Markdown.Extensions.CliReference; internal sealed partial record CliSupplementalDoc( + string? FrontMatter, string? Description, Dictionary OptionOverrides, Dictionary ArgumentOverrides, @@ -18,13 +19,14 @@ internal sealed partial record CliSupplementalDoc( if (raw is null) return null; - var trimmed = raw.Trim(); + var (frontMatter, rawContent) = ExtractFrontMatter(raw); + var trimmed = rawContent.Trim(); if (string.IsNullOrWhiteSpace(trimmed)) - return null; + return string.IsNullOrWhiteSpace(frontMatter) ? null : new CliSupplementalDoc(frontMatter, null, [], [], null); // Backward compat: no ## headings → entire content is description if (!trimmed.Contains("\n## ") && !trimmed.StartsWith("## ", StringComparison.Ordinal)) - return new CliSupplementalDoc(trimmed, [], [], null); + return new CliSupplementalDoc(frontMatter, trimmed, [], [], null); var sections = SplitSections(trimmed); string? description = null; @@ -55,7 +57,16 @@ internal sealed partial record CliSupplementalDoc( } var postContent = postParts.Count > 0 ? string.Join("\n\n", postParts) : null; - return new CliSupplementalDoc(description, optionOverrides, argumentOverrides, postContent); + return new CliSupplementalDoc(frontMatter, description, optionOverrides, argumentOverrides, postContent); + } + + private static (string? FrontMatter, string Content) ExtractFrontMatter(string raw) + { + var match = FrontMatterRegex().Match(raw); + if (!match.Success) + return (null, raw); + + return (match.Value.Trim(), raw[match.Length..]); } private static List<(string? heading, string body)> SplitSections(string text) @@ -124,4 +135,7 @@ private static string NormalizeKey(string raw) // Matches: `: `--flag`` or `: --flag` or `: ` [GeneratedRegex(@"^:\s+(`[^`]+`|--[\w-]+|<[\w-]+>)")] private static partial Regex TermLineRegex(); + + [GeneratedRegex(@"\A---\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)")] + private static partial Regex FrontMatterRegex(); } diff --git a/tests/Elastic.Markdown.Tests/CliReference/CliSupplementalDocTests.cs b/tests/Elastic.Markdown.Tests/CliReference/CliSupplementalDocTests.cs new file mode 100644 index 000000000..5de73f634 --- /dev/null +++ b/tests/Elastic.Markdown.Tests/CliReference/CliSupplementalDocTests.cs @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using AwesomeAssertions; +using Elastic.Documentation.Configuration.Toc.CliReference; +using Elastic.Markdown.Extensions.CliReference; + +namespace Elastic.Markdown.Tests.CliReference; + +public class CliSupplementalDocTests +{ + [Fact] + public void RootPage_PreservesFrontMatterAsMetadata() + { + var schema = CreateSchema(); + const string raw = """ + --- + description: Use the Elastic CLI from the command line. + applies_to: + stack: preview + --- + """; + + var supplemental = CliSupplementalDoc.Parse(raw); + var markdown = CliMarkdownGenerator.RootPage(schema, supplemental).ReplaceLineEndings("\n"); + + var expectedStart = """ + --- + description: Use the Elastic CLI from the command line. + applies_to: + stack: preview + --- + + # elastic + """.ReplaceLineEndings("\n"); + + markdown.Should().StartWith(expectedStart); + markdown.Should().NotContain("description: Use the Elastic CLI from the command line.\n\n"); + } + + [Fact] + public void RootPage_StripsFrontMatterBeforeParsingDescription() + { + var schema = CreateSchema(); + const string raw = """ + --- + description: Metadata description. + --- + + User-facing supplemental description. + """; + + var supplemental = CliSupplementalDoc.Parse(raw); + var markdown = CliMarkdownGenerator.RootPage(schema, supplemental).ReplaceLineEndings("\n"); + + markdown.Should().Contain("\n# elastic\n\nUser-facing supplemental description.\n"); + markdown.Should().NotContain("\nMetadata description.\n"); + } + + private static CliSchema CreateSchema() => new( + SchemaVersion: 1, + Name: "elastic", + Description: "Schema description.", + GlobalOptions: [], + RootDefault: null, + Commands: [], + Namespaces: [] + ); +}