Skip to content

Commit 3c6a0e1

Browse files
Support highlighting callout quote markers
resolves: #24 # Details Figuring out which callout, if any, is in a `block_quote` is non trivial using treesitter as the links in a `block_quote` are markdown_inline while the markers themselves are standard markdown. Passing information between these 2 parsers is not really possible, i.e. we cannot take a markdown `TSNode` and query for markdown_inline elements. Re-parsing the contents of the node is likely possible though I imagine there would need to be some handling of (start/end) (row/col). To get around this compexity we take an approach that's really similar to how we compute list levels to pick a bullet point. When handling a `quote_marker` find the closest parent `block_quote` element. From there we get the text of the `block_quote` node and do a naive string contains check to determine if the `block_quote` is a callout and, if so, which kind it is. Based on this select an appropriate highlight. This likely has edge cases just based on how adhoc the logic is, though, at the moment, I cannot think of any. Release this into the wild and hopefully any bugs will be reported so I can iron out the kinks.
1 parent 09a7afb commit 3c6a0e1

File tree

6 files changed

+89
-31
lines changed

6 files changed

+89
-31
lines changed

demo/callout.gif

-15.7 KB
Loading

justfile

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
init := "tests/minimal.lua"
2-
default_cols := "55"
32

43
test:
54
nvim --headless --noplugin -u {{init}} \
@@ -8,17 +7,27 @@ test:
87
health:
98
nvim -c "checkhealth render-markdown" -- .
109

11-
demo-all cols=default_cols:
12-
just demo {{cols}} "30" "heading_code" "## Heading 2"
13-
just demo {{cols}} "30" "list_table" ""
14-
just demo {{cols}} "15" "box_dash_quote" ""
15-
just demo {{cols}} "15" "latex" ""
16-
just demo {{cols}} "30" "callout" ""
10+
demo-all: demo-heading demo-list demo-box demo-latex demo-callout
1711

18-
demo cols rows file content:
12+
demo-heading:
13+
just demo "heading_code" "30" "## Heading 2"
14+
15+
demo-list:
16+
just demo "list_table" "30" ""
17+
18+
demo-box:
19+
just demo "box_dash_quote" "15" ""
20+
21+
demo-latex:
22+
just demo "latex" "15" ""
23+
24+
demo-callout:
25+
just demo "callout" "30" ""
26+
27+
demo file rows content:
1928
rm -f demo/{{file}}.gif
2029
python demo/record.py \
21-
--cols {{cols}} \
30+
--cols "55" \
2231
--rows {{rows}} \
2332
--file demo/{{file}}.md \
2433
--cast {{file}}.cast \

lua/render-markdown/callout.lua

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
local callout_to_key = {
2+
['[!NOTE]'] = 'note',
3+
['[!TIP]'] = 'tip',
4+
['[!IMPORTANT]'] = 'important',
5+
['[!WARNING]'] = 'warning',
6+
['[!CAUTION]'] = 'caution',
7+
}
8+
9+
local M = {}
10+
11+
---@param value string
12+
---@return string?
13+
M.get_key_exact = function(value)
14+
return callout_to_key[value]
15+
end
16+
17+
---@param value string
18+
---@return string?
19+
M.get_key_contains = function(value)
20+
for callout, key in pairs(callout_to_key) do
21+
if value:find(callout, 1, true) then
22+
return key
23+
end
24+
end
25+
return nil
26+
end
27+
28+
return M

lua/render-markdown/handler/markdown.lua

