describe('presets', function() local presets before_each(function() presets = require('preview.presets') end) local ctx = { bufnr = 1, file = '/tmp/document.typ', root = '/tmp', ft = 'typst', } describe('typst', function() it('has ft', function() assert.are.equal('typst', presets.typst.ft) end) it('has cmd', function() assert.are.same({ 'typst', 'compile' }, presets.typst.cmd) end) it('returns args with diagnostic format and file path', function() local args = presets.typst.args(ctx) assert.is_table(args) assert.are.same({ '--diagnostic-format', 'short', '/tmp/document.typ' }, args) end) it('returns pdf output path', function() local output = presets.typst.output(ctx) assert.is_string(output) assert.are.equal('/tmp/document.pdf', output) end) it('returns clean command', function() assert.are.same({ 'rm', '-f', '/tmp/document.pdf' }, presets.typst.clean(ctx)) end) it('has open enabled', function() assert.is_true(presets.typst.open) end) it('has reload as a function', function() assert.is_function(presets.typst.reload) end) it('reload returns typst watch command', function() local result = presets.typst.reload(ctx) assert.is_table(result) assert.are.equal('typst', result[1]) assert.are.equal('watch', result[2]) assert.are.equal('--diagnostic-format', result[3]) assert.are.equal('short', result[4]) assert.are.equal(ctx.file, result[5]) end) it('parses errors from stderr', function() local stderr = table.concat({ 'main.typ:5:23: error: unexpected token', 'main.typ:12:1: warning: unused variable', }, '\n') local diagnostics = presets.typst.error_parser(stderr, ctx) assert.is_table(diagnostics) assert.are.equal(2, #diagnostics) assert.are.equal(4, diagnostics[1].lnum) assert.are.equal(22, diagnostics[1].col) assert.are.equal('unexpected token', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) assert.is_nil(diagnostics[1].source) assert.are.equal(11, diagnostics[2].lnum) assert.are.equal(0, diagnostics[2].col) assert.are.equal('unused variable', diagnostics[2].message) assert.are.equal(vim.diagnostic.severity.WARN, diagnostics[2].severity) end) it('returns empty table for clean stderr', function() local diagnostics = presets.typst.error_parser('', ctx) assert.are.same({}, diagnostics) end) end) describe('latex', function() local tex_ctx = { bufnr = 1, file = '/tmp/document.tex', root = '/tmp', ft = 'tex', } it('has ft', function() assert.are.equal('tex', presets.latex.ft) end) it('has cmd', function() assert.are.same({ 'latexmk' }, presets.latex.cmd) end) it('returns args with pdf flag and file path', function() local args = presets.latex.args(tex_ctx) assert.is_table(args) assert.are.same({ '-pdf', '-interaction=nonstopmode', '-synctex=1', '-pdflatex=pdflatex -file-line-error -interaction=nonstopmode %O %S', '/tmp/document.tex', }, args) end) it('returns pdf output path', function() local output = presets.latex.output(tex_ctx) assert.is_string(output) assert.are.equal('/tmp/document.pdf', output) end) it('returns clean command', function() local clean = presets.latex.clean(tex_ctx) assert.is_table(clean) assert.are.same({ 'latexmk', '-c', '/tmp/document.tex' }, clean) end) it('has open enabled', function() assert.is_true(presets.latex.open) end) it('parses file-line-error format from output', function() local output = table.concat({ './document.tex:10: Undefined control sequence.', 'l.10 \\badcommand', 'Collected error summary (may duplicate other messages):', " pdflatex: Command for 'pdflatex' gave return code 256", }, '\n') local diagnostics = presets.latex.error_parser(output, tex_ctx) assert.is_table(diagnostics) assert.is_true(#diagnostics > 0) assert.are.equal(9, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('Undefined control sequence.', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('parses collected error summary', function() local output = table.concat({ 'Latexmk: Errors, so I did not complete making targets', 'Collected error summary (may duplicate other messages):', " pdflatex: Command for 'pdflatex' gave return code 256", }, '\n') local diagnostics = presets.latex.error_parser(output, tex_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(0, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal( "pdflatex: Command for 'pdflatex' gave return code 256", diagnostics[1].message ) end) it('returns empty table for clean stderr', function() local diagnostics = presets.latex.error_parser('', tex_ctx) assert.are.same({}, diagnostics) end) end) describe('pdflatex', function() local tex_ctx = { bufnr = 1, file = '/tmp/document.tex', root = '/tmp', ft = 'tex', } it('has ft', function() assert.are.equal('tex', presets.pdflatex.ft) end) it('has cmd', function() assert.are.same({ 'pdflatex' }, presets.pdflatex.cmd) end) it('returns args with flags and file path', function() local args = presets.pdflatex.args(tex_ctx) assert.are.same( { '-interaction=nonstopmode', '-file-line-error', '-synctex=1', '/tmp/document.tex' }, args ) end) it('returns pdf output path', function() assert.are.equal('/tmp/document.pdf', presets.pdflatex.output(tex_ctx)) end) it('has open enabled', function() assert.is_true(presets.pdflatex.open) end) it('returns clean command removing pdf and aux files', function() local clean = presets.pdflatex.clean(tex_ctx) assert.are.same({ 'rm', '-f', '/tmp/document.pdf', '/tmp/document.aux', '/tmp/document.log', '/tmp/document.synctex.gz', }, clean) end) it('has no reload', function() assert.is_nil(presets.pdflatex.reload) end) it('parses file-line-error format', function() local output = './document.tex:10: Undefined control sequence.' local diagnostics = presets.pdflatex.error_parser(output, tex_ctx) assert.are.equal(1, #diagnostics) assert.are.equal(9, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('Undefined control sequence.', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('returns empty table for clean output', function() assert.are.same({}, presets.pdflatex.error_parser('', tex_ctx)) end) end) describe('tectonic', function() local tex_ctx = { bufnr = 1, file = '/tmp/document.tex', root = '/tmp', ft = 'tex', } it('has ft', function() assert.are.equal('tex', presets.tectonic.ft) end) it('has cmd', function() assert.are.same({ 'tectonic' }, presets.tectonic.cmd) end) it('returns args with file path', function() assert.are.same({ '/tmp/document.tex' }, presets.tectonic.args(tex_ctx)) end) it('returns pdf output path', function() assert.are.equal('/tmp/document.pdf', presets.tectonic.output(tex_ctx)) end) it('has open enabled', function() assert.is_true(presets.tectonic.open) end) it('returns clean command removing pdf', function() assert.are.same({ 'rm', '-f', '/tmp/document.pdf' }, presets.tectonic.clean(tex_ctx)) end) it('has no reload', function() assert.is_nil(presets.tectonic.reload) end) it('parses file-line-error format', function() local output = './document.tex:5: Missing $ inserted.' local diagnostics = presets.tectonic.error_parser(output, tex_ctx) assert.are.equal(1, #diagnostics) assert.are.equal(4, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('Missing $ inserted.', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('returns empty table for clean output', function() assert.are.same({}, presets.tectonic.error_parser('', tex_ctx)) end) end) describe('markdown', function() local md_ctx = { bufnr = 1, file = '/tmp/document.md', root = '/tmp', ft = 'markdown', output = '/tmp/document.html', } it('has ft', function() assert.are.equal('markdown', presets.markdown.ft) end) it('has cmd', function() assert.are.same({ 'pandoc' }, presets.markdown.cmd) end) it('returns args with standalone and katex flags', function() local args = presets.markdown.args(md_ctx) assert.is_table(args) assert.are.same( { '/tmp/document.md', '-s', '--katex', '-o', '/tmp/document.html' }, args ) end) it('returns html output path', function() local output = presets.markdown.output(md_ctx) assert.is_string(output) assert.are.equal('/tmp/document.html', output) end) it('returns clean command', function() local clean = presets.markdown.clean(md_ctx) assert.is_table(clean) assert.are.same({ 'rm', '-f', '/tmp/document.html' }, clean) end) it('has open enabled', function() assert.is_true(presets.markdown.open) end) it('has reload enabled for SSE', function() assert.is_true(presets.markdown.reload) end) it('parses YAML metadata errors with multiline message', function() local output = table.concat({ 'Error parsing YAML metadata at "/tmp/test.md" (line 1, column 1):', 'YAML parse exception at line 1, column 9:', 'mapping values are not allowed in this context', }, '\n') local diagnostics = presets.markdown.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(0, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('mapping values are not allowed in this context', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('parses Error at format', function() local output = 'Error at "source" (line 75, column 1): unexpected end of input' local diagnostics = presets.markdown.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(74, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('unexpected end of input', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('parses generic pandoc errors', function() local output = 'pandoc: Could not find data file templates/default.html5' local diagnostics = presets.markdown.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(0, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('Could not find data file templates/default.html5', diagnostics[1].message) end) it('returns empty table for clean output', function() local diagnostics = presets.markdown.error_parser('', md_ctx) assert.are.same({}, diagnostics) end) end) describe('github', function() local md_ctx = { bufnr = 1, file = '/tmp/document.md', root = '/tmp', ft = 'markdown', output = '/tmp/document.html', } it('has ft', function() assert.are.equal('markdown', presets.github.ft) end) it('has cmd', function() assert.are.same({ 'pandoc' }, presets.github.cmd) end) it('returns args with standalone, katex, and css flags', function() local args = presets.github.args(md_ctx) assert.is_table(args) assert.are.same({ '-f', 'gfm', '/tmp/document.md', '-s', '--katex', '--css', 'https://cdn.jsdelivr.net/gh/pixelbrackets/gfm-stylesheet@master/dist/gfm.css', '-o', '/tmp/document.html', }, args) end) it('args include -f and gfm flags', function() local args = presets.github.args(md_ctx) local idx = nil for i, v in ipairs(args) do if v == '-f' then idx = i break end end assert.is_not_nil(idx) assert.are.equal('gfm', args[idx + 1]) end) it('returns html output path', function() local output = presets.github.output(md_ctx) assert.is_string(output) assert.are.equal('/tmp/document.html', output) end) it('returns clean command', function() local clean = presets.github.clean(md_ctx) assert.is_table(clean) assert.are.same({ 'rm', '-f', '/tmp/document.html' }, clean) end) it('has open enabled', function() assert.is_true(presets.github.open) end) it('has reload enabled for SSE', function() assert.is_true(presets.github.reload) end) it('parses YAML metadata errors with multiline message', function() local output = table.concat({ 'Error parsing YAML metadata at "/tmp/test.md" (line 1, column 1):', 'YAML parse exception at line 1, column 9:', 'mapping values are not allowed in this context', }, '\n') local diagnostics = presets.github.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(0, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('mapping values are not allowed in this context', diagnostics[1].message) end) it('parses Error at format', function() local output = 'Error at "document.md" (line 12, column 5): unexpected "}" expecting letter' local diagnostics = presets.github.error_parser(output, md_ctx) assert.is_table(diagnostics) assert.are.equal(1, #diagnostics) assert.are.equal(11, diagnostics[1].lnum) assert.are.equal(4, diagnostics[1].col) assert.are.equal('unexpected "}" expecting letter', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('returns empty table for clean output', function() local diagnostics = presets.github.error_parser('', md_ctx) assert.are.same({}, diagnostics) end) end) describe('asciidoctor', function() local adoc_ctx = { bufnr = 1, file = '/tmp/document.adoc', root = '/tmp', ft = 'asciidoc', output = '/tmp/document.html', } it('has ft', function() assert.are.equal('asciidoc', presets.asciidoctor.ft) end) it('has cmd', function() assert.are.same({ 'asciidoctor' }, presets.asciidoctor.cmd) end) it('returns args with file and output', function() assert.are.same( { '--failure-level', 'ERROR', '/tmp/document.adoc', '-o', '/tmp/document.html' }, presets.asciidoctor.args(adoc_ctx) ) end) it('returns html output path', function() assert.are.equal('/tmp/document.html', presets.asciidoctor.output(adoc_ctx)) end) it('returns clean command', function() assert.are.same({ 'rm', '-f', '/tmp/document.html' }, presets.asciidoctor.clean(adoc_ctx)) end) it('has open enabled', function() assert.is_true(presets.asciidoctor.open) end) it('has reload enabled for SSE', function() assert.is_true(presets.asciidoctor.reload) end) it('parses error messages', function() local output = 'asciidoctor: ERROR: document.adoc: line 8: invalid part, must have at least one section' local diagnostics = presets.asciidoctor.error_parser(output, adoc_ctx) assert.are.equal(1, #diagnostics) assert.are.equal(7, diagnostics[1].lnum) assert.are.equal(0, diagnostics[1].col) assert.are.equal('invalid part, must have at least one section', diagnostics[1].message) assert.are.equal(vim.diagnostic.severity.ERROR, diagnostics[1].severity) end) it('parses warning messages', function() local output = 'asciidoctor: WARNING: document.adoc: line 52: section title out of sequence' local diagnostics = presets.asciidoctor.error_parser(output, adoc_ctx) assert.are.equal(1, #diagnostics) assert.are.equal(51, diagnostics[1].lnum) assert.are.equal(vim.diagnostic.severity.WARN, diagnostics[1].severity) end) it('returns empty table for clean output', function() assert.are.same({}, presets.asciidoctor.error_parser('', adoc_ctx)) end) end) describe('quarto', function() local qmd_ctx = { bufnr = 1, file = '/tmp/document.qmd', root = '/tmp', ft = 'quarto', output = '/tmp/document.html', } it('has ft', function() assert.are.equal('quarto', presets.quarto.ft) end) it('has cmd', function() assert.are.same({ 'quarto' }, presets.quarto.cmd) end) it('returns args with render subcommand and html format', function() assert.are.same( { 'render', '/tmp/document.qmd', '--to', 'html', '--embed-resources' }, presets.quarto.args(qmd_ctx) ) end) it('returns html output path', function() assert.are.equal('/tmp/document.html', presets.quarto.output(qmd_ctx)) end) it('returns clean command removing html and _files directory', function() assert.are.same( { 'rm', '-rf', '/tmp/document.html', '/tmp/document_files' }, presets.quarto.clean(qmd_ctx) ) end) it('has open enabled', function() assert.is_true(presets.quarto.open) end) it('has reload enabled for SSE', function() assert.is_true(presets.quarto.reload) end) it('has no error_parser', function() assert.is_nil(presets.quarto.error_parser) end) end) end)