Skip to content

Commit a273033

Browse files
feat: Add alignment indicator icons to tables
## Details Add configurable alignment_indicator field to pipe_table. The indicator is positioned in the left, right, or center of every column with alignment information. The indicator must be exactly a single display width. While the change is simple at a high level, to implement it we needed to really parse through the delimiter row. Previously we did some very simply gsub replacement on the entire line. But now we need to compute the width between pipes to accurately measure what's underneath since cells themselves are free to occupy as much or as little space as they like. As a result a parser needed to be added which mostly iterates through the delimiter row and does a good amount of sanity checking before doing anything with the table. This information can likely be used to improve how tables are handled in the future, for now we just add the indicator. Alignmnet itself is completely up to the user.
1 parent 87b9fda commit a273033

File tree

11 files changed

+235
-73
lines changed

11 files changed

+235
-73
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,8 @@ require('render-markdown').setup({
333333
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
334334
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
335335
cell = 'padded',
336+
-- Gets placed in delimiter row for each column, position is based on alignmnet
337+
alignment_indicator = '',
336338
-- Characters used to replace table border
337339
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
338340
-- stylua: ignore
@@ -614,6 +616,8 @@ require('render-markdown').setup({
614616
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
615617
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
616618
cell = 'padded',
619+
-- Gets placed in delimiter row for each column, position is based on alignmnet
620+
alignment_indicator = '',
617621
-- Characters used to replace table border
618622
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
619623
-- stylua: ignore

demo/list_table.gif

-95 Bytes
Loading

demo/list_table.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313

1414
1. Item 1
1515
2. Item 2
16-
3. Item 3
1716

1817
# Table
1918

20-
| `Inline Heading` | *Italic Heading* |
21-
| ---------------- | ------------------ |
22-
| Regular Item | **Bold Item** |
23-
| `Item Inline` | [Link Item](/test) |
19+
| `Left` | *Center* | Right | None |
20+
| :--- | :----: |------:| -----|
21+
| `Code` | **Bold** | Plain | Item |
22+
| Item | [link](/test) | Item | Item |
2423

2524
[example]: https://example.com

doc/render-markdown.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ Full Default Configuration ~
374374
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
375375
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
376376
cell = 'padded',
377+
-- Gets placed in delimiter row for each column, position is based on alignmnet
378+
alignment_indicator = '━',
377379
-- Characters used to replace table border
378380
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
379381
-- stylua: ignore
@@ -660,6 +662,8 @@ TABLES *render-markdown-setup-tables*
660662
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
661663
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
662664
cell = 'padded',
665+
-- Gets placed in delimiter row for each column, position is based on alignmnet
666+
alignment_indicator = '━',
663667
-- Characters used to replace table border
664668
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
665669
-- stylua: ignore

lua/render-markdown/handler/markdown.lua

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ local component = require('render-markdown.component')
33
local icons = require('render-markdown.icons')
44
local list = require('render-markdown.list')
55
local logger = require('render-markdown.logger')
6+
local pipe_table_parser = require('render-markdown.parser.pipe_table')
67
local shared = require('render-markdown.handler.shared')
78
local state = require('render-markdown.state')
89
local str = require('render-markdown.str')
@@ -506,48 +507,55 @@ function M.render_table(buf, info)
506507
if not pipe_table.enabled or pipe_table.style == 'none' then
507508
return {}
508509
end
509-
local marks = {}
510+
local parsed_table = pipe_table_parser.parse(buf, info)
511+
if parsed_table == nil then
512+
return {}
513+
end
510514

511-
local delim = nil
512-
local first = nil
513-
local last = nil
514-
for row_node in info.node:iter_children() do
515-
local row = ts.info(row_node, buf)
516-
if row.type == 'pipe_table_delimiter_row' then
517-
delim = row
518-
list.add_mark(marks, M.render_table_delimiter(row))
519-
elseif row.type == 'pipe_table_header' then
520-
first = row
521-
vim.list_extend(marks, M.render_table_row(buf, row, pipe_table.head))
522-
elseif row.type == 'pipe_table_row' then
523-
if last == nil or row.start_row > last.start_row then
524-
last = row
525-
end
526-
vim.list_extend(marks, M.render_table_row(buf, row, pipe_table.row))
527-
else
528-
logger.unhandled_type('markdown', 'row', row.type)
529-
end
515+
local marks = {}
516+
vim.list_extend(marks, M.render_table_row(buf, parsed_table.head, pipe_table.head))
517+
list.add_mark(marks, M.render_table_delimiter(parsed_table.delim, parsed_table.columns))
518+
for _, row in ipairs(parsed_table.rows) do
519+
vim.list_extend(marks, M.render_table_row(buf, row, pipe_table.row))
530520
end
531521
if pipe_table.style == 'full' then
532-
vim.list_extend(marks, M.render_table_full(buf, delim, first, last))
522+
vim.list_extend(marks, M.render_table_full(buf, parsed_table))
533523
end
534-
535524
return marks
536525
end
537526

538527
---@private
539528
---@param row render.md.NodeInfo
529+
---@param columns render.md.parsed.TableColumn[]
540530
---@return render.md.Mark
541-
function M.render_table_delimiter(row)
531+
function M.render_table_delimiter(row, columns)
542532
local pipe_table = state.config.pipe_table
533+
local indicator = pipe_table.alignment_indicator
543534
local border = pipe_table.border
544-
-- Order matters here, in particular handling inner intersections before left & right
545-
local delimiter = row.text
546-
:gsub(' ', '-')
547-
:gsub('%-|%-', border[11] .. border[5] .. border[11])
548-
:gsub('|%-', border[4] .. border[11])
549-
:gsub('%-|', border[11] .. border[6])
550-
:gsub('%-', border[11])
535+
local sections = vim.tbl_map(
536+
---@param column render.md.parsed.TableColumn
537+
---@return string
538+
function(column)
539+
-- If column is small there's no good place to put the alignment indicator
540+
-- Alignment indicator must be exactly one character wide
541+
-- We do not put an indicator for default alignment
542+
if column.width < 4 or str.width(indicator) ~= 1 or column.alignment == 'default' then
543+
return border[11]:rep(column.width)
544+
end
545+
-- Handle the various alignmnet possibilities
546+
local left = border[11]:rep(math.floor(column.width / 2))
547+
local right = border[11]:rep(math.ceil(column.width / 2) - 1)
548+
if column.alignment == 'left' then
549+
return indicator .. left .. right
550+
elseif column.alignment == 'right' then
551+
return left .. right .. indicator
552+
else
553+
return left .. indicator .. right
554+
end
555+
end,
556+
columns
557+
)
558+
local delimiter = border[4] .. table.concat(sections, border[5]) .. border[6]
551559
---@type render.md.Mark
552560
return {
553561
conceal = true,
@@ -629,16 +637,11 @@ end
629637

630638
---@private
631639
---@param buf integer
632-
---@param delim? render.md.NodeInfo
633-
---@param first? render.md.NodeInfo
634-
---@param last? render.md.NodeInfo
640+
---@param parsed_table render.md.parsed.Table
635641
---@return render.md.Mark[]
636-
function M.render_table_full(buf, delim, first, last)
642+
function M.render_table_full(buf, parsed_table)
637643
local pipe_table = state.config.pipe_table
638644
local border = pipe_table.border
639-
if delim == nil or first == nil or last == nil then
640-
return {}
641-
end
642645

643646
---@param info render.md.NodeInfo
644647
---@return integer
@@ -652,18 +655,25 @@ function M.render_table_full(buf, delim, first, last)
652655
return result
653656
end
654657

658+
local first = parsed_table.head
659+
local last = parsed_table.rows[#parsed_table.rows]
660+
655661
-- Do not need to account for concealed / inlined text on delimiter row
656-
local delim_width = str.width(delim.text)
657-
if delim_width ~= width(first) or delim_width ~= width(last) then
662+
local delim_width = str.width(parsed_table.delim.text)
663+
if delim_width ~= width(parsed_table.head) or delim_width ~= width(last) then
658664
return {}
659665
end
660666

661-
local headings = vim.split(delim.text, '|', { plain = true, trimempty = true })
662-
local lengths = vim.tbl_map(function(cell)
663-
return border[11]:rep(str.width(cell))
664-
end, headings)
667+
local sections = vim.tbl_map(
668+
---@param column render.md.parsed.TableColumn
669+
---@return string
670+
function(column)
671+
return border[11]:rep(column.width)
672+
end,
673+
parsed_table.columns
674+
)
665675

666-
local line_above = border[1] .. table.concat(lengths, border[2]) .. border[3]
676+
local line_above = border[1] .. table.concat(sections, border[2]) .. border[3]
667677
---@type render.md.Mark
668678
local above_mark = {
669679
conceal = false,
@@ -675,7 +685,7 @@ function M.render_table_full(buf, delim, first, last)
675685
},
676686
}
677687

678-
local line_below = border[7] .. table.concat(lengths, border[8]) .. border[9]
688+
local line_below = border[7] .. table.concat(sections, border[8]) .. border[9]
679689
---@type render.md.Mark
680690
local below_mark = {
681691
conceal = false,

lua/render-markdown/init.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ local M = {}
3535
---@field public style? 'full'|'normal'|'none'
3636
---@field public cell? 'padded'|'raw'|'overlay'
3737
---@field public border? string[]
38+
---@field public alignment_indicator? string
3839
---@field public head? string
3940
---@field public row? string
4041
---@field public filler? string
@@ -360,6 +361,8 @@ M.default_config = {
360361
-- raw: replaces only the '|' characters in each row, leaving the cells unmodified
361362
-- padded: raw + cells are padded with inline extmarks to make up for any concealed text
362363
cell = 'padded',
364+
-- Gets placed in delimiter row for each column, position is based on alignmnet
365+
alignment_indicator = '',
363366
-- Characters used to replace table border
364367
-- Correspond to top(3), delimiter(3), bottom(3), vertical, & horizontal
365368
-- stylua: ignore
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
local logger = require('render-markdown.logger')
2+
local ts = require('render-markdown.ts')
3+
4+
---@class render.md.parser.PipeTable
5+
local M = {}
6+
7+
---@class render.md.parsed.TableColumn
8+
---@field width integer
9+
---@field alignment 'left'|'right'|'center'|'default'
10+
11+
---@class render.md.parsed.Table
12+
---@field head render.md.NodeInfo
13+
---@field delim render.md.NodeInfo
14+
---@field columns render.md.parsed.TableColumn[]
15+
---@field rows render.md.NodeInfo[]
16+
17+
---@param buf integer
18+
---@param info render.md.NodeInfo
19+
---@return render.md.parsed.Table?
20+
function M.parse(buf, info)
21+
local head = nil
22+
local delim = nil
23+
local pipes = {}
24+
local cells = {}
25+
local rows = {}
26+
for row_node in info.node:iter_children() do
27+
local row = ts.info(row_node, buf)
28+
if row.type == 'pipe_table_header' then
29+
head = row
30+
elseif row.type == 'pipe_table_delimiter_row' then
31+
delim = row
32+
for cell_node in row.node:iter_children() do
33+
local cell = ts.info(cell_node, buf)
34+
if cell.type == '|' then
35+
table.insert(pipes, cell)
36+
elseif cell.type == 'pipe_table_delimiter_cell' then
37+
table.insert(cells, cell)
38+
else
39+
logger.unhandled_type('markdown', 'delim cell', cell.type)
40+
end
41+
end
42+
elseif row.type == 'pipe_table_row' then
43+
table.insert(rows, row)
44+
else
45+
logger.unhandled_type('markdown', 'row', row.type)
46+
end
47+
end
48+
-- Check for empty heading / delimiter
49+
if head == nil or delim == nil then
50+
return nil
51+
end
52+
local columns = M.parse_columns(buf, pipes, cells)
53+
-- Check for missing row information
54+
if #rows == 0 then
55+
return nil
56+
end
57+
ts.sort_inplace(rows)
58+
---@type render.md.parsed.Table
59+
return { head = head, delim = delim, columns = columns, rows = rows }
60+
end
61+
62+
---@private
63+
---@param buf integer
64+
---@param pipes render.md.NodeInfo[]
65+
---@param cells render.md.NodeInfo[]
66+
---@return render.md.parsed.TableColumn[]
67+
function M.parse_columns(buf, pipes, cells)
68+
-- Check for missing column information
69+
if #pipes == 0 or #cells == 0 then
70+
return {}
71+
end
72+
-- Check for mismatch in column fence posts
73+
if #pipes ~= #cells + 1 then
74+
return {}
75+
end
76+
ts.sort_inplace(pipes)
77+
ts.sort_inplace(cells)
78+
local columns = {}
79+
for i = 1, #cells do
80+
local width = pipes[i + 1].start_col - pipes[i].end_col
81+
if width < 0 then
82+
return {}
83+
end
84+
---@type render.md.parsed.TableColumn
85+
local column = { width = width, alignment = M.parse_alignment(buf, cells[i]) }
86+
table.insert(columns, column)
87+
end
88+
return columns
89+
end
90+
91+
---@private
92+
---@param buf integer
93+
---@param cell render.md.NodeInfo
94+
---@return 'left'|'right'|'center'|'default'
95+
function M.parse_alignment(buf, cell)
96+
local align_left = ts.child(buf, cell, 'pipe_table_align_left') ~= nil
97+
local align_right = ts.child(buf, cell, 'pipe_table_align_right') ~= nil
98+
if align_left and align_right then
99+
return 'center'
100+
elseif align_left then
101+
return 'left'
102+
elseif align_right then
103+
return 'right'
104+
else
105+
return 'default'
106+
end
107+
end
108+
109+
return M

lua/render-markdown/state.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ function state.validate()
201201
style = one_of(pipe_table.style, { 'full', 'normal', 'none' }),
202202
cell = one_of(pipe_table.cell, { 'padded', 'raw', 'overlay' }),
203203
border = string_array(pipe_table.border),
204+
alignment_indicator = { pipe_table.alignment_indicator, 'string' },
204205
head = { pipe_table.head, 'string' },
205206
row = { pipe_table.row, 'string' },
206207
filler = { pipe_table.filler, 'string' },

lua/render-markdown/ts.lua

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ function M.info(node, source)
3737
}
3838
end
3939

40+
---@param infos render.md.NodeInfo[]
41+
function M.sort_inplace(infos)
42+
table.sort(infos, function(info1, info2)
43+
if info1.start_row ~= info2.start_row then
44+
return info1.start_row < info2.start_row
45+
else
46+
return info1.start_col < info2.start_col
47+
end
48+
end)
49+
end
50+
4051
---Walk through parent nodes, count the number of target nodes
4152
---@param info render.md.NodeInfo
4253
---@param target string
@@ -71,12 +82,12 @@ end
7182
---@param buf integer
7283
---@param info render.md.NodeInfo
7384
---@param target_type string
74-
---@param target_row integer
85+
---@param target_row? integer
7586
---@return render.md.NodeInfo?
7687
function M.child(buf, info, target_type, target_row)
7788
for child in info.node:iter_children() do
7889
if child:type() == target_type then
79-
if child:range() == target_row then
90+
if target_row == nil or child:range() == target_row then
8091
return M.info(child, buf)
8192
end
8293
end

lua/render-markdown/types.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
---@field public style 'full'|'normal'|'none'
2121
---@field public cell 'padded'|'raw'|'overlay'
2222
---@field public border string[]
23+
---@field public alignment_indicator string
2324
---@field public head string
2425
---@field public row string
2526
---@field public filler string

0 commit comments

Comments
 (0)