Skip to content

Commit 166a254

Browse files
feat: handle imperfectly spaced tables with inline padding
## Details Currently when rendering tables we take any concealed space and fill that back in with padding. This effectively means tables will look nice if they are evenly spaced in their raw text form and otherwise any poorly aligned text will propagate when rendered. However we can be smarter about how we calculate the padding needed. Instead of trying to get back to the raw view when rendering we now instead calculate the maximum width for every column taking into account concealed text. From their we caclulate the amount of padding needed from this maximum value instead. This means nicely aligned tables continue to work in the exact same way as before but tables with all sorts of random spacing should now also look beautful. I will likely follow up with respecting text alignment since this should be easier to do now, woo!
1 parent ced4a66 commit 166a254

File tree

8 files changed

+240
-156
lines changed

8 files changed

+240
-156
lines changed

Diff for: README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ Plugin to improve viewing Markdown files in Neovim
5555
- List bullet points: replace with provided icon based on level
5656
- Checkboxes: replace with provided icon based on whether they are checked
5757
- Block quotes: replace leading `>` with provided icon
58-
- Tables: replace border characters, does NOT automatically align
58+
- Tables: replace border characters, handles misaligned tables but does NOT align
59+
according to delimiter indicator
5960
- [Callouts](https://github.com/orgs/community/discussions/16925)
6061
- Github & Obsidian out of the box, supports user defined as well
6162
- Custom checkbox states [^1], function similar to `callouts`

Diff for: benches/medium_table_spec.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ local util = require('benches.util')
44

55
describe('medium-table.md', function()
66
it('default', function()
7-
local base_marks = 468
7+
local base_marks = 180
88
util.less_than(util.setup('temp/medium-table.md'), 105)
99
util.num_marks(base_marks)
1010

Diff for: benches/readme_spec.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ local util = require('benches.util')
44

55
describe('README.md', function()
66
it('default', function()
7-
local base_marks = 54
7+
local base_marks = 45
88
util.less_than(util.setup('README.md'), 50)
99
util.num_marks(base_marks)
1010

1111
util.less_than(util.move_down(1), 0.5)
1212
util.num_marks(base_marks + 2)
1313

14-
util.less_than(util.insert_mode(), 20)
14+
util.less_than(util.insert_mode(), 5)
1515
util.num_marks(base_marks + 2)
1616
end)
1717
end)

Diff for: doc/render-markdown.txt

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

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*
@@ -74,7 +74,8 @@ Plugin to improve viewing Markdown files in Neovim
7474
- List bullet points: replace with provided icon based on level
7575
- Checkboxes: replace with provided icon based on whether they are checked
7676
- Block quotes: replace leading `>` with provided icon
77-
- Tables: replace border characters, does NOT automatically align
77+
- Tables: replace border characters, handles misaligned tables but does NOT align
78+
according to delimiter indicator
7879
- Callouts <https://github.com/orgs/community/discussions/16925>
7980
- Github & Obsidian out of the box, supports user defined as well
8081
- Custom checkbox states , function similar to `callouts`

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

+6
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ function Context:contains_node(node)
7474
return top < self.bottom and bottom >= self.top
7575
end
7676

77+
---@param info render.md.NodeInfo
78+
---@return boolean
79+
function Context:contains_info(info)
80+
return info.start_row < self.bottom and info.end_row >= self.top
81+
end
82+
7783
---@param root TSNode
7884
---@param query vim.treesitter.Query
7985
---@param callback fun(capture: string, node: TSNode, metadata: vim.treesitter.query.TSMetadata)

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

+88-93
Original file line numberDiff line numberDiff line change
@@ -519,93 +519,91 @@ function Handler:pipe_table(info)
519519
if not pipe_table.enabled or pipe_table.style == 'none' then
520520
return
521521
end
522-
local parsed_table = pipe_table_parser.parse(info)
522+
local parsed_table = pipe_table_parser.parse(self.context, info)
523523
if parsed_table == nil then
524524
return
525525
end
526526

527-
self:table_row(parsed_table.head, pipe_table.head)
528-
self:table_delimiter(parsed_table.delim, parsed_table.columns)
527+
self:table_delimiter(parsed_table.delim)
529528
for _, row in ipairs(parsed_table.rows) do
530-
self:table_row(row, pipe_table.row)
529+
self:table_row(parsed_table.delim, row)
531530
end
532531
if pipe_table.style == 'full' then
533532
self:table_full(parsed_table)
534533
end
535534
end
536535

537536
---@private
538-
---@param row render.md.NodeInfo
539-
---@param columns render.md.parsed.TableColumn[]
540-
function Handler:table_delimiter(row, columns)
537+
---@param delim render.md.parsed.table.DelimRow
538+
function Handler:table_delimiter(delim)
541539
local pipe_table = self.config.pipe_table
542540
local indicator = pipe_table.alignment_indicator
543541
local border = pipe_table.border
544-
local sections = vim.tbl_map(
545-
---@param column render.md.parsed.TableColumn
546-
---@return string
547-
function(column)
548-
-- If column is small there's no good place to put the alignment indicator
549-
-- Alignment indicator must be exactly one character wide
550-
-- We do not put an indicator for default alignment
551-
if column.width < 4 or str.width(indicator) ~= 1 or column.alignment == 'default' then
552-
return border[11]:rep(column.width)
553-
end
554-
-- Handle the various alignmnet possibilities
555-
local left = border[11]:rep(math.floor(column.width / 2))
556-
local right = border[11]:rep(math.ceil(column.width / 2) - 1)
557-
if column.alignment == 'left' then
558-
return indicator .. left .. right
559-
elseif column.alignment == 'right' then
560-
return left .. right .. indicator
561-
else
562-
return left .. indicator .. right
563-
end
564-
end,
565-
columns
566-
)
542+
local sections = vim.tbl_map(function(column)
543+
-- If column is small there's no good place to put the alignment indicator
544+
-- Alignment indicator must be exactly one character wide
545+
-- Do not put an indicator for default alignment
546+
if column.width < 4 or str.width(indicator) ~= 1 or column.alignment == 'default' then
547+
return border[11]:rep(column.width)
548+
end
549+
-- Handle the various alignmnet possibilities
550+
local left = border[11]:rep(math.floor(column.width / 2))
551+
local right = border[11]:rep(math.ceil(column.width / 2) - 1)
552+
if column.alignment == 'left' then
553+
return indicator .. left .. right
554+
elseif column.alignment == 'right' then
555+
return left .. right .. indicator
556+
else
557+
return left .. indicator .. right
558+
end
559+
end, delim.columns)
567560
local delimiter = border[4] .. table.concat(sections, border[5]) .. border[6]
568-
self:add(true, row.start_row, row.start_col, {
569-
end_row = row.end_row,
570-
end_col = row.end_col,
561+
self:add(true, delim.info.start_row, delim.info.start_col, {
562+
end_row = delim.info.end_row,
563+
end_col = delim.info.end_col,
571564
virt_text = { { delimiter, pipe_table.head } },
572565
virt_text_pos = 'overlay',
573566
})
574567
end
575568

576569
---@private
577-
---@param row render.md.NodeInfo
578-
---@param highlight string
579-
function Handler:table_row(row, highlight)
570+
---@param delim render.md.parsed.table.DelimRow
571+
---@param row render.md.parsed.table.Row
572+
function Handler:table_row(delim, row)
580573
local pipe_table = self.config.pipe_table
581-
if vim.tbl_contains({ 'raw', 'padded' }, pipe_table.cell) then
582-
row:for_each_child(function(cell)
583-
if cell.type == '|' then
584-
self:add(true, cell.start_row, cell.start_col, {
585-
end_row = cell.end_row,
586-
end_col = cell.end_col,
587-
virt_text = { { pipe_table.border[10], highlight } },
588-
virt_text_pos = 'overlay',
574+
local highlight = row.info.type == 'pipe_table_header' and pipe_table.head or pipe_table.row
575+
if pipe_table.cell == 'padded' then
576+
for _, pipe in ipairs(row.pipes) do
577+
self:add(true, pipe.start_row, pipe.start_col, {
578+
end_row = pipe.end_row,
579+
end_col = pipe.end_col,
580+
virt_text = { { pipe_table.border[10], highlight } },
581+
virt_text_pos = 'overlay',
582+
})
583+
end
584+
for i, column in ipairs(row.columns) do
585+
local offset = delim.columns[i].width - column.width
586+
if offset > 0 then
587+
self:add(true, column.info.start_row, column.info.end_col - 1, {
588+
virt_text = { { str.pad(offset), pipe_table.filler } },
589+
virt_text_pos = 'inline',
589590
})
590-
elseif cell.type == 'pipe_table_cell' then
591-
if pipe_table.cell == 'padded' then
592-
local offset = self:table_visual_offset(cell)
593-
if offset > 0 then
594-
self:add(true, cell.start_row, cell.end_col - 1, {
595-
virt_text = { { str.pad(offset), pipe_table.filler } },
596-
virt_text_pos = 'inline',
597-
})
598-
end
599-
end
600-
else
601-
logger.unhandled_type('markdown', 'cell', cell.type)
602591
end
603-
end)
592+
end
593+
elseif pipe_table.cell == 'raw' then
594+
for _, pipe in ipairs(row.pipes) do
595+
self:add(true, pipe.start_row, pipe.start_col, {
596+
end_row = pipe.end_row,
597+
end_col = pipe.end_col,
598+
virt_text = { { pipe_table.border[10], highlight } },
599+
virt_text_pos = 'overlay',
600+
})
601+
end
604602
elseif pipe_table.cell == 'overlay' then
605-
self:add(true, row.start_row, row.start_col, {
606-
end_row = row.end_row,
607-
end_col = row.end_col,
608-
virt_text = { { row.text:gsub('|', pipe_table.border[10]), highlight } },
603+
self:add(true, row.info.start_row, row.info.start_col, {
604+
end_row = row.info.end_row,
605+
end_col = row.info.end_col,
606+
virt_text = { { row.info.text:gsub('|', pipe_table.border[10]), highlight } },
609607
virt_text_pos = 'overlay',
610608
})
611609
end
@@ -617,56 +615,53 @@ function Handler:table_full(parsed_table)
617615
local pipe_table = self.config.pipe_table
618616
local border = pipe_table.border
619617

620-
---@param info render.md.NodeInfo
621-
---@return integer
622-
local function width(info)
623-
local result = str.width(info.text)
624-
if pipe_table.cell == 'raw' then
625-
-- For the raw cell style we want the lengths to match after
626-
-- concealing & inlined elements
627-
result = result - self:table_visual_offset(info)
618+
---@param row render.md.parsed.table.Row
619+
---@param delim render.md.parsed.table.DelimRow
620+
---@return boolean
621+
local function width_equal(row, delim)
622+
if pipe_table.cell == 'padded' then
623+
-- Assume table was padded to match
624+
return true
625+
elseif pipe_table.cell == 'raw' then
626+
-- Want the computed widths to match
627+
for i, column in ipairs(row.columns) do
628+
if delim.columns[i].width ~= column.width then
629+
return false
630+
end
631+
end
632+
return true
633+
elseif pipe_table.cell == 'overlay' then
634+
-- Want the underlying text widths to match
635+
return str.width(delim.info.text) == str.width(row.info.text)
636+
else
637+
return false
628638
end
629-
return result
630639
end
631640

632-
local first = parsed_table.head
641+
local delim = parsed_table.delim
642+
local first = parsed_table.rows[1]
633643
local last = parsed_table.rows[#parsed_table.rows]
634-
635-
-- Do not need to account for concealed / inlined text on delimiter row
636-
local delim_width = str.width(parsed_table.delim.text)
637-
if delim_width ~= width(first) or delim_width ~= width(last) then
638-
return {}
644+
if not width_equal(first, delim) or not width_equal(last, delim) then
645+
return
639646
end
640647

641-
local sections = vim.tbl_map(
642-
---@param column render.md.parsed.TableColumn
643-
---@return string
644-
function(column)
645-
return border[11]:rep(column.width)
646-
end,
647-
parsed_table.columns
648-
)
648+
local sections = vim.tbl_map(function(column)
649+
return border[11]:rep(column.width)
650+
end, delim.columns)
649651

650652
local line_above = border[1] .. table.concat(sections, border[2]) .. border[3]
651-
self:add(false, first.start_row, first.start_col, {
653+
self:add(false, first.info.start_row, first.info.start_col, {
652654
virt_lines_above = true,
653655
virt_lines = { { { line_above, pipe_table.head } } },
654656
})
655657

656658
local line_below = border[7] .. table.concat(sections, border[8]) .. border[9]
657-
self:add(false, last.start_row, last.start_col, {
659+
self:add(false, last.info.start_row, last.info.start_col, {
658660
virt_lines_above = false,
659661
virt_lines = { { { line_below, pipe_table.row } } },
660662
})
661663
end
662664

663-
---@private
664-
---@param info render.md.NodeInfo
665-
---@return integer
666-
function Handler:table_visual_offset(info)
667-
return self.context:concealed(info) - self.context:link_width(info)
668-
end
669-
670665
---@class render.md.handler.Markdown: render.md.Handler
671666
local M = {}
672667

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local M = {}
55

66
---@private
77
---@type string
8-
M.version = '6.0.13'
8+
M.version = '6.1.1'
99

1010
function M.check()
1111
vim.health.start('render-markdown.nvim [version]')

0 commit comments

Comments
 (0)