+31-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local callout = require('render-markdown.callout')
12
local list = require('render-markdown.list')
23
local logger = require('render-markdown.logger')
34
local state = require('render-markdown.state')
@@ -75,7 +76,17 @@ M.render = function(namespace, root, buf)
7576
})
7677
end
7778
elseif capture == 'quote_marker' then
78-
local virt_text = { value:gsub('>', state.config.quote), highlights.quote }
79+
local highlight = highlights.quote
80+
local quote = M.get_quote(node)
81+
if quote ~= nil then
82+
local quote_value = vim.treesitter.get_node_text(quote, buf)
83+
local key = callout.get_key_contains(quote_value)
84+
if key ~= nil then
85+
highlight = highlights.callout[key]
86+
end
87+
end
88+
89+
local virt_text = { value:gsub('>', state.config.quote), highlight }
7990
vim.api.nvim_buf_set_extmark(buf, namespace, start_row, start_col, {
8091
end_row = end_row,
8192
end_col = end_col,
@@ -167,7 +178,6 @@ M.is_sibling_checkbox = function(node)
167178
end
168179

169180
--- Walk through all parent nodes and count the number of nodes with type list
170-
--- to calculate the level of the given node
171181
---@param node TSNode
172182
---@return integer
173183
M.calculate_list_level = function(node)
@@ -176,8 +186,7 @@ M.calculate_list_level = function(node)
176186
while parent ~= nil do
177187
local parent_type = parent:type()
178188
if vim.tbl_contains({ 'section', 'document' }, parent_type) then
179-
-- when reaching a section or the document we are clearly at the
180-
-- top of the list
189+
-- reaching a section or document means we are clearly at the top of the list
181190
break
182191
elseif parent_type == 'list' then
183192
-- found a list increase the level and continue
@@ -188,4 +197,22 @@ M.calculate_list_level = function(node)
188197
return level
189198
end
190199

200+
--- Walk through parent nodes until block_quote, return first found
201+
---@param node TSNode
202+
---@return TSNode?
203+
M.get_quote = function(node)
204+
local parent = node:parent()
205+
while parent ~= nil do
206+
local parent_type = parent:type()
207+
if vim.tbl_contains({ 'section', 'document' }, parent_type) then
208+
-- reaching a section or document means we are clearly outside of a quote
209+
break
210+
elseif parent_type == 'block_quote' then
211+
return parent
212+
end
213+
parent = parent:parent()
214+
end
215+
return nil
216+
end
217+
191218
return M

lua/render-markdown/handler/markdown_inline.lua

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
local callout = require('render-markdown.callout')
12
local logger = require('render-markdown.logger')
23
local state = require('render-markdown.state')
34

@@ -21,14 +22,7 @@ M.render = function(namespace, root, buf)
2122
hl_group = highlights.code,
2223
})
2324
elseif capture == 'callout' then
24-
local value_to_key = {
25-
['[!NOTE]'] = 'note',
26-
['[!TIP]'] = 'tip',
27-
['[!IMPORTANT]'] = 'important',
28-
['[!WARNING]'] = 'warning',
29-
['[!CAUTION]'] = 'caution',
30-
}
31-
local key = value_to_key[value]
25+
local key = callout.get_key_exact(value)
3226
if key ~= nil then
3327
local virt_text = { state.config.callout[key], highlights.callout[key] }
3428
vim.api.nvim_buf_set_extmark(buf, namespace, start_row, start_col, {

tests/init_spec.lua

+10-10
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ async_tests.describe('init', function()
409409
{
410410
row = { 2, 2 },
411411
col = { 0, 2 },
412-
virt_text = { { '', '@markup.quote' } },
412+
virt_text = { { '', 'DiagnosticInfo' } },
413413
virt_text_pos = 'overlay',
414414
},
415415
-- Callout text
@@ -423,7 +423,7 @@ async_tests.describe('init', function()
423423
{
424424
row = { 3, 3 },
425425
col = { 0, 2 },
426-
virt_text = { { '', '@markup.quote' } },
426+
virt_text = { { '', 'DiagnosticInfo' } },
427427
virt_text_pos = 'overlay',
428428
},
429429
})
@@ -443,7 +443,7 @@ async_tests.describe('init', function()
443443
{
444444
row = { 7, 7 },
445445
col = { 0, 2 },
446-
virt_text = { { '', '@markup.quote' } },
446+
virt_text = { { '', 'DiagnosticOk' } },
447447
virt_text_pos = 'overlay',
448448
},
449449
-- Callout text
@@ -457,7 +457,7 @@ async_tests.describe('init', function()
457457
{
458458
row = { 8, 8 },
459459
col = { 0, 2 },
460-
virt_text = { { '', '@markup.quote' } },
460+
virt_text = { { '', 'DiagnosticOk' } },
461461
virt_text_pos = 'overlay',
462462
},
463463
})
@@ -477,7 +477,7 @@ async_tests.describe('init', function()
477477
{
478478
row = { 12, 12 },
479479
col = { 0, 2 },
480-
virt_text = { { '', '@markup.quote' } },
480+
virt_text = { { '', 'DiagnosticHint' } },
481481
virt_text_pos = 'overlay',
482482
},
483483
-- Callout text
@@ -491,7 +491,7 @@ async_tests.describe('init', function()
491491
{
492492
row = { 13, 13 },
493493
col = { 0, 2 },
494-
virt_text = { { '', '@markup.quote' } },
494+
virt_text = { { '', 'DiagnosticHint' } },
495495
virt_text_pos = 'overlay',
496496
},
497497
})
@@ -511,7 +511,7 @@ async_tests.describe('init', function()
511511
{
512512
row = { 17, 17 },
513513
col = { 0, 2 },
514-
virt_text = { { '', '@markup.quote' } },
514+
virt_text = { { '', 'DiagnosticWarn' } },
515515
virt_text_pos = 'overlay',
516516
},
517517
-- Callout text
@@ -525,7 +525,7 @@ async_tests.describe('init', function()
525525
{
526526
row = { 18, 18 },
527527
col = { 0, 2 },
528-
virt_text = { { '', '@markup.quote' } },
528+
virt_text = { { '', 'DiagnosticWarn' } },
529529
virt_text_pos = 'overlay',
530530
},
531531
})
@@ -545,7 +545,7 @@ async_tests.describe('init', function()
545545
{
546546
row = { 22, 22 },
547547
col = { 0, 2 },
548-
virt_text = { { '', '@markup.quote' } },
548+
virt_text = { { '', 'DiagnosticError' } },
549549
virt_text_pos = 'overlay',
550550
},
551551
-- Callout text
@@ -559,7 +559,7 @@ async_tests.describe('init', function()
559559
{
560560
row = { 23, 23 },
561561
col = { 0, 2 },
562-
virt_text = { { '', '@markup.quote' } },
562+
virt_text = { { '', 'DiagnosticError' } },
563563
virt_text_pos = 'overlay',
564564
},
565565
})

0 commit comments

Comments
 (0)