From addceb7afe861b54e50a0a7e1d117ef516169fb7 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 10 Oct 2025 11:21:17 +0200 Subject: [PATCH] Fix console escaping of placeholder values --- docs/syntax/code.md | 52 +++++++++---- .../Myst/CodeBlocks/CodeViewModel.cs | 7 +- .../CodeBlocks/ConsoleCodeBlockTests.cs | 75 +++++++++++++++++++ 3 files changed, 116 insertions(+), 18 deletions(-) diff --git a/docs/syntax/code.md b/docs/syntax/code.md index 43f784437..8b739c141 100644 --- a/docs/syntax/code.md +++ b/docs/syntax/code.md @@ -135,12 +135,9 @@ render(input2); :::: +#### Automatic callouts - - -#### Magic Callouts - -If a code block contains code comments in the form of `//` or `#`, callouts will be magically created 🪄. +If a code block contains code comments in the form of `//` or `#`, callouts are automatically created. ::::{tab-set} @@ -231,9 +228,9 @@ bazbazbaz: 3 <3> :::: -#### Disable callouts +#### Turn off callouts -You can disable callouts by adding a code block argument `callouts=false`. +You can turn off callouts by adding a code block argument `callouts=false`. ::::{tab-set} @@ -276,27 +273,52 @@ In a console code block, the first line is highlighted as a dev console string a :::{tab-item} Output ```console -GET /mydocuments/_search +POST _reindex { - "from": 1, + "source": { + "remote": { + "host": "", + "username": "user", + "password": "pass" + }, + "index": "my-index-000001", "query": { - "match_all" {} + "match": { + "test": "data" + } } + }, + "dest": { + "index": "my-new-index-000001" + } } ``` + ::: :::{tab-item} Markdown ````markdown ```console -GET /mydocuments/_search +POST _reindex { - "from": 1, + "source": { + "remote": { + "host": "", + "username": "user", + "password": "pass" + }, + "index": "my-index-000001", "query": { - "match_all" {} + "match": { + "test": "data" + } } + }, + "dest": { + "index": "my-new-index-000001" + } } ``` ```` @@ -402,7 +424,7 @@ This `code` is inline. :::: -## Supported Languages +## Supported languages -Please refer to [hljs.ts](https://github.com/elastic/docs-builder/blob/main/src/Elastic.Documentation.Site/Assets/hljs.ts) +Refer to [hljs.ts](https://github.com/elastic/docs-builder/blob/main/src/Elastic.Documentation.Site/Assets/hljs.ts) for a complete list of supported languages. diff --git a/src/Elastic.Markdown/Myst/CodeBlocks/CodeViewModel.cs b/src/Elastic.Markdown/Myst/CodeBlocks/CodeViewModel.cs index 44323e3f5..0155ce0e0 100644 --- a/src/Elastic.Markdown/Myst/CodeBlocks/CodeViewModel.cs +++ b/src/Elastic.Markdown/Myst/CodeBlocks/CodeViewModel.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.Net; using Elastic.Markdown.Helpers; using Microsoft.AspNetCore.Html; @@ -33,11 +34,11 @@ public HtmlString RenderBlock() public HtmlString RenderLineWithCallouts(string content, int lineNumber) { if (EnhancedCodeBlock?.CallOuts == null) - return new HtmlString(content); + return new HtmlString(WebUtility.HtmlEncode(content)); var callouts = EnhancedCodeBlock.CallOuts.Where(c => c.Line == lineNumber); if (!callouts.Any()) - return new HtmlString(content); + return new HtmlString(WebUtility.HtmlEncode(content)); var line = content; var html = new System.Text.StringBuilder(); @@ -50,7 +51,7 @@ public HtmlString RenderLineWithCallouts(string content, int lineNumber) } line = line.TrimEnd(); - _ = html.Append(line); + _ = html.Append(WebUtility.HtmlEncode(line)); // Add callout HTML after the line foreach (var callout in callouts) diff --git a/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs b/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs index 5b9c122b3..9f92f3252 100644 --- a/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs +++ b/tests/Elastic.Markdown.Tests/CodeBlocks/ConsoleCodeBlockTests.cs @@ -346,3 +346,78 @@ public void RendersCalloutsInJsonContent() public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); } +public class ConsoleWithHtmlCharsTests(ITestOutputHelper output) : ConsoleCodeBlockTests(output, +""" +```console +POST /auth/login +{ + "username": "", + "password": "", + "token": "" +} +``` +""" +) +{ + [Fact] + public void CreatesApiSegmentWithHtmlChars() + { + Block!.ApiSegments.Should().HaveCount(1); + var segment = Block.ApiSegments[0]; + segment.Header.Should().Be("POST /auth/login"); + segment.ContentLines.Should().Contain(line => line.Contains("")); + segment.ContentLines.Should().Contain(line => line.Contains("")); + segment.ContentLines.Should().Contain(line => line.Contains("")); + } + + [Fact] + public void EscapesHtmlCharsInRenderedOutput() + { + var viewModel = new CodeViewModel + { + ApiSegments = Block!.ApiSegments, + Language = Block.Language, + Caption = null, + CrossReferenceName = null, + RawIncludedFileContents = null, + EnhancedCodeBlock = Block + }; + + var contentHtml = viewModel.RenderContentLinesWithCallouts(Block.ApiSegments[0].ContentLinesWithNumbers); + + // HTML characters should be escaped in the output + contentHtml.Value.Should().Contain("<username>"); + contentHtml.Value.Should().Contain("<password>"); + contentHtml.Value.Should().Contain("<api_token>"); + + // Original unescaped versions should NOT be present + contentHtml.Value.Should().NotContain("\"username\": \"\""); + contentHtml.Value.Should().NotContain("\"password\": \"\""); + contentHtml.Value.Should().NotContain("\"token\": \"\""); + } + + [Fact] + public void EscapesHtmlCharsInHeader() + { + var viewModel = new CodeViewModel + { + ApiSegments = Block!.ApiSegments, + Language = Block.Language, + Caption = null, + CrossReferenceName = null, + RawIncludedFileContents = null, + EnhancedCodeBlock = Block + }; + + // Test header with HTML chars + var headerWithHtml = "GET /api/"; + var headerHtml = viewModel.RenderLineWithCallouts(headerWithHtml, 1); + + headerHtml.Value.Should().Contain("<resource>"); + headerHtml.Value.Should().NotContain(""); + } + + [Fact] + public void HasNoErrors() => Collector.Diagnostics.Should().BeEmpty(); +} +