Skip to content

Commit 3b19f12

Browse files
committed
chore: fix issue when markdown text and pre tag exists on same line
1 parent 58c5cba commit 3b19f12

File tree

2 files changed

+257
-18
lines changed

2 files changed

+257
-18
lines changed

src/Docfx.Dotnet/Parsers/XmlComment.Extensions.cs

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Buffers;
45
using System.Diagnostics;
56
using System.Text;
67
using System.Xml;
@@ -64,6 +65,7 @@ public static string GetInnerXml(this XElement elem)
6465
xml = RemoveCommonIndent(xml);
6566

6667
// Trim beginning spaces/lines if text starts with HTML tag.
68+
// It is necessary to avoid it being handled as a markdown code block.
6769
var firstNode = nodes.FirstOrDefault(x => !x.IsWhitespaceNode());
6870
if (firstNode != null && firstNode.NodeType == XmlNodeType.Element)
6971
xml = xml.TrimStart();
@@ -103,7 +105,7 @@ private static string RemoveCommonIndent(string text)
103105
minIndent = 0;
104106

105107
// 2nd pass: build result
106-
var sb = new StringBuilder(text.Length + 8);
108+
var sb = new StringBuilder(text.Length);
107109

108110
inPre = false;
109111
pos = 0;
@@ -192,7 +194,7 @@ static file class XNodeExtensions
192194
/// </summary>
193195
private static readonly Dictionary<(NodeKind prev, NodeKind next), bool> NeedEmptyLineRules = new()
194196
{
195-
//Block-> *
197+
//Block -> *
196198
[(NodeKind.Block, NodeKind.Other)] = false,
197199
[(NodeKind.Block, NodeKind.Block)] = false,
198200
[(NodeKind.Block, NodeKind.Pre)] = true,
@@ -294,9 +296,9 @@ private static bool NeedEmptyLine(this XElement node, Direction direction)
294296
return NeedEmptyLineRules.TryGetValue((leftKind, rightKind), out var result) && result;
295297
}
296298

297-
private static void EnsureEmptyLine(this XNode node, Direction direction)
299+
private static void EnsureEmptyLine(this XElement node, Direction direction)
298300
{
299-
var adjacentNode = GetAdjacentNode(node, direction);
301+
var adjacentNode = node.GetAdjacentNode(direction);
300302

301303
switch (adjacentNode)
302304
{
@@ -309,22 +311,26 @@ private static void EnsureEmptyLine(this XNode node, Direction direction)
309311
return;
310312

311313
case XText textNode:
314+
int count = textNode.CountConsecutiveNewLines(direction, out var insertIndex);
315+
var indent = GetIndentToInsert(node, direction, insertIndex);
312316

313-
int count = textNode.CountNewLines(direction, out var insertIndex);
317+
var newLineChars = count switch
318+
{
319+
0 => "\n\n",
320+
1 => "\n",
321+
_ => "",
322+
};
314323

315-
switch (count)
324+
if (newLineChars == "")
316325
{
317-
case 0:
318-
textNode.Value = textNode.Value.Insert(insertIndex, "\n\n");
319-
return;
320-
case 1:
321-
textNode.Value = textNode.Value.Insert(insertIndex, "\n");
322-
return;
323-
default:
324-
Debug.Assert(textNode.HasEmptyLine(direction));
325-
return;
326+
// It's not expected to be called. Because it's skipped by NeedEmptyLine check.
327+
Debug.Assert(textNode.HasEmptyLine(direction));
328+
return;
326329
}
327330

331+
textNode.Value = textNode.Value.Insert(insertIndex, $"{newLineChars}{indent}");
332+
return;
333+
328334
default:
329335
return;
330336
}
@@ -364,13 +370,35 @@ private static NodeKind GetNodeKind(XNode node)
364370
return current;
365371
}
366372

373+
private static T? FindNeighbor<T>(this XNode node, Direction direction)
374+
where T : XNode
375+
{
376+
var current = node.GetAdjacentNode(direction);
377+
378+
while (current != null && current is not T)
379+
current = current.GetAdjacentNode(direction);
380+
381+
return (T?)current;
382+
}
383+
367384
private static bool HasEmptyLine(this XText node, Direction direction)
368-
=> CountNewLines(node, direction, out _) >= 2;
385+
=> node.CountConsecutiveNewLines(direction, out _) >= 2;
369386

370387
/// <summary>
371-
/// Get count of new lines. space and tabs are ignored.
388+
/// Counts consecutive '\n' characters that exist before/after
372389
/// </summary>
373-
private static int CountNewLines(this XText node, Direction direction, out int insertIndex)
390+
/// <param name="direction">
391+
/// Direction.Before scans from the end
392+
/// Direction.After scans from the beginning.
393+
/// </param>
394+
/// <param name="insertIndex">
395+
/// The position where new content should be inserted.
396+
/// It's determined from the first newline found.
397+
/// </param>
398+
/// <returns>
399+
/// The number of consecutive newline characters found.
400+
/// </returns>
401+
private static int CountConsecutiveNewLines(this XText node, Direction direction, out int insertIndex)
374402
{
375403
var span = node.Value.AsSpan();
376404
int count = 0;
@@ -420,6 +448,110 @@ private static int CountNewLines(this XText node, Direction direction, out int i
420448
}
421449
}
422450

