Skip to content

Commit

Permalink
feat: add LaTex rendering for inline equations (#1133)
Browse files Browse the repository at this point in the history
This commit adds `image.nvim` integration as well as latex rendering integration into Neorg.
  • Loading branch information
nolbap authored Nov 17, 2023
1 parent d5f3ad0 commit b5393e8
Show file tree
Hide file tree
Showing 3 changed files with 277 additions and 1 deletion.
4 changes: 3 additions & 1 deletion lua/neorg/modules/core/defaults/module.lua
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ return modules.create_meta(
"core.esupports.indent",
"core.esupports.metagen",
"core.integrations.treesitter",
"core.integrations.image",
"core.itero",
"core.journal",
"core.keybinds",
Expand All @@ -46,5 +47,6 @@ return modules.create_meta(
"core.qol.todo_items",
"core.storage",
"core.tangle",
"core.upgrade"
"core.upgrade",
"core.latex.renderer"
)
55 changes: 55 additions & 0 deletions lua/neorg/modules/core/integrations/image/module.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
local neorg = require("neorg.core")
local module = neorg.modules.create("core.integrations.image")

module.load = function()
local success, image = pcall(require, "image")

assert(success, "Unable to load image.nvim plugin")

module.private.image = image
end

module.private = {
image = nil,
}

module.public = {
new_image = function(buffernr, png_path, position, window, scale, virtual_padding)
local geometry = {
x = position.column_start,
y = position.row_start + (virtual_padding and 1 or 0),
width = position.column_end - position.column_start,
height = scale,
}
local image = require("image").from_file(png_path, {
window = window,
buffer = buffernr,
with_virtual_padding = virtual_padding,
})
image:render(geometry)
end,
get_images = function()
return (require("image").get_images())
end,
render = function(images)
for _, image in pairs(images) do
image:clear()
image:render()
end
end,
clear = function()
local images = module.public.get_images()
for _, image in pairs(images) do
image:clear()
end
end,
clear_at_cursor = function(images, row)
for _, image in pairs(images) do
if image.geometry.y == row then
image:clear()
end
end
end,
}

return module
219 changes: 219 additions & 0 deletions lua/neorg/modules/core/latex/renderer/module.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
local neorg = require("neorg.core")
local module = neorg.modules.create("core.latex.renderer")
local modules = neorg.modules

assert(vim.re ~= nil, "Neovim 0.10.0+ is required to run the `core.renderer.latex` module! ")

module.setup = function()
return {
requires = {
"core.integrations.treesitter",
"core.autocommands",
"core.neorgcmd",
},
}
end

module.load = function()
local success, image = pcall(neorg.modules.get_module, module.config.public.renderer)

assert(success, "Unable to load image module")

module.private.image = image

module.required["core.autocommands"].enable_autocommand("BufWinEnter")
module.required["core.autocommands"].enable_autocommand("CursorMoved")
module.required["core.autocommands"].enable_autocommand("TextChanged")
module.required["core.autocommands"].enable_autocommand("TextChangedI")
module.required["core.autocommands"].enable_autocommand("TextChangedP")
module.required["core.autocommands"].enable_autocommand("TextChangedT")

modules.await("core.neorgcmd", function(neorgcmd)
neorgcmd.add_commands_from_table({
["render-latex"] = {
name = "core.latex.renderer.render",
args = 0,
condition = "norg",
},
})
end)
end

module.public = {
latex_renderer = function()
Ranges = {}
module.required["core.integrations.treesitter"].execute_query(
[[
(
(inline_math) @latex
(#offset! @latex 0 1 0 -1)
)
]],
function(query, id, node)
if query.captures[id] ~= "latex" then
return
end

local latex_snippet =
module.required["core.integrations.treesitter"].get_node_text(node, vim.api.nvim_get_current_buf())

local png_location = module.public.parse_latex(latex_snippet)

module.private.image.new_image(
vim.api.nvim_get_current_buf(),
png_location,
module.required["core.integrations.treesitter"].get_node_range(node),
vim.api.nvim_get_current_win(),
module.config.public.scale,
not module.config.public.conceal
)

table.insert(Ranges, { node:range() })
end
)
Images = module.private.image.get_images()
end,
create_latex_document = function(snippet)
local tempname = vim.fn.tempname()

local tempfile = io.open(tempname, "w")

if not tempfile then
return
end

local content = table.concat({
"\\documentclass[6pt]{standalone}",
"\\usepackage{amsmath}",
"\\usepackage{amssymb}",
"\\usepackage{graphicx}",
"\\begin{document}",
snippet,
"\\end{document}",
}, "\n")

tempfile:write(content)
tempfile:close()

return tempname
end,

-- Returns a handle to an image containing
-- the rendered snippet.
-- This handle can then be delegated to an external renderer.
parse_latex = function(snippet)
local document_name = module.public.create_latex_document(snippet)

if not document_name then
return
end

local cwd = vim.fn.fnamemodify(document_name, ":h")
vim.fn.jobwait({
vim.fn.jobstart(
"latex --interaction=nonstopmode --output-dir=" .. cwd .. " --output-format=dvi " .. document_name,
{ cwd = cwd }
),
})

local png_result = vim.fn.tempname()
-- TODO: Make the conversions async via `on_exit`
vim.fn.jobwait({
vim.fn.jobstart(
"dvipng -D "
.. tostring(module.config.public.dpi)
.. " -T tight -bg Transparent -fg 'cmyk 0.00 0.04 0.21 0.02' -o "
.. png_result
.. " "
.. document_name
.. ".dvi",
{ cwd = vim.fn.fnamemodify(document_name, ":h") }
),
})

return png_result
end,
render_inline_math = function(images)
local conceal_on = (vim.wo.conceallevel >= 2) and module.config.public.conceal
if conceal_on then
table.sort(images, function(a, b)
return a.internal_id < b.internal_id
end)

for i, range in ipairs(Ranges) do
vim.api.nvim_buf_set_extmark(
vim.api.nvim_get_current_buf(),
vim.api.nvim_create_namespace("concealer"),
range[1],
range[2],
{
id = i,
end_col = range[4],
conceal = "",
virt_text = { { (" "):rep(images[i].rendered_geometry.width) } },
virt_text_pos = "inline",
}
)
end
end
end,
}

module.config.public = {
-- TODO: Documentation
conceal = true,
dpi = 350,
render_on_enter = false,
renderer = "core.integrations.image",
scale = 1,
}

local function render_latex()
module.private.image.clear(Images)
neorg.modules.get_module("core.latex.renderer").latex_renderer()
neorg.modules.get_module("core.latex.renderer").render_inline_math(Images)
end

local function clear_latex()
module.private.image.clear(Images)
end

local function clear_at_cursor()
if Images ~= nil then
module.private.image.render(Images)
module.private.image.clear_at_cursor(Images, vim.api.nvim_win_get_cursor(0)[1] - 1)
end
end

local event_handlers = {
["core.neorgcmd.events.core.latex.renderer.render"] = render_latex,
["core.autocommands.events.bufwinenter"] = render_latex,
["core.autocommands.events.cursormoved"] = clear_at_cursor,
["core.autocommands.events.textchanged"] = clear_latex,
["core.autocommands.events.textchangedi"] = clear_latex,
["core.autocommands.events.textchangedp"] = clear_latex,
["core.autocommands.events.textchangedt"] = clear_latex,
}

module.on_event = function(event)
if event.referrer == "core.autocommands" and vim.bo[event.buffer].ft ~= "norg" then
return
end

return event_handlers[event.type](event)
end

module.events.subscribed = {
["core.autocommands"] = {
bufwinenter = module.config.public.render_on_enter,
cursormoved = true,
textchanged = true,
textchangedi = true,
textchangedp = true,
textchangedt = true,
},
["core.neorgcmd"] = {
["core.latex.renderer.render"] = true,
},
}
return module

0 comments on commit b5393e8

Please sign in to comment.