Skip to content

Commit 299898f

Browse files
Allow comments between code blocks and callouts (#695)
* allow comments between code blocks and callouts * Fix failing test * Apply suggestions from code review Co-authored-by: Jan Calanog <[email protected]> * Remove extra blank lines in method * Revert back to checking the name of the commentblock * Refactor IsCommentBlock method --------- Co-authored-by: Jan Calanog <[email protected]>
1 parent 6a91063 commit 299898f

File tree

2 files changed

+242
-40
lines changed

2 files changed

+242
-40
lines changed

src/Elastic.Markdown/Myst/CodeBlocks/EnhancedCodeBlockHtmlRenderer.cs

Lines changed: 72 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System.Diagnostics.CodeAnalysis;
66
using Elastic.Markdown.Diagnostics;
7+
using Elastic.Markdown.Myst.Comments;
78
using Elastic.Markdown.Slices.Directives;
89
using Markdig.Helpers;
910
using Markdig.Renderers;
@@ -107,6 +108,15 @@ private static int CountIndentation(StringSlice slice)
107108
return indentCount;
108109
}
109110

111+
private static bool IsCommentBlock(Block block)
112+
{
113+
if (block is CommentBlock)
114+
{
115+
return true;
116+
}
117+
return false;
118+
}
119+
110120
protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block)
111121
{
112122
if (block is AppliesToDirective appliesToDirective)
@@ -130,59 +140,82 @@ protected override void Write(HtmlRenderer renderer, EnhancedCodeBlock block)
130140
{
131141
var index = block.Parent!.IndexOf(block);
132142
if (index == block.Parent!.Count - 1)
143+
{
133144
block.EmitError("Code block with annotations is not followed by any content, needs numbered list");
134-
else
145+
return;
146+
}
147+
148+
var nonCommentNonListCount = 0;
149+
ListBlock? listBlock = null;
150+
var currentIndex = index + 1;
151+
152+
// Process blocks between the code block and ordered list, removing comments and allowing only one non-comment block
153+
while (currentIndex < block.Parent.Count)
135154
{
136-
var siblingBlock = block.Parent[index + 1];
137-
if (siblingBlock is not ListBlock)
155+
var nextBlock = block.Parent[currentIndex];
156+
157+
if (nextBlock is ListBlock lb)
138158
{
139-
// allow one block of content in between
140-
// render it immediately and remove it, so it's not rendered twice
141-
if (index + 2 <= block.Parent.Count - 1)
142-
{
143-
_ = renderer.Render(block.Parent[index + 1]);
144-
_ = block.Parent.Remove(block.Parent[index + 1]);
145-
siblingBlock = block.Parent[index + 1];
146-
}
147-
if (siblingBlock is not ListBlock)
148-
{
149-
block.EmitError("Code block with annotations is not followed by a list");
150-
}
159+
listBlock = lb;
160+
break;
151161
}
152-
if (siblingBlock is ListBlock l && l.Count < callOuts.Count)
162+
else if (IsCommentBlock(nextBlock))
153163
{
154-
block.EmitError(
155-
$"Code block has {callOuts.Count} callouts but the following list only has {l.Count}");
164+
_ = renderer.Render(nextBlock);
165+
_ = block.Parent.Remove(nextBlock);
156166
}
157-
else if (siblingBlock is ListBlock listBlock)
167+
else
158168
{
159-
_ = block.Parent.Remove(listBlock);
160-
_ = renderer.WriteLine("<ol class=\"code-callouts\">");
161-
foreach (var child in listBlock)
169+
nonCommentNonListCount++;
170+
if (nonCommentNonListCount > 1)
162171
{
163-
var listItem = (ListItemBlock)child;
164-
var previousImplicit = renderer.ImplicitParagraph;
165-
renderer.ImplicitParagraph = !listBlock.IsLoose;
172+
block.EmitError("More than one content block between code block with annotations and its list");
173+
return;
174+
}
166175

167-
_ = renderer.EnsureLine();
168-
if (renderer.EnableHtmlForBlock)
169-
{
170-
_ = renderer.Write("<li");
171-
_ = renderer.WriteAttributes(listItem);
172-
_ = renderer.Write('>');
173-
}
176+
_ = renderer.Render(nextBlock);
177+
_ = block.Parent.Remove(nextBlock);
178+
}
179+
}
180+
181+
if (listBlock == null)
182+
{
183+
block.EmitError("Code block with annotations is not followed by a list");
184+
return;
185+
}
174186

175-
renderer.WriteChildren(listItem);
187+
if (listBlock.Count < callOuts.Count)
188+
{
189+
block.EmitError($"Code block has {callOuts.Count} callouts but the following list only has {listBlock.Count}");
190+
return;
191+
}
176192

177-
if (renderer.EnableHtmlForBlock)
178-
_ = renderer.WriteLine("</li>");
193+
_ = block.Parent.Remove(listBlock);
179194

180-
_ = renderer.EnsureLine();
181-
renderer.ImplicitParagraph = previousImplicit;
182-
}
183-
_ = renderer.WriteLine("</ol>");
195+
_ = renderer.WriteLine("<ol class=\"code-callouts\">");
196+
foreach (var child in listBlock)
197+
{
198+
var listItem = (ListItemBlock)child;
199+
var previousImplicit = renderer.ImplicitParagraph;
200+
renderer.ImplicitParagraph = !listBlock.IsLoose;
201+
202+
_ = renderer.EnsureLine();
203+
if (renderer.EnableHtmlForBlock)
204+
{
205+
_ = renderer.Write("<li");
206+
_ = renderer.WriteAttributes(listItem);
207+
_ = renderer.Write('>');
184208
}
209+
210+
renderer.WriteChildren(listItem);
211+
212+
if (renderer.EnableHtmlForBlock)
213+
_ = renderer.WriteLine("</li>");
214+
215+
_ = renderer.EnsureLine();
216+
renderer.ImplicitParagraph = previousImplicit;
185217
}
218+
_ = renderer.WriteLine("</ol>");
186219
}
187220
else if (block.InlineAnnotations)
188221
{

tests/Elastic.Markdown.Tests/CodeBlocks/CallOutTests.cs

Lines changed: 170 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ public void ParsesMagicCallOuts() => Block!.CallOuts
165165

166166
[Fact]
167167
public void RequiresContentToFollow() => Collector.Diagnostics.Should().HaveCount(1)
168-
.And.OnlyContain(c => c.Message.StartsWith("Code block with annotations is not followed by a list"));
168+
.And.OnlyContain(c => c.Message.StartsWith("More than one content block between code block with annotations and its list"));
169169
}
170170

