ssh git@grit.deadkey.org man

Commit e8441ae h

Pat Ambrosio · 26-09-05 17:01

Let a thematic break end a quoted paragraph

Changes: 2 files +72 -3

Changes (Side-by-Side)

markdown.awk +26 -3

nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +| )/ { nnl = nl; }
938 } 938 }
939 # Horizontal rules 939 # Horizontal rules
940 { hr = 0; } 940 { hr = 0; }
941 (blank || (!text && !code)) && /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ {
941 # A thematic break also ends a quoted paragraph that has no blank line after
942 # it. The rule used to require !text, so after "> quote" the break was not
943 # recognised at all and the dashes were swallowed into the paragraph as an
944 # em-dash. A paragraph inside a quote is flushed first, then the break closes
945 # the quote -- which is what CommonMark asks for:
946 #
947 # > quote <blockquote><p>quote</p></blockquote>
948 # --- <hr>
949 (blank || (!text && !code) || (text && block[nl] == "blockquote")) && \
950 /^ ? ? ?([-*_][ ]*)([-*_][ ]*)([-*_][ ]*)+$/ {
942 if(code){ 951 if(code){
943 oprint(code_held "</code></pre>"); code_held = ""; 952 oprint(code_held "</code></pre>"); code_held = "";
944 code = 0; 953 code = 0;
945 } 954 }
955 if(text)
956 printp(par ? par : "p");
946 blank = 0; 957 blank = 0;
947 nnl = 0; 958 nnl = 0;
948 hr = 1; 959 hr = 1;
code {
1053 } 1064 }
1054 1065
1055 # Setex-style Headers 1066 # Setex-style Headers
1056 text && /^=+$/ {printp("h1"); next;}
1057 text && /^-+$/ {printp("h2"); next;}
1067 #
1068 # An underline belongs to the paragraph directly above it, and a paragraph
1069 # inside a blockquote is not directly above a line outside one. CommonMark is
1070 # explicit about the case:
1071 #
1072 # > foo
1073 # ---
1074 #
1075 # is a quoted paragraph followed by a thematic break, not a quoted heading.
1076 # The underline was reaching into the quote and turning "foo" into an <h2>
1077 # still inside it -- so the rule stops at the edge of a quote, and the line
1078 # falls through to the thematic-break handling that closes it.
1079 text && /^=+$/ && block[nl] != "blockquote" {printp("h1"); next;}
1080 text && /^-+$/ && block[nl] != "blockquote" {printp("h2"); next;}
1058 1081
1059 # Atx-Style headers 1082 # Atx-Style headers
1060 /^#+/ && (!newli || par=="p" || /^##/) { 1083 /^#+/ && (!newli || par=="p" || /^##/) {

test/regressions.sh +46 -0

a > b
246 assert_contains "$notmer" '<pre><code' 246 assert_contains "$notmer" '<pre><code'
247 assert_not_contains "$notmer" '<div class="diagram' 247 assert_not_contains "$notmer" '<div class="diagram'
248 248
249 suite "a break after a quote closes it"
250
251 # CommonMark is explicit that
252 #
253 # > quote
254 # ---
255 #
256 # is a quoted paragraph followed by a thematic break. Two rules were wrong
257 # here at once: the setext underline reached into the quote and made the text
258 # an <h2> still inside it, and once that was stopped the break rule would not
259 # fire either -- it required no pending text -- so the dashes were swallowed
260 # into the paragraph as an em-dash.
261
262 it "a break after a quoted line closes the quote"
263 out=$(md_render '> quote
264 ---')
265 assert_contains "$out" '<p id="p-48bc76c8">quote</p>'
266 assert_contains "$out" '</blockquote>'
267
268 it "the break lands outside the quote"
269 # The order is what matters: the hr must follow the closing tag, not sit
270 # inside it.
271 out=$(md_render '> quote
272 ---' | tr '\n' ' ')
273 assert_contains "$out" '</blockquote> <hr>'
274
275 it "the dashes are not absorbed into the paragraph"
276 assert_not_contains "$(md_render '> quote
277 ---')" '&#8212;'
278
279 it "asterisks and underscores behave the same"
280 assert_contains "$(md_render '> q
281 ***' | tr '\n' ' ')" '</blockquote> <hr>'
282 assert_contains "$(md_render '> q
283 ___' | tr '\n' ' ')" '</blockquote> <hr>'
284
285 it "a setext underline outside a quote still makes a heading"
286 assert_contains "$(md_render 'Title
287 ---')" '<h2 id="title">Title</h2>'
288 assert_contains "$(md_render 'Title
289 ===')" '<h1 id="title">Title</h1>'
290
291 it "a quote at the end of the document is closed"
292 assert_contains "$(md_render '> quote')" '<p id="p-48bc76c8">quote</p>'
293
249 summary 294 summary
295

Raw

commit e8441aefdd1ad8431a3e7ceda1d1f1e2f54225d6
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date:   Sat Sep 5 17:01:23 2026 +0800

    Let a thematic break end a quoted paragraph
    
    CommonMark has three examples of it and they all say the same thing:
    
        > quote        <blockquote><p>quote</p></blockquote>
        ---            <hr />
    
    Two rules were wrong at once, which is why fixing either one alone looks
    like it works. The setext underline reached into the quote and turned the
    text into an <h2> still inside it. Stop that and the break rule still
    would not fire -- it required no pending text -- so the dashes were
    absorbed into the paragraph as an em-dash instead. Both are needed: the
    underline stops at the edge of a quote, and a break flushes a quoted
    paragraph before closing it.
    
    The EOF case reported alongside this one does not reproduce. A quote as
    the last block, with or without a trailing newline, already emits its
    <p>; so does a lazy continuation line, a nested quote, and a quote inside
    a list item. Left alone rather than changed on spec.

diff --git a/markdown.awk b/markdown.awk
index f001ec1..1647c81 100755
--- a/markdown.awk
+++ b/markdown.awk
@@ -938,11 +938,22 @@ nnl < nl && !blank && text && ! /^ ? ? ?([*+-]|([0-9]+\.)+)( +|	)/ { nnl = nl; }
 }
 #   Horizontal rules
 { hr = 0; }
-(blank || (!text && !code)) && /^ ? ? ?([-*_][ 	]*)([-*_][ 	]*)([-*_][ 	]*)+$/ {
+# A thematic break also ends a quoted paragraph that has no blank line after
+# it. The rule used to require !text, so after "> quote" the break was not
+# recognised at all and the dashes were swallowed into the paragraph as an
+# em-dash. A paragraph inside a quote is flushed first, then the break closes
+# the quote -- which is what CommonMark asks for:
+#
+#     > quote        <blockquote><p>quote</p></blockquote>
+#     ---            <hr>
+(blank || (!text && !code) || (text && block[nl] == "blockquote")) && \
+/^ ? ? ?([-*_][ 	]*)([-*_][ 	]*)([-*_][ 	]*)+$/ {
 	if(code){
 		oprint(code_held "</code></pre>"); code_held = "";
 		code = 0;
 	}
+	if(text)
+		printp(par ? par : "p");
 	blank = 0;
 	nnl = 0;
 	hr = 1;
@@ -1053,8 +1064,20 @@ code {
 }
 
 # Setex-style Headers
-text && /^=+$/ {printp("h1"); next;}
-text && /^-+$/ {printp("h2"); next;}
+#
+# An underline belongs to the paragraph directly above it, and a paragraph
+# inside a blockquote is not directly above a line outside one. CommonMark is
+# explicit about the case:
+#
+#     > foo
+#     ---
+#
+# is a quoted paragraph followed by a thematic break, not a quoted heading.
+# The underline was reaching into the quote and turning "foo" into an <h2>
+# still inside it -- so the rule stops at the edge of a quote, and the line
+# falls through to the thematic-break handling that closes it.
+text && /^=+$/ && block[nl] != "blockquote" {printp("h1"); next;}
+text && /^-+$/ && block[nl] != "blockquote" {printp("h2"); next;}
 
 # Atx-Style headers
 /^#+/ && (!newli || par=="p" || /^##/) {
diff --git a/test/regressions.sh b/test/regressions.sh
index 04595e9..9235a68 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -246,4 +246,50 @@ a > b
 assert_contains "$notmer" '<pre><code'
 assert_not_contains "$notmer" '<div class="diagram'
 
+suite "a break after a quote closes it"
+
+# CommonMark is explicit that
+#
+#     > quote
+#     ---
+#
+# is a quoted paragraph followed by a thematic break. Two rules were wrong
+# here at once: the setext underline reached into the quote and made the text
+# an <h2> still inside it, and once that was stopped the break rule would not
+# fire either -- it required no pending text -- so the dashes were swallowed
+# into the paragraph as an em-dash.
+
+it "a break after a quoted line closes the quote"
+out=$(md_render '> quote
+---')
+assert_contains "$out" '<p id="p-48bc76c8">quote</p>'
+assert_contains "$out" '</blockquote>'
+
+it "the break lands outside the quote"
+# The order is what matters: the hr must follow the closing tag, not sit
+# inside it.
+out=$(md_render '> quote
+---' | tr '\n' ' ')
+assert_contains "$out" '</blockquote> <hr>'
+
+it "the dashes are not absorbed into the paragraph"
+assert_not_contains "$(md_render '> quote
+---')" '&#8212;'
+
+it "asterisks and underscores behave the same"
+assert_contains "$(md_render '> q
+***' | tr '\n' ' ')" '</blockquote> <hr>'
+assert_contains "$(md_render '> q
+___' | tr '\n' ' ')" '</blockquote> <hr>'
+
+it "a setext underline outside a quote still makes a heading"
+assert_contains "$(md_render 'Title
+---')" '<h2 id="title">Title</h2>'
+assert_contains "$(md_render 'Title
+===')" '<h1 id="title">Title</h1>'
+
+it "a quote at the end of the document is closed"
+assert_contains "$(md_render '> quote')" '<p id="p-48bc76c8">quote</p>'
+
 summary
+