From c509da93703e7218e2f2acda3e197aa481f35f45 Mon Sep 17 00:00:00 2001 From: Rens Date: Wed, 9 Sep 2026 19:25:21 +0200 Subject: [PATCH] table dashes communicate widht --- README.md | 2 +- docs/DIALECT.md | 10 +++++--- docs/sample.html | 6 ++--- examples/sample-clean.md | 6 ++--- examples/sample-render.md | 12 +++++----- examples/sample.md | 6 ++--- src/block.rs | 14 +++++++---- tests/canonical.rs | 11 +++++++++ tests/fixtures/dialect.md | 2 +- tests/source/cmark-gfm/extensions.txt | 8 +++---- tests/source/cmark-gfm/spec.txt | 2 +- .../php-markdown-extra.mdtest/Tables.text | 2 +- tests/test_focused.py | 24 +++++++++++++++++++ 13 files changed, 75 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 235a3ba..9acc42c 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The dialect deviates from CommonMark for three reasons: ## Implemented syntax - Core block syntax: paragraphs, ATX headings, thematic breaks, block quotes, ordered/unordered lists, indented code, raw HTML, link reference definitions. -- Tables: GFM/PHP Extra pipe tables with alignment. Use raw HTML for tables with row spans, column spans, or block content in cells. Table elements are included in the HTML subset. +- Tables: GFM/PHP Extra pipe tables with alignment. Prefer explicit widths such as `{: colwidths="1fr 3fr"}`. Unequal separator dash counts provide shorthand (`-|---` requests a 25% / 75% split); equal counts leave sizing automatic. Explicit `colwidths` overrides dash widths. Use raw HTML for tables with row spans, column spans, or block content in cells. Table elements are included in the HTML subset. - GFM: task lists, `~~x~~` strikethrough, angle autolinks, and bare autolinks. Bare URL and email autolinking is on by default. Disable it with `bare_autolinks=False`. Explicit CommonMark angle autolinks remain enabled. - Code: backtick/tilde fenced code blocks, info strings, and Pandoc-style code attributes. - HTML-in-md: elements that `md` can emit, conventional phrasing tags (`u`, `kbd`, `b`, `i`, `ins`, `s`), and custom elements. Other tags render as literal text. `{=html}` raw blocks pass arbitrary HTML through. diff --git a/docs/DIALECT.md b/docs/DIALECT.md index 5cb4a37..9f1c5f7 100644 --- a/docs/DIALECT.md +++ b/docs/DIALECT.md @@ -202,7 +202,7 @@ Fenced and indented code use `pre > code`. A language becomes a `language-*` cla ```markdown | Feature | Status | Notes | -|:--------|:------:|------:| +|:------|:------:|------:| | Tables | ready | yes | ``` @@ -214,7 +214,11 @@ MDHTML: Pipe tables require a header. Cells use `align`; MDHTML deliberately retains `align` because it directly expresses the value converters need. Complex tables — row and column spans, block cell content, headerless bodies, footers — are written as raw HTML table soup, which is in the raw HTML subset; those cells carry `rowspan` and `colspan` as ordinary attributes. -A table may carry mixed fixed and proportional widths as a `colwidths` attribute (`colwidths="1.2in 1fr 2fr"`, on a pipe table via its caption or IAL, or directly on a raw ``). Lengths fix columns; `fr` tracks share the remaining width; the HTML exporter lowers the attribute to a `colgroup`. A `width` attribute on the table (same spellings) lowers to an inline style width: a CSS length verbatim, a bare number as px, an invalid value left as a visible attribute; it merges after `colwidths`' lowering, so an explicit width beats its `width:100%`. Non-HTML exporters ignore both. +A table may carry mixed fixed and proportional widths as a `colwidths` attribute (`colwidths="1.2in 1fr 2fr"`, on a pipe table via its caption or IAL, or directly on a raw `
`). Lengths fix columns; `fr` tracks share the remaining width. The HTML exporter lowers the attribute to a `colgroup`; the Typst exporter uses the tracks directly. + +Unequal dash counts in a pipe table's separator row are shorthand for proportional `colwidths`: `-|---` sets `colwidths="1fr 3fr"`. Only dashes count; alignment colons and surrounding whitespace do not affect widths. Equal dash counts leave sizing automatic. Use an explicit attribute such as `{: colwidths="1fr 1fr"}` to request equal widths. Explicit `colwidths` overrides inferred widths. Inference applies regardless of source line length or cell content. Markdown export writes widths as an explicit attribute rather than reconstructing separator lengths. + +A `width` attribute on the table lowers to an inline style width: a CSS length verbatim, a bare number as px, an invalid value left as a visible attribute. It merges after `colwidths`' lowering, so an explicit width beats its `width:100%`. The Typst exporter ignores the table's `width` attribute. ## Definition lists and fenced divs @@ -348,7 +352,7 @@ A caption line glued directly below a table becomes `caption`; its trailing attr ```markdown | Stage | Days | -|:------|-----:| +|:------|------:| | Ship | 3 | : Delivery stages {#tbl-stages} ``` diff --git a/docs/sample.html b/docs/sample.html index 43935db..27deb15 100644 --- a/docs/sample.html +++ b/docs/sample.html @@ -91,7 +91,7 @@

Block quotes and rules

Tables

| Feature | Status | Notes |
-|:--------|:------:|------:|
+|:------|:------:|------:|
 | Tables | ready | aligned columns |
 | Math | ready | brackets mode |
 | HTML | ready | raw or markdown-enabled |
@@ -306,7 +306,7 @@ 

Captions and figures

![A cute puppy](puppy.jpg){#fig-diagram width="180"}
 
 | Stage | Days |
-|:------|-----:|
+|:------|------:|
 | Ship  | 3    |
 | Clear | 5    |
 : Delivery stages {#tbl-stages}
@@ -378,7 +378,7 @@ 

Template tokens and filling

{{/office}} | Item | Qty | -|------|-----| +|------|------| {{#items}} | {{name}} | {{qty}} | {{/items}} diff --git a/examples/sample-clean.md b/examples/sample-clean.md index 8ef3a7d..ba3b736 100644 --- a/examples/sample-clean.md +++ b/examples/sample-clean.md @@ -73,7 +73,7 @@ Angle links work too: . ## Tables | Feature | Status | Notes | -|:--------|:------:|------:| +|:------|:------:|------:| | Tables | ready | aligned columns | | Math | ready | brackets mode | | HTML | ready | raw or markdown-enabled | @@ -232,7 +232,7 @@ its trailing attribute list applies to the table: ![A cute puppy](puppy.jpg){#fig-diagram width="180"} | Stage | Days | -|:------|-----:| +|:------|------:| | Ship | 3 | | Clear | 5 | : Delivery stages {#tbl-stages} @@ -286,7 +286,7 @@ The {{name}} office ({{city}}) handles this matter. {{/office}} | Item | Qty | -|------|-----| +|------|------| {{#items}} | {{name}} | {{qty}} | {{/items}} diff --git a/examples/sample-render.md b/examples/sample-render.md index 6ce030a..2a06e7f 100644 --- a/examples/sample-render.md +++ b/examples/sample-render.md @@ -122,14 +122,14 @@ Angle links work too: . `````markdown | Feature | Status | Notes | -|:--------|:------:|------:| +|:------|:------:|------:| | Tables | ready | aligned columns | | Math | ready | brackets mode | | HTML | ready | raw or markdown-enabled | ````` | Feature | Status | Notes | -|:--------|:------:|------:| +|:------|:------:|------:| | Tables | ready | aligned columns | | Math | ready | brackets mode | | HTML | ready | raw or markdown-enabled | @@ -419,7 +419,7 @@ its trailing attribute list applies to the table: ![A cute puppy](puppy.jpg){#fig-diagram width="180"} | Stage | Days | -|:------|-----:| +|:------|------:| | Ship | 3 | | Clear | 5 | : Delivery stages {#tbl-stages} @@ -428,7 +428,7 @@ its trailing attribute list applies to the table: ![A cute puppy](puppy.jpg){#fig-diagram width="180"} | Stage | Days | -|:------|-----:| +|:------|------:| | Ship | 3 | | Clear | 5 | : Delivery stages {#tbl-stages} @@ -506,7 +506,7 @@ The {{name}} office ({{city}}) handles this matter. {{/office}} | Item | Qty | -|------|-----| +|------|------| {{#items}} | {{name}} | {{qty}} | {{/items}} @@ -523,7 +523,7 @@ The {{name}} office ({{city}}) handles this matter. {{/office}} | Item | Qty | -|------|-----| +|------|------| {{#items}} | {{name}} | {{qty}} | {{/items}} diff --git a/examples/sample.md b/examples/sample.md index 8e0641d..2ed39bb 100644 --- a/examples/sample.md +++ b/examples/sample.md @@ -84,7 +84,7 @@ Angle links work too: . `````markdown | Feature | Status | Notes | -|:--------|:------:|------:| +|:------|:------:|------:| | Tables | ready | aligned columns | | Math | ready | brackets mode | | HTML | ready | raw or markdown-enabled | @@ -271,7 +271,7 @@ its trailing attribute list applies to the table: ![A cute puppy](puppy.jpg){#fig-diagram width="180"} | Stage | Days | -|:------|-----:| +|:------|------:| | Ship | 3 | | Clear | 5 | : Delivery stages {#tbl-stages} @@ -333,7 +333,7 @@ The {{name}} office ({{city}}) handles this matter. {{/office}} | Item | Qty | -|------|-----| +|------|------| {{#items}} | {{name}} | {{qty}} | {{/items}} diff --git a/src/block.rs b/src/block.rs index 91e44bb..23d0c38 100644 --- a/src/block.rs +++ b/src/block.rs @@ -1432,7 +1432,7 @@ impl<'a> ContainerBuilder<'a> { let Some(header_line) = lines.last().cloned() else { return false }; let paragraph_len = lines.len(); let Some(header) = split_table_row(&header_line) else { return false }; - let Some(aligns) = parse_table_separator(line) else { return false }; + let Some((aligns, attrs)) = parse_table_separator(line) else { return false }; if header.len() != aligns.len() { return false; } if self.record_trace { let lead = self.cur_offset + (line.len() - line.trim_start().len()); @@ -1443,7 +1443,7 @@ impl<'a> ContainerBuilder<'a> { } let head = header.into_iter().map(|cell| cell.trim().to_string()).collect(); let table = BuildKind::Table { - attrs: Attr::default(), + attrs, caption: None, row_tokens: Vec::new(), head: vec![draft_inline_table_row(head, &aligns)], @@ -3003,15 +3003,17 @@ fn raw_table_cells(line: &str) -> Vec { cells } -fn parse_table_separator(line: &str) -> Option> { +fn parse_table_separator(line: &str) -> Option<(Vec, Attr)> { let cells = split_table_row(line)?; let mut aligns = Vec::new(); + let mut widths = Vec::new(); for cell in cells { let c = cell.trim(); let left = c.starts_with(':'); let right = c.ends_with(':'); let dashes = c.trim_matches(':'); if dashes.is_empty() || !dashes.chars().all(|x| x == '-') { return None; } + widths.push(dashes.len()); aligns.push(match (left, right) { (true, true) => Align::Center, (true, false) => Align::Left, @@ -3019,7 +3021,11 @@ fn parse_table_separator(line: &str) -> Option> { _ => Align::None, }); } - Some(aligns) + let mut attrs = Attr::default(); + if widths.windows(2).any(|w| w[0] != w[1]) { + attrs.set_pair("colwidths", widths.iter().map(|w| format!("{w}fr")).collect::>().join(" ")); + } + Some((aligns, attrs)) } pub(crate) fn paragraph_interrupts(line: &str) -> bool { starts_block(line) || list_interrupts_paragraph(line) || def_marker(line).is_some() } diff --git a/tests/canonical.rs b/tests/canonical.rs index a9b134f..0fc68ab 100644 --- a/tests/canonical.rs +++ b/tests/canonical.rs @@ -12,6 +12,17 @@ fn panel_titles_and_nested_panels_roundtrip() { assert!(!html.contains(" diff --git a/tests/source/cmark-gfm/spec.txt b/tests/source/cmark-gfm/spec.txt index 922010e..ff38c67 100644 --- a/tests/source/cmark-gfm/spec.txt +++ b/tests/source/cmark-gfm/spec.txt @@ -3376,7 +3376,7 @@ they are. Likewise, use of leading and trailing pipes may be inconsistent: ```````````````````````````````` example table | abc | defghi | -:-: | -----------: +:-: | -: bar | baz .
diff --git a/tests/source/php-markdown-extra.mdtest/Tables.text b/tests/source/php-markdown-extra.mdtest/Tables.text index 713cd89..cc3b4e8 100644 --- a/tests/source/php-markdown-extra.mdtest/Tables.text +++ b/tests/source/php-markdown-extra.mdtest/Tables.text @@ -60,7 +60,7 @@ Table alignement: Table alignement (alternate spacing): | Default | Right | Center | Left | -| --------- | :-------- | :-------: | --------: | +| ------- | :------- | :-------: | -------: | | Long Cell | Long Cell | Long Cell | Long Cell | | Cell | Cell | Cell | Cell | diff --git a/tests/test_focused.py b/tests/test_focused.py index 3e3510f..c5e46e5 100644 --- a/tests/test_focused.py +++ b/tests/test_focused.py @@ -140,6 +140,30 @@ def test_table_ial_line_attaches(): assert html.startswith('
') assert '{: .c}' not in html + +def test_table_dash_widths(): + from mdhtml import mdhtml2md + for sep, widths in (("|---|---------|", "3fr 9fr"), (" :--- | ---------: ", "3fr 9fr"), ("|:-:|---|", "1fr 3fr")): + src = f"| A | B |\n{sep}\n| x | [link](https://example.com) |\n" + html = md2mdhtml(src) + assert f'colwidths="{widths}"' in html + assert_html(html, md2mdhtml(src + f'{{: colwidths="{widths}"}}')) + assert_html(md2mdhtml(mdhtml2md(html)), html) + nested = "> - | A | B |\n> |:---|---------:|\n> | x | y |\n" + assert 'colwidths="3fr 9fr"' in md2mdhtml(nested) + + +def test_table_dash_widths_auto_and_explicit(): + for sep in ("|---|---|", "|:---|---:|", "|:---:| --- |", "|------|------|"): + assert "colwidths" not in md2mdhtml(f"| A | B |\n{sep}\n| x | y |\n") + assert "colwidths" not in md2mdhtml("| A |\n|:-----:|\n| x |\n") + src = "| A | B |\n|---|---------|\n| x | y |\n" + ial = '{: colwidths="1fr 1fr"}' + for authored in (ial + "\n" + src, src + ial, src + ': Caption {colwidths="1fr 1fr"}'): + html = md2mdhtml(authored) + assert 'colwidths="1fr 1fr"' in html + assert 'colwidths="3fr 9fr"' not in html + def test_raw_attribute_blocks_and_inlines(): html = md2mdhtml('```{=docx}\n\n```\n') assert html == '\n'