Skip to content

Commit 8c71558

Browse files
fix: Raw tables match full when rendered uniformally
## Details The current logic for rendering a full table only checks the raw text to make sure the widths line up. This makes sense for 'overlay' & 'padded' tables but not so much for 'raw' tables. Rather than using the same computed width use the width after text has been concealed and inline elements have been added. This is essentially the inverse operation of padded. Where for padded tables we fill any removed space, for raw tables we use the removed space as the width. Other minor changes: - Add a class to each module to improve LSP diagnostics, immediately calls out invalid method calls - Move width calculation into str module, will make changing it in the future easier as well as making consistency more likely - Refactor table rendering into multiple top level methods - Add a shared module for resolving link icons in the same way for markdown & markdown_inline handlers
1 parent 9b7fdea commit 8c71558

18 files changed

+293
-252
lines changed

Diff for: README.md

+1-3
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,7 @@ require('render-markdown').setup({
148148
149149
(shortcut_link) @callout
150150
151-
(inline_link) @link
152-
153-
(image) @image
151+
[(inline_link) (image)] @link
154152
]],
155153
-- Query to be able to identify links in nodes
156154
inline_link_query = '[(inline_link) (image)] @link',

Diff for: doc/render-markdown.txt

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For 0.10.0 Last change: 2024 July 11
1+
*render-markdown.txt* For 0.10.0 Last change: 2024 July 12
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*
@@ -185,9 +185,7 @@ Full Default Configuration ~
185185

186186
(shortcut_link) @callout
187187

188-
(inline_link) @link
189-
190-
(image) @image
188+
[(inline_link) (image)] @link
191189
]],
192190
-- Query to be able to identify links in nodes
193191
inline_link_query = '[(inline_link) (image)] @link',

Diff for: lua/render-markdown/component.lua

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ local state = require('render-markdown.state')
44
---@field text string
55
---@field highlight string
66

7+
---@class render.md.ComponentHelper
78
local M = {}
89

910
---@param text string

Diff for: lua/render-markdown/handler/latex.lua

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ local cache = {
1010
expressions = {},
1111
}
1212

13+
---@class render.md.handler.Latex
1314
local M = {}
1415

1516
---@param namespace integer

Diff for: lua/render-markdown/handler/markdown.lua

+145-117
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ local component = require('render-markdown.component')
22
local icons = require('render-markdown.icons')
33
local list = require('render-markdown.list')
44
local logger = require('render-markdown.logger')
5+
local shared = require('render-markdown.handler.shared')
56
local state = require('render-markdown.state')
67
local str = require('render-markdown.str')
78
local ts = require('render-markdown.ts')
89
local util = require('render-markdown.util')
910

11+
---@class render.md.handler.Markdown
1012
local M = {}
1113

1214
---@param namespace integer
@@ -32,15 +34,15 @@ M.render_node = function(namespace, buf, capture, node)
3234
if not heading.enabled then
3335
return
3436
end
35-
local level = vim.fn.strdisplaywidth(info.text)
37+
local level = str.width(info.text)
3638

3739
local icon = list.cycle(heading.icons, level)
3840
local background = list.clamp(heading.backgrounds, level)
3941
local foreground = list.clamp(heading.foregrounds, level)
4042

4143
-- Available width is level + 1, where level = number of `#` characters and one is
4244
-- added to account for the space after the last `#` but before the heading title
43-
local padding = level + 1 - vim.fn.strdisplaywidth(icon)
45+
local padding = level + 1 - str.width(icon)
4446

