Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/_snippets/applies-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
::::{applies-switch}

:::{applies-item} stack:
Content for Stack
:::

:::{applies-item} serverless:
Content for Serverless
:::

::::
7 changes: 7 additions & 0 deletions docs/testing/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,10 @@ const foo = "bar"; <1>
```

1. This is a JavaScript code block.


:::{include} /_snippets/applies-switch.md
:::

:::{include} /_snippets/applies-switch.md
:::
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,12 @@ public class AppliesSwitchBlock(DirectiveBlockParser parser, ParserContext conte

private int _index = -1;

// For simplicity, we use the line number as the index.
// It's not ideal, but it's unique.
// This is more efficient than finding the root block and then finding the index.
public int FindIndex()
{
if (_index > -1)
return _index;

_index = Line;
_index = GetUniqueLineIndex();
return _index;
}
}
Expand Down
14 changes: 14 additions & 0 deletions src/Elastic.Markdown/Myst/Directives/DirectiveBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public abstract class DirectiveBlock(

public bool SkipValidation { get; } = context.SkipValidation;

/// <summary>
/// The line number of the include directive that brought this block into the document.
/// Null if the block is not inside an included snippet.
/// </summary>
protected int? IncludeLine { get; } = context.IncludeLine;

public int OpeningLength => Directive.Length;

public abstract string Directive { get; }
Expand Down Expand Up @@ -148,4 +154,12 @@ protected bool PropBool(params string[] keys)
return default;
}

/// <summary>
/// Gets a unique index based on the block's line number that accounts for include context.
/// When the block is inside an included snippet, combines the include directive's line
/// with the snippet line to ensure uniqueness across multiple includes and multiple blocks.
/// </summary>
/// <returns>A unique integer index suitable for generating HTML IDs.</returns>
protected int GetUniqueLineIndex() =>
IncludeLine.HasValue ? (IncludeLine.Value * 1000) + Line : Line;
}
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ private static void WriteIncludeBlock(HtmlRenderer renderer, IncludeBlock block)
var snippet = block.Build.ReadFileSystem.FileInfo.New(block.IncludePath);

var parentPath = block.Context.MarkdownParentPath ?? block.Context.MarkdownSourcePath;
var document = MarkdownParser.ParseSnippetAsync(block.Build, block.Context, snippet, parentPath, block.Context.YamlFrontMatter, default)
var document = MarkdownParser.ParseSnippetAsync(block.Build, block.Context, snippet, parentPath, block.Context.YamlFrontMatter, default, block.Line)
.GetAwaiter().GetResult();

var html = document.ToHtml(MarkdownParser.Pipeline);
Expand Down
5 changes: 1 addition & 4 deletions src/Elastic.Markdown/Myst/Directives/Tabs/TabSetBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,12 @@ public class TabSetBlock(DirectiveBlockParser parser, ParserContext context)

private int _index = -1;

// For simplicity, we use the line number as the index.
// It's not ideal, but it's unique.
// This is more efficient than finding the root block and then finding the index.
public int FindIndex()
{
if (_index > -1)
return _index;

_index = Line;
_index = GetUniqueLineIndex();
return _index;
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/Elastic.Markdown/Myst/MarkdownParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public static MarkdownDocument ParseMarkdownStringAsync(BuildContext build, IPar
}

public static Task<MarkdownDocument> ParseSnippetAsync(BuildContext build, IParserResolvers resolvers, IFileInfo path, IFileInfo parentPath,
YamlFrontMatter? matter, Cancel ctx)
YamlFrontMatter? matter, Cancel ctx, int? includeLine = null)
{
var state = new ParserState(build)
{
Expand All @@ -93,7 +93,8 @@ public static Task<MarkdownDocument> ParseSnippetAsync(BuildContext build, IPars
TryFindDocumentByRelativePath = resolvers.TryFindDocumentByRelativePath,
CrossLinkResolver = resolvers.CrossLinkResolver,
NavigationTraversable = resolvers.NavigationTraversable,
ParentMarkdownPath = parentPath
ParentMarkdownPath = parentPath,
IncludeLine = includeLine
};
var context = new ParserContext(state);
return ParseAsync(path, context, Pipeline, ctx);
Expand Down
13 changes: 13 additions & 0 deletions src/Elastic.Markdown/Myst/ParserContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public record ParserState(BuildContext Build) : ParserResolvers

public IFileInfo? ParentMarkdownPath { get; init; }
public bool SkipValidation { get; init; }

/// <summary>
/// The line number of the include directive in the parent file.
/// Used to generate unique IDs for blocks within included snippets.
/// </summary>
public int? IncludeLine { get; init; }
}

public class ParserContext : MarkdownParserContext, IParserResolvers
Expand All @@ -72,13 +78,20 @@ public class ParserContext : MarkdownParserContext, IParserResolvers
public IReadOnlyDictionary<string, string> ContextSubstitutions { get; }
public INavigationTraversable NavigationTraversable { get; }

/// <summary>
/// The line number of the include directive in the parent file.
/// Used to generate unique IDs for blocks within included snippets.
/// </summary>
public int? IncludeLine { get; }

public ParserContext(ParserState state)
{
Build = state.Build;
Configuration = state.Configuration;
YamlFrontMatter = state.YamlFrontMatter;
SkipValidation = state.SkipValidation;
MarkdownParentPath = state.ParentMarkdownPath;
IncludeLine = state.IncludeLine;

CrossLinkResolver = state.CrossLinkResolver;
MarkdownSourcePath = state.MarkdownSourcePath;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,7 @@ private void WriteIncludeBlock(LlmMarkdownRenderer renderer, IncludeBlock block)
try
{
var parentPath = block.Context.MarkdownParentPath ?? block.Context.MarkdownSourcePath;
var document = MarkdownParser.ParseSnippetAsync(block.Build, block.Context, snippet, parentPath, block.Context.YamlFrontMatter, Cancel.None)
var document = MarkdownParser.ParseSnippetAsync(block.Build, block.Context, snippet, parentPath, block.Context.YamlFrontMatter, Cancel.None, block.Line)
.GetAwaiter().GetResult();
_ = renderer.Render(document);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 System.IO.Abstractions.TestingHelpers;
using Elastic.Markdown.Myst.Directives.Include;
using Elastic.Markdown.Tests.Directives;
using FluentAssertions;

namespace Elastic.Markdown.Tests.FileInclusion;

/// <summary>
/// Tests that when the same snippet containing applies-switch is included multiple times,
/// each include generates unique IDs to avoid HTML ID collisions.
/// </summary>
public class IncludedAppliesSwitchTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
:::{include} _snippets/applies-switch.md
:::

Some content between includes.

:::{include} _snippets/applies-switch.md
:::
"""
)
{
protected override void AddToFileSystem(MockFileSystem fileSystem)
{
// language=markdown
var snippet =
"""
::::{applies-switch}
:::{applies-item} stack:
Content for Stack
:::
:::{applies-item} serverless:
Content for Serverless
:::
::::
""";
fileSystem.AddFile(@"docs/_snippets/applies-switch.md", snippet);
}

[Fact]
public void ParsesBlock() => Block.Should().NotBeNull();

[Fact]
public void EachIncludeHasUniqueIds()
{
// First include at line 2: (2 * 1000) + 0 = 2000
// Second include at line 7: (7 * 1000) + 0 = 7000
Html.Should().Contain("applies-switch-item-2000-0");
Html.Should().Contain("applies-switch-item-2000-1");
Html.Should().Contain("applies-switch-set-2000");

Html.Should().Contain("applies-switch-item-7000-0");
Html.Should().Contain("applies-switch-item-7000-1");
Html.Should().Contain("applies-switch-set-7000");
}
}

/// <summary>
/// Tests that a snippet with multiple applies-switches generates unique IDs for each one.
/// </summary>
public class IncludedMultipleAppliesSwitchTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
:::{include} _snippets/multi-applies-switch.md
:::
"""
)
{
protected override void AddToFileSystem(MockFileSystem fileSystem)
{
// language=markdown
var snippet =
"""
::::{applies-switch}
:::{applies-item} stack:
First switch - Stack
:::
::::

Some content between.

::::{applies-switch}
:::{applies-item} serverless:
Second switch - Serverless
:::
::::
""";
fileSystem.AddFile(@"docs/_snippets/multi-applies-switch.md", snippet);
}

[Fact]
public void ParsesBlock() => Block.Should().NotBeNull();

[Fact]
public void EachAppliesSwitchHasUniqueIds()
{
// Include at line 2, first applies-switch at line 0: (2 * 1000) + 0 = 2000
// Include at line 2, second applies-switch at line 8: (2 * 1000) + 8 = 2008
Html.Should().Contain("applies-switch-set-2000");
Html.Should().Contain("applies-switch-set-2008");
}
}
106 changes: 106 additions & 0 deletions tests/Elastic.Markdown.Tests/FileInclusion/IncludedTabSetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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 System.IO.Abstractions.TestingHelpers;
using Elastic.Markdown.Myst.Directives.Include;
using Elastic.Markdown.Tests.Directives;
using FluentAssertions;

namespace Elastic.Markdown.Tests.FileInclusion;

/// <summary>
/// Tests that when the same snippet containing tab-set is included multiple times,
/// each include generates unique IDs to avoid HTML ID collisions.
/// </summary>
public class IncludedTabSetTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
:::{include} _snippets/tab-set.md
:::

Some content between includes.

:::{include} _snippets/tab-set.md
:::
"""
)
{
protected override void AddToFileSystem(MockFileSystem fileSystem)
{
// language=markdown
var snippet =
"""
::::{tab-set}
:::{tab-item} First
Content for first tab
:::
:::{tab-item} Second
Content for second tab
:::
::::
""";
fileSystem.AddFile(@"docs/_snippets/tab-set.md", snippet);
}

[Fact]
public void ParsesBlock() => Block.Should().NotBeNull();

[Fact]
public void EachIncludeHasUniqueIds()
{
// First include at line 2: (2 * 1000) + 0 = 2000
// Second include at line 7: (7 * 1000) + 0 = 7000
Html.Should().Contain("tabs-item-2000-0");
Html.Should().Contain("tabs-item-2000-1");
Html.Should().Contain("tabs-set-2000");

Html.Should().Contain("tabs-item-7000-0");
Html.Should().Contain("tabs-item-7000-1");
Html.Should().Contain("tabs-set-7000");
}
}

/// <summary>
/// Tests that a snippet with multiple tab-sets generates unique IDs for each one.
/// </summary>
public class IncludedMultipleTabSetTests(ITestOutputHelper output) : DirectiveTest<IncludeBlock>(output,
"""
:::{include} _snippets/multi-tab-set.md
:::
"""
)
{
protected override void AddToFileSystem(MockFileSystem fileSystem)
{
// language=markdown
var snippet =
"""
::::{tab-set}
:::{tab-item} First
First tab set
:::
::::

Some content between.

::::{tab-set}
:::{tab-item} Second
Second tab set
:::
::::
""";
fileSystem.AddFile(@"docs/_snippets/multi-tab-set.md", snippet);
}

[Fact]
public void ParsesBlock() => Block.Should().NotBeNull();

[Fact]
public void EachTabSetHasUniqueIds()
{
// Include at line 2, first tab-set at line 0: (2 * 1000) + 0 = 2000
// Include at line 2, second tab-set at line 8: (2 * 1000) + 8 = 2008
Html.Should().Contain("tabs-set-2000");
Html.Should().Contain("tabs-set-2008");
}
}
Loading