Skip to content

Commit 9a746ff

Browse files
feat: combine latex virtual lines use center positioning
## Details Request: #520 This is a fairly substantial re-work of how latex formulas ultimately get rendered. The first major change is now, regardless of positioning, we will attempt to combine virtual lines related to latex formulas. This means for instance if the position is set to `above`, there are 3 formulas on a particular row, and each of them renders as 3 lines, these will get padded and combined together such that only 3 virtual lines are added to show all the formulas, rather than 3 lines each, producing 9 hard to follow virtual lines. This is possible thanks to the recent changes that allow us to process all the nodes at once and do a row level ordering. The second change is the addition of the `center` position value, which is also the new default. When this is set virtual lines flow through the actual text. What this means is if a formula is rendered as 3 lines, one virtual line will be added above, one virtual line will be added below, and the center line will be inlined into the actual text. This makes the rendering feel much more like it is part of the document rather than entirely separate. This is only possible if the underlying latex nodes are a single line exactly. Once you have multi-line nodes we end up with much more edge cases, so multi-line nodes are rendered using the `above` value if `center` is set. If the position is set to `above` or `below` then no override happens. The `latex.virtual` parameter has been removed, as it is now redundant. Users who set a position of `above` or `below` will always get only virtual lines so there is no need for the additional setting. It is now the case that only the `center` position is capable of any amount of inline rendering, and again, it is limited to single line equations only.
1 parent 2c6cf12 commit 9a746ff

File tree

8 files changed

+159
-61
lines changed

8 files changed

+159
-61
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
## Pre-release
44

