diff --git a/static/js/markdown.js b/static/js/markdown.js index 2d2f6ca..efd2d02 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -5,6 +5,7 @@ */ import uiModule from './ui.js'; +import { splitTableRow } from './markdown/tableRow.js'; var escapeHtml = uiModule.esc; @@ -535,7 +536,7 @@ export function mdToHtml(src) { let html = ''; rows.forEach((row, idx) => { - const cells = row.split('|').filter(cell => cell.trim() !== ''); + const cells = splitTableRow(row); if (cells.length === 0) return; html += idx === 1 ? '' : ''; diff --git a/static/js/markdown/tableRow.js b/static/js/markdown/tableRow.js new file mode 100644 index 0000000..5cdd8c0 --- /dev/null +++ b/static/js/markdown/tableRow.js @@ -0,0 +1,18 @@ +// static/js/markdown/tableRow.js +// +// Pure helper for splitting a markdown table row into cells. No DOM — +// safe to import anywhere and to unit-test under node. + +// Split a "| a | b | c |" row into trimmed cell strings. +// +// Strip only the optional leading/trailing pipe, then split — filtering out +// every empty cell (the old behaviour) dropped intentionally-empty interior +// cells too, so "| a | | c |" collapsed to 2 columns and misaligned with the +// header. +export function splitTableRow(row) { + return (row || '') + .replace(/^\s*\|/, '') + .replace(/\|\s*$/, '') + .split('|') + .map((cell) => cell.trim()); +} diff --git a/tests/test_markdown_table_row_js.py b/tests/test_markdown_table_row_js.py new file mode 100644 index 0000000..a089154 --- /dev/null +++ b/tests/test_markdown_table_row_js.py @@ -0,0 +1,47 @@ +"""Pin the pure splitTableRow helper (static/js/markdown/tableRow.js). + +Driven through `node --input-type=module` (same approach as test_compare_js.py); +skips when `node` is not installed. + +Regression: the old split filtered out every empty cell, so an intentionally +empty interior cell ("| a | | c |") collapsed the row to 2 columns and +misaligned it with the header. +""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_HELPER = _REPO / "static" / "js" / "markdown" / "tableRow.js" +_HAS_NODE = shutil.which("node") is not None + + +def _split(row: str): + js = f""" + import {{ splitTableRow }} from '{_HELPER.as_posix()}'; + console.log(JSON.stringify(splitTableRow({json.dumps(row)}))); + """ + proc = subprocess.run( + ["node", "--input-type=module"], + input=js, capture_output=True, text=True, cwd=str(_REPO), timeout=30, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout.strip()) + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_keeps_empty_interior_cell(): + assert _split("| a | | c |") == ["a", "", "c"] + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_rows_without_outer_pipes(): + assert _split("a | b | c") == ["a", "b", "c"] + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_header_row_unaffected(): + assert _split("| h1 | h2 | h3 |") == ["h1", "h2", "h3"]