4547
vim.api.nvim_buf_set_extmark(buf, namespace, info.start_row, 0, {
4648
end_row = info.end_row + 1,
@@ -212,85 +214,7 @@ M.render_node = function(namespace, buf, capture, node)
212214
virt_text_pos = 'overlay',
213215
})
214216
elseif capture == 'table' then
215-
local pipe_table = state.config.pipe_table
216-
if not pipe_table.enabled then
217-
return
218-
end
219-
if pipe_table.style == 'none' then
220-
return
221-
end
222-
local border = pipe_table.border
223-
224-
local function render_table_full()
225-
local delim_node = ts.child(info.node, 'pipe_table_delimiter_row')
226-
if delim_node == nil then
227-
return
228-
end
229-
230-
local delim = ts.info(delim_node, buf)
231-
local lines = vim.api.nvim_buf_get_lines(buf, info.start_row, info.end_row, true)
232-
233-
local delim_width = vim.fn.strdisplaywidth(delim.text)
234-
local start_width = vim.fn.strdisplaywidth(list.first(lines))
235-
local end_width = vim.fn.strdisplaywidth(list.last(lines))
236-
if delim_width ~= start_width or start_width ~= end_width then
237-
return
238-
end
239-
240-
local headings = vim.split(delim.text, '|', { plain = true, trimempty = true })
241-
local lengths = vim.tbl_map(function(cell)
242-
return border[11]:rep(vim.fn.strdisplaywidth(cell))
243-
end, headings)
244-
245-
local line_above = border[1] .. table.concat(lengths, border[2]) .. border[3]
246-
vim.api.nvim_buf_set_extmark(buf, namespace, info.start_row, info.start_col, {
247-
virt_lines_above = true,
248-
virt_lines = { { { line_above, pipe_table.head } } },
249-
})
250-
251-
local line_below = border[7] .. table.concat(lengths, border[8]) .. border[9]
252-
vim.api.nvim_buf_set_extmark(buf, namespace, info.end_row, info.start_col, {
253-
virt_lines_above = true,
254-
virt_lines = { { { line_below, pipe_table.row } } },
255-
})
256-
end
257-
258-
---@param row_info render.md.NodeInfo
259-
local function render_table_delimiter(row_info)
260-
-- Order matters here, in particular handling inner intersections before left & right
261-
local row = row_info.text
262-
:gsub(' ', '-')
263-
:gsub('%-|%-', border[11] .. border[5] .. border[11])
264-
:gsub('|%-', border[4] .. border[11])
265-
:gsub('%-|', border[11] .. border[6])
266-
:gsub('%-', border[11])
267-
268-
vim.api.nvim_buf_set_extmark(buf, namespace, row_info.start_row, row_info.start_col, {
269-
end_row = row_info.end_row,
270-
end_col = row_info.end_col,
271-
virt_text = { { row, pipe_table.head } },
272-
virt_text_pos = 'overlay',
273-
})
274-
end
275-
276-
if pipe_table.style == 'full' then
277-
render_table_full()
278-
end
279-
280-
for row in info.node:iter_children() do
281-
local row_info = ts.info(row, buf)
282-
local row_type = row_info.node:type()
283-
if row_type == 'pipe_table_delimiter_row' then
284-
render_table_delimiter(row_info)
285-
elseif row_type == 'pipe_table_header' then
286-
M.render_table_row(namespace, buf, row_info, pipe_table.head)
287-
elseif row_type == 'pipe_table_row' then
288-
M.render_table_row(namespace, buf, row_info, pipe_table.row)
289-
else
290-
-- Should only get here if markdown introduces more row types, currently unhandled
291-
logger.error('Unhandled markdown row type: ' .. row_type)
292-
end
293-
end
217+
M.render_table(namespace, buf, info)
294218
else
295219
-- Should only get here if user provides custom capture, currently unhandled
296220
logger.error('Unhandled markdown capture: ' .. capture)
@@ -300,62 +224,166 @@ end
300224
---@param namespace integer
301225
---@param buf integer
302226
---@param info render.md.NodeInfo
303-
---@param highlight string
304-
M.render_table_row = function(namespace, buf, info, highlight)
305-
---@param text string
306-
---@return integer
307-
local function inline_width(text)
308-
local query = state.inline_link_query
309-
local tree = vim.treesitter.get_string_parser(text, 'markdown_inline')
310-
local result = 0
311-
for id in query:iter_captures(tree:parse()[1]:root(), text) do
312-
if query.captures[id] == 'link' then
313-
result = result + vim.fn.strdisplaywidth(state.config.link.hyperlink)
227+
M.render_table = function(namespace, buf, info)
228+
local pipe_table = state.config.pipe_table
229+
if not pipe_table.enabled then
230+
return
231+
end
232+
if pipe_table.style == 'none' then
233+
return
234+
end
235+
local delim = nil
236+
local first = nil
237+
local last = nil
238+
for row_node in info.node:iter_children() do
239+
local row = ts.info(row_node, buf)
240+
if row.type == 'pipe_table_delimiter_row' then
241+
delim = row
242+
M.render_table_delimiter(namespace, buf, row)
243+
elseif row.type == 'pipe_table_header' then
244+
first = row
245+
M.render_table_row(namespace, buf, row, pipe_table.head)
246+
elseif row.type == 'pipe_table_row' then
247+
if last == nil or row.start_row > last.start_row then
248+
last = row
314249
end
250+
M.render_table_row(namespace, buf, row, pipe_table.row)
251+
else
252+
-- Should only get here if markdown introduces more row types, currently unhandled
253+
logger.error('Unhandled markdown row type: ' .. row.type)
315254
end
316-
return result
317255
end
256+
if pipe_table.style == 'full' then
257+
M.render_table_full(namespace, buf, delim, first, last)
258+
end
259+
end
318260