451+
private static string GetIndentToInsert(XElement node, Direction direction, int insertIndex)
452+
{
453+
// Check whether there is an existing indent.
454+
if (node.TryGetCurrentIndent(direction, out _))
455+
return "";
456+
457+
// Try to get indent from text node that is placed before.
458+
var beforeTextNode = node.FindNeighbor<XText>(Direction.Before);
459+
if (beforeTextNode != null)
460+
return beforeTextNode.GetIndentFromLastLine();
461+
462+
return "";
463+
}
464+
465+
private static bool TryGetCurrentIndent(this XElement node, Direction direction, out string indent)
466+
{
467+
indent = "";
468+
469+
var adjacentNode = node.GetAdjacentNode(direction);
470+
if (adjacentNode == null || adjacentNode is not XText textNode)
471+
return false;
472+
473+
ReadOnlySpan<char> result = direction switch
474+
{
475+
Direction.Before => GetIndentBefore(textNode.Value),
476+
Direction.After => GetIndentAfter(textNode.Value),
477+
_ => throw new UnreachableException()
478+
};
479+
480+
if (result.IsEmpty)
481+
return false;
482+
483+
indent = result.ToString();
484+
return true;
485+
}
486+
487+
private static string GetIndentBefore(ReadOnlySpan<char> span)
488+
{
489+
int lastNewLine = span.LastIndexOf('\n');
490+
if (lastNewLine < 0)
491+
return "";
492+
493+
var lastLineSpan = span[(lastNewLine + 1)..];
494+
if (lastLineSpan.Length == 0)
495+
return "";
496+
497+
if (lastLineSpan.ContainsAnyExcept([' ', '\t']))
498+
return "";
499+
500+
return lastLineSpan.ToString();
501+
}
502+
503+
private static string GetIndentAfter(ReadOnlySpan<char> span)
504+
{
505+
int i = 0;
506+
507+
while (i < span.Length)
508+
{
509+
int lineStart = i;
510+
511+
int indentLength = span[i..].IndexOfAnyExcept([' ', '\t']);
512+
if (indentLength < 0)
513+
return "";
514+
515+
i += indentLength;
516+
int indentEnd = i;
517+
518+
// Skip empty line
519+
if (span[i] == '\n')
520+
{
521+
i++;
522+
continue;
523+
}
524+
525+
// Return indent
526+
return span.Slice(lineStart, indentEnd - lineStart).ToString();
527+
}
528+
529+
return "";
530+
}
531+
532+
private static string GetIndentFromLastLine(this XText textNode)
533+
{
534+
ReadOnlySpan<char> text = textNode.Value;
535+
536+
int lastNewLineIndex = text.LastIndexOf('\n');
537+
if (lastNewLineIndex < 0)
538+
return "";
539+
540+
var line = text.Slice(lastNewLineIndex + 1);
541+
542+
if (line.IsEmpty)
543+
return "";
544+
545+
if (!line.ContainsAnyExcept([' ', '\t']))
546+
return line.ToString();
547+
548+
int index = line.IndexOfAnyExcept([' ', '\t']);
549+
if (index <= 0)
550+
return "";
551+
552+
return line.Slice(0, index).ToString();
553+
}
554+
423555
private static bool IsPreTag(this XElement elem)
424556
=> elem.Name.LocalName == "pre";
425557

test/Docfx.Dotnet.Tests/XmlCommentTests/XmlCommentSummaryTest.Code.cs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,111 @@ public void Code_MultipleBlockWithoutNewLine()
175175
Paragraph2
176176
""");
177177
}
178+
179+
[Fact]
180+
public void Code_StartsWithSameLine()
181+
{
182+
ValidateSummary(
183+
// Input XML
184+
"""
185+
<summary>
186+
<example>
187+
Paragraph: <code><![CDATA[
188+
Code]]></code>
189+
</example>
190+
</summary>
191+
""",
192+
// Expected Markdown
193+
"""
194+
<example>
195+
Paragraph:
196+
197+
<pre><code class="lang-csharp">Code</code></pre>
198+
199+
</example>
200+
""");
201+
}
202+
203+
[Fact]
204+
public void Code_SingleLineWithParagraph()
205+
{
206+
ValidateSummary(
207+
// Input XML
208+
"""
209+
<summary>
210+
<example>
211+
Paragraph1<code>Code</code>
212+
Paragraph2
213+
</example>
214+
</summary>
215+
""",
216+
// Expected Markdown
217+
"""
218+
<example>
219+
Paragraph1
220+
221+
<pre><code class="lang-csharp">Code</code></pre>
222+
223+
Paragraph2
224+
</example>
225+
""");
226+
}
227+
228+
[Fact]
229+
public void Code_SingleLineWithMultipleParagraphs()
230+
{
231+
ValidateSummary(
232+
// Input XML
233+
"""
234+
<summary>
235+
<example>
236+
aaa<p>bbb</p>ccc<code>Code</code>ddd<p>eee</p>fff
237+
</example>
238+
</summary>
239+
""",
240+
// Expected Markdown
241+
"""
242+
<example>
243+
aaa
244+
245+
<p>bbb</p>
246+
247+
ccc
248+
249+
<pre><code class="lang-csharp">Code</code></pre>
250+
251+
ddd
252+
253+
<p>eee</p>
254+
255+
fff
256+
</example>
257+
""");
258+
}
259+
260+
[Fact]
261+
public void Code_Indented()
262+
{
263+
ValidateSummary(
264+
// Input XML
265+
"""
266+
<summary>
267+
<example>
268+
Paragraph
269+
<code>
270+
Code
271+
</code>
272+
</example>
273+
</summary>
274+
""",
275+
// Expected Markdown
276+
"""
277+
<example>
278+
Paragraph
279+
280+
<pre><code class="lang-csharp">Code</code></pre>
281+
282+
</example>
283+
""");
284+
}
178285
}

0 commit comments

Comments
 (0)