import { describe, expect, test } from "bun:test" import { probeConversion } from "./conversion-probe.mjs" import { snapshotFromHtml } from "./source-snapshot.mjs" // One table per class of conversion artifact. Each row is one form: the raw source, the converted // text (BUILTIN = the built-in snapshot of the raw HTML), and the exact `altered` and `lost` lists. // `attributes: false` is the built-in snapshot path, which writes no attribute text and no syntax. type Row = { form: string; raw: string; converted: string; altered: string[]; lost: string[]; attributes?: boolean } const BUILTIN = "\u0000builtin" function check(row: Row) { const builtin = row.converted === BUILTIN const converted = builtin ? snapshotFromHtml(row.raw) : row.converted const result = probeConversion(row.raw, converted, { attributes: builtin ? false : (row.attributes ?? true) }) expect({ altered: result.altered, lost: result.lost }).toEqual({ altered: row.altered, lost: row.lost }) } const rows = (table: Row[]) => table.map((row) => [row.form, row] as const) const OL = "
Margin 12%1
Filing.back
", converted: "> 1. A", altered: [], lost: [] }, { form: "footnote list (references ol)", raw: '
- A
Revenue rose 4%.[1]
\uB9E4\uCD9C 3,234\uC5B5 \uC6D01
1. Alpha
", converted: "1\\. Alpha", altered: [], lost: [] }, { form: "numbered paragraph, unescaped", raw: "1. Alpha
", converted: "1. Alpha", altered: [], lost: [] }, { form: "bare '1. ' with no ol in the source", raw: "Alpha
", converted: "1. Alpha", altered: ["1"], lost: [] }, { form: "ul source written as '1. '", raw: "\uB9E4\uCD9C \uC99D\uAC00
", converted: "1. \uB9E4\uCD9C \uC99D\uAC00", altered: ["1"], lost: [] }, { form: "more markers than ol items", raw: "
report
", converted: "[report][1]", altered: ["1"], lost: [] }, { form: "reference definition with a title", raw: '', converted: '[report][1]\n\n[1]: https://example.com/r/7 "Fig 4"', altered: [], lost: [] }, { form: "footnote definition text is prose, not link syntax", raw: '\uB9E4\uCD9C1 \uCD9C\uCC98
Filing.
", converted: "[^1]: Filing.", altered: ["1"], lost: [] }, { form: "attribute digit written into prose", raw: '', converted: "page 42", altered: ["42"], lost: [] }, { form: "built-in path: attribute digit in link syntax", raw: '', converted: "[page](/p/42)", altered: ["42"], lost: [], attributes: false }, { form: "href page number beside a glued footnote", raw: 'Output reached 109 kWh
', converted: "Output reached 109 kWh\n\n[book](https://b.example/b?pg=PA109)", altered: ["109"], lost: ["10", "9"] }, ] const SIGNS: Row[] = [ { form: "U+2212 written as a hyphen", raw: "fell \u22123.5 points", converted: "fell -3.5 points", altered: [], lost: [] }, { form: "U+2212 dropped", raw: "fell \u22123.5 points", converted: "fell 3.5 points", altered: ["3.5"], lost: ["\u22123.5"] }, { form: "hyphen-minus dropped", raw: "fell -3.5", converted: "fell 3.5", altered: ["3.5"], lost: ["-3.5"] }, { form: "en dash minus after a space dropped", raw: "fell \u20135%", converted: "fell 5%", altered: ["5"], lost: ["\u20135"] }, { form: "en dash minus written as a hyphen", raw: "fell \u20135%", converted: "fell -5%", altered: [], lost: [] }, { form: "hyphen after a space written as U+2212", raw: "change -4.2", converted: "change \u22124.2", altered: [], lost: [] }, { form: "U+2212 inside parentheses dropped", raw: "(\u22125)", converted: "(5)", altered: ["5"], lost: ["\u22125"] }, { form: "U+2212 after one letter dropped", raw: "\u0394\u22125 points", converted: "\u03945 points", altered: ["5"], lost: ["\u22125"] }, { form: "hyphen after one letter dropped", raw: "x-5", converted: "x5", altered: ["5"], lost: ["-5"] }, { form: "U+2212 after one letter written as a hyphen", raw: "\u0394\u22125", converted: "\u0394-5", altered: [], lost: [] }, { form: "en dash after one letter is a connector", raw: "A\u20135", converted: "A5", altered: [], lost: [] }, { form: "range after a digit with U+2212", raw: "2019\u22122023", converted: "2019-2023", altered: [], lost: [] }, { form: "range after a digit with an en dash", raw: "2019\u20132023", converted: "2019-2023", altered: [], lost: [] }, { form: "ISO date", raw: "2026-10-26", converted: "2026-10-26", altered: [], lost: [] }, { form: "hyphen after Hangul dropped", raw: "\uAC10\uC18C\uC728-3.5%", converted: "\uAC10\uC18C\uC728 3.5%", altered: ["3.5"], lost: ["-3.5"] }, { form: "U+2212 after Hangul dropped", raw: "\uC804\uB144 \uB300\uBE44\u22122.1%", converted: "\uC804\uB144 \uB300\uBE44 2.1%", altered: ["2.1"], lost: ["\u22122.1"] }, { form: "U+2212 after Hangul written as a hyphen", raw: "\uC99D\uAC10\uB960\u22122.1%", converted: "\uC99D\uAC10\uB960-2.1%", altered: [], lost: [] }, { form: "identifier cite_note-7", raw: "cite_note-7", converted: "cite_note7", altered: [], lost: [] }, { form: "identifier ISO-8601", raw: "ISO-8601", converted: "ISO 8601", altered: [], lost: [] }, { form: "identifier UTF-8", raw: "UTF-8 text", converted: "UTF8 text", altered: [], lost: [] }, { form: "identifier COVID-19", raw: "COVID-19 cases", converted: "COVID 19 cases", altered: [], lost: [] }, { form: "two-letter code AB-12", raw: "AB-12", converted: "AB12", altered: [], lost: [] }, { form: "digit-letter code 3x-5", raw: "3x-5", converted: "3x5", altered: [], lost: [] }, { form: "exponent minus dropped", raw: "1e-5", converted: "1e5", altered: ["5"], lost: ["-5"] }, { form: "exponent U+2212 written as a hyphen", raw: "2.5E\u22123", converted: "2.5E-3", altered: [], lost: [] }, { form: "superscript exponent flattened onto its base", raw: "10\u22125
", converted: "10-5", altered: ["5"], lost: ["\u22125"] }, { form: "superscript exponent written with a caret", raw: "10\u22125
", converted: "10^-5", altered: [], lost: [] }, { form: "negative cells kept", raw: "| Q4 | -5 | \u22123.2 |
| Q4 | -5 | \u22123.2 |
| \uC99D\uAC10 | \u22122.1% |
|---|
| Q4 | \u22123.2 |
| 3 | 250 | 100 |
| 12 | 345 |
|---|
| 12 |
| 345 |
3
250
", converted: "3\n\n250", altered: [], lost: [] }, { form: "div / div", raw: "345
", converted: "## 12\n\n345", altered: [], lost: [] }, { form: "dt / dd", raw: "12
345
", converted: "> 12\n\n345", altered: [], lost: [] }, { form: "br -> line break", raw: "12
345
| 2,500 300 |
12
345
in 2023
500 sites
1,250\n300
", converted: "1,250 300", altered: [], lost: [] }, { form: "comma group | next cell", raw: "| 1,250 | 300 |
| 1.234 | 567 |
| \uB9E4\uCD9C | 3,234 | \uC5B5\uC6D0 |
12
345\uC5B5 \uC6D0
", converted: "12\n\n345\uC5B5 \uC6D0", altered: [], lost: [] }, { form: "spaces inside one cell group", raw: "| 1 234 567 | 8 |
1 234 567
", converted: "1 234 567", altered: [], lost: [] }, { form: "figure space entity groups", raw: "1 234
", converted: "1 234", altered: [], lost: [] }, { form: "span-split groups", raw: "1 234 567
", converted: "1 234 567", altered: [], lost: [] }, { form: "source newline between spans is a space", raw: "1\n234
", converted: "1 234", altered: [], lost: [] }, { form: "KO space group with a unit", raw: "1 234 567\uC6D0
", converted: "1 234 567\uC6D0", altered: [], lost: [] }, { form: "KO comma group in a cell", raw: "| 3,234\uC5B5 \uC6D0 |
Population 1 234 567 people
", converted: "Population 1 234\n567 people", altered: [], lost: [] }, { form: "KO hard wrap inside a space group", raw: "\uC778\uAD6C 1 234 567\uBA85
", converted: "\uC778\uAD6C 1 234\n567\uBA85", altered: [], lost: [] }, { form: "hard wrap between two separate numbers", raw: "in 2023 500 sites
", converted: "in 2023\n500 sites", altered: [], lost: [] }, { form: "a wrap is not rejoined when the raw side holds both halves too", raw: "12 345 and 12 and 345
", converted: "12\n345 and 12 and 345", altered: ["12", "345"], lost: ["12 345"] }, { form: "a blank line never rejoins a group", raw: "Population 1 234 567 people
", converted: "Population 1 234\n\n567 people", altered: ["1 234", "567"], lost: ["1 234 567"] }, { form: "footnote glued onto a space group", raw: "1 2345
", converted: "1 2345", altered: ["1", "2345"], lost: ["1 234", "5"] }, { form: "built-in path: td | td | td", raw: "| 3 | 250 | 100 |
12
345
| 12 |
| 345 |
| 1 234 567\uC6D0 | 8 |
${c}`, md: (c) => `\`${c}\`` },
{ form: "``", html: (c) => `${c}`, md: (c) => `\`\`${c}\`\`` },
{ form: "[](url)", html: (c) => `${c}`, md: (c) => `[${c}](/c)` },
{ form: "[][ref]", html: (c) => `${c}`, md: (c) => `[${c}][c]` },
{ form: "[] shortcut", html: (c) => `${c}`, md: (c) => `[${c}]` },
]
const DASHES = [["hyphen", "-"], ["en dash", "\u2013"], ["U+2212", "\u2212"]] as const
const CLOSED = [
{ form: "a digit", text: "2019", gap: "", after: "2023", signed: (_dash: string) => false },
{ form: "a Latin word", text: "COVID", gap: "", after: "19", signed: (_dash: string) => false },
{ form: "one Latin letter", text: "x", gap: "", after: "5", signed: (dash: string) => dash !== "\u2013" },
{ form: "KO Hangul", text: "\uAC10\uC18C", gap: "", after: "3", signed: (_dash: string) => true },
{ form: "a space", text: "fell", gap: " ", after: "5", signed: (_dash: string) => true },
]
// Every closer shape a CommonMark converter writes after text: inline links and images with a
// "title", 'title' or (title), a target with balanced parentheses or in <...>, collapsed and full
// reference forms, a linked image, and backslash escapes. An escaped closer is skipped like the raw
// character it stands for; an escaped opener (`opaque`) is a real character, so every dash after it is a sign.
const IMG = '
`${c}`, md: (c) => `[${c}](/c "t")` },
{ form: "[](url 'title')", html: (c) => `${c}`, md: (c) => `[${c}](/c 't')` },
{ form: "[](url (title))", html: (c) => `${c}`, md: (c) => `[${c}](/c (t))` },
{ form: '[](url "title with \\" and )")', html: (c) => `${c}`, md: (c) => `[${c}](/c "a \\" )")` },
{ form: "[](url_(x))", html: (c) => `${c}`, md: (c) => `[${c}](/F_(x))` },
{ form: '[](url_(x) "title")', html: (c) => `${c}`, md: (c) => `[${c}](/F_(x) "t")` },
{ form: "[](
`, md: (c) => `${c}.png)` },
{ form: '.png "title")', html: (c) => `${c}
`, md: (c) => `${c}.png "t")` },
{ form: " => `${c}![x]()` },
{ form: "![][ref]", html: (c) => `${c}${IMG}>`, md: (c) => `${c}![x][i]` },
{ form: "[](url)", html: (c) => `${c}${IMG}>`, md: (c) => `${c}[](/p)` },
{ form: "****", html: (c) => `${c}${IMG}>`, md: (c) => `${c}****` },
...["*", "_", "]", ")", "~", "`"].map((x): Shape => ({ form: `escaped \\${x}`, html: (c) => `${c}${x}`, md: (c) => `${c}\\${x}` })),
...["[", "("].map((x): Shape => ({ form: `escaped \\${x} (an opener)`, html: (c) => `${c}${x}`, md: (c) => `${c}\\${x}`, opaque: true })),
]
const closerRows = (shapes: Shape[]): Row[] => shapes.flatMap((m) => DASHES.flatMap(([dash, d]) => CLOSED.flatMap((c) => {
const raw = `${m.html(c.text)}${c.gap}${d}${c.after}
` const kept: Row = { form: `${m.form} closing ${c.form}, then a ${dash}`, raw, converted: `${m.md(c.text)}${c.gap}${d}${c.after}`, altered: [], lost: [] } if (!(m.opaque || c.signed(d))) return [kept] return [kept, { form: `${m.form} closing ${c.form}, ${dash} dropped`, raw, converted: `${m.md(c.text)}${c.gap}${c.after}`, altered: [c.after], lost: [`${d}${c.after}`] }] }))) const SIGN_MARKUP: Row[] = [ ...closerRows(MARKUP), ...closerRows(CLOSER_SHAPES), ...MARKUP.flatMap((m) => [ ...DASHES.flatMap(([dash, d]) => [ { form: `${m.form} around a ${dash} number after a space`, raw: `fell ${m.html(`${d}5`)}
`, converted: `fell ${m.md(`${d}5`)}`, altered: [], lost: [] }, { form: `${m.form} around a ${dash} number at line start`, raw: `${m.html(`${d}5`)} pts
`, converted: `${m.md(`${d}5`)} pts`, altered: [], lost: [] }, { form: `${m.form} opened between a ${dash} and its digits`, raw: `fell ${d}${m.html("5")}
`, converted: `fell ${d}${m.md("5")}`, altered: [], lost: [] }, ]), { form: `${m.form} around a number whose U+2212 was dropped`, raw: `fell ${m.html("\u22125")}
`, converted: `fell ${m.md("5")}`, altered: ["5"], lost: ["\u22125"] }, ]), { form: "reviewer: **2019**-2023", raw: "2019-2023
", converted: "**2019**-2023", altered: [], lost: [] }, { form: "reviewer: *UTF*-8", raw: "UTF-8
", converted: "*UTF*-8", altered: [], lost: [] }, { form: "reviewer: `v1`-2", raw: "v1-2
Q
\u22125
\uC99D\uAC10
\u22122.1%
https://x.example/a\u22125
', converted: "cite_note-7
", converted: "**cite_note**-7", altered: [], lost: [] }, { form: "two letters split by markup stay an identifier", raw: "ab\u22125
", converted: "a**b**-5", altered: [], lost: [] }, { form: "reviewer r5: titled link before an en dash range", raw: 'From 1990\u20132000', converted: 'From [1990](/wiki/1990 "1990")\u20132000', altered: [], lost: [] }, { form: "reviewer r5: titled link before a hyphen range", raw: 'From 1990-2000', converted: 'From [1990](/wiki/1990 "1990")-2000', altered: [], lost: [] }, { form: "reviewer r5: titled link on a word", raw: 'ab-5
', converted: '[ab](/x "t")-5', altered: [], lost: [] }, { form: "reviewer r5: titled image after a word", raw: 'ab
-5
fell
-5
`, md: (c) => `${c}
` },
{ form: "
after the text", html: (c) => `${c}
`, md: (c) => `${c}
` },
{ form: " after the text", html: (c) => `${c}`, md: (c) => `${c}` },
{ form: "****", html: (c) => `${c}`, md: (c) => `**${c}**` },
{ form: "[link](url)", html: (c) => `${c}`, md: (c) => `[${c}](/c)` },
]
const RAW_HTML: Row[] = [
...closerRows(HTML_SHAPES),
{ form: "reviewer r6: pandoc 10-5", raw: "10-5 m
", converted: "10-5 m", altered: [], lost: [] }, { form: "reviewer r6: pandoc 10U+2212 5", raw: "10\u22125 m
", converted: "10\u22125 m", altered: [], lost: [] }, { form: "reviewer r6: pandoc 109", raw: "Output reached 109 kWh
", converted: "Output reached 109 kWh", altered: [], lost: [] }, { form: "reviewer r6: a-5", raw: "a-5
", converted: "a-5", altered: [], lost: [] }, { form: 'reviewer r6:
', raw: '
', altered: [], lost: [] },
{ form: "reviewer r6: 5 apples
\uC0AC\uACFC 5\uAC1C
2019a-2023
", converted: "2019a-2023", altered: [], lost: [] }, { form: "KO 3,2341\uC5B5 kept as raw HTML", raw: "\uB9E4\uCD9C 3,2341\uC5B5 \uC6D0
", converted: "\uB9E4\uCD9C 3,2341\uC5B5 \uC6D0", altered: [], lost: [] }, { form: "a superscript the converter flattened is still altered", raw: "10-5 m
", converted: "10-5 m", altered: ["5"], lost: ["-5"] }, { form: "a glued footnote is still altered next to raw HTML", raw: "4.22 bn
", converted: "4.22 bn", altered: ["4.22"], lost: ["4.2", "2"] }, { form: "an img width written into prose is still altered", raw: " 5
a<b>7
", converted: "a\\7", altered: [], lost: [] }, { form: "a < with no tag end stays text", raw: "a <sup 5
", converted: "a a <span5 x>
", converted: "a ", altered: [], lost: [] }, ] // A reference definition's title may sit alone on the next line (CommonMark): its digits are link // syntax there too. Only the title is read across the line break; text after it makes it prose. const REF_TITLES: Row[] = [ ...[['"Fig 2019"', "double quotes"], ["'Fig 2019'", "single quotes"], ["(Fig 2019)", "parentheses"]].flatMap(([title, form]) => [ { form: `title in ${form} on the next line, indented`, raw: '', converted: `[fig][1]\n\n[1]: /x\n ${title}`, altered: [], lost: [] }, { form: `title in ${form} on the next line, not indented`, raw: '', converted: `[fig][1]\n\n[1]: /x\n${title}\n`, altered: [], lost: [] }, ]), { form: "KO title on the next line", raw: '', converted: '[\uADF8\uB9BC][1]\n\n[1]: /x\n "\uADF8\uB9BC 2019"', altered: [], lost: [] }, { form: "two definitions, the first with its title on the next line", raw: '', converted: '[a][1] [b][2]\n\n[1]: /x\n "Fig 2019"\n[2]: /y/7', altered: [], lost: [] }, { form: "a quoted line with text after it is prose, not a title", raw: '', converted: '[fig][1]\n\n[1]: /x\n"Fig 2019" was cited', altered: ["2019"], lost: [] }, { form: "a next-line title digit the source does not hold is altered", raw: '', converted: '[fig][1]\n\n[1]: /x\n "Fig 2019"', altered: ["2019"], lost: [] }, ] // Declared out of scope (references/fact-ledger.md): constructs outside the CommonMark inline grammar, // the html-lite tag set and the named-entity table. Each row ASSERTS the safe-direction report, so // the boundary is tested: the numbers are altered and `source snapshot --replace` recovers. const OUT_OF_SCOPE: Row[] = [ { form: "custom converter syntax: a Hugo figure shortcode", raw: '

\u00BE of sites
", converted: "$\\frac{3}{4}$ of sites", altered: ["3", "4"], lost: [] }, { form: "KO LaTeX: \\frac{3}{4}", raw: "\uC0AC\uC774\uD2B8\uC758 \u00BE
", converted: "\uC0AC\uC774\uD2B8\uC758 $\\frac{3}{4}$", altered: ["3", "4"], lost: [] }, { form: "MathJax: \\sqrt[3]{8} for a cube root", raw: "\u221B8
", converted: "\\(\\sqrt[3]{8}\\)", altered: ["3"], lost: [] }, { form: "non-CommonMark table: AsciiDoc cols", raw: "| A | B |
| 5 |
Margin 12%a
Filing.
', converted: 'Margin 12%[^a]\n\n[^a]: Filing. [\u21A9](#fnref-a "Jump back to footnote 2 in the text")', altered: ["2"], lost: [] }, { form: "reference definition with its destination on the next line", raw: '', converted: "[fig][1]\n\n[1]:\n /x/2019", altered: ["2019"], lost: [] }, { form: "an unescaped & whose text spells a reference (turndown)", raw: "Type −5 to negate
", converted: "Type −5 to negate", altered: ["\u22125"], lost: ["5"] }, { form: "a legacy reference without its semicolon ( )", raw: "1 234 567
", converted: "1\u00A0234 567", altered: ["1 234 567"], lost: ["1", "234 567"] }, { form: "a numeric reference padded past 6 hex digits", raw: "−5
", converted: "\u22125", altered: ["\u22125"], lost: ["00002212", "5"] }, ] // Named character references whose decoded character the probe reads as a sign or a separator (or // as markup), decoded on the raw side as a converter decodes them: each in a sign position and a // separator position, EN and KO, kept as the decoded character, plus the sign position with the // character dropped (a sign is lost; any other character changes nothing). const ENTITIES: [string, string][] = [ ["minus", "\u2212"], ["ndash", "\u2013"], ["mdash", "\u2014"], ["hyphen", "\u2010"], ["dash", "\u2010"], ["thinsp", "\u2009"], ["ThinSpace", "\u2009"], ["numsp", "\u2007"], ["puncsp", "\u2008"], ["hairsp", "\u200A"], ["VeryThinSpace", "\u200A"], ["ensp", "\u2002"], ["emsp", "\u2003"], ["emsp13", "\u2004"], ["emsp14", "\u2005"], ["MediumSpace", "\u205F"], ["nbsp", "\u00A0"], ["NonBreakingSpace", "\u00A0"], ["comma", ","], ["period", "."], ["plus", "+"], ["lowbar", "_"], ["ast", "*"], ["midast", "*"], ["grave", "`"], ["DiacriticalGrave", "`"], ["lpar", "("], ["rpar", ")"], ["lsqb", "["], ["lbrack", "["], ["rsqb", "]"], ["rbrack", "]"], ] const SIGN_CHARS = new Set(["\u2212", "\u2013"]) const ENTITY_ROWS: Row[] = ENTITIES.flatMap(([name, ch]) => { const e = `&${name};` return [ { form: `&${name}; in a sign position`, raw: `fell ${e}5 pts
`, converted: `fell ${ch}5 pts`, altered: [], lost: [] }, { form: `KO &${name}; in a sign position`, raw: `\uAC10\uC18C${e}3%
`, converted: `\uAC10\uC18C${ch}3%`, altered: [], lost: [] }, { form: `&${name}; as a separator`, raw: `1${e}234${e}567 people
`, converted: `1${ch}234${ch}567 people`, altered: [], lost: [] }, { form: `KO &${name}; as a separator`, raw: `\uC778\uAD6C 1${e}234${e}567\uBA85
`, converted: `\uC778\uAD6C 1${ch}234${ch}567\uBA85`, altered: [], lost: [] }, { form: `&${name}; in a sign position, dropped`, raw: `fell ${e}5 pts
`, converted: "fell 5 pts", altered: SIGN_CHARS.has(ch) ? ["5"] : [], lost: SIGN_CHARS.has(ch) ? [`${ch}5`] : [] }, { form: `KO &${name}; in a sign position, dropped`, raw: `\uAC10\uC18C${e}3%
`, converted: "\uAC10\uC18C3%", altered: SIGN_CHARS.has(ch) ? ["3"] : [], lost: SIGN_CHARS.has(ch) ? [`${ch}3`] : [] }, { form: `&${name}; kept as the reference in a sign position`, raw: `fell ${e}5 pts
`, converted: `fell ${e}5 pts`, altered: [], lost: [] }, { form: `KO &${name}; kept as the reference in a sign position`, raw: `\uAC10\uC18C${e}3%
`, converted: `\uAC10\uC18C${e}3%`, altered: [], lost: [] }, { form: `&${name}; kept as the reference as a separator`, raw: `1${e}234${e}567 people
`, converted: `1${e}234${e}567 people`, altered: [], lost: [] }, { form: `KO &${name}; kept as the reference as a separator`, raw: `\uC778\uAD6C 1${e}234${e}567\uBA85
`, converted: `\uC778\uAD6C 1${e}234${e}567\uBA85`, altered: [], lost: [] }, { form: `&${name}; in a verbatim plain-text copy (sign position)`, raw: `fell ${e}5 pts`, converted: `fell ${e}5 pts`, altered: [], lost: [] }, { form: `&${name}; in a verbatim plain-text copy (separator)`, raw: `1${e}234${e}567 people`, converted: `1${e}234${e}567 people`, altered: [], lost: [] }, ] }) ENTITY_ROWS.push( { form: "reviewer r5: − in a text with no tags", raw: "x −3.5%", converted: "x \u22123.5%", altered: [], lost: [] }, { form: "reviewer r5: groups in a text with no tags", raw: "1 234 567", converted: "1\u2009234\u2009567", altered: [], lost: [] }, { form: "reviewer r5: − in a paragraph", raw: "x −3.5%
", converted: "x\u00A0\u22123.5%", altered: [], lost: [] }, { form: "an unterminated reference in a text with no tags stays literal", raw: "a < b & c 5", converted: "a < b & c 5", altered: [], lost: [] }, { form: "reviewer r6: 'Rate −5 and 1 234' copied verbatim", raw: "Rate −5 and 1 234", converted: "Rate −5 and 1 234", altered: [], lost: [] }, { form: "KO verbatim copy with − and ", raw: "\uC99D\uAC10 −5%, \uC778\uAD6C 1 234\uBA85", converted: "\uC99D\uAC10 −5%, \uC778\uAD6C 1 234\uBA85", altered: [], lost: [] }, { form: "a sign the converted side spells as another reference is still dropped", raw: "fell −5 pts
", converted: "fell 5 pts", altered: ["5"], lost: ["\u22125"] }, { form: "a separator spelled as a reference on the converted side still joins two numbers", raw: "12
345
", converted: "12 345", altered: ["12 345"], lost: ["12", "345"] }, ) describe("conversion probe classes (one row per form, EN and KO)", () => { test.each(rows(CONVERTER_DIGITS))("converter-emitted digits: %s", (_form, row) => check(row)) test.each(rows(SIGNS))("signs and separators around numbers: %s", (_form, row) => check(row)) test.each(rows(GROUPING))("grouping boundaries: %s", (_form, row) => check(row)) test.each(rows(SIGN_MARKUP))("sign context through Markdown markup: %s", (_form, row) => check(row)) test.each(rows(ENTITY_ROWS))("named character references on both sides: %s", (_form, row) => check(row)) test.each(rows(RAW_HTML))("raw inline HTML in the converted Markdown: %s", (_form, row) => check(row)) test.each(rows(REF_TITLES))("reference definition titles on the next line: %s", (_form, row) => check(row)) test.each(rows(OUT_OF_SCOPE))("declared out of scope (reported as altered): %s", (_form, row) => check(row)) })