From 3e7a8f5aeaf927196b749881cd67b22e7ef1d45b Mon Sep 17 00:00:00 2001 From: Dan Vu Date: Sun, 13 Sep 2026 08:25:46 +0200 Subject: [PATCH] Fix comment undecoration when ddoc code fences are present --- src/dparse/trivia.d | 117 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 21 deletions(-) diff --git a/src/dparse/trivia.d b/src/dparse/trivia.d index a1649f09..43a5d69b 100644 --- a/src/dparse/trivia.d +++ b/src/dparse/trivia.d @@ -142,7 +142,12 @@ unittest "/**b1\n*b2\n*b3*/", "/**c1\n *c2\n *c3*/", "/**d1\n *d2\n *d3\n*/", - "///a\fbc\n///def" + "///a\fbc\n///def", + // code fence directly before the closing delimiter (Phobos writeln + // style): the fence lines and their content are not decorated + "/**\n * a\n * Example:\n * c.\n---\ncode\n---\n */", + "/**\n * a\n---\ncode\n---\n * b\n */", + "/++\n + a\n + Example:\n + c.\n---\ncode\n---\n +/" ]; string[] outputs = [ "", @@ -167,7 +172,10 @@ unittest "b1\nb2\nb3", "c1\nc2\nc3", "d1\nd2\nd3", - "a\fbc\ndef" + "a\fbc\ndef", + "a\nExample:\n c.\n---\ncode\n---", + "a\n---\ncode\n---\nb", + "a\nExample:\n c.\n---\ncode\n---" ]; // tests where * and + are not interchangeable @@ -331,33 +339,100 @@ private: const size_t hi = (lines[$-1].length > 1 && (lines[$-1][0] == commentChar || lines[$-1][0..2] == pattern[patternIndex])) ? lines.length : lines.length-1; - // deco with a leading white - foreach (const i; lo .. hi) + // DDoc code fences (lines of `---`) and their content are not + // decorated even in an otherwise fully decorated comment (e.g. + // Phobos writeln). Lines inside a fence must not participate in + // the allDecorated detection, and must not have decoration + // stripped (there is none to strip). + // Returns true when every line in [lo, hi) is decorated (with the + // given leading pattern), a fence line, or content inside a fence. + // `pattern2` is the two-char leading pattern (" *" / "++"), while + // `pattern1` is the single-char decoration ("*" / "+"). + static bool allLinesDecorated(in char[][] lines, size_t lo, size_t hi, + const char[2] pattern2, char pattern1) @safe @nogc pure nothrow { - if (lines[i].length < 2) - break; - else if (lines[i][0..2] != pattern[patternIndex]) - break; - else if (i == hi-1) - allDecorated = true; - } - // deco w/o leading white - if (!allDecorated) + bool inFence; foreach (const i; lo .. hi) - { - if (lines[i].length == 0) - break; - if (lines[i][0] != commentChar) - break; - else if (i == hi-1) - allDecorated = true; + { + if (lines[i].length == 0) + { + // empty lines are valid inside a fence; outside one + // they end the decoration scan (like the original + // break-on-empty behavior) + if (inFence) + continue; + return false; + } + else if (lines[i][0] == pattern1 + || lines[i].length >= 2 && lines[i][0..2] == pattern2) + continue; + else if (isFenceLine(lines[i])) + inFence = !inFence; + else if (!inFence) + return false; + // decorated content inside a fence: keep scanning + } + return !inFence; } + + // deco with a leading white + allDecorated = allLinesDecorated(lines, lo, hi, + pattern[patternIndex], commentChar); if (!allDecorated) return; const size_t indexToChange = (lines[lo][0] == commentChar) ? 0 : 1; + // Does the comment contain a code fence? Fence lines and their + // content are undecorated, which changes how the decoration must + // be removed (see below). + bool hasFence; foreach (ref line; lines[lo .. hi]) - line[indexToChange] = ' '; + if (isFenceLine(line)) + hasFence = true; + bool inFence; + foreach (ref line; lines[lo .. hi]) + { + if (isFenceLine(line)) + inFence = !inFence; + else if (!inFence) + { + if (!hasFence) + { + line[indexToChange] = ' '; + continue; + } + // The comment mixes decorated lines with undecorated + // fence content, so stripLeft cannot remove the decoration + // column (the fence lines have no leading whitespace). + // Remove the leading whitespace, the decoration marker and + // ONE separator whitespace by shifting the content left in + // place; relative indentation beyond that is preserved. + size_t start; + while (start < line.length && line[start].isWhite) + start++; + if (start < line.length && line[start] == commentChar) + start++; + if (start < line.length && line[start].isWhite) + start++; + foreach (const j; start .. line.length) + line[j - start] = line[j]; + line = line[0 .. $ - start]; + } + } + } + + /// A DDoc code fence line: `---` possibly indented, with optional + /// trailing whitespace. + static bool isFenceLine(const char[] line) @safe @nogc pure nothrow + { + size_t i; + while (i < line.length && line[i].isWhite) + i++; + size_t dashes; + while (i + dashes < line.length && line[i + dashes] == '-') + dashes++; + return dashes >= 3 + && i + dashes == line.length; } void stripLeft() @safe @nogc pure nothrow