5+
### Features
6+
7+
- remove delimiters around latex text before converting [2c6cf12](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/2c6cf127c577712bd29d38f6391b3045c5f0180a)
8+
- convert latex asynchronously [44cbac6](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/44cbac697e50ae32e4644652da08cf692b9a5a57)
9+
10+
### Bug Fixes
11+
12+
- use display width of concealed ranges [b4885a9](https://github.com/MeanderingProgrammer/render-markdown.nvim/commit/b4885a95e5082a6ed164830c581aac257a74f355)
13+
514
## 8.8.0 (2025-09-09)
615

716
### Features

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -276,15 +276,14 @@ require('render-markdown').setup({
276276
-- Highlight for latex blocks.
277277
highlight = 'RenderMarkdownMath',
278278
-- Determines where latex formula is rendered relative to block.
279-
-- | above | above latex block |
280-
-- | below | below latex block |
281-
position = 'above',
279+
-- | above | above latex block |
280+
-- | below | below latex block |
281+
-- | center | centered with latex block (must be single line) |
282+
position = 'center',
282283
-- Number of empty lines above latex blocks.
283284
top_pad = 0,
284285
-- Number of empty lines below latex blocks.
285286
bottom_pad = 0,
286-
-- Always use virtual lines for rendering instead of attempting to inline.
287-
virtual = false,
288287
},
289288
on = {
290289
-- Called when plugin initially attaches to a buffer.

doc/render-markdown.txt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*render-markdown.txt* For NVIM v0.11.4 Last change: 2025 September 14
1+
*render-markdown.txt* For NVIM v0.11.4 Last change: 2025 September 15
22

33
==============================================================================
44
Table of Contents *render-markdown-table-of-contents*
@@ -338,15 +338,14 @@ Default Configuration ~
338338
-- Highlight for latex blocks.
339339
highlight = 'RenderMarkdownMath',
340340
-- Determines where latex formula is rendered relative to block.
341-
-- | above | above latex block |
342-
-- | below | below latex block |
343-
position = 'above',
341+
-- | above | above latex block |
342+
-- | below | below latex block |
343+
-- | center | centered with latex block (must be single line) |
344+
position = 'center',
344345
-- Number of empty lines above latex blocks.
345346
top_pad = 0,
346347
-- Number of empty lines below latex blocks.
347348
bottom_pad = 0,
348-
-- Always use virtual lines for rendering instead of attempting to inline.
349-
virtual = false,
350349
},
351350
on = {
352351
-- Called when plugin initially attaches to a buffer.

lua/render-markdown/config/latex.lua

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
---@field position render.md.latex.Position
55
---@field top_pad integer
66
---@field bottom_pad integer
7-
---@field virtual boolean
87

98
---@enum render.md.latex.Position
109
local Position = {
1110
above = 'above',
1211
below = 'below',
12+
center = 'center',
1313
}
1414

1515
---@class render.md.latex.Cfg
@@ -26,15 +26,14 @@ M.default = {
2626
-- Highlight for latex blocks.
2727
highlight = 'RenderMarkdownMath',
2828
-- Determines where latex formula is rendered relative to block.
29-
-- | above | above latex block |
30-
-- | below | below latex block |
31-
position = 'above',
29+
-- | above | above latex block |
30+
-- | below | below latex block |
31+
-- | center | centered with latex block (must be single line) |
32+
position = 'center',
3233
-- Number of empty lines above latex blocks.
3334
top_pad = 0,
3435
-- Number of empty lines below latex blocks.
3536
bottom_pad = 0,
36-
-- Always use virtual lines for rendering instead of attempting to inline.
37-
virtual = false,
3837
}
3938

4039
---@return render.md.Schema
@@ -45,7 +44,6 @@ function M.schema()
4544
position = { enum = Position },
4645
top_pad = { type = 'number' },
4746
bottom_pad = { type = 'number' },
48-
virtual = { type = 'boolean' },
4947
})
5048
end
5149

lua/render-markdown/handler/latex.lua

Lines changed: 128 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ function Handler:run(root, last)
4444
if last then
4545
local nodes = self.context.latex:get()
4646
self:convert(nodes)
47-
for _, row in ipairs(self:rows(nodes)) do
48-
self:render(row)
47+
local rows = self:rows(nodes)
48+
for row, row_nodes in pairs(rows) do
49+
self:render(row, row_nodes)
4950
end
5051
end
5152
return self.marks:get()
@@ -90,67 +91,156 @@ end
9091

9192
---@private
9293
---@param nodes render.md.Node[]
93-
---@return render.md.Node[][]
94+
---@return table<integer, render.md.Node[]>
9495
function Handler:rows(nodes)
95-
table.sort(nodes)
96-
local result = {} ---@type render.md.Node[][]
97-
result[#result + 1] = { nodes[1] }
98-
for i = 2, #nodes do
99-
local node, last = nodes[i], result[#result]
100-
if node.start_row == last[#last].start_row then
101-
last[#last + 1] = node
96+
local position = self.config.position
97+
98+
---@param node render.md.Node
99+
---@return integer, integer
100+
local function get(node)
101+
if position == 'below' and node:height() > 1 then
102+
return node.end_row, 0
103+
else
104+
return node.start_row, node.start_col
105+
end
106+
end
107+
108+
table.sort(nodes, function(a, b)
109+
local a_row, a_col = get(a)
110+
local b_row, b_col = get(b)
111+
if a_row ~= b_row then
112+
return a_row < b_row
102113
else
103-
result[#result + 1] = { node }
114+
return a_col < b_col
115+
end
116+
end)
117+
118+
local result = {} ---@type table<integer, render.md.Node[]>
119+
for _, node in ipairs(nodes) do
120+
local node_row = get(node)
121+
if not result[node_row] then
122+
result[node_row] = {}
104123
end
124+
local row = result[node_row]
125+
row[#row + 1] = node
105126
end
106127
return result
107128
end
108129

109130
---@private
131+
---@param row integer
110132
---@param nodes render.md.Node[]
111-
function Handler:render(nodes)
133+
function Handler:render(row, nodes)
112134
local first = nodes[1]
113135
local indent = self:indent(first)
114-
local _, line = first:line('first', 0)
136+
137+
local lines_above = {} ---@type string[]
138+
local lines_below = {} ---@type string[]
139+
local current = 0
115140

116141
for _, node in ipairs(nodes) do
117142
local output = str.split(Handler.cache[Handler.text(node)], '\n', true)
118-
if self.config.virtual or #output > 1 then
119-
local col = node.start_col
120-
local prefix = str.pad(line and str.width(line:sub(1, col)) or col)
121-
local width = vim.fn.max(iter.list.map(output, str.width))
122-
123-
local texts = {} ---@type string[]
124-
for _ = 1, self.config.top_pad do
125-
texts[#texts + 1] = ''
126-
end
127-
for _, text in ipairs(output) do
128-
local suffix = str.pad(width - str.width(text))
129-
texts[#texts + 1] = prefix .. text .. suffix
130-
end
131-
for _ = 1, self.config.bottom_pad do
132-
texts[#texts + 1] = ''
133-
end
134143

135-
local lines = iter.list.map(texts, function(text)
136-
return indent:copy():text(text, self.config.highlight):get()
137-
end)
144+
-- pad lines to the same width
145+
local width = vim.fn.max(iter.list.map(output, str.width))
146+
for i, line in ipairs(output) do
147+
output[i] = line .. str.pad(width - str.width(line))
148+
end
138149

139-
local above = self.config.position == 'above'
140-
local row = above and node.start_row or node.end_row
150+
-- center is only possible if formula is a single line
151+
local position = self.config.position
152+
if position == 'center' and node:height() > 1 then
153+
position = 'above'
154+
end
141155

142-
self.marks:add(self.config, 'virtual_lines', row, 0, {
143-
virt_lines = lines,
144-
virt_lines_above = above,
156+
-- absolute formula column
157+
local col ---@type integer
158+
if position == 'below' and node:height() > 1 then
159+
-- latex blocks include last line, unlike markdown blocks
160+
local _, line = node:line('below', 1)
161+
col = line and str.spaces('start', line) or 0
162+
else
163+
local _, line = node:line('above', 0)
164+
col = self.context:width({
165+
text = line and line:sub(1, node.start_col) or '',
166+
start_row = node.start_row,
167+
start_col = 0,
168+
end_row = node.start_row,
169+
end_col = node.start_col,
145170
})
171+
end
172+
173+
-- convert column to relative offset, include padding between formulas
174+
local prefix = math.max(col - current, current == 0 and 0 or 1)
175+
176+
local above ---@type integer
177+
local below ---@type integer
178+
if position == 'above' then
179+
above = #output
180+
below = 0
181+
elseif position == 'below' then
182+
above = 0
183+
below = #output
146184
else
185+
assert(node:height() == 1, 'invalid center height')
186+
local center = math.floor(#output / 2) + 1
187+
above = center - 1
188+
below = #output - center
147189
self.marks:over(self.config, true, node, {
148-
virt_text = { { output[1], self.config.highlight } },
190+
virt_text = { { output[center], self.config.highlight } },
149191
virt_text_pos = 'inline',
150192
conceal = '',
151193
})
152194
end
195+
196+
-- fill in new lines at top and bottom
197+
while #lines_above < above do
198+
table.insert(lines_above, 1, str.pad(current))
199+
end
200+
while #lines_below < below do
201+
lines_below[#lines_below + 1] = str.pad(current)
202+
end
203+
204+
-- concatenate output onto lines
205+
for i, line in ipairs(lines_above) do
206+
local index = i - (#lines_above - above)
207+
local body = output[index] or str.pad(width)
208+
lines_above[i] = line .. str.pad(prefix) .. body
209+
end
210+
for i, line in ipairs(lines_below) do
211+
local index = i + (#output - below)
212+
local body = output[index] or str.pad(width)
213+
lines_below[i] = line .. str.pad(prefix) .. body
214+
end
215+
216+
-- update current width of lines
217+
current = current + prefix + width
153218
end
219+
220+
---@param lines string[]
221+
---@param top boolean
222+
---@param bottom boolean
223+
---@param above boolean
224+
local function add_lines(lines, top, bottom, above)
225+
if #lines == 0 then
226+
return
227+
end
228+
for _ = 1, (top and self.config.top_pad or 0) do
229+
table.insert(lines, 1, '')
230+
end
231+
for _ = 1, (bottom and self.config.bottom_pad or 0) do
232+
lines[#lines + 1] = ''
233+
end
234+
self.marks:add(self.config, 'virtual_lines', row, 0, {
235+
virt_lines = iter.list.map(lines, function(line)
236+
return indent:copy():text(line, self.config.highlight):get()
237+
end),
238+
virt_lines_above = above,
239+
})
240+
end
241+
242+
add_lines(lines_above, true, #lines_below == 0, true)
243+
add_lines(lines_below, #lines_above == 0, true, false)
154244
end
155245

156246
---@private

lua/render-markdown/health.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ local state = require('render-markdown.state')
55
local M = {}
66

77
---@private
8-
M.version = '8.8.5'
8+
M.version = '8.8.6'
99

1010
function M.check()
1111
M.start('versions')

lua/render-markdown/lib/node.lua

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ function Node:get()
6363
return self.node
6464
end
6565

66+
---@return integer
67+
function Node:height()
68+
return self.end_row - self.start_row + 1
69+
end
70+
6671
---@return integer[]
6772
function Node:sections()
6873
local result = {} ---@type integer[]
@@ -218,15 +223,14 @@ end
218223
---@return integer, string?
219224
function Node:line(position, by)
220225
local row ---@type integer
221-
local single = self.start_row == self.end_row
222226
if position == Position.above then
223227
row = self.start_row - by
224228
elseif position == Position.first then
225229
row = self.start_row + by
226230
elseif position == Position.below then
227-
row = self.end_row - (single and 0 or 1) + by
231+
row = self.end_row - (self:height() == 1 and 0 or 1) + by
228232
elseif position == Position.last then
229-
row = self.end_row - (single and 0 or 1) - by
233+
row = self.end_row - (self:height() == 1 and 0 or 1) - by
230234
else
231235
error(('invalid position: %s'):format(position))
232236
end

lua/render-markdown/types.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@
198198
---@field position? render.md.latex.Position
199199
---@field top_pad? integer
200200
---@field bottom_pad? integer
201-
---@field virtual? boolean
202201

203202
---@class (exact) render.md.link.UserConfig: render.md.base.UserConfig
204203
---@field footnote? render.md.link.footnote.UserConfig

0 commit comments

Comments
 (0)