261+
---@param namespace integer
262+
---@param buf integer
263+
---@param row render.md.NodeInfo
264+
M.render_table_delimiter = function(namespace, buf, row)
319265
local pipe_table = state.config.pipe_table
320266
local border = pipe_table.border
267+
-- Order matters here, in particular handling inner intersections before left & right
268+
local delimiter = row.text
269+
:gsub(' ', '-')
270+
:gsub('%-|%-', border[11] .. border[5] .. border[11])
271+
:gsub('|%-', border[4] .. border[11])
272+
:gsub('%-|', border[11] .. border[6])
273+
:gsub('%-', border[11])
321274

275+
vim.api.nvim_buf_set_extmark(buf, namespace, row.start_row, row.start_col, {
276+
end_row = row.end_row,
277+
end_col = row.end_col,
278+
virt_text = { { delimiter, pipe_table.head } },
279+
virt_text_pos = 'overlay',
280+
})
281+
end
282+
283+
---@param namespace integer
284+
---@param buf integer
285+
---@param row render.md.NodeInfo
286+
---@param highlight string
287+
M.render_table_row = function(namespace, buf, row, highlight)
288+
local pipe_table = state.config.pipe_table
322289
if vim.tbl_contains({ 'raw', 'padded' }, pipe_table.cell) then
323-
for cell in info.node:iter_children() do
324-
local cell_info = ts.info(cell, buf)
325-
local cell_type = cell_info.node:type()
326-
if cell_type == '|' then
327-
vim.api.nvim_buf_set_extmark(buf, namespace, cell_info.start_row, cell_info.start_col, {
328-
end_row = cell_info.end_row,
329-
end_col = cell_info.end_col,
330-
virt_text = { { border[10], highlight } },
290+
for cell_node in row.node:iter_children() do
291+
local cell = ts.info(cell_node, buf)
292+
if cell.type == '|' then
293+
vim.api.nvim_buf_set_extmark(buf, namespace, cell.start_row, cell.start_col, {
294+
end_row = cell.end_row,
295+
end_col = cell.end_col,
296+
virt_text = { { pipe_table.border[10], highlight } },
331297
virt_text_pos = 'overlay',
332298
})
333-
elseif cell_type == 'pipe_table_cell' then
334-
if pipe_table.cell == 'padded' then
335-
-- Requires inline extmarks
336-
if util.has_10 then
337-
local concealed = ts.concealed(buf, cell_info) - inline_width(cell_info.text)
338-
if concealed > 0 then
339-
vim.api.nvim_buf_set_extmark(buf, namespace, cell_info.start_row, cell_info.end_col, {
340-
virt_text = { { str.pad('', concealed), pipe_table.filler } },
341-
virt_text_pos = 'inline',
342-
})
343-
end
299+
elseif cell.type == 'pipe_table_cell' then
300+
-- Requires inline extmarks
301+
if pipe_table.cell == 'padded' and util.has_10 then
302+
local offset = M.table_visual_offset(buf, cell)
303+
if offset > 0 then
304+
vim.api.nvim_buf_set_extmark(buf, namespace, cell.start_row, cell.end_col, {
305+
virt_text = { { str.pad('', offset), pipe_table.filler } },
306+
virt_text_pos = 'inline',
307+
})
344308
end
345309
end
346310
else
347311
-- Should only get here if markdown introduces more cell types, currently unhandled
348-
logger.error('Unhandled markdown cell type: ' .. cell_type)
312+
logger.error('Unhandled markdown cell type: ' .. cell.type)
349313
end
350314
end
351315
elseif pipe_table.cell == 'overlay' then
352-
vim.api.nvim_buf_set_extmark(buf, namespace, info.start_row, info.start_col, {
353-
end_row = info.end_row,
354-
end_col = info.end_col,
355-
virt_text = { { info.text:gsub('|', border[10]), highlight } },
316+
vim.api.nvim_buf_set_extmark(buf, namespace, row.start_row, row.start_col, {
317+
end_row = row.end_row,
318+
end_col = row.end_col,
319+
virt_text = { { row.text:gsub('|', pipe_table.border[10]), highlight } },
356320
virt_text_pos = 'overlay',
357321
})
358322
end
359323
end
360324

325+
---@param namespace integer
326+
---@param buf integer
327+
---@param delim? render.md.NodeInfo
328+
---@param first? render.md.NodeInfo
329+
---@param last? render.md.NodeInfo
330+
M.render_table_full = function(namespace, buf, delim, first, last)
331+
local pipe_table = state.config.pipe_table
332+
local border = pipe_table.border
333+
if delim == nil or first == nil or last == nil then
334+
return
335+
end
336+
337+
---@param info render.md.NodeInfo
338+
---@return integer
339+
local function width(info)
340+
local result = str.width(info.text)
341+
if pipe_table.cell == 'raw' then
342+
-- For the raw cell style we want the lengths to match after
343+
-- concealing & inlined elements
344+
result = result - M.table_visual_offset(buf, info)
345+
end
346+
return result
347+
end
348+
349+
-- Do not need to account for concealed / inlined text on delimiter row
350+
local delim_width = str.width(delim.text)
351+
if delim_width ~= width(first) or delim_width ~= width(last) then
352+
return
353+
end
354+
355+
local headings = vim.split(delim.text, '|', { plain = true, trimempty = true })
356+
local lengths = vim.tbl_map(function(cell)
357+
return border[11]:rep(str.width(cell))
358+
end, headings)
359+
360+
local line_above = border[1] .. table.concat(lengths, border[2]) .. border[3]
361+
vim.api.nvim_buf_set_extmark(buf, namespace, first.start_row, first.start_col, {
362+
virt_lines_above = true,
363+
virt_lines = { { { line_above, pipe_table.head } } },
364+
})
365+
366+
local line_below = border[7] .. table.concat(lengths, border[8]) .. border[9]
367+
vim.api.nvim_buf_set_extmark(buf, namespace, last.start_row, last.start_col, {
368+
virt_lines_above = false,
369+
virt_lines = { { { line_below, pipe_table.row } } },
370+
})
371+
end
372+
373+
---@param buf integer
374+
---@param info render.md.NodeInfo
375+
---@return integer
376+
M.table_visual_offset = function(buf, info)
377+
local result = ts.concealed(buf, info)
378+
local query = state.inline_link_query
379+
local tree = vim.treesitter.get_string_parser(info.text, 'markdown_inline')
380+
for id, node in query:iter_captures(tree:parse()[1]:root(), info.text) do
381+
if query.captures[id] == 'link' then
382+
local link_info = ts.info(node, info.text)
383+
result = result - str.width(shared.link_icon(link_info))
384+
end
385+
end
386+
return result
387+
end
388+
361389
return M

0 commit comments

Comments
 (0)