@@ -3270,7 +3270,7 @@ def test_markdown_escaping():
32703270 # ~~ inside del content must not close the strikethrough early
32713271 tree = etree .fromstring (b"<body><p><del>a~~b</del></p></body>" )
32723272 result = xml .xmltotxt (tree , include_formatting = True )
3273- assert "~~a~\\ ~b~~" in result
3273+ assert "~~a\\ ~\\ ~b~~" in result
32743274
32753275 # del as a direct child of a table cell (no enclosing p)
32763276 result = extract (
@@ -3314,6 +3314,174 @@ def test_markdown_escaping():
33143314 assert xml .xmltotxt (etree .fromstring (struck_img ), True ).strip () == "~~x  y~~"
33153315
33163316
3317+ def test_markdown_asterisk_escaping ():
3318+ "Literal '*' in source text must be escaped so CommonMark renders it literally, not as emphasis."
3319+
3320+ def md (b ):
3321+ return xml .xmltotxt (etree .fromstring (b ), include_formatting = True )
3322+
3323+ # a bare asterisk in plain paragraph text must not turn into emphasis
3324+ assert (
3325+ md (b"<body><p>This *should* not be italic and 3*4=12</p></body>" ).strip ()
3326+ == "This \\ *should\\ * not be italic and 3\\ *4=12"
3327+ )
3328+
3329+ # a real <hi> bold marker must survive untouched next to escaped literal asterisks
3330+ tree = etree .fromstring (b'<body><p><hi rend="#b">bold</hi> and *literal* asterisks</p></body>' )
3331+ assert xml .xmltotxt (tree , include_formatting = True ).strip () == "**bold** and \\ *literal\\ * asterisks"
3332+
3333+ # asterisks inside a heading are escaped, the '#' prefix is untouched
3334+ assert md (b'<body><head rend="h2">Title *with* asterisk</head></body>' ).strip () == "## Title \\ *with\\ * asterisk"
3335+
3336+ # asterisks inside a list item are escaped, the '- ' marker is untouched
3337+ assert md (b"<body><list><item>list item *text*</item></list></body>" ).strip () == "- list item \\ *text\\ *"
3338+
3339+ # asterisks inside table cell text are escaped alongside the pre-existing pipe escaping
3340+ tree = etree .fromstring (b"<body><table><row><cell>a*b|c</cell></row></table></body>" )
3341+ assert "a\\ *b\\ |c" in xml .xmltotxt (tree , include_formatting = True )
3342+
3343+ # asterisks inside link text are escaped, the [text](url) syntax is untouched
3344+ tree = etree .fromstring (b'<body><p><ref target="http://x.com">link *text*</ref></p></body>' )
3345+ assert "[link \\ *text\\ *](http://x.com)" in xml .xmltotxt (tree , include_formatting = True )
3346+
3347+ # asterisks inside image alt text are escaped
3348+ tree = etree .fromstring (b'<body><graphic src="i.png" alt="a*b"/></body>' )
3349+ assert "" in xml .xmltotxt (tree , include_formatting = True )
3350+
3351+ # asterisks inside struck-through (del) text are escaped, the ~~ markers are untouched
3352+ tree = etree .fromstring (b"<body><p><del>a*b</del></p></body>" )
3353+ assert xml .xmltotxt (tree , include_formatting = True ).strip () == "~~a\\ *b~~"
3354+
3355+ # a bold link keeps both its ** markers and its brackets: nested markup must not be
3356+ # re-escaped as if it were literal source text
3357+ tree = etree .fromstring (b'<body><p><ref target="http://x.com"><hi rend="#b">bold link</hi></ref></p></body>' )
3358+ assert xml .xmltotxt (tree , include_formatting = True ).strip () == "[**bold link**](http://x.com)"
3359+
3360+ # asterisks inside verbatim code (block and inline) must NOT be escaped
3361+ assert xml .xmltotxt (etree .fromstring (b"<body><code>a * b</code></body>" ), True ).strip () == "`a * b`"
3362+ tree = etree .fromstring (b'<body><p><hi rend="#t">a*b</hi></p></body>' )
3363+ assert xml .xmltotxt (tree , include_formatting = True ).strip () == "`a*b`"
3364+
3365+ # a run of consecutive asterisks (e.g. a plain-text divider) must not be read as emphasis
3366+ assert md (b"<body><p>*** section break ***</p></body>" ).strip () == "\\ *\\ *\\ * section break \\ *\\ *\\ *"
3367+
3368+ # an even-length run must also be escaped character-by-character, not skipped as a pair
3369+ assert md (b"<body><p>**not bold**</p></body>" ).strip () == "\\ *\\ *not bold\\ *\\ *"
3370+
3371+ # a literal '*' touching a real <hi> open/close boundary (immediately inside or outside
3372+ # the tag, singly or doubled) must be escaped without disturbing the real emphasis
3373+ # marker. #i is the higher-risk case since its marker is a single '*', the same
3374+ # character being escaped, while #b's '**' is a different-looking two-char sequence.
3375+ for rend , marker in (("#i" , "*" ), ("#b" , "**" )):
3376+ # just outside the opening tag
3377+ assert md (f'<body><p>*<hi rend="{ rend } ">x</hi></p></body>' .encode ()).strip () == f"\\ *{ marker } x{ marker } "
3378+ # just inside the opening tag (first character of the hi's own text)
3379+ assert md (f'<body><p><hi rend="{ rend } ">*x</hi></p></body>' .encode ()).strip () == f"{ marker } \\ *x{ marker } "
3380+ # just outside the closing tag
3381+ assert md (f'<body><p><hi rend="{ rend } ">x</hi>*</p></body>' .encode ()).strip () == f"{ marker } x{ marker } \\ *"
3382+ # just inside the closing tag (last character of the hi's own text)
3383+ assert md (f'<body><p><hi rend="{ rend } ">x*</hi></p></body>' .encode ()).strip () == f"{ marker } x\\ *{ marker } "
3384+ # wrapping the text from inside the tag on both sides
3385+ assert md (f'<body><p><hi rend="{ rend } ">*x*</hi></p></body>' .encode ()).strip () == f"{ marker } \\ *x\\ *{ marker } "
3386+ # wrapping the whole <hi> element from outside on both sides
3387+ assert md (f'<body><p>*<hi rend="{ rend } ">x</hi>*</p></body>' .encode ()).strip () == f"\\ *{ marker } x{ marker } \\ *"
3388+ # a literal '**' run just inside the tag on both sides
3389+ assert md (f'<body><p><hi rend="{ rend } ">**x**</hi></p></body>' .encode ()).strip () == f"{ marker } \\ *\\ *x\\ *\\ *{ marker } "
3390+ # a literal '**' run just outside the tag on both sides
3391+ assert md (f'<body><p>**<hi rend="{ rend } ">x</hi>**</p></body>' .encode ()).strip () == f"\\ *\\ *{ marker } x{ marker } \\ *\\ *"
3392+
3393+ # a pre-existing literal backslash immediately before an asterisk must round-trip:
3394+ # rendered output should still show exactly one backslash then one asterisk
3395+ tree = etree .fromstring (b"<body><p>Edge case: a\\ *b</p></body>" )
3396+ assert xml .xmltotxt (tree , include_formatting = True ).strip () == "Edge case: a\\ \\ \\ *b"
3397+
3398+ # include_formatting=False is plain-text mode: asterisks must be left completely alone
3399+ assert xml .xmltotxt (etree .fromstring (b"<body><p>This *stays* as-is</p></body>" ), False ).strip () == "This *stays* as-is"
3400+
3401+
3402+ def test_markdown_special_char_escaping ():
3403+ """Literal CommonMark metacharacters other than '*' must also be escaped so they render as
3404+ themselves: '_', '`', '[', ']', '<', '~' are ambiguous wherever they appear in prose, while
3405+ '#', '-', '+', '.'/')' after digits and '>' only matter at the very start of a fresh block
3406+ line (heading/list/blockquote syntax). Everything else (quotes, parens, punctuation) is inert
3407+ in body text and must be left alone.
3408+ """
3409+
3410+ def md (b , include_formatting = True ):
3411+ return xml .xmltotxt (etree .fromstring (b ), include_formatting = include_formatting ).strip ()
3412+
3413+ # underscore, backtick, brackets and '<' are escaped anywhere in a paragraph
3414+ assert (
3415+ md (b"<body><p>a_b_c and `code` and [bracket] and <tag> text</p></body>" )
3416+ == "a\\ _b\\ _c and \\ `code\\ ` and \\ [bracket\\ ] and \\ <tag> text"
3417+ )
3418+ # a lone '~' is also escaped: GitHub renders a single tilde as strikethrough, not just '~~'
3419+ assert md (b"<body><p>~single~ tilde</p></body>" ) == "\\ ~single\\ ~ tilde"
3420+
3421+ # a leading '#' in a plain paragraph must not become an ATX heading
3422+ assert md (b"<body><p># not a heading</p></body>" ) == "\\ # not a heading"
3423+ # a leading '>' in a plain paragraph must not become a blockquote
3424+ assert md (b"<body><p>> not a quote</p></body>" ) == "\\ > not a quote"
3425+ # a leading '-'/'+' in a plain paragraph must not become a bullet list
3426+ assert md (b"<body><p>- not a list</p></body>" ) == "\\ - not a list"
3427+ assert md (b"<body><p>+ not a list</p></body>" ) == "\\ + not a list"
3428+ # a leading digit + '.'/')' in a plain paragraph must not become an ordered list
3429+ assert md (b"<body><p>1. not a list</p></body>" ) == "1\\ . not a list"
3430+ assert md (b"<body><p>1) not a list</p></body>" ) == "1\\ ) not a list"
3431+
3432+ # the same markers are safe mid-line and must be left alone (only line-start is risky)
3433+ assert (
3434+ md (b"<body><p>mid-line - and 3.14 and 100% and word) stay put</p></body>" )
3435+ == "mid-line - and 3.14 and 100% and word) stay put"
3436+ )
3437+
3438+ # a list item's own content starting with a marker-like character must not nest as a
3439+ # sub-list; the item's real '- '/'N. ' marker (added separately) is untouched
3440+ assert md (b"<body><list><item>- nested looking</item></list></body>" ) == "- \\ - nested looking"
3441+ assert (
3442+ md (b'<body><list rend="ol"><item>2. nested ordered looking</item></list></body>' ) == "1. 2\\ . nested ordered looking"
3443+ )
3444+
3445+ # a blockquote element's own leading '>' must also be escaped
3446+ assert md (b"<body><quote>> quoted text with - marker</quote></body>" ) == "\\ > quoted text with - marker"
3447+
3448+ # a heading's own '#' prefix already claims the line, so an inner leading '#' is inert
3449+ assert md (b'<body><head rend="h2"># nested hash in heading</head></body>' ) == "## # nested hash in heading"
3450+
3451+ # table cells are inline-only in GFM: a leading marker-like character there is never risky
3452+ assert "| - looks like a list |" in md (b"<body><table><row><cell>- looks like a list</cell></row></table></body>" )
3453+
3454+ # inert punctuation (quotes, parens, and other characters with no CommonMark meaning in
3455+ # body text) must be left completely alone
3456+ inert = b"<body><p>quote " apostrophe ' paren ( ) percent % dollar $ at @ caret ^ eq = brace { } colon : semi ; comma , bang !</p></body>"
3457+ assert (
3458+ md (inert )
3459+ == "quote \" apostrophe ' paren ( ) percent % dollar $ at @ caret ^ eq = brace { } colon : semi ; comma , bang !"
3460+ )
3461+ # '!' alone (not immediately escaping a '[') does not need its own escaping
3462+ assert md (b"<body><p>! not an image on its own</p></body>" ) == "! not an image on its own"
3463+
3464+ # verbatim code (block and inline) must not have any of these characters escaped
3465+ assert md (b"<body><code>a_b`c[d]e<f></code></body>" ) == "``a_b`c[d]e<f>``"
3466+ assert md (b'<body><p><hi rend="#t">a_b`c</hi></p></body>' ) == "``a_b`c``"
3467+
3468+ # brackets in link/image text are still escaped so a false image ('![...]') can't form,
3469+ # without needing to escape '!' itself: an escaped '[' already blocks the image syntax
3470+ assert (
3471+ md (b'<body><p><ref target="http://x.com">![img]-like text</ref></p></body>' ) == "[!\\ [img\\ ]-like text](http://x.com)"
3472+ )
3473+
3474+ # a pre-existing literal backslash immediately before an underscore must round-trip:
3475+ # rendered output should still show exactly one backslash then one underscore
3476+ assert md (b"<body><p>Edge case: a\\ _b</p></body>" ) == "Edge case: a\\ \\ \\ _b"
3477+
3478+ # include_formatting=False is plain-text mode: none of these characters are touched
3479+ assert (
3480+ md (b"<body><p>This _stays_ as-is with `code` [b] <tag></p></body>" , include_formatting = False )
3481+ == "This _stays_ as-is with `code` [b] <tag>"
3482+ )
3483+
3484+
33173485def test_markdown_link_angle_bracket_targets ():
33183486 "A '<' or '>' in a link/image target must stay inside the angle-bracket destination."
33193487 # each URL forces the angle-bracket form (space/paren/</>) and must round-trip:
0 commit comments