Skip to content

Commit 277daa6

Browse files
authored
feat(presets): add error parsers for built-in presets (#9)
Problem: none of the four presets defined an error_parser, so the diagnostic infrastructure went unused out of the box. Solution: add parsers for typst (file:line:col short format), latexmk (pdflatex file-line-error + summary), and pandoc (parse errors, YAML exceptions, generic errors). Enable machine-parseable output flags in typst and latex args. Pandoc parser is shared between markdown and github presets.
1 parent b00b169 commit 277daa6

2 files changed

Lines changed: 232 additions & 5 deletions

File tree

lua/preview/presets.lua

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,108 @@
11
local M = {}
22

3+
---@param stderr string
4+
---@return preview.Diagnostic[]
5+
local function parse_typst(stderr)
6+
local diagnostics = {}
7+
for line in stderr:gmatch('[^\r\n]+') do
8+
local file, lnum, col, severity, msg = line:match('^(.+):(%d+):(%d+): (%w+): (.+)$')
9+
if lnum then
10+
local sev = vim.diagnostic.severity.ERROR
11+
if severity == 'warning' then
12+
sev = vim.diagnostic.severity.WARN
13+
end
14+
table.insert(diagnostics, {
15+
lnum = tonumber(lnum) - 1,
16+
col = tonumber(col) - 1,
17+
message = msg,
18+
severity = sev,
19+
source = file,
20+
})
21+
end
22+
end
23+
return diagnostics
24+
end
25+
26+
---@param stderr string
27+
---@return preview.Diagnostic[]
28+
local function parse_latexmk(stderr)
29+
local diagnostics = {}
30+
for line in stderr:gmatch('[^\r\n]+') do
31+
local _, lnum, msg = line:match('^%.?/?(.+%.tex):(%d+): (.+)$')
32+
if lnum then
33+
table.insert(diagnostics, {
34+
lnum = tonumber(lnum) - 1,
35+
col = 0,
36+
message = msg,
37+
severity = vim.diagnostic.severity.ERROR,
38+
})
39+
else
40+
local rule_msg = line:match('^%s+(%S.+gave return code %d+)$')
41+
if rule_msg then
42+
table.insert(diagnostics, {
43+
lnum = 0,
44+
col = 0,
45+
message = rule_msg,
46+
severity = vim.diagnostic.severity.ERROR,
47+
})
48+
end
49+
end
50+
end
51+
return diagnostics
52+
end
53+
54+
---@param stderr string
55+
---@return preview.Diagnostic[]
56+
local function parse_pandoc(stderr)
57+
local diagnostics = {}
58+
for line in stderr:gmatch('[^\r\n]+') do
59+
local lnum, col, msg = line:match('Error at .+ %(line (%d+), column (%d+)%): (.+)$')
60+
if lnum then
61+
table.insert(diagnostics, {
62+
lnum = tonumber(lnum) - 1,
63+
col = tonumber(col) - 1,
64+
message = msg,
65+
severity = vim.diagnostic.severity.ERROR,
66+
})
67+
else
68+
local ylnum, ycol, ymsg =
69+
line:match('YAML parse exception at line (%d+), column (%d+)[,:]%s*(.+)$')
70+
if ylnum then
71+
table.insert(diagnostics, {
72+
lnum = tonumber(ylnum) - 1,
73+
col = tonumber(ycol) - 1,
74+
message = ymsg,
75+
severity = vim.diagnostic.severity.ERROR,
76+
})
77+
else
78+
local errmsg = line:match('^pandoc: (.+)$')
79+
if errmsg and not errmsg:match('^Error at') then
80+
table.insert(diagnostics, {
81+
lnum = 0,
82+
col = 0,
83+
message = errmsg,
84+
severity = vim.diagnostic.severity.ERROR,
85+
})
86+
end
87+
end
88+
end
89+
end
90+
return diagnostics
91+
end
92+
393
---@type preview.ProviderConfig
494
M.typst = {
595
ft = 'typst',
696
cmd = { 'typst', 'compile' },
797
args = function(ctx)
8-
return { ctx.file }
98+
return { '--diagnostic-format', 'short', ctx.file }
999
end,
10100
output = function(ctx)
11101
return (ctx.file:gsub('%.typ$', '.pdf'))
12102
end,
103+
error_parser = function(stderr)
104+
return parse_typst(stderr)
105+
end,
13106
open = true,
14107
}
15108

@@ -18,11 +111,19 @@ M.latex = {
18111
ft = 'tex',
19112
cmd = { 'latexmk' },
20113
args = function(ctx)
21-
return { '-pdf', '-interaction=nonstopmode', ctx.file }
114+
return {
115+
'-pdf',
116+
'-interaction=nonstopmode',
117+
'-pdflatex=pdflatex -file-line-error -interaction=nonstopmode %O %S',
118+
ctx.file,
119+
}
22120
end,
23121
output = function(ctx)
24122
return (ctx.file:gsub('%.tex$', '.pdf'))
25123
end,
124+
error_parser = function(stderr)
125+
return parse_latexmk(stderr)
126+
end,
26127
clean = function(ctx)
27128
return { 'latexmk', '-c', ctx.file }
28129
end,
@@ -40,6 +141,9 @@ M.markdown = {
40141
output = function(ctx)
41142
return (ctx.file:gsub('%.md$', '.html'))
42143
end,
144+
error_parser = function(stderr)
145+
return parse_pandoc(stderr)
146+
end,
43147
clean = function(ctx)
44148
return { 'rm', '-f', (ctx.file:gsub('%.md$', '.html')) }
45149
end,
@@ -67,6 +171,9 @@ M.github = {
67171
output = function(ctx)
68172
return (ctx.file:gsub('%.md$', '.html'))
69173
end,
174+
error_parser = function(stderr)
175+
return parse_pandoc(stderr)
176+
end,
70177
clean = function(ctx)
71178
return { 'rm', '-f', (ctx.file:gsub('%.md$', '.html')) }
72179
end,

spec/presets_spec.lua

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ describe('presets', function()
2121
assert.are.same({ 'typst', 'compile' }, presets.typst.cmd)
2222
end)
2323

24-
it('returns args with file path', function()
24+
it('returns args with diagnostic format and file path', function()
2525
local args = presets.typst.args(ctx)
2626
assert.is_table(args)
27-
assert.are.same({ '/tmp/document.typ' }, args)
27+
assert.are.same({ '--diagnostic-format', 'short', '/tmp/document.typ' }, args)
2828
end)
2929

3030
it('returns pdf output path', function()
@@ -36,6 +36,30 @@ describe('presets', function()
3636
it('has open enabled', function()
3737
assert.is_true(presets.typst.open)
3838
end)
39+
40+
it('parses errors from stderr', function()
41+
local stderr = table.concat({
42+
'main.typ:5:23: error: unexpected token',
43+
'main.typ:12:1: warning: unused variable',
44+
}, '\n')
45+
local diagnostics = presets.typst.error_parser(stderr, ctx)
46+
assert.is_table(diagnostics)
47+
assert.are.equal(2, #diagnostics)
48+
assert.are.equal(4, diagnostics[1].lnum)
49+
assert.are.equal(22, diagnostics[1].col)
50+
assert.are.equal('unexpected token', diagnostics[1].message)
51+
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
52+
assert.are.equal('main.typ', diagnostics[1].source)
53+
assert.are.equal(11, diagnostics[2].lnum)
54+
assert.are.equal(0, diagnostics[2].col)
55+
assert.are.equal('unused variable', diagnostics[2].message)
56+
assert.are.equal(vim.diagnostic.severity.WARN, diagnostics[2].severity)
57+
end)
58+
59+
it('returns empty table for clean stderr', function()
60+
local diagnostics = presets.typst.error_parser('', ctx)
61+
assert.are.same({}, diagnostics)
62+
end)
3963
end)
4064

4165
describe('latex', function()
@@ -57,7 +81,12 @@ describe('presets', function()
5781
it('returns args with pdf flag and file path', function()
5882
local args = presets.latex.args(tex_ctx)
5983
assert.is_table(args)
60-
assert.are.same({ '-pdf', '-interaction=nonstopmode', '/tmp/document.tex' }, args)
84+
assert.are.same({
85+
'-pdf',
86+
'-interaction=nonstopmode',
87+
'-pdflatex=pdflatex -file-line-error -interaction=nonstopmode %O %S',
88+
'/tmp/document.tex',
89+
}, args)
6190
end)
6291

6392
it('returns pdf output path', function()
@@ -75,6 +104,44 @@ describe('presets', function()
75104
it('has open enabled', function()
76105
assert.is_true(presets.latex.open)
77106
end)
107+
108+
it('parses file-line-error format from stderr', function()
109+
local stderr = table.concat({
110+
'./document.tex:10: Undefined control sequence.',
111+
'l.10 \\badcommand',
112+
'Collected error summary (may duplicate other messages):',
113+
" pdflatex: Command for 'pdflatex' gave return code 256",
114+
}, '\n')
115+
local diagnostics = presets.latex.error_parser(stderr, tex_ctx)
116+
assert.is_table(diagnostics)
117+
assert.is_true(#diagnostics > 0)
118+
assert.are.equal(9, diagnostics[1].lnum)
119+
assert.are.equal(0, diagnostics[1].col)
120+
assert.are.equal('Undefined control sequence.', diagnostics[1].message)
121+
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
122+
end)
123+
124+
it('parses collected error summary', function()
125+
local stderr = table.concat({
126+
'Latexmk: Errors, so I did not complete making targets',
127+
'Collected error summary (may duplicate other messages):',
128+
" pdflatex: Command for 'pdflatex' gave return code 256",
129+
}, '\n')
130+
local diagnostics = presets.latex.error_parser(stderr, tex_ctx)
131+
assert.is_table(diagnostics)
132+
assert.are.equal(1, #diagnostics)
133+
assert.are.equal(0, diagnostics[1].lnum)
134+
assert.are.equal(0, diagnostics[1].col)
135+
assert.are.equal(
136+
"pdflatex: Command for 'pdflatex' gave return code 256",
137+
diagnostics[1].message
138+
)
139+
end)
140+
141+
it('returns empty table for clean stderr', function()
142+
local diagnostics = presets.latex.error_parser('', tex_ctx)
143+
assert.are.same({}, diagnostics)
144+
end)
78145
end)
79146

80147
describe('markdown', function()
@@ -117,6 +184,43 @@ describe('presets', function()
117184
it('has open enabled', function()
118185
assert.is_true(presets.markdown.open)
119186
end)
187+
188+
it('parses pandoc parse errors from stderr', function()
189+
local stderr = 'Error at "source" (line 75, column 1): unexpected end of input'
190+
local diagnostics = presets.markdown.error_parser(stderr, md_ctx)
191+
assert.is_table(diagnostics)
192+
assert.are.equal(1, #diagnostics)
193+
assert.are.equal(74, diagnostics[1].lnum)
194+
assert.are.equal(0, diagnostics[1].col)
195+
assert.are.equal('unexpected end of input', diagnostics[1].message)
196+
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
197+
end)
198+
199+
it('parses YAML parse exceptions from stderr', function()
200+
local stderr =
201+
'YAML parse exception at line 3, column 2, while scanning a block scalar: did not find expected comment or line break'
202+
local diagnostics = presets.markdown.error_parser(stderr, md_ctx)
203+
assert.is_table(diagnostics)
204+
assert.are.equal(1, #diagnostics)
205+
assert.are.equal(2, diagnostics[1].lnum)
206+
assert.are.equal(1, diagnostics[1].col)
207+
assert.is_string(diagnostics[1].message)
208+
end)
209+
210+
it('parses generic pandoc errors from stderr', function()
211+
local stderr = 'pandoc: Could not find data file templates/default.html5'
212+
local diagnostics = presets.markdown.error_parser(stderr, md_ctx)
213+
assert.is_table(diagnostics)
214+
assert.are.equal(1, #diagnostics)
215+
assert.are.equal(0, diagnostics[1].lnum)
216+
assert.are.equal(0, diagnostics[1].col)
217+
assert.are.equal('Could not find data file templates/default.html5', diagnostics[1].message)
218+
end)
219+
220+
it('returns empty table for clean stderr', function()
221+
local diagnostics = presets.markdown.error_parser('', md_ctx)
222+
assert.are.same({}, diagnostics)
223+
end)
120224
end)
121225

122226
describe('github', function()
@@ -179,5 +283,21 @@ describe('presets', function()
179283
it('has open enabled', function()
180284
assert.is_true(presets.github.open)
181285
end)
286+
287+
it('parses pandoc parse errors from stderr', function()
288+
local stderr = 'Error at "document.md" (line 12, column 5): unexpected "}" expecting letter'
289+
local diagnostics = presets.github.error_parser(stderr, md_ctx)
290+
assert.is_table(diagnostics)
291+
assert.are.equal(1, #diagnostics)
292+
assert.are.equal(11, diagnostics[1].lnum)
293+
assert.are.equal(4, diagnostics[1].col)
294+
assert.are.equal('unexpected "}" expecting letter', diagnostics[1].message)
295+
assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity)
296+
end)
297+
298+
it('returns empty table for clean stderr', function()
299+
local diagnostics = presets.github.error_parser('', md_ctx)
300+
assert.are.same({}, diagnostics)
301+
end)
182302
end)
183303
end)

0 commit comments

Comments
 (0)