Commit e8441ae h
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 | ---')" '—' |
||
| 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
+---')" '—'
+
+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
+