diff --git a/docs/syntax/code.md b/docs/syntax/code.md index 8b739c141a..4f5ecd4f79 100644 --- a/docs/syntax/code.md +++ b/docs/syntax/code.md @@ -35,14 +35,72 @@ project: ### Code callouts -There are two ways to add callouts to a code block. When using callouts, you must use one callout format. You cannot combine explicit and magic callouts. +There are two ways to add callouts to a code block: **automatic** and **explicit**. Automatic callouts are best for single-line descriptions. Numbering is handled for you, so adding or removing a callout does not require renumbering. Each description sits next to the code it annotates, making the source easier to read. Use explicit callouts when a description needs multiple lines, block-level content, or links. -#### Explicit callouts +You cannot combine both formats in the same code block. `docs-builder` will throw an error if both are present. + +#### Automatic callouts + +If a code block contains code comments in the form of `//` or `#`, callouts are automatically created. The comment must follow code on the same line. A comment on its own line is not treated as a callout. + +Callout text supports inline Markdown formatting such as `` `code` ``, **bold**, and *italic*. Automatic callouts do not currently support links. + +::::{tab-set} + +:::{tab-item} Output + +```python +api_key = ApiKey("") # Set up the `api` key +client = Elasticsearch("", api_key) # Connect to your **Elastic Cloud** deployment +response = client.search(index="my-index") # Returns a *list* of matching documents +``` + +::: -Add `<\d+>` to the end of a line to explicitly create a code callout. +:::{tab-item} Markdown -An ordered list with the same number of items as callouts must follow the code block. If the number of list items doesn’t match the callouts, docs-builder will throw an error. +````markdown +```python +api_key = ApiKey("") # Set up the `api` key +client = Elasticsearch("", api_key) # Connect to your **Elastic Cloud** deployment +response = client.search(index="my-index") # Returns a *list* of matching documents +``` +```` +::: + +:::: +Comments on their own line are left as-is: + +::::{tab-set} + +:::{tab-item} Output + +```csharp +// THIS IS NOT A CALLOUT +var apiKey = new ApiKey(""); // This is a callout +var client = new ElasticsearchClient("", apiKey); +``` + +::: + +:::{tab-item} Markdown + +````markdown +```csharp +// THIS IS NOT A CALLOUT +var apiKey = new ApiKey(""); // This is a callout +var client = new ElasticsearchClient("", apiKey); +``` +```` + +::: + +:::: + +#### Explicit callouts + +Add a numbered marker like `<1>`, `<2>`, etc. to the end of a line to create a callout. An ordered list with the same number of items must follow the code block. If the counts don’t match, docs-builder throws an error. ::::{tab-set} @@ -74,7 +132,7 @@ project: :::: -You can also have one block element in between the code block and the callout list: +You can have one block element between the code block and the callout list: ::::{tab-set} @@ -135,64 +193,7 @@ render(input2); :::: -#### Automatic callouts - -If a code block contains code comments in the form of `//` or `#`, callouts are automatically created. - - -::::{tab-set} - -:::{tab-item} Output - -```csharp -var apiKey = new ApiKey(""); // Set up the api key -var client = new ElasticsearchClient("", apiKey); -``` - -::: - -:::{tab-item} Markdown - -````markdown -```csharp -var apiKey = new ApiKey(""); // Set up the api key -var client = new ElasticsearchClient("", apiKey); -``` -```` -::: - - -:::: - -Code comments must follow code to be hoisted as a callout. For example: - -::::{tab-set} - -:::{tab-item} Output - -```csharp -// THIS IS NOT A CALLOUT -var apiKey = new ApiKey(""); // This is a callout -var client = new ElasticsearchClient("", apiKey); -``` - -::: - -:::{tab-item} Markdown - -````markdown -```csharp -// THIS IS NOT A CALLOUT -var apiKey = new ApiKey(""); // This is a callout -var client = new ElasticsearchClient("", apiKey); -``` -```` - -::: - -:::: - -#### Align callouts +##### Align callouts You can align callouts with spaces. diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs index bf8dad361c..fdf7af77bd 100644 --- a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs +++ b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs @@ -23,6 +23,8 @@ public class EnhancedCodeBlock(BlockParser parser, ParserContext context) { public BuildContext Build { get; } = context.Build; + public ParserContext Context { get; } = context; + public IFileInfo CurrentFile { get; } = context.MarkdownSourcePath; public bool SkipValidation { get; } = context.SkipValidation; diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs index 8dd769c57e..4adf9f9d14 100644 --- a/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs +++ b/src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs @@ -9,10 +9,12 @@ using Elastic.Markdown.Myst.Comments; using Elastic.Markdown.Myst.Directives.AppliesTo; using Elastic.Markdown.Myst.Directives.Contributors; +using Markdig; using Markdig.Helpers; using Markdig.Renderers; using Markdig.Renderers.Html; using Markdig.Syntax; +using Microsoft.AspNetCore.Html; using RazorSlices; namespace Elastic.Markdown.Myst.CodeBlocks; @@ -238,7 +240,7 @@ protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block) foreach (var c in block.UniqueCallOuts) { _ = renderer.WriteLine("
  • "); - _ = renderer.WriteLine(c.Text); + _ = renderer.WriteLine(RenderCalloutMarkdown(block, c).Value ?? string.Empty); _ = renderer.WriteLine("
  • "); } @@ -246,6 +248,40 @@ protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block) } } + private static HtmlString RenderCalloutMarkdown(EnhancedCodeBlock block, CallOut callOut) + { + if (string.IsNullOrWhiteSpace(callOut.Text)) + return HtmlString.Empty; + + var document = MarkdownParser.ParseMarkdownStringAsync( + block.Build, + block.Context, + callOut.Text, + block.CurrentFile, + 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()); + } + [SuppressMessage("Reliability", "CA2012:Use ValueTasks correctly")] private static void RenderAppliesToHtml(HtmlRenderer renderer, AppliesToDirective appliesToDirective) { diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs index b6cc865a58..ebca2e7bf5 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.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.TestingHelpers; using AwesomeAssertions; using Elastic.Markdown.Myst.CodeBlocks; using Elastic.Markdown.Tests.Inline; @@ -51,6 +52,29 @@ public void ParsesMagicCallOuts() => Block!.CallOuts public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); } +public class MagicCallOutWithFormatting(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp", +""" +var x = 1; // this uses `formatting` and a [link](testing/req.md) +""" + ) +{ + protected override void AddToFileSystem(MockFileSystem fileSystem) => + fileSystem.AddFile("docs/testing/req.md", new MockFileData("# Requirements")); + + [Fact] + public void RendersFormattedInlineMarkdown() => + Html.ShouldContainHtml( + """ +
      +
    1. this uses formatting and a link
    2. +
    + """ + ); + + [Fact] + public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0); +} + public class ClassicCallOutsRequiresContent(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp", """ var x = 1; <1>