Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/1291.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Preserve emphasis around nonbreaking and other Unicode spaces in Markdown output.
29 changes: 17 additions & 12 deletions src/turbohtml/_c/serialize/markdown.c
Original file line number Diff line number Diff line change
Expand Up @@ -490,21 +490,26 @@ static void md_settle_pending(md_ctx *ctx) {
/* What a character is to the CommonMark flanking rules: whitespace, ASCII punctuation,
or (0) anything else. */
enum { MD_EDGE_SPACE = 1, MD_EDGE_PUNCT = 2 };
static const uint8_t MD_EDGE[128] = {
['\t'] = MD_EDGE_SPACE, ['\n'] = MD_EDGE_SPACE, ['\f'] = MD_EDGE_SPACE, ['\r'] = MD_EDGE_SPACE,
[' '] = MD_EDGE_SPACE, ['!'] = MD_EDGE_PUNCT, ['"'] = MD_EDGE_PUNCT, ['#'] = MD_EDGE_PUNCT,
['$'] = MD_EDGE_PUNCT, ['%'] = MD_EDGE_PUNCT, ['&'] = MD_EDGE_PUNCT, ['\''] = MD_EDGE_PUNCT,
['('] = MD_EDGE_PUNCT, [')'] = MD_EDGE_PUNCT, ['*'] = MD_EDGE_PUNCT, ['+'] = MD_EDGE_PUNCT,
[','] = MD_EDGE_PUNCT, ['-'] = MD_EDGE_PUNCT, ['.'] = MD_EDGE_PUNCT, ['/'] = MD_EDGE_PUNCT,
[':'] = MD_EDGE_PUNCT, [';'] = MD_EDGE_PUNCT, ['<'] = MD_EDGE_PUNCT, ['='] = MD_EDGE_PUNCT,
['>'] = MD_EDGE_PUNCT, ['?'] = MD_EDGE_PUNCT, ['@'] = MD_EDGE_PUNCT, ['['] = MD_EDGE_PUNCT,
['\\'] = MD_EDGE_PUNCT, [']'] = MD_EDGE_PUNCT, ['^'] = MD_EDGE_PUNCT, ['_'] = MD_EDGE_PUNCT,
['`'] = MD_EDGE_PUNCT, ['{'] = MD_EDGE_PUNCT, ['|'] = MD_EDGE_PUNCT, ['}'] = MD_EDGE_PUNCT,
['~'] = MD_EDGE_PUNCT,
/* U+3000 is the highest Zs space; direct indexing keeps the ASCII lookup unchanged. */
static const uint8_t MD_EDGE[0x3001] = {
['\t'] = MD_EDGE_SPACE, ['\n'] = MD_EDGE_SPACE, ['\f'] = MD_EDGE_SPACE, ['\r'] = MD_EDGE_SPACE,
[' '] = MD_EDGE_SPACE, ['!'] = MD_EDGE_PUNCT, ['"'] = MD_EDGE_PUNCT, ['#'] = MD_EDGE_PUNCT,
['$'] = MD_EDGE_PUNCT, ['%'] = MD_EDGE_PUNCT, ['&'] = MD_EDGE_PUNCT, ['\''] = MD_EDGE_PUNCT,
['('] = MD_EDGE_PUNCT, [')'] = MD_EDGE_PUNCT, ['*'] = MD_EDGE_PUNCT, ['+'] = MD_EDGE_PUNCT,
[','] = MD_EDGE_PUNCT, ['-'] = MD_EDGE_PUNCT, ['.'] = MD_EDGE_PUNCT, ['/'] = MD_EDGE_PUNCT,
[':'] = MD_EDGE_PUNCT, [';'] = MD_EDGE_PUNCT, ['<'] = MD_EDGE_PUNCT, ['='] = MD_EDGE_PUNCT,
['>'] = MD_EDGE_PUNCT, ['?'] = MD_EDGE_PUNCT, ['@'] = MD_EDGE_PUNCT, ['['] = MD_EDGE_PUNCT,
['\\'] = MD_EDGE_PUNCT, [']'] = MD_EDGE_PUNCT, ['^'] = MD_EDGE_PUNCT, ['_'] = MD_EDGE_PUNCT,
['`'] = MD_EDGE_PUNCT, ['{'] = MD_EDGE_PUNCT, ['|'] = MD_EDGE_PUNCT, ['}'] = MD_EDGE_PUNCT,
['~'] = MD_EDGE_PUNCT, [0xA0] = MD_EDGE_SPACE, [0x1680] = MD_EDGE_SPACE, [0x2000] = MD_EDGE_SPACE,
[0x2001] = MD_EDGE_SPACE, [0x2002] = MD_EDGE_SPACE, [0x2003] = MD_EDGE_SPACE, [0x2004] = MD_EDGE_SPACE,
[0x2005] = MD_EDGE_SPACE, [0x2006] = MD_EDGE_SPACE, [0x2007] = MD_EDGE_SPACE, [0x2008] = MD_EDGE_SPACE,
[0x2009] = MD_EDGE_SPACE, [0x200A] = MD_EDGE_SPACE, [0x202F] = MD_EDGE_SPACE, [0x205F] = MD_EDGE_SPACE,
[0x3000] = MD_EDGE_SPACE,
};

static inline uint8_t md_edge(Py_UCS4 ch) {
return ch < 128 ? MD_EDGE[ch] : 0;
return ch < sizeof(MD_EDGE) ? MD_EDGE[ch] : 0;
}

static int md_is_ascii_punct(Py_UCS4 ch) {
Expand Down
18 changes: 18 additions & 0 deletions tests/serialize/test_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,24 @@ def test_inline_emphasis(html: str, expected: str) -> None:
assert md(html) == expected


@pytest.mark.parametrize(
("html", "expected"),
[
pytest.param("<p><i>&nbsp;</i></p>", "<em>\u00a0</em>", id="emphasis-only-nbsp"),
pytest.param("<p><b>&nbsp;x</b></p>", "<strong>\u00a0x</strong>", id="strong-leading-nbsp"),
pytest.param("<p><b>x&nbsp;</b></p>", "<strong>x\u00a0</strong>", id="strong-trailing-nbsp"),
pytest.param("<p>a<b>&nbsp;</b>z</p>", "a<strong>\u00a0</strong>z", id="strong-between-text"),
],
)
def test_nonbreaking_space_emphasis(html: str, expected: str) -> None:
assert md(html) == expected


@pytest.mark.parametrize("space", ["\u00a0", "\u1680", "\u2000", "\u200a", "\u202f", "\u205f", "\u3000"])
def test_unicode_space_emphasis(space: str) -> None:
assert md(f"<p><b>{space}</b></p>") == f"<strong>{space}</strong>"


@pytest.mark.parametrize("tag", ["code", "kbd", "samp"])
@pytest.mark.parametrize(
("content", "expected"),
Expand Down
Loading