diff --git a/docs/_snippets/sample-data-markdown.csv b/docs/_snippets/sample-data-markdown.csv
new file mode 100644
index 000000000..8a868bca6
--- /dev/null
+++ b/docs/_snippets/sample-data-markdown.csv
@@ -0,0 +1,6 @@
+Name,Notes,Links
+Alpha,"**Bold** text, _italic_ text, and `code`.","[Google](https://www.google.com)"
+Bravo,"Inline role: {kbd}`Ctrl+C`.","[Elastic docs](https://www.elastic.co/docs)"
+Charlie,"Mixed **bold** and _italic_, plus `inline code`.","[Search](https://www.elastic.co/docs/solutions/search)"
+Delta,"Feature {applies_to}`stack: ga 9.1` available now.","[Elastic](https://www.elastic.co)"
+Echo,"This is {preview}`9.1` functionality.","[Preview docs](https://www.elastic.co/docs)"
diff --git a/docs/syntax/csv-include.md b/docs/syntax/csv-include.md
index 6353a51b3..c55fd0785 100644
--- a/docs/syntax/csv-include.md
+++ b/docs/syntax/csv-include.md
@@ -57,6 +57,34 @@ The directive includes built-in performance limits to handle large files efficie
- **Column limit**: Maximum of 10 columns will be displayed
- **File size limit**: Maximum file size of 10MB
+### Markdown rendering in cells
+
+Cells are parsed as Markdown, so they can render inline formatting and links. For example, a cell containing `**Bold**` becomes bold text, and `[Text](https://www.google.com)` becomes a link.
+
+Here is a complete example that uses multiple Markdown formats:
+
+:::::{tab-set}
+
+::::{tab-item} Output
+
+:::{csv-include} ../_snippets/sample-data-markdown.csv
+:caption: Sample data with Markdown formatting
+:::
+
+::::
+
+::::{tab-item} Markdown
+
+```markdown
+:::{csv-include} _snippets/sample-data-markdown.csv
+:caption: Sample data with Markdown formatting
+:::
+```
+
+::::
+
+:::::
+
## Performance considerations
The CSV directive is optimized for large files:
diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs
index 1fffcb5ba..0413c2d85 100644
--- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs
+++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeBlock.cs
@@ -2,6 +2,7 @@
// 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;
using Elastic.Markdown.Diagnostics;
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
@@ -10,6 +11,10 @@ public class CsvIncludeBlock(DirectiveBlockParser parser, ParserContext context)
{
public override string Directive => "csv-include";
+ public ParserContext Context { get; } = context;
+
+ public IFileInfo IncludeFrom { get; } = context.MarkdownSourcePath;
+
public string? CsvFilePath { get; private set; }
public string? CsvFilePathRelativeToSource { get; private set; }
public bool Found { get; private set; }
diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeView.cshtml b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeView.cshtml
index 9f3b84026..9a2be7f38 100644
--- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeView.cshtml
+++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeView.cshtml
@@ -26,7 +26,7 @@
@for (var i = 0; i < csvRows[0].Length; i++)
{
- | @csvRows[0][i] |
+ @Model.RenderCell(csvRows[0][i]) |
}
@@ -37,7 +37,7 @@
@for (var i = 0; i < csvRows[rowIndex].Length; i++)
{
- | @csvRows[rowIndex][i] |
+ @Model.RenderCell(csvRows[rowIndex][i]) |
}
}
diff --git a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeViewModel.cs b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeViewModel.cs
index 072cbb187..0bfb1a57e 100644
--- a/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeViewModel.cs
+++ b/src/Elastic.Markdown/Myst/Directives/CsvInclude/CsvIncludeViewModel.cs
@@ -3,11 +3,14 @@
// See the LICENSE file in the project root for more information
using Elastic.Markdown.Diagnostics;
+using Microsoft.AspNetCore.Html;
namespace Elastic.Markdown.Myst.Directives.CsvInclude;
public class CsvIncludeViewModel : DirectiveViewModel
{
+ public required Func RenderMarkdown { get; init; }
+
public IEnumerable GetCsvRows()
{
if (DirectiveBlock is not CsvIncludeBlock csvBlock || !csvBlock.Found || string.IsNullOrEmpty(csvBlock.CsvFilePath))
@@ -48,9 +51,18 @@ public IEnumerable GetCsvRows()
});
}
- public static CsvIncludeViewModel Create(CsvIncludeBlock csvBlock) =>
+ public HtmlString RenderCell(string? value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return HtmlString.Empty;
+
+ return RenderMarkdown(value);
+ }
+
+ public static CsvIncludeViewModel Create(CsvIncludeBlock csvBlock, Func renderMarkdown) =>
new()
{
- DirectiveBlock = csvBlock
+ DirectiveBlock = csvBlock,
+ RenderMarkdown = renderMarkdown
};
}
diff --git a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
index e342ae0d2..6a3808373 100644
--- a/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
+++ b/src/Elastic.Markdown/Myst/Directives/DirectiveHtmlRenderer.cs
@@ -2,6 +2,7 @@
// 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;
using System.Diagnostics.CodeAnalysis;
using Elastic.Documentation.AppliesTo;
using Elastic.Markdown.Diagnostics;
@@ -29,6 +30,7 @@
using Markdig.Renderers.Html;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
+using Microsoft.AspNetCore.Html;
using RazorSlices;
using YamlDotNet.Core;
@@ -517,11 +519,45 @@ void Render(Block o)
private static void WriteCsvIncludeBlock(HtmlRenderer renderer, CsvIncludeBlock block)
{
- var viewModel = CsvIncludeViewModel.Create(block);
+ var viewModel = CsvIncludeViewModel.Create(block, value => RenderCsvCellMarkdown(block, value));
var slice = CsvIncludeView.Create(viewModel);
RenderRazorSlice(slice, renderer);
}
+ private static HtmlString RenderCsvCellMarkdown(CsvIncludeBlock block, string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return HtmlString.Empty;
+
+ var document = MarkdownParser.ParseMarkdownStringAsync(
+ block.Build,
+ block.Context,
+ value,
+ block.IncludeFrom,
+ block.Context.YamlFrontMatter,
+ MarkdownParser.Pipeline);
+
+ if (document.Count == 1 && document.FirstOrDefault() is ParagraphBlock paragraph && paragraph.Inline != null)
+ return RenderInlineMarkdown(paragraph);
+
+ var html = document.ToHtml(MarkdownParser.Pipeline);
+ return new HtmlString(html.EnsureTrimmed());
+ }
+
+ private static HtmlString RenderInlineMarkdown(ParagraphBlock paragraph)
+ {
+ if (paragraph.Inline is null)
+ return HtmlString.Empty;
+
+ var subscription = DocumentationObjectPoolProvider.HtmlRendererPool.Get();
+ subscription.HtmlRenderer.WriteChildren(paragraph.Inline);
+
+ var result = subscription.RentedStringBuilder?.ToString();
+ DocumentationObjectPoolProvider.HtmlRendererPool.Return(subscription);
+
+ return result == null ? HtmlString.Empty : new HtmlString(result.EnsureTrimmed());
+ }
+
private static void WriteChangelogBlock(HtmlRenderer renderer, ChangelogBlock block)
{
if (!block.Found || block.BundlesFolderPath is null)
diff --git a/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs b/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs
index adc952347..1b4de4851 100644
--- a/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs
+++ b/tests/Elastic.Markdown.Tests/Directives/CsvIncludeTests.cs
@@ -123,6 +123,21 @@ public void HandlesEscapedQuotes()
}
}
+public class CsvIncludeRenderLinksTests(ITestOutputHelper output) : DirectiveTest(output,
+"""
+::::{csv-include} test-data.csv
+::::
+""")
+{
+ protected override void AddToFileSystem(MockFileSystem fileSystem) =>
+ fileSystem.AddFile("docs/test-data.csv", new MockFileData(
+@"Name,Link
+Search,[Text](https://www.google.com)"));
+
+ [Fact]
+ public void RendersMarkdownLinkAsLink() => Html.Should().Contain(">Text");
+}
+
public class CsvIncludeNotFoundTests(ITestOutputHelper output) : DirectiveTest(output,
"""
:::{csv-include} missing-file.csv