diff --git a/lua/markview/parsers/markdown.lua b/lua/markview/parsers/markdown.lua index 53e19db..795d982 100644 --- a/lua/markview/parsers/markdown.lua +++ b/lua/markview/parsers/markdown.lua @@ -885,8 +885,19 @@ markdown.table = function (_, _, text, range) end for l, line in ipairs(text) do + --- Strip block_continuation prefixes(e.g. `> `) + --- from lines after the first. + --- `get_node_text()` only applies col_start to line 1. + if l > 1 then + line = line:sub(range.col_start + 1); + end + local row_text = line; + if row_text == "" or row_text:match("^%s*$") then + goto continue; + end + if l == 1 then header = line_processor(row_text); elseif l == 2 then @@ -917,6 +928,8 @@ markdown.table = function (_, _, text, range) else table.insert(rows, line_processor(row_text)) end + + ::continue:: end local top_border, border_overlap = overlap(range.row_start); diff --git a/lua/markview/renderers/markdown.lua b/lua/markview/renderers/markdown.lua index 30b6152..f5a9f97 100644 --- a/lua/markview/renderers/markdown.lua +++ b/lua/markview/renderers/markdown.lua @@ -1567,9 +1567,6 @@ markdown.table = function (buffer, item) rows = {} }; - ---@type integer[] Invisible width used for text wrapping in Neovim. - local vim_width = {}; - ---@type integer Current column number. local c = 1; @@ -1584,14 +1581,6 @@ markdown.table = function (buffer, item) col_widths[c] = o; end - local vim_col_width = vim.fn.strdisplaywidth(col.text); - - if not vim_width[c] then - vim_width[c] = vim_col_width; - elseif vim_col_width > vim_width[c] then - vim_width[c] = vim_col_width; - end - c = c + 1; end end @@ -1606,14 +1595,6 @@ markdown.table = function (buffer, item) col_widths[c] = o; end - local vim_col_width = vim.fn.strdisplaywidth(col.text); - - if not vim_width[c] then - vim_width[c] = vim_col_width; - elseif vim_col_width > vim_width[c] then - vim_width[c] = vim_col_width; - end - c = c + 1; end end @@ -1633,14 +1614,6 @@ markdown.table = function (buffer, item) col_widths[c] = o; end - local vim_col_width = vim.fn.strdisplaywidth(col.text); - - if not vim_width[c] then - vim_width[c] = vim_col_width; - elseif vim_col_width > vim_width[c] then - vim_width[c] = vim_col_width; - end - c = c + 1; end end @@ -1648,19 +1621,31 @@ markdown.table = function (buffer, item) if is_wrapped == true then local win = utils.buf_getwin(buffer); - local width = vim.api.nvim_win_get_width(win); + local textoff = vim.fn.getwininfo(win)[1].textoff; + local text_width = vim.api.nvim_win_get_width(win) - textoff; local table_width = 1; - for _, col in ipairs(vim_width) do + for _, col in ipairs(col_widths) do table_width = table_width + 1 + col; end - if table_width >= width * 0.9 then - --- Most likely the text was wrapped somewhere. - --- TODO, Check if a more accurate(& faster) method exists or not. + if table_width >= text_width then + --- The rendered table is at least as wide as the usable + --- text area, so it will wrap and the border layout would + --- be broken. Bail out of rendering entirely. return; end + + --- NOTE: We intentionally do NOT check the raw (unconcealed) + --- line width here. Neovim's soft-wrap calculation ignores + --- conceal, so a line with a long URL may wrap internally + --- even though the *rendered* table fits. Bailing out in + --- that case would prevent rendering of any table that + --- contains wide inline elements (links, images, …) which + --- defeats the purpose of the preview. Accept the possible + --- wrap artefact — a rendered table with a minor visual + --- glitch is strictly better than no rendering at all. end ---@type markview.config.markdown.tables.parts @@ -1711,23 +1696,21 @@ markdown.table = function (buffer, item) table.insert(tmp, { top, - is_wrapped and "@punctuation.special.markdown" or utils.set_hl(top_hl) + utils.set_hl(top_hl) }); - if is_wrapped == false then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start, range.col_start + part.col_start, { - undo_restore = false, invalidate = true, - end_col = range.col_start + part.col_end, - conceal = "", + vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start, range.col_start + part.col_start, { + undo_restore = false, invalidate = true, + end_col = range.col_start + part.col_end, + conceal = "", - virt_text_pos = "inline", - virt_text = { - { border, border_hl } - }, + virt_text_pos = "inline", + virt_text = { + { border, border_hl } + }, - hl_mode = "combine" - }) - end + hl_mode = "combine" + }) if p == #item.header and config.block_decorator == true then @@ -1748,7 +1731,7 @@ markdown.table = function (buffer, item) hl_mode = "combine" }) elseif item.top_border == true and range.row_start > 0 then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start - 1, math.min(range.col_start, prev_line), { + item.__top_border_id = vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start - 1, math.min(range.col_start, prev_line), { undo_restore = false, invalidate = true, virt_text_pos = "inline", virt_text = tmp, @@ -1763,7 +1746,7 @@ markdown.table = function (buffer, item) table.insert(tmp, { top, - is_wrapped and "@punctuation.special.markdown" or utils.set_hl(top_hl) + utils.set_hl(top_hl) }); vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start, range.col_start + part.col_start, { @@ -1775,7 +1758,7 @@ markdown.table = function (buffer, item) virt_text = { { border, - is_wrapped and "@punctuation.special.markdown" or utils.set_hl(border_hl) + utils.set_hl(border_hl) } }, @@ -1847,7 +1830,7 @@ markdown.table = function (buffer, item) table.insert(tmp, { string.rep(top, column_width), - is_wrapped and "@punctuation.special.markdown" or utils.set_hl(top_hl) + utils.set_hl(top_hl) }); if visible_width < column_width then @@ -1859,6 +1842,7 @@ markdown.table = function (buffer, item) { string.rep(" ", math.max(0, column_width - visible_width)) } }, + right_gravity = false, hl_mode = "combine" }); elseif item.alignments[c] == "right" then @@ -1869,6 +1853,7 @@ markdown.table = function (buffer, item) { string.rep(" ", math.max(0, column_width - visible_width)) } }, + right_gravity = false, hl_mode = "combine" }); else @@ -1879,6 +1864,7 @@ markdown.table = function (buffer, item) { string.rep(" ", math.ceil((column_width - visible_width) / 2)) } }, + right_gravity = false, hl_mode = "combine" }); vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start, range.col_start + part.col_end, { @@ -1888,6 +1874,7 @@ markdown.table = function (buffer, item) { string.rep(" ", math.floor((column_width - visible_width) / 2)) } }, + right_gravity = false, hl_mode = "combine" }); end @@ -1904,10 +1891,6 @@ markdown.table = function (buffer, item) local y = range.col_start + sep.col_start; if sep.class == "separator" then - if is_wrapped == true then - goto continue; - end - local border, border_hl = get_border("separator", 4); if s == 1 then @@ -1935,7 +1918,7 @@ markdown.table = function (buffer, item) undo_restore = false, invalidate = true, virt_text_pos = "inline", virt_text = { - is_wrapped == true and { "|", "@punctuation.special.markdown" } or { border, border_hl } + { border, border_hl } }, right_gravity = s ~= 1, @@ -1948,23 +1931,7 @@ markdown.table = function (buffer, item) local width = vim.fn.strdisplaywidth(sep.text); local left = col_widths[c] - width; - if is_wrapped == true then - if left > 0 then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, x, (range.col_start + sep.col_end) - 1, { - undo_restore = false, invalidate = true, - - virt_text_pos = "inline", - virt_text = { - { - string.rep("-", left), - "@punctuation.special.markdown" - } - }, - - hl_mode = "combine" - }); - end - elseif item.alignments[c] == "default" then + if item.alignments[c] == "default" then if left > 0 then vim.api.nvim_buf_set_extmark(buffer, markdown.ns, x, y, { undo_restore = false, invalidate = true, @@ -1989,6 +1956,7 @@ markdown.table = function (buffer, item) { string.rep(border, left), utils.set_hl(border_hl) }, }, + right_gravity = false, hl_mode = "combine" }); else @@ -2036,6 +2004,7 @@ markdown.table = function (buffer, item) { string.rep(border, left), utils.set_hl(border_hl) }, }, + right_gravity = false, hl_mode = "combine" }); else @@ -2084,6 +2053,7 @@ markdown.table = function (buffer, item) { align, utils.set_hl(align_hl) } }, + right_gravity = false, hl_mode = "combine" }); else @@ -2133,6 +2103,7 @@ markdown.table = function (buffer, item) { align[2], utils.set_hl(align_hl[2]) } }, + right_gravity = false, hl_mode = "combine" }); else @@ -2178,37 +2149,32 @@ markdown.table = function (buffer, item) border, border_hl = get_border("row", 3); end - if is_wrapped == false then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start + 1 + r, range.col_start + part.col_start, { - undo_restore = false, invalidate = true, - end_col = range.col_start + part.col_end, - conceal = "", + vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start + 1 + r, range.col_start + part.col_start, { + undo_restore = false, invalidate = true, + end_col = range.col_start + part.col_end, + conceal = "", - virt_text_pos = "inline", - virt_text = { - { border, border_hl } - }, + virt_text_pos = "inline", + virt_text = { + { border, border_hl } + }, - hl_mode = "combine" - }) - end + hl_mode = "combine" + }) elseif part.class == "missing_seperator" then local border, border_hl = get_border("row", r == 1 and 1 or 3); vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_start + 1 + r, range.col_start + part.col_start, { undo_restore = false, invalidate = true, virt_text_pos = "inline", - virt_text = { - is_wrapped and { - "|", - "@punctuation.special.markdown" - } or { - border, - utils.set_hl(border_hl) - } - }, + virt_text = { + { + border, + utils.set_hl(border_hl) + } + }, - right_gravity = r ~= 1, + right_gravity = r ~= 1, hl_mode = "combine" }) elseif part.class == "column" then @@ -2310,23 +2276,21 @@ markdown.table = function (buffer, item) table.insert(tmp, { bottom, - is_wrapped and "@punctuation.special.markdown" or utils.set_hl(bottom_hl) + utils.set_hl(bottom_hl) }); - if is_wrapped == false then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end - 1, range.col_start + part.col_start, { - undo_restore = false, invalidate = true, - end_col = range.col_start + part.col_end, - conceal = "", + vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end - 1, range.col_start + part.col_start, { + undo_restore = false, invalidate = true, + end_col = range.col_start + part.col_end, + conceal = "", - virt_text_pos = "inline", - virt_text = { - { border, border_hl } - }, + virt_text_pos = "inline", + virt_text = { + { border, border_hl } + }, - hl_mode = "combine" - }); - end + hl_mode = "combine" + }); if p == #item.header and config.block_decorator == true then local next_line = range.row_end == vim.api.nvim_buf_line_count(buffer) and 0 or #vim.api.nvim_buf_get_lines(buffer, range.row_end, range.row_end + 1, false)[1]; @@ -2345,7 +2309,7 @@ markdown.table = function (buffer, item) hl_mode = "combine" }) elseif range.row_end <= vim.api.nvim_buf_line_count(buffer) and item.bottom_border == true then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end, math.min(next_line, range.col_start), { + item.__bottom_border_id = vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end, math.min(next_line, range.col_start), { virt_text_pos = "inline", virt_text = tmp, @@ -2359,7 +2323,7 @@ markdown.table = function (buffer, item) table.insert(tmp, { bottom, - is_wrapped == true and "@punctuation.special.markdown" or utils.set_hl(bottom_hl) + utils.set_hl(bottom_hl) }); vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end - 1, range.col_start + part.col_start, { @@ -2369,10 +2333,7 @@ markdown.table = function (buffer, item) virt_text_pos = "inline", virt_text = { - is_wrapped and { - "|", - "@punctuation.special.markdown" - } or { + { border, utils.set_hl(border_hl) } @@ -2399,7 +2360,7 @@ markdown.table = function (buffer, item) hl_mode = "combine" }) elseif range.row_end <= vim.api.nvim_buf_line_count(buffer) and item.bottom_border == true then - vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end, math.min(next_line, range.col_start), { + item.__bottom_border_id = vim.api.nvim_buf_set_extmark(buffer, markdown.ns, range.row_end, math.min(next_line, range.col_start), { virt_text_pos = "inline", virt_text = tmp, @@ -2440,7 +2401,7 @@ markdown.table = function (buffer, item) table.insert(tmp, { string.rep(bottom, column_width), - is_wrapped and "@punctuation.special.markdown" or utils.set_hl(bottom_hl) + utils.set_hl(bottom_hl) }); if visible_width < column_width then @@ -2493,6 +2454,250 @@ markdown.table = function (buffer, item) c = c + 1; end end + + --- Store data needed for wrap continuation borders. + --- The actual placement is deferred to `markdown.__table` (post_render), + --- which runs after all renderers (including markdown_inline) have + --- placed their extmarks. This ensures `nvim_win_text_height` reflects + --- the true visual line height including inline conceal/padding. + if is_wrapped == true then + --- Build continuation line virtual text from col_widths. + --- Pattern: ││ + local continuation_vt = {}; + local left_border, left_hl = get_border("row", 1); + + table.insert(continuation_vt, { left_border, left_hl }); + + for col_c = 1, #col_widths do + table.insert(continuation_vt, { string.rep(" ", col_widths[col_c]) }); + + if col_c < #col_widths then + local mid_border, mid_hl = get_border("row", 2); + table.insert(continuation_vt, { mid_border, mid_hl }); + else + local right_border, right_hl = get_border("row", 3); + table.insert(continuation_vt, { right_border, right_hl }); + end + end + + item.__continuation_vt = continuation_vt; + + --- Right border info for the first screen row of wrapping lines. + --- The concealed right pipe ends up on the continuation line, so + --- the first screen row needs an explicit right border overlay. + local right_border, right_hl = get_border("row", 3); + item.__right_border_vt = { { right_border, right_hl } }; + item.__table_width = utils.virt_len(continuation_vt); + + --- Register for post_render so __table runs after inline extmarks. + table.insert(markdown.cache, item); + end + + --- Register for post_render to fix border indentation when org_indent + --- adds extra spacing on the border lines (e.g. tables inside list items). + if item.__top_border_id or item.__bottom_border_id then + if not item.__continuation_vt then + table.insert(markdown.cache, item); + end + end +end + + + ----------------------------------------------------------------------------------------- + + +--- Post-render handler for tables. +--- +--- 1. Fixes top/bottom border indentation when org_indent adds extra +--- spacing on the border lines (e.g. tables inside list items). +--- 2. Places wrap continuation borders and right-border overlays +--- (must run after markdown_inline so `nvim_win_text_height` is accurate). +---@param buffer integer +---@param item markview.parsed.markdown.tables +markdown.__table = function (buffer, item) + local range = item.range; + + --- Fix border indentation by accounting for org_indent marks. + --- org_indent may add spacing on the border lines that conflicts + --- with the table's own col_start-based indentation. + --- + --- Strategy: compute the target visual indent from a data row, then + --- adjust each border's leading spaces based on how much org_indent + --- already contributes on the border's line. + if item.__top_border_id or item.__bottom_border_id then + --- Compute target indent from the first data row's org_indent. + local data_org_visual = 0; + local data_conceal_end = 0; + local data_marks = vim.api.nvim_buf_get_extmarks(buffer, markdown.ns, + { range.row_start, 0 }, { range.row_start, range.col_start }, { details = true }); + + for _, m in ipairs(data_marks) do + local d = m[4]; + + if d.conceal == "" and d.virt_text then + local vt_text = ""; + + for _, c in ipairs(d.virt_text) do + vt_text = vt_text .. (c[1] or ""); + end + + if vt_text:match("^%s+$") then + data_org_visual = data_org_visual + vim.fn.strdisplaywidth(vt_text); + data_conceal_end = math.max(data_conceal_end, d.end_col or 0); + end + end + end + + --- Target = org_indent visual width + remaining raw indent. + local target_indent = data_org_visual > 0 + and (data_org_visual + math.max(0, range.col_start - data_conceal_end)) + or nil; + + if target_indent then + for _, key in ipairs({ "__top_border_id", "__bottom_border_id" }) do + local mark_id = item[key]; + + if not mark_id then + goto next_border; + end + + local mark = vim.api.nvim_buf_get_extmark_by_id(buffer, markdown.ns, mark_id, { details = true }); + + if not mark or not mark[3] or not mark[3].virt_text then + goto next_border; + end + + local mark_row = mark[1]; + + --- Find org_indent marks on the border line. + local border_org_visual = 0; + local border_marks = vim.api.nvim_buf_get_extmarks(buffer, markdown.ns, + { mark_row, 0 }, { mark_row, range.col_start }, { details = true }); + + for _, m in ipairs(border_marks) do + local d = m[4]; + + if m[1] ~= mark_id and d.conceal == "" and d.virt_text then + local vt_text = ""; + + for _, c in ipairs(d.virt_text) do + vt_text = vt_text .. (c[1] or ""); + end + + if vt_text:match("^%s+$") then + border_org_visual = border_org_visual + vim.fn.strdisplaywidth(vt_text); + end + end + end + + --- Border leading spaces = target - what org_indent already provides. + local leading = math.max(0, target_indent - border_org_visual); + local vt = mark[3].virt_text; + + if vt[1] and type(vt[1][1]) == "string" and vt[1][1]:match("^%s*$") then + vt[1][1] = string.rep(" ", leading); + end + + vim.api.nvim_buf_set_extmark(buffer, markdown.ns, mark_row, mark[2], { + id = mark_id, + undo_restore = false, invalidate = true, + virt_text_pos = "inline", + virt_text = vt, + hl_mode = "combine", + }); + + ::next_border:: + end + end + end + + --- Wrap continuation borders. + local continuation_vt = item.__continuation_vt; + + if not continuation_vt then + return; + end + + local win = utils.buf_getwin(buffer); + + if not win then + return; + end + + local range = item.range; + + --- Compute the window's text-area width (excluding sign/number columns). + local textoff = vim.fn.getwininfo(win)[1].textoff; + local text_width = vim.api.nvim_win_get_width(win) - textoff; + + vim.api.nvim_win_call(win, function() + for row = range.row_start, range.row_end - 1 do + local height = vim.api.nvim_win_text_height(win, { + start_row = row, end_row = row + }); + + if height.all > 1 then + --- Place right border on first screen row. + --- The concealed right pipe wraps to a continuation line, + --- leaving the first screen row without a right border. + if item.__right_border_vt and item.__table_width then + vim.api.nvim_buf_set_extmark(buffer, markdown.ns, row, 0, { + undo_restore = false, invalidate = true, + virt_text = item.__right_border_vt, + virt_text_win_col = item.__table_width - 1, + hl_mode = "combine", + }); + end + + --- Find the first byte on each continuation (wrapped) + --- screen row via binary search over virtual columns. + --- + --- nvim_win_text_height with start_vcol/end_vcol operates + --- in the same coordinate space Neovim uses for wrapping + --- (raw text width + inline virt_text, ignoring conceal). + --- This makes it the correct predicate for locating wrap + --- boundaries — unlike an analytical walk over rendered + --- widths, which underestimates when cells contain + --- concealed URLs that still count towards wrap width. + --- + --- Upper bound: height.all * text_width is guaranteed to + --- exceed the effective wrap width (including any inline + --- virt_text additions that push past strdisplaywidth). + local lnum = row + 1; + local hi_bound = height.all * text_width; + + for w = 1, height.all - 1 do + local lo, hi = 1, hi_bound; + + while lo < hi do + local mid = math.floor((lo + hi) / 2); + + if vim.api.nvim_win_text_height(win, { + start_row = row, end_row = row, + start_vcol = 0, end_vcol = mid, + }).all <= w then + lo = mid + 1; + else + hi = mid; + end + end + + --- lo is the first vcol on wrap line w+1. + --- Convert to a byte column for the extmark anchor. + local byte_col = vim.fn.virtcol2col(win, lnum, lo); + + if byte_col >= 1 then + vim.api.nvim_buf_set_extmark(buffer, markdown.ns, row, byte_col - 1, { + undo_restore = false, invalidate = true, + virt_text = continuation_vt, + virt_text_win_col = 0, + hl_mode = "combine", + }); + end + end + end + end + end); end diff --git a/lua/markview/renderers/markdown/tostring.lua b/lua/markview/renderers/markdown/tostring.lua index f573ac5..6a73c82 100644 --- a/lua/markview/renderers/markdown/tostring.lua +++ b/lua/markview/renderers/markdown/tostring.lua @@ -364,6 +364,21 @@ md_str.escape = function (match) return char; end +---@param match string +---@return string +md_str.strikethrough = function (match) + ---|fS + + if string.match(match, "%s+%~%~$") then + return match; + end + + local removed = string.gsub(match, "^%~%~", ""):gsub("%~%~$", ""); + return removed; + + ---|fE +end + ---@param match string ---@return string md_str.italic = function (match) @@ -727,11 +742,14 @@ local emoji = lpeg.C( lpeg.P(":") * emoji_char^1 * lpeg.P(":") ) / md_str.emoji; local hl_content = lpeg.P("\\=") + ( 1 - lpeg.P("=") ); local hl = lpeg.C( lpeg.P("==") * hl_content^1 * lpeg.P("==") ) / md_str.highlight; +local strike_content = lpeg.P("\\~") + ( 1 - lpeg.P("~") ); +local strike = lpeg.C( lpeg.P("~~") * strike_content^1 * lpeg.P("~~") ) / md_str.strikethrough; + local any = lpeg.P(1); local token = escape + emoji + entity + - hl + block_ref + embed + internal + + hl + strike + block_ref + embed + internal + email + auto + footnote + img + hyperlink + code + diff --git a/lua/markview/renderers/markdown_inline.lua b/lua/markview/renderers/markdown_inline.lua index b16c552..44ae71f 100644 --- a/lua/markview/renderers/markdown_inline.lua +++ b/lua/markview/renderers/markdown_inline.lua @@ -615,6 +615,11 @@ inline.link_hyperlink = function (buffer, item) hl_group = utils.set_hl(config.hl) }); + --- NOTE: hl_mode must NOT be "combine" here. This extmark conceals + --- the URL portion `](https://…)` which can span hundreds of bytes. + --- With "combine" the virt_text highlight (e.g. underline) bleeds + --- across every concealed byte, producing ghost underlines on the + --- phantom screen rows created by soft-wrap of the hidden text. vim.api.nvim_buf_set_extmark(buffer, inline.ns, r_label[3], r_label[4], { undo_restore = false, invalidate = true, end_row = range.row_end, @@ -626,8 +631,6 @@ inline.link_hyperlink = function (buffer, item) { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) }, { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) } }, - - hl_mode = "combine" }); if r_label[1] == r_label[3] then @@ -734,6 +737,8 @@ inline.link_image = function (buffer, item) hl_group = utils.set_hl(config.hl) }); + --- NOTE: hl_mode must NOT be "combine" here — same reason as link_hyperlink. + --- See the comment there for full explanation. vim.api.nvim_buf_set_extmark(buffer, inline.ns, r_label[3], r_label[4], { undo_restore = false, invalidate = true, end_row = range.row_end, @@ -745,8 +750,6 @@ inline.link_image = function (buffer, item) { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) }, { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) } }, - - hl_mode = "combine" }); if r_label[1] == r_label[3] then diff --git a/test/regression-examples.md b/test/regression-examples.md new file mode 100644 index 0000000..46a4af6 --- /dev/null +++ b/test/regression-examples.md @@ -0,0 +1,229 @@ +# Regression Testing Examples + +Instructions: Open this file with `set wrap` enabled. Walk through each +numbered section matching the regression matrix. + +--- + +## 1 — Concealed content: table with long URLs renders + +The table below has long URLs that get concealed. It should still render +as a proper table (borders, padding, alignment) — not fall back to raw text. + +| Feature | Status | Docs | +|---------|--------|------| +| **Bold** | ✅ Done | [spec](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis-with-asterisks-and-underscores-rule-1-through-17) | +| `code` | ⚠️ Partial | [GFM](https://github.github.com/gfm/#strikethrough-extension-with-tildes-and-double-tildes-for-del-elements) | +| Nested lists | 🔧 WIP | [deep](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#nested-lists-ordered-and-unordered-mixing-indentation-levels) | + +--- + +## 2 — Wrap continuation lines show table borders + +Shrink the window width until the table above wraps. Continuation lines +should show `│...│` table borders — not raw text leaking through. + +(Use the same table from §1 above. Narrow the window to ~60 columns.) + +--- + +## 3 — Strikethrough doesn't inflate column widths + +The `~~Strikethrough~~` column should be the same visual width as the text +without the `~~` markers. Compare column widths with and without. + +| Style | Example | Notes | +|-------|---------|-------| +| Plain | hello | baseline | +| ~~Strikethrough~~ | ~~deleted~~ | width should match "deleted" | +| **Bold** | **strong** | width should match "strong" | + +--- + +## 4 — Table borders use MarkviewTable* highlights + +All table borders (`│`, `─`, `┼`, `╭`, `╮`, etc.) should use `MarkviewTableBorder` +or `MarkviewTableHeader` highlight groups — not treesitter `@punctuation.special`. + +Inspect the borders of this table: + +| A | B | +|---|---| +| 1 | 2 | + +--- + +## 5 — Right border on first screen row of wrapping lines + +When a table row wraps, the **first** screen row of that wrapped line should +still have a right border `│` at the correct position. + +(Use the long-URL table from §1. Shrink window until rows wrap. Check the +right edge of the first screen row of each wrapping line.) + +--- + +## 6 — Top/bottom border indent for nested tables (list context) + +The table below is inside an ordered list. The top and bottom borders should +be indented to align with the table content — not flush-left. + +1. Here is an item with a nested table: + + | Key | Val | + |-----|-----| + | `x` | 42 | + | `y` | 99 | + +2. Another list item. + +And a deeper nesting: + +1. Level 1 + - Level 2 + 1. Level 3 table: + + | A | B | C | + |---|---|---| + | 1 | 2 | 3 | + + 2. Back to list. + +--- + +## 7 — Separator decorations stable in hybrid mode + +Move your cursor in and out of the table below. The separator row decorations +(`─`, `╶`, `┼`) should **not** swap order when the cursor enters/leaves. + +| Left | Center | Right | +|:-----|:------:|------:| +| aaa | bbb | ccc | +| ddd | eee | fff | + +Also test with cursor on this heading, then move into the table above. + +--- + +## 8 — Tables inside blockquotes render fully + +The table inside this blockquote should render the separator row AND all +data rows — not just the header. + +> | Name | Value | +> |------|-------| +> | alpha | 1 | +> | beta | 2 | +> | gamma | 3 | + +Nested blockquote: + +> > | X | Y | +> > |---|---| +> > | a | b | + +--- + +## 9 — Simple table renders correctly (nowrap) + +Set `nowrap`. This simple table should render with proper borders. + +| One | +|-----| +| 1 | + +And a wider one: + +| Col A | Col B | Col C | Col D | +|-------|-------|-------|-------| +| foo | bar | baz | qux | +| alpha | beta | gamma | delta | + +--- + +## 10 — Alignment markers render correctly + +Each column should show the correct alignment decoration in the separator row. + +| Default | Left | Center | Right | +|---------|:-----|:------:|------:| +| none | left | center | right | +| aaa | bbb | ccc | ddd | + +--- + +## 11 — Blockquote borders alongside table borders + +Both the blockquote border (`▋`) and table borders (`│`) should be visible +side by side. + +> | Animal | Sound | +> |--------|-------| +> | Cat | Meow | +> | Dog | Woof | + +--- + +## 12 — Hybrid mode: cursor-line un-renders/re-renders cleanly + +Move your cursor row-by-row through this table. Each row should un-render +when the cursor is on it (showing raw markdown) and re-render when the +cursor leaves. + +| Language | Typing | Speed | +|----------|--------|-------| +| Lua | dynamic | fast | +| Rust | static | fast | +| Python | dynamic | moderate | +| C | static | very fast | + +--- + +## 13 — Code blocks inside lists render correctly + +The code block below is nested inside a list. It should render with proper +syntax highlighting and code-block decorations. + +1. **First item** + - Sub-item with code: + + ```lua + local M = {} + function M.setup(opts) + return vim.tbl_deep_extend("force", {}, opts or {}) + end + return M + ``` + + - Another sub-item + +2. **Second item** + +--- + +## 14 — Headings, horizontal rules, inline formatting unaffected + +### This is an H3 + +#### This is an H4 + +##### This is an H5 + +--- + +Inline formatting: **bold**, *italic*, ***bold-italic***, `inline code`, +~~strikethrough~~, and [a link](https://example.com). + +A horizontal rule below: + +--- + +And another: + +*** + +--- + +## End of regression examples + +Replace ☐ with ✅ or ❌ in `test/regression-matrix.md` as you test each case. diff --git a/test/stress.md b/test/stress.md new file mode 100644 index 0000000..559a245 --- /dev/null +++ b/test/stress.md @@ -0,0 +1,130 @@ +Here's a stress test for your markdown renderer: + +--- + +### Feature Matrix + + +| Feature | Status | Docs | +|---------|--------|------| +| **Bold** & *Italic* | ✅ Done | [spec](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis-with-asterisks-and-underscores-rule-1-through-17) | +| ~~Strikethrough~~ | ⚠️ Partial | [GFM](https://github.github.com/gfm/#strikethrough-extension-with-tildes-and-double-tildes-for-del-elements) | +| `inline code` | ✅ Done | [ref](https://spec.commonmark.org/0.31.2/#code-spans-backtick-strings-and-their-matching-rules-for-inline-code) | +| Nested lists | 🔧 WIP | [deep](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#nested-lists-ordered-and-unordered-mixing-indentation-levels) | + +### Inline Conceal Torture + +| Kind | Example | With long URL | +|------|---------|---------------| +| Hyperlink | [short](https://example.com) | [Neovim API reference](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark()-nvim_buf_del_extmark()-nvim_buf_get_extmarks()-and-related-extmark-functions) | +| Image | ![icon](https://example.com/icon.svg) | ![screenshot of the full treesitter playground](https://raw.githubusercontent.com/nvim-treesitter/playground/master/assets/screenshot-with-custom-queries-and-hl-groups.png) | +| URI autolink | | | +| Email autolink | | | +| Inline code | `short` | `vim.api.nvim_buf_set_extmark(buffer, ns, row, col, opts)` | +| Highlight | ==marked== | ==this is a rather long highlighted span that should test wrapping== | +| Entity | & and < | & < > → ← ♥ ∞ — | +| Escaped | \* not bold \* | \* \[ \] \( \) \` \~ \\ \# \! | +| Emoji | :rocket: launch | :tada: :sparkles: :rocket: :fire: :bug: :memo: :bulb: :wrench: | +| Footnote | see [^1] | see [^long-descriptive-footnote-name-that-tests-width] | +| Bold + link | **[bold link](https://example.com)** | **[bold link with long URL](https://spec.commonmark.org/0.31.2/#emphasis-and-strong-emphasis-combined-with-links-and-images)** | +| Code + link | `code` then [link](https://a.co) | `vim.api.nvim_buf_set_extmark()` then [docs](https://neovim.io/doc/user/api.html#nvim_buf_set_extmark()-full-details) | +| Multi-conceal | **bold** `code` *italic* [lnk](https://x.co) | **bold** `code` *ital* ==hl== [lnk](https://neovim.io/doc/user/api.html#multi-conceal-stress-test-row) :rocket: | + +[^1]: A short footnote. +[^long-descriptive-footnote-name-that-tests-width]: This footnote has a very long reference label to test how concealment handles it in table cells. + +### Alignment Torture + +| Left | Center | Right | Mixed | +|:-----|:------:|------:|-------| +| `vim.api` | **strong** | 42 | [API](https://neovim.io/doc/user/api.html#nvim_buf_set_lines()-nvim_buf_get_lines()-and-other-buffer-manipulation-functions) | +| `vim.lsp` | *emphasis* | 3.14 | [LSP](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion_resolve_and_other_request_types) | +| `vim.treesitter` | ***both*** | 0xDEAD | [TS](https://tree-sitter.github.io/tree-sitter/using-parsers/queries/pattern-matching-with-predicates-and-anchors#the-match-predicate) | + +### Nested Structures + +1. **First level** + - Bullet with `code` and [a link](https://example.com) + - Another bullet + 1. Ordered inside unordered + 2. With a table inside: + + | Key | Val | + |-----|-----| + | `a` | 1 | + + 3. Back to the list + - > A blockquote inside a list item + > spanning multiple lines +2. **Second level** — with a long code block: + + ````lua + local M = {} + -- nested code fences should survive + function M.setup(opts) + opts = vim.tbl_deep_extend("force", { + enabled = true, + style = { bold = true, italic = false }, + }, opts or {}) + return opts + end + return M + ```` + +3. ***Third*** with a task list: + - [x] Completed task + - [ ] Pending task + - [ ] Another one + +### Inline Chaos + +This paragraph has **bold**, *italic*, ***bold-italic***, `inline code`, ~~deleted~~, and [a very descriptively titled link](https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.29.1.tgz#some-very-long-anchor-fragment-that-keeps-going-and-going-forever) all in one sentence. + +### Fenced Blocks Parade + +````python +# Python +def f(x: int) -> dict[str, list[int]]: + return {"result": [i**2 for i in range(x)]} +```` + +````bash +# Shell with pipes +cat /proc/cpuinfo | grep -i "model name" | head -1 | awk -F: '{print $2}' +```` + +````json +{ + "nested": { "deep": { "value": [1, 2, 3] } }, + "escaped": "quotes \"inside\" strings" +} +```` + +### Horizontal Rules vs. Table Edges + +--- + +| Single col | +|------------| +| lonely | + +--- + +> ### Blockquote with heading +> And a table: +> +> | A | B | +> |---|---| +> | 1 | 2 | +> +> And some `code` too. + +### Math-ish (if supported) + +Euler: $e^{i\pi} + 1 = 0$ + +$$ +\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} +$$ + +---