From 06fd34e772e18f024dfb8801e3e6a3ed5b662133 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 30 Jun 2025 10:13:35 +0200 Subject: [PATCH 1/6] Add semantic steps to stepper --- src/Elastic.Markdown/IO/MarkdownFile.cs | 57 ++++++++++++++++--- .../Myst/Directives/Stepper/StepView.cshtml | 4 +- .../Myst/Directives/Stepper/StepperBlock.cs | 6 ++ 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index dae259a96..18b113d83 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -14,6 +14,7 @@ using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; using Elastic.Markdown.Myst.Directives.Include; +using Elastic.Markdown.Myst.Directives.Stepper; using Elastic.Markdown.Myst.FrontMatter; using Elastic.Markdown.Myst.InlineParsers; using Markdig; @@ -263,22 +264,50 @@ public static List GetAnchors( .ToArray(); var includedTocs = includes.SelectMany(i => i!.TableOfContentItems).ToArray(); - var toc = document + + // Collect headings from standard markdown + var headingTocs = document .Descendants() .Where(block => block is { Level: >= 2 }) - .Select(h => (h.GetData("header") as string, h.GetData("anchor") as string, h.Level)) + .Select(h => (h.GetData("header") as string, h.GetData("anchor") as string, h.Level, h.Line)) .Where(h => h.Item1 is not null) .Select(h => { var header = h.Item1!.StripMarkdown(); - return new PageTocItem + return new { - Heading = header, - Slug = (h.Item2 ?? h.Item1).Slugify(), - Level = h.Level + TocItem = new PageTocItem + { + Heading = header, + Slug = (h.Item2 ?? h.Item1).Slugify(), + Level = h.Level + }, + h.Line }; - }) - .Concat(includedTocs) + }); + + // Collect headings from Stepper steps + var stepperTocs = document + .Descendants() + .OfType() + .Where(step => !string.IsNullOrEmpty(step.Title)) + .Where(step => !IsNestedInOtherDirective(step)) + .Select(step => new + { + TocItem = new PageTocItem + { + Heading = step.Title, + Slug = step.Anchor, + Level = 3 // Same level as h3 elements they render as + }, + step.Line + }); + + var toc = headingTocs + .Concat(stepperTocs) + .Concat(includedTocs.Select(item => new { TocItem = item, Line = 0 })) + .OrderBy(item => item.Line) + .Select(item => item.TocItem) .Select(toc => subs.Count == 0 ? toc : toc.Heading.AsSpan().ReplaceSubstitutions(subs, set.Context.Collector, out var r) @@ -301,6 +330,18 @@ public static List GetAnchors( return toc; } + private static bool IsNestedInOtherDirective(DirectiveBlock block) + { + var parent = block.Parent; + while (parent is not null) + { + if (parent is DirectiveBlock { } otherDirective && otherDirective != block && otherDirective is not StepperBlock) + return true; + parent = parent.Parent; + } + return false; + } + private YamlFrontMatter ProcessYamlFrontMatter(MarkdownDocument document) { if (document.FirstOrDefault() is not YamlFrontMatterBlock yaml) diff --git a/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml b/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml index 870b3dd72..c7601448e 100644 --- a/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml @@ -2,11 +2,11 @@
  • @if (!string.IsNullOrEmpty(Model.Title)) { -

    +

    @Model.Title -

    +

    } @Model.RenderBlock()
  • diff --git a/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs b/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs index 2add20803..02481d0ba 100644 --- a/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs @@ -25,5 +25,11 @@ public override void FinalizeAndValidate(ParserContext context) { Title = Arguments ?? string.Empty; Anchor = Prop("anchor") ?? Title.Slugify(); + + // Set CrossReferenceName so this step can be found by ToC generation + if (!string.IsNullOrEmpty(Title)) + { + CrossReferenceName = Anchor; + } } } From b163863541c7b00d64b0272c0114608a70ee1bdf Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 30 Jun 2025 10:15:55 +0200 Subject: [PATCH 2/6] Update docs --- docs/syntax/stepper.md | 50 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/docs/syntax/stepper.md b/docs/syntax/stepper.md index 4415dd658..8909c4096 100644 --- a/docs/syntax/stepper.md +++ b/docs/syntax/stepper.md @@ -12,8 +12,6 @@ But you can override the default anchor by adding the `:anchor:` option to the s ::::::{tab-item} Output :::::{stepper} -:::::{stepper} - ::::{step} Install First install the dependencies. ```shell @@ -203,3 +201,51 @@ To see how dynamic mapping works, add a new document to the `books` index with a :::::: ::::::: + +## Table of Contents Integration + +Stepper step titles automatically appear in the page's "On this page" table of contents (ToC) sidebar, making it easier for users to navigate directly to specific steps. + +### How it works + +- **Automatic inclusion**: Step titles are automatically detected and added to the ToC during document parsing. +- **Proper nesting**: Steps appear as sub-items under their parent section heading with appropriate indentation. +- **Clickable navigation**: Users can click on step titles in the ToC to jump directly to that step. + +### Nested steppers + +When steppers are nested inside other directive components (like `{tab-set}`, `{dropdown}`, or other containers), their step titles are **not** included in the ToC to avoid: + +- Duplicate or competing headings across multiple tabs +- Links to content that might be collapsed or hidden +- General confusion about content hierarchy + +**Example of excluded stepper:** +```markdown +::::{tab-set} +:::{tab-item} Tab 1 +::{stepper} +:{step} This step won't appear in ToC +Content here... +: +:: +::: +:::: +``` + +**Example of included stepper:** +```markdown +## Installation Guide + +::::{stepper} +:::{step} Download +Download the software... +::: + +:::{step} Install +Run the installer... +::: +:::: +``` + +In the second example, both "Download" and "Install" steps will appear in the ToC as sub-items under "Installation Guide". From dc79c1246ae3ab56eaa85fcecbff050b2fed994e Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 30 Jun 2025 10:53:28 +0200 Subject: [PATCH 3/6] Incremental steps --- docs/contribute/locally.md | 2 +- src/Elastic.Markdown/IO/MarkdownFile.cs | 2 +- .../Myst/Directives/DirectiveHtmlRenderer.cs | 3 +- .../Myst/Directives/Stepper/StepView.cshtml | 7 ++-- .../Myst/Directives/Stepper/StepViewModel.cs | 1 + .../Myst/Directives/Stepper/StepperBlock.cs | 33 +++++++++++++++++++ 6 files changed, 42 insertions(+), 6 deletions(-) diff --git a/docs/contribute/locally.md b/docs/contribute/locally.md index bcbe30299..4437c71b0 100644 --- a/docs/contribute/locally.md +++ b/docs/contribute/locally.md @@ -105,7 +105,7 @@ In this guide, we'll clone the [`docs-content`](https://github.com/elastic/docs- git clone https://github.com/elastic/docs-content.git ``` -## Serve the documentation [#step-three] +### Serve the documentation [#step-three] Static-site generators like docs-builder can serve docs locally. This means you can edit the source and see the result in the browser in real time. diff --git a/src/Elastic.Markdown/IO/MarkdownFile.cs b/src/Elastic.Markdown/IO/MarkdownFile.cs index 18b113d83..592d69832 100644 --- a/src/Elastic.Markdown/IO/MarkdownFile.cs +++ b/src/Elastic.Markdown/IO/MarkdownFile.cs @@ -298,7 +298,7 @@ public static List GetAnchors( { Heading = step.Title, Slug = step.Anchor, - Level = 3 // Same level as h3 elements they render as + Level = step.HeadingLevel // Use dynamic heading level }, step.Line }); diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs index 2c2a6cd28..a15ac0ac9 100644 --- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs @@ -124,7 +124,8 @@ private static void WriteStepBlock(HtmlRenderer renderer, StepBlock block) { DirectiveBlock = block, Title = block.Title, - Anchor = block.Anchor + Anchor = block.Anchor, + HeadingLevel = block.HeadingLevel }); RenderRazorSlice(slice, renderer); } diff --git a/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml b/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml index c7601448e..c659c5970 100644 --- a/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml @@ -2,11 +2,12 @@
  • @if (!string.IsNullOrEmpty(Model.Title)) { -

    - + var headingTag = $"h{Model.HeadingLevel}"; + <@headingTag id="@Model.Anchor"> + @Model.Title -

    + } @Model.RenderBlock()
  • diff --git a/src/Elastic.Markdown/Myst/Directives/Stepper/StepViewModel.cs b/src/Elastic.Markdown/Myst/Directives/Stepper/StepViewModel.cs index 412c176cc..2efcb442e 100644 --- a/src/Elastic.Markdown/Myst/Directives/Stepper/StepViewModel.cs +++ b/src/Elastic.Markdown/Myst/Directives/Stepper/StepViewModel.cs @@ -8,4 +8,5 @@ public class StepViewModel : DirectiveViewModel { public required string Title { get; init; } public required string Anchor { get; init; } + public required int HeadingLevel { get; init; } } diff --git a/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs b/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs index 02481d0ba..a0ad9e509 100644 --- a/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs +++ b/src/Elastic.Markdown/Myst/Directives/Stepper/StepperBlock.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Markdown.Helpers; +using Markdig.Syntax; namespace Elastic.Markdown.Myst.Directives.Stepper; @@ -20,16 +21,48 @@ public class StepBlock(DirectiveBlockParser parser, ParserContext context) : Dir public override string Directive => "step"; public string Title { get; private set; } = string.Empty; public string Anchor { get; private set; } = string.Empty; + public int HeadingLevel { get; private set; } = 3; // Default to h3 public override void FinalizeAndValidate(ParserContext context) { Title = Arguments ?? string.Empty; Anchor = Prop("anchor") ?? Title.Slugify(); + // Calculate heading level based on preceding heading + HeadingLevel = CalculateHeadingLevel(); + // Set CrossReferenceName so this step can be found by ToC generation if (!string.IsNullOrEmpty(Title)) { CrossReferenceName = Anchor; } } + + private int CalculateHeadingLevel() + { + // Find the document root + var current = (ContainerBlock)this; + while (current.Parent != null) + current = current.Parent; + + // Find all headings that come before this step in document order + var allBlocks = current.Descendants().ToList(); + var thisIndex = allBlocks.IndexOf(this); + + if (thisIndex == -1) + return 3; // Default fallback + + // Look backwards for the most recent heading + for (var i = thisIndex - 1; i >= 0; i--) + { + if (allBlocks[i] is HeadingBlock heading) + { + // Step should be one level deeper than the preceding heading + return Math.Min(heading.Level + 1, 6); // Cap at h6 + } + } + + // No preceding heading found, default to h2 (level 2) + return 2; + } } From 6e52e863967b7b09673c9582eaee3087c4b3c3a3 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 30 Jun 2025 10:58:13 +0200 Subject: [PATCH 4/6] Adapt heading style --- docs/contribute/locally.md | 2 +- .../Myst/Directives/Stepper/StepView.cshtml | 39 ++++++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/contribute/locally.md b/docs/contribute/locally.md index 4437c71b0..bcbe30299 100644 --- a/docs/contribute/locally.md +++ b/docs/contribute/locally.md @@ -105,7 +105,7 @@ In this guide, we'll clone the [`docs-content`](https://github.com/elastic/docs- git clone https://github.com/elastic/docs-content.git ``` -### Serve the documentation [#step-three] +## Serve the documentation [#step-three] Static-site generators like docs-builder can serve docs locally. This means you can edit the source and see the result in the browser in real time. diff --git a/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml b/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml index c659c5970..e6712b38c 100644 --- a/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml +++ b/src/Elastic.Markdown/Myst/Directives/Stepper/StepView.cshtml @@ -2,12 +2,39 @@
  • @if (!string.IsNullOrEmpty(Model.Title)) { - var headingTag = $"h{Model.HeadingLevel}"; - <@headingTag id="@Model.Anchor"> - - @Model.Title - - + @switch (Model.HeadingLevel) + { + case 1: +

    + @Model.Title +

    + break; + case 2: +

    + @Model.Title +

    + break; + case 3: +

    + @Model.Title +

    + break; + case 4: +

    + @Model.Title +

    + break; + case 5: +
    + @Model.Title +
    + break; + default: +
    + @Model.Title +
    + break; + } } @Model.RenderBlock()
  • From 1f58766577b00dd5b0f677058a03a549a87f259e Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 30 Jun 2025 11:01:15 +0200 Subject: [PATCH 5/6] Docs update --- docs/syntax/stepper.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/syntax/stepper.md b/docs/syntax/stepper.md index 8909c4096..9a041cde9 100644 --- a/docs/syntax/stepper.md +++ b/docs/syntax/stepper.md @@ -249,3 +249,40 @@ Run the installer... ``` In the second example, both "Download" and "Install" steps will appear in the ToC as sub-items under "Installation Guide". + +## Dynamic heading levels + +Stepper step titles automatically adjust their heading level based on the preceding heading in the document, ensuring proper document hierarchy and semantic structure. + +### Examples + +**Stepper after H2 heading:** +```markdown +## Installation Guide + +::::{stepper} +:::{step} Download +Step titles render as H3 (visible in ToC) +::: +:::: +``` + +**Stepper after H3 heading:** +```markdown +### Setup Process + +::::{stepper} +:::{step} Configure +Step titles render as H4 (not in ToC, but proper hierarchy) +::: +:::: +``` + +**Stepper with no preceding heading:** +```markdown +::::{stepper} +:::{step} First Step +Step titles default to H2 level +::: +:::: +``` From 27760114767d011a4828bfbfd32dd6bd35fbd08f Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 30 Jun 2025 11:03:10 +0200 Subject: [PATCH 6/6] Doc edit --- docs/syntax/stepper.md | 71 +++--------------------------------------- 1 file changed, 5 insertions(+), 66 deletions(-) diff --git a/docs/syntax/stepper.md b/docs/syntax/stepper.md index 9a041cde9..8b30c92e0 100644 --- a/docs/syntax/stepper.md +++ b/docs/syntax/stepper.md @@ -6,7 +6,7 @@ to break down processes into manageable stages. By default every step title is a link with a generated anchor. But you can override the default anchor by adding the `:anchor:` option to the step. -## Basic Stepper +## Basic stepper :::::::{tab-set} ::::::{tab-item} Output @@ -75,7 +75,7 @@ npm run test ::::::: -## Advanced Example +## Advanced example :::::::{tab-set} @@ -202,23 +202,13 @@ To see how dynamic mapping works, add a new document to the `books` index with a ::::::: -## Table of Contents Integration +## Table of contents integration Stepper step titles automatically appear in the page's "On this page" table of contents (ToC) sidebar, making it easier for users to navigate directly to specific steps. -### How it works - -- **Automatic inclusion**: Step titles are automatically detected and added to the ToC during document parsing. -- **Proper nesting**: Steps appear as sub-items under their parent section heading with appropriate indentation. -- **Clickable navigation**: Users can click on step titles in the ToC to jump directly to that step. - ### Nested steppers -When steppers are nested inside other directive components (like `{tab-set}`, `{dropdown}`, or other containers), their step titles are **not** included in the ToC to avoid: - -- Duplicate or competing headings across multiple tabs -- Links to content that might be collapsed or hidden -- General confusion about content hierarchy +When steppers are nested inside other directive components (like `{tab-set}`, `{dropdown}`, or other containers), their step titles are **not** included in the ToC to avoid duplicate or competing headings across multiple tabs or links to content that might be collapsed or hidden. **Example of excluded stepper:** ```markdown @@ -232,57 +222,6 @@ Content here... ::: :::: ``` - -**Example of included stepper:** -```markdown -## Installation Guide - -::::{stepper} -:::{step} Download -Download the software... -::: - -:::{step} Install -Run the installer... -::: -:::: -``` - -In the second example, both "Download" and "Install" steps will appear in the ToC as sub-items under "Installation Guide". - ## Dynamic heading levels -Stepper step titles automatically adjust their heading level based on the preceding heading in the document, ensuring proper document hierarchy and semantic structure. - -### Examples - -**Stepper after H2 heading:** -```markdown -## Installation Guide - -::::{stepper} -:::{step} Download -Step titles render as H3 (visible in ToC) -::: -:::: -``` - -**Stepper after H3 heading:** -```markdown -### Setup Process - -::::{stepper} -:::{step} Configure -Step titles render as H4 (not in ToC, but proper hierarchy) -::: -:::: -``` - -**Stepper with no preceding heading:** -```markdown -::::{stepper} -:::{step} First Step -Step titles default to H2 level -::: -:::: -``` +Stepper step titles automatically adjust their heading level based on the preceding heading in the document, ensuring proper document hierarchy and semantic structure. \ No newline at end of file