Skip to content

Commit 473e48d

Browse files
Add support for user defined handlers
# Details Allow users to specify handlers for whatever language they want using the `custom_handlers` configuration parameter. As an implementation it is meant to give users the same ability as the native renderers for `markdown`, `markdown_inline` and `latex`. Convenience methods are not provided and users are expected to use the vim api directly. Provide an example as well as some details in the README. By integrating with this plugin the `extmarks` set by the user will behave identically to the ones created by the plugin. All validation on buffers, windows and state also works as expected.
1 parent d4b63e2 commit 473e48d

File tree

10 files changed

+211
-31
lines changed

10 files changed

+211
-31
lines changed

Diff for: README.md

+71
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Plugin to improve viewing Markdown files in Neovim
2020
- Basic support for `LaTeX` if `pylatexenc` is installed on system
2121
- Disable rendering when file is larger than provided value
2222
- Support for [callouts](https://github.com/orgs/community/discussions/16925)
23+
- Support custom handlers which are ran identically to native handlers
2324

2425
# Dependencies
2526

@@ -149,6 +150,9 @@ require('render-markdown').setup({
149150
-- normal: renders the rows of tables
150151
-- none: disables rendering, use this if you prefer having cell highlights
151152
table_style = 'full',
153+
-- Mapping from treesitter language to user defined handlers
154+
-- See 'Custom Handlers' section for more info
155+
custom_handlers = {},
152156
-- Define the highlight groups to use when rendering various components
153157
highlights = {
154158
heading = {
@@ -204,6 +208,73 @@ require('render-markdown').setup({
204208

205209
- Function can also be accessed directly through `require('render-markdown').toggle()`
206210

211+
# Custom Handlers
212+
213+
Custom handlers allow users to integrate custom rendering for either unsupported
214+
languages or to override the native implementations. This can also be used to
215+
disable a native language, as custom handlers have priority.
216+
217+
For example disabling the `LaTeX` handler can be done with:
218+
219+
```lua
220+
require('render-markdown').setup({
221+
custom_handlers = {
222+
latex = { render = function() end },
223+
},
224+
}
225+
```
226+
227+
Each handler must conform to the following interface:
228+
229+
```lua
230+
---@class render.md.Handler
231+
---@field public render fun(namespace: integer, root: TSNode, buf: integer)
232+
```
233+
234+
The `render` function parameters are:
235+
236+
- `namespace`: The id that this plugin interacts with when setting and clearing `extmark`s
237+
- `root`: The root treesitter node for the specified language
238+
- `buf`: The buffer containing the root node
239+
240+
Custom handlers are ran identically to native ones, so by writing custom `extmark`s
241+
(see :h nvim_buf_set_extmark()) to the provided `namespace` this plugin will handle
242+
clearing the `extmark`s on mode changes as well as re-calling the `render` function
243+
when needed.
244+
245+
This is a high level interface, as such creating, parsing, and iterating through
246+
a treesitter query is entirely up to the user if the functionality they want needs
247+
this. We do not provide any convenience functions, but you are more than welcome
248+
to use patterns from the native handlers.
249+
250+
## More Complex Example
251+
252+
Lets say for `python` we want to highlight lines with function definitions.
253+
254+
```lua
255+
-- Parse query outside of the render function to avoid doing it for each call
256+
local query = vim.treesitter.query.parse('python', '(function_definition) @def')
257+
local function render_python(namespace, root, buf)
258+
for id, node in query:iter_captures(root, buf) do
259+
local capture = query.captures[id]
260+
local start_row, _, _, _ = node:range()
261+
if capture == 'def' then
262+
vim.api.nvim_buf_set_extmark(buf, namespace, start_row, 0, {
263+
end_row = start_row + 1,
264+
end_col = 0,
265+
hl_group = 'DiffDelete',
266+
hl_eol = true,
267+
})
268+
end
269+
end
270+
end
271+
require('render-markdown').setup({
272+
custom_handlers = {
273+
python = { render = render_python },
274+
},
275+
}
276+
```
277+
207278
# Purpose
208279

209280
There are many existing markdown rendering plugins in the Neovim ecosystem. However,

Diff for: doc/render-markdown.txt

+83-7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For 0.10.0 Last change: 2024 May 22
1+
*render-markdown.txt* For 0.10.0 Last change: 2024 May 31
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*
@@ -11,12 +11,14 @@ Table of Contents *render-markdown-table-of-contents*
1111
- packer.nvim |render-markdown-install-packer.nvim|
1212
5. Setup |render-markdown-setup|
1313
6. Commands |render-markdown-commands|
14-
7. Purpose |render-markdown-purpose|
15-
8. Markdown Ecosystem |render-markdown-markdown-ecosystem|
14+
7. Custom Handlers |render-markdown-custom-handlers|
15+
- More Complex Example|render-markdown-custom-handlers-more-complex-example|
16+
8. Purpose |render-markdown-purpose|
17+
9. Markdown Ecosystem |render-markdown-markdown-ecosystem|
1618
- Render in Neovim |render-markdown-markdown-ecosystem-render-in-neovim|
1719
- Render in Browser |render-markdown-markdown-ecosystem-render-in-browser|
1820
- Orthogonal |render-markdown-markdown-ecosystem-orthogonal|
19-
9. Links |render-markdown-links|
21+
10. Links |render-markdown-links|
2022

2123
==============================================================================
2224
1. markdown.nvim *render-markdown-markdown.nvim*
@@ -41,6 +43,7 @@ Plugin to improve viewing Markdown files in Neovim
4143
- Basic support for `LaTeX` if `pylatexenc` is installed on system
4244
- Disable rendering when file is larger than provided value
4345
- Support for callouts <https://github.com/orgs/community/discussions/16925>
46+
- Support custom handlers which are ran identically to native handlers
4447

4548

4649
==============================================================================
@@ -178,6 +181,9 @@ modified by the user.
178181
-- normal: renders the rows of tables
179182
-- none: disables rendering, use this if you prefer having cell highlights
180183
table_style = 'full',
184+
-- Mapping from treesitter language to user defined handlers
185+
-- See 'Custom Handlers' section for more info
186+
custom_handlers = {},
181187
-- Define the highlight groups to use when rendering various components
182188
highlights = {
183189
heading = {
@@ -237,7 +243,77 @@ modified by the user.
237243

238244

239245
==============================================================================
240-
7. Purpose *render-markdown-purpose*
246+
7. Custom Handlers *render-markdown-custom-handlers*
247+
248+
Custom handlers allow users to integrate custom rendering for either
249+
unsupported languages or to override the native implementations. This can also
250+
be used to disable a native language, as custom handlers have priority.
251+
252+
For example disabling the `LaTeX` handler can be done with:
253+
254+
>lua
255+
require('render-markdown').setup({
256+
custom_handlers = {
257+
latex = { render = function() end },
258+
},
259+
}
260+
<
261+
262+
Each handler must conform to the following interface:
263+
264+
>lua
265+
---@class render.md.Handler
266+
---@field public render fun(namespace: integer, root: TSNode, buf: integer)
267+
<
268+
269+
The `render` function parameters are:
270+
271+
- `namespace`: The id that this plugin interacts with when setting and clearing `extmark`s
272+
- `root`: The root treesitter node for the specified language
273+
- `buf`: The buffer containing the root node
274+
275+
Custom handlers are ran identically to native ones, so by writing custom
276+
`extmark`s (see :h nvim_buf_set_extmark()) to the provided `namespace` this
277+
plugin will handle clearing the `extmark`s on mode changes as well as
278+
re-calling the `render` function when needed.
279+
280+
This is a high level interface, as such creating, parsing, and iterating
281+
through a treesitter query is entirely up to the user if the functionality they
282+
want needs this. We do not provide any convenience functions, but you are more
283+
than welcome to use patterns from the native handlers.
284+
285+
286+
MORE COMPLEX EXAMPLE *render-markdown-custom-handlers-more-complex-example*
287+
288+
Lets say for `python` we want to highlight lines with function definitions.
289+
290+
>lua
291+
-- Parse query outside of the render function to avoid doing it for each call
292+
local query = vim.treesitter.query.parse('python', '(function_definition) @def')
293+
local function render_python(namespace, root, buf)
294+
for id, node in query:iter_captures(root, buf) do
295+
local capture = query.captures[id]
296+
local start_row, _, _, _ = node:range()
297+
if capture == 'def' then
298+
vim.api.nvim_buf_set_extmark(buf, namespace, start_row, 0, {
299+
end_row = start_row + 1,
300+
end_col = 0,
301+
hl_group = 'DiffDelete',
302+
hl_eol = true,
303+
})
304+
end
305+
end
306+
end
307+
require('render-markdown').setup({
308+
custom_handlers = {
309+
python = { render = render_python },
310+
},
311+
}
312+
<
313+
314+
315+
==============================================================================
316+
8. Purpose *render-markdown-purpose*
241317

242318
There are many existing markdown rendering plugins in the Neovim ecosystem.
243319
However, most of these rely on syncing a separate browser window with the
@@ -254,7 +330,7 @@ this plugin.
254330

255331

256332
==============================================================================
257-
8. Markdown Ecosystem *render-markdown-markdown-ecosystem*
333+
9. Markdown Ecosystem *render-markdown-markdown-ecosystem*
258334

259335
There are many `markdown` plugins that specialize in different aspects of
260336
interacting with `markdown` files. This plugin specializes in rendering the
@@ -297,7 +373,7 @@ also have no issues running alongside this plugin.
297373
specific keybindings for interacting with `markdown` files
298374

299375
==============================================================================
300-
9. Links *render-markdown-links*
376+
10. Links *render-markdown-links*
301377

302378
1. *Demo*: demo/demo.gif
303379

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ local cache = {
1111

1212
local M = {}
1313

14-
---@param namespace number
14+
---@param namespace integer
1515
---@param root TSNode
1616
---@param buf integer
1717
M.render = function(namespace, root, buf)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local util = require('render-markdown.util')
55

66
local M = {}
77

8-
---@param namespace number
8+
---@param namespace integer
99
---@param root TSNode
1010
---@param buf integer
1111
M.render = function(namespace, root, buf)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ local state = require('render-markdown.state')
33

44
local M = {}
55

6-
---@param namespace number
6+
---@param namespace integer
77
---@param root TSNode
88
---@param buf integer
99
M.render = function(namespace, root, buf)

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ local function check_keys(t1, t2, path)
4040
elseif type(v1) ~= type(v2) then
4141
table.insert(errors, string.format('Invalid type: %s, expected %s but found %s', key, type(v1), type(v2)))
4242
elseif type(v1) == 'table' and type(v2) == 'table' then
43-
vim.list_extend(errors, check_keys(v1, v2, key_path))
43+
-- Some tables are meant to have unrestricted keys
44+
if k ~= 'custom_handlers' then
45+
vim.list_extend(errors, check_keys(v1, v2, key_path))
46+
end
4447
end
4548
end
4649
return errors

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

+7
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ local M = {}
3434
---@field public quote? string
3535
---@field public callout? render.md.UserCallout
3636

37+
---@class render.md.Handler
38+
---@field public render fun(namespace: integer, root: TSNode, buf: integer)
39+
3740
---@class render.md.UserConceal
3841
---@field public default? integer
3942
---@field public rendered? integer
@@ -58,6 +61,7 @@ local M = {}
5861
---@field public callout? render.md.UserCallout
5962
---@field public conceal? render.md.UserConceal
6063
---@field public table_style? 'full'|'normal'|'none'
64+
---@field public custom_handlers? table<string, render.md.Handler>
6165
---@field public highlights? render.md.UserHighlights
6266

6367
---@type render.md.Config
@@ -147,6 +151,9 @@ M.default_config = {
147151
-- normal: renders the rows of tables
148152
-- none: disables rendering, use this if you prefer having cell highlights
149153
table_style = 'full',
154+
-- Mapping from treesitter language to user defined handlers
155+
-- See 'Custom Handlers' section for more info
156+
custom_handlers = {},
150157
-- Define the highlight groups to use when rendering various components
151158
highlights = {
152159
heading = {

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

+1
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@
5252
---@field public callout render.md.Callout
5353
---@field public conceal render.md.Conceal
5454
---@field public table_style 'full'|'normal'|'none'
55+
---@field public custom_handlers table<string, render.md.Handler>
5556
---@field public highlights render.md.Highlights

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

+13-7
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,20 @@ M.refresh = function(buf)
3333
parser:for_each_tree(function(tree, language_tree)
3434
local language = language_tree:lang()
3535
logger.debug({ language = language })
36-
if language == 'markdown' then
37-
markdown.render(M.namespace, tree:root(), buf)
38-
elseif language == 'markdown_inline' then
39-
markdown_inline.render(M.namespace, tree:root(), buf)
40-
elseif language == 'latex' then
41-
latex.render(M.namespace, tree:root(), buf)
36+
local user_handler = state.config.custom_handlers[language]
37+
if user_handler == nil then
38+
if language == 'markdown' then
39+
markdown.render(M.namespace, tree:root(), buf)
40+
elseif language == 'markdown_inline' then
41+
markdown_inline.render(M.namespace, tree:root(), buf)
42+
elseif language == 'latex' then
43+
latex.render(M.namespace, tree:root(), buf)
44+
else
45+
logger.debug('No handler found')
46+
end
4247
else
43-
logger.debug('No handler found')
48+
logger.debug('Using user defined handler')
49+
user_handler.render(M.namespace, tree:root(), buf)
4450
end
4551
end)
4652
logger.flush()

Diff for: scripts/update.py

+29-13
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,34 @@ def main() -> None:
1010

1111

1212
def update_types(init_file: Path) -> None:
13-
lines: list[str] = []
13+
# Group comments into class + fields
14+
lua_classes: list[list[str]] = []
15+
current_class: list[str] = []
1416
for comment in get_comments(init_file):
1517
comment_type: str = comment.split()[0].split("@")[-1]
16-
if comment_type in ["class", "field"]:
17-
comment = comment.replace("User", "")
18-
if comment_type == "field":
19-
assert "?" in comment, f"All fields must be optional: {comment}"
20-
comment = comment.replace("?", "")
21-
if comment_type == "class" and len(lines) > 0:
22-
lines.append("")
23-
lines.append(comment)
24-
lines.append("")
18+
if comment_type == "class":
19+
if len(current_class) > 0:
20+
lua_classes.append(current_class)
21+
current_class = [comment]
22+
elif comment_type == "field":
23+
current_class.append(comment)
24+
lua_classes.append(current_class)
25+
26+
# Generate lines that get written to types.lua
27+
lines: list[str] = []
28+
for lua_class in lua_classes:
29+
name, fields = lua_class[0], lua_class[1:]
30+
if "User" in name:
31+
# User classes are expected to have optional fields
32+
lines.append(name.replace("User", ""))
33+
for field in fields:
34+
assert "?" in field, f"User fields must be optional: {field}"
35+
lines.append(field.replace("User", "").replace("?", ""))
36+
lines.append("")
37+
else:
38+
# Non user classes are expected to have mandatory fields
39+
for field in fields:
40+
assert "?" not in field, f"Non user fields must be mandatory: {field}"
2541

2642
types_file: Path = Path("lua/render-markdown/types.lua")
2743
types_file.write_text("\n".join(lines))
@@ -62,9 +78,9 @@ def get_default_config(file: Path) -> str:
6278
def get_readme_config(file: Path) -> str:
6379
query = "(code_fence_content) @content"
6480
code_blocks = ts_query(file, query, "content")
65-
query_code_blocks = [code for code in code_blocks if "query" in code]
66-
assert len(query_code_blocks) == 1
67-
return query_code_blocks[0]
81+
code_blocks = [code for code in code_blocks if "enabled" in code]
82+
assert len(code_blocks) == 1
83+
return code_blocks[0]
6884

6985

7086
def ts_query(file: Path, query_string: str, target: str) -> list[str]:

0 commit comments

Comments
 (0)