171171

@@ -340,3 +340,172 @@ public void ParsesMagicCallOuts() => Block!.CallOuts
340340
[Fact]
341341
public void HasNoErrors() => Collector.Diagnostics.Should().HaveCount(0);
342342
}
343+
344+
public class CodeBlockWithCommentBlocksThenList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
345+
"""
346+
var x = 1; <1>
347+
var y = x - 2;
348+
var z = y - 2; <2>
349+
""",
350+
"""
351+
% TEST[s/"basque_keywords",//]
352+
% TEST[s/\n$/\nstartyaml\n - compare_analyzers: {index: basque_example, first: basque, second: rebuilt_basque}\nendyaml\n/]
353+
354+
1. First callout
355+
2. Second callout
356+
"""
357+
)
358+
{
359+
[Fact]
360+
public void ParsesCallouts() => Block!.CallOuts
361+
.Should().NotBeNullOrEmpty()
362+
.And.HaveCount(2)
363+
.And.OnlyContain(c => c.Text.StartsWith('<'));
364+
365+
[Fact]
366+
public void HandlesCommentBlocksCorrectly() => Collector.Diagnostics.Should().BeEmpty();
367+
368+
[Fact]
369+
public void RenderedHtmlContainsCallouts() =>
370+
Html.Should().Contain("""
371+
<ol class="code-callouts">
372+
<li>First callout</li>
373+
<li>Second callout</li>
374+
</ol>
375+
""");
376+
}
377+
378+
public class CodeBlockWithMultipleCommentTypesThenList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
379+
"""
380+
var x = 1; <1>
381+
var y = x - 2;
382+
var z = y - 2; <2>
383+
""",
384+
"""
385+
% This is an HTML-style comment that starts with %
386+
% TEST[catch:bad_request]
387+
388+
1. First callout
389+
2. Second callout
390+
"""
391+
)
392+
{
393+
[Fact]
394+
public void ParsesCallouts() => Block!.CallOuts
395+
.Should().NotBeNullOrEmpty()
396+
.And.HaveCount(2);
397+
398+
[Fact]
399+
public void HandlesCommentBlocksCorrectly() => Collector.Diagnostics.Should().BeEmpty();
400+
}
401+
402+
public class CodeBlockWithCommentBlocksParagraphThenList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
403+
"""
404+
var x = 1; <1>
405+
var y = x - 2;
406+
var z = y - 2; <2>
407+
""",
408+
"""
409+
% TEST[s/"basque_keywords",//]
410+
% TEST[catch:bad_request]
411+
412+
**This is an intermediate paragraph**
413+
414+
1. First callout
415+
2. Second callout
416+
"""
417+
)
418+
{
419+
[Fact]
420+
public void ParsesCallouts() => Block!.CallOuts
421+
.Should().NotBeNullOrEmpty()
422+
.And.HaveCount(2);
423+
424+
[Fact]
425+
public void HandlesCommentBlocksAndParagraphCorrectly() => Collector.Diagnostics.Should().BeEmpty();
426+
427+
[Fact]
428+
public void RendersIntermediateParagraph() =>
429+
Html.Should().Contain("""
430+
<p><strong>This is an intermediate paragraph</strong></p>
431+
<ol class="code-callouts">
432+
""");
433+
}
434+
435+
public class CodeBlockWithCommentBlocksTwoParagraphsThenList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
436+
"""
437+
var x = 1; <1>
438+
var y = x - 2;
439+
var z = y - 2; <2>
440+
""",
441+
"""
442+
% TEST[s/"basque_keywords",//]
443+
444+
**This is an intermediate paragraph**
445+
446+
**This is a second paragraph which should cause an error**
447+
448+
1. First callout
449+
2. Second callout
450+
"""
451+
)
452+
{
453+
[Fact]
454+
public void ParsesCallouts() => Block!.CallOuts
455+
.Should().NotBeNullOrEmpty()
456+
.And.HaveCount(2);
457+
458+
[Fact]
459+
public void EmitsErrorForTooManyParagraphs() => Collector.Diagnostics.Should().HaveCount(1)
460+
.And.OnlyContain(c => c.Message.StartsWith("More than one content block between code block with annotations and its list"));
461+
}
462+
463+
public class CodeBlockWithManyCommentBlocksNoList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
464+
"""
465+
var x = 1; <1>
466+
var y = x - 2;
467+
var z = y - 2; <2>
468+
""",
469+
"""
470+
% TEST[s/"basque_keywords",//]
471+
% TEST[s/\n$/\nstartyaml\n - compare_analyzers: {index: basque_example, first: basque, second: rebuilt_basque}\nendyaml\n/]
472+
% TEST[catch:bad_request]
473+
"""
474+
)
475+
{
476+
[Fact]
477+
public void ParsesCallouts() => Block!.CallOuts
478+
.Should().NotBeNullOrEmpty()
479+
.And.HaveCount(2);
480+
481+
[Fact]
482+
public void EmitsErrorForNoList() => Collector.Diagnostics.Should().HaveCount(1)
483+
.And.OnlyContain(c => c.Message.StartsWith("Code block with annotations is not followed by a list"));
484+
}
485+
486+
public class CodeBlockWithCommentsAfterList(ITestOutputHelper output) : CodeBlockCallOutTests(output, "csharp",
487+
"""
488+
var x = 1; <1>
489+
var y = x - 2;
490+
var z = y - 2; <2>
491+
""",
492+
"""
493+
1. First callout
494+
2. Second callout
495+
496+
% TEST[s/"basque_keywords",//]
497+
"""
498+
)
499+
{
500+
[Fact]
501+
public void ParsesCallouts() => Block!.CallOuts
502+
.Should().NotBeNullOrEmpty()
503+
.And.HaveCount(2);
504+
505+
[Fact]
506+
public void HandlesCommentsCorrectly() => Collector.Diagnostics.Should().BeEmpty();
507+
508+
[Fact]
509+
public void RenderedHtmlDoesNotContainComments() =>
510+
Html.Should().NotContain("basque_keywords");
511+
}

0 commit comments

Comments
 (0)