From 62de491cb2b5f0c61b1a4d2a55dfdc69c058d193 Mon Sep 17 00:00:00 2001 From: Immanuel Haffner Date: Fri, 3 Jul 2026 15:53:25 +0000 Subject: [PATCH] fix(tostring): account for tag decoration in table column width A `#`-prefixed token in a table cell (`#foo`, `#bug`, `#51`) is parsed as an Obsidian-style tag. The tag renderer conceals the leading `#` and injects padding_left/padding_right (and, if configured, corner/icon) as inline virtual text, so the rendered cell is wider than its raw text. The table column-width calculation, however, runs through renderers/markdown/tostring.lua, which had no tag handler: it measured the raw text (`#foo` = 4 columns) and was blind to the conceal + padding. The mismatch drifted the cell's right border (about +1 per tag cell with the default padding). This is not limited to numeric refs -- `#foo` drifts exactly like `#51`. The cause is the missing tag-width accounting in tostring, not whether the tag body is numeric (the separate question of whether purely-numeric `#123` should be a tag at all, per Obsidian, is orthogonal and left unchanged). Fix: give tostring a tag handler that mirrors the renderer -- strip the `#`, resolve the tag config via utils.match, and emit corner_left + padding_left + icon + label + padding_right + corner_right -- plus an lpeg tag pattern guarded like the parser (start/after-whitespace, not an internal-link `#^section`). The computed width now matches what is drawn. Renames the fixture to test/table_ref_tag_drift.md and generalizes it: non-numeric tags drift too, and the repro documents the tostring root cause. --- lua/markview/renderers/markdown/tostring.lua | 55 +++++++++++++++++- test/table_ref_tag_drift.md | 59 ++++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 test/table_ref_tag_drift.md diff --git a/lua/markview/renderers/markdown/tostring.lua b/lua/markview/renderers/markdown/tostring.lua index 8f8e7c5c..61019159 100644 --- a/lua/markview/renderers/markdown/tostring.lua +++ b/lua/markview/renderers/markdown/tostring.lua @@ -65,6 +65,7 @@ md_str.update_cache = function () footnotes = spec.get({ "markdown_inline", "footnotes" }, { fallback = nil }), internal_links = spec.get({ "markdown_inline", "internal_links" }, { fallback = nil }), uri_autolinks = spec.get({ "markdown_inline", "uri_autolinks" }, { fallback = nil }), + tags = spec.get({ "markdown_inline", "tags" }, { fallback = nil }), }; ---|fE @@ -361,6 +362,50 @@ md_str.footnote = function (match) ---|fE end +---@param match string +---@return string +md_str.tag = function (match) + ---|fS + + local label = string.gsub(match, "^#", ""); + + if md_str.cached_config and md_str.cached_config.tags then + ---@type markview.config.__inline? + local config = require("markview.utils").match(md_str.cached_config.tags, label, { + eval_args = { + md_str.buffer, + { + class = "inline_tag", + text = { match }, + + label = label, + } + } + }); + + if config then + --- NOTE: The tag renderer conceals the leading `#` and adds + --- corner/padding/icon around the label. Mirror that here so the + --- table column-width calculation matches what is drawn; otherwise a + --- tag in a cell drifts the right border by the padding width. + return table.concat({ + config.corner_left or "", + config.padding_left or "", + + config.icon or "", + label, + + config.padding_right or "", + config.corner_right or "", + }, ""); + end + end + + return match; + + ---|fE +end + ---@param match string ---@return string md_str.escape = function (match) @@ -795,11 +840,19 @@ local hl = lpeg.C( lpeg.P("==") * hl_content^1 * lpeg.P("==") ) / md_str.highlig local strike_content = lpeg.P("\\~") + ( 1 - lpeg.P("~") ); local strike = lpeg.C( lpeg.P("~~") * strike_content^1 * lpeg.P("~~") ) / md_str.strikethrough; +-- Obsidian-style tag: `#` followed by tag characters, only at the start of a +-- token position (start-of-string or after whitespace, via `at_valid`) and not +-- part of an internal-link `#^section` (guarded by the trailing `-]]`). Mirrors +-- the tag parser in `parsers/markdown_inline.lua` so the width computed here +-- matches what the tag renderer draws (concealed `#` + paddings). +local tag_char = lpeg.R("09", "az", "AZ") + lpeg.S("_-"); +local tag = lpeg.C( at_valid * lpeg.P("#") * tag_char^1 * -lpeg.P("]]") ) / md_str.tag; + local any = lpeg.P(1); local token = escape + emoji + entity + - hl + strike + block_ref + embed + internal + + hl + strike + tag + block_ref + embed + internal + email + auto + footnote + img + hyperlink + code + diff --git a/test/table_ref_tag_drift.md b/test/table_ref_tag_drift.md new file mode 100644 index 00000000..5f6158e0 --- /dev/null +++ b/test/table_ref_tag_drift.md @@ -0,0 +1,59 @@ +; Tags/refs in table cells — right-border drift +A `#`-prefixed token in a table cell (`#foo`, `#bug`, `#51`, `#107`) is parsed +as an Obsidian-style tag. The tag renderer conceals the leading `#` and injects +`padding_left` + `padding_right` (and, if configured, `corner`/`icon`) as inline +virtual text. That decoration changes the rendered cell width, but the table's +column-width calculation is computed via `renderers/markdown/tostring.lua`, which +has NO tag handler — it measures the raw cell text (`#foo` = 4 columns) and is +blind to the conceal + padding. The mismatch drifts the cell's right border +(about +1 per tag cell with the default padding; more if an icon is configured). + +This is NOT limited to numeric refs: `#foo` drifts exactly like `#51`. The cause +is the missing tag-width accounting in `tostring`, not whether the tag body is +numeric. + +Fix: teach `tostring` about tags so the computed column width matches what the +renderer draws (conceal the `#`, add the paddings). + +Open this file with hybrid/preview mode and check every right border lines up. +Each tag cell must align with the plain control cell of equal rendered width. + +### Non-numeric tags drift too (primary repro) + +| Ref | Note | Plain | +|------|---------------|-------| +| #foo | word ref | XXXX | +| #bug | word tag | XXXX | +| #wip | word tag | XXXX | +| Xctl | plain control | XXXX | + +### Numeric refs drift the same way + +| Ref | Note | Plain | +|------|---------------|-------| +| #51 | first ref | XXX | +| #107 | second ref | XXXX | +| #26 | third ref | XXX | +| #1 | single digit | XX | +| Xctl | plain control | XXXX | + +### Varied tag bodies (digits, letters, _ and -) + +| Kind | Value | +|------------------|----------| +| Word tag | #bug | +| Version tag | #v2 | +| Mixed (digits+) | #123abc | +| Underscore | #wip_now | +| Hyphen | #in-prog | +| Numeric only | #123 | + +### Mixed content — refs and tags side by side + +| When | Ref | Tag | Cancel | +|----------|------|--------|----------| +| 11:38:13 | #51 | #bug | 14:54:27 | +| 06:34:40 | #107 | #v2 | 07:51:34 | +| 08:28:44 | #26 | #wip | 08:30:15 | + +