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
127 changes: 64 additions & 63 deletions docs/syntax/code.md
Original file line number Diff line number Diff line change
Expand Up @@ -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("<API_KEY>") # Set up the `api` key
client = Elasticsearch("<CLOUD_ID>", 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("<API_KEY>") # Set up the `api` key
client = Elasticsearch("<CLOUD_ID>", 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("<API_KEY>"); // This is a callout
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
```

:::

:::{tab-item} Markdown

````markdown
```csharp
// THIS IS NOT A CALLOUT
var apiKey = new ApiKey("<API_KEY>"); // This is a callout
var client = new ElasticsearchClient("<CLOUD_ID>", 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}

Expand Down Expand Up @@ -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}

Expand Down Expand Up @@ -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("<API_KEY>"); // Set up the api key
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
```

:::

:::{tab-item} Markdown

````markdown
```csharp
var apiKey = new ApiKey("<API_KEY>"); // Set up the api key
var client = new ElasticsearchClient("<CLOUD_ID>", 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("<API_KEY>"); // This is a callout
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
```

:::

:::{tab-item} Markdown

````markdown
```csharp
// THIS IS NOT A CALLOUT
var apiKey = new ApiKey("<API_KEY>"); // This is a callout
var client = new ElasticsearchClient("<CLOUD_ID>", apiKey);
```
````

:::

::::

#### Align callouts
##### Align callouts

You can align callouts with spaces.

Expand Down
2 changes: 2 additions & 0 deletions src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -238,14 +240,48 @@ protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block)
foreach (var c in block.UniqueCallOuts)
{
_ = renderer.WriteLine("<li>");
_ = renderer.WriteLine(c.Text);
_ = renderer.WriteLine(RenderCalloutMarkdown(block, c).Value ?? string.Empty);
_ = renderer.WriteLine("</li>");
}

_ = renderer.WriteLine("</ol>");
}
}

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)
{
Expand Down
24 changes: 24 additions & 0 deletions tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
"""
<ol class="code-callouts">
<li>this uses <code>formatting</code> and a <a href="/docs/testing/req" hx-get="/docs/testing/req" hx-select-oob="#content-container,#toc-nav" hx-swap="none" hx-push-url="true" hx-indicator="#htmx-indicator" preload="mousedown">link</a></li>
Comment on lines +57 to +69
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theletterf apparently the ShouldContainHtml helper in PrettyHtmlExtensions.cs:84 has a bug: it assigns actual to expectedCompare instead of expected, so the contains check is actual.Contains(actual) which always passes. This means any test using ShouldContainHtml silently passes regardless of output. The MagicCallOutWithFormatting test in this PR is affected: its input contains a / which the callout parser regex rejects, so no callout is generated, but the test still passes.

out of scope for this PR presumably

</ol>
"""
);

[Fact]
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
}

public class ClassicCallOutsRequiresContent(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
"""
var x = 1; <1>
Expand Down
Loading