feat(image): added support for math expressions in latex and markdown doc + images in latex. Closes #1223

This commit is contained in:
Folke Lemaitre 2025-02-17 12:50:35 +01:00
parent 0bf0c6223d
commit 1bca71a133
No known key found for this signature in database
GPG key ID: 41F8B1FBACAE2040

View file

@ -6,16 +6,35 @@ local M = {}
---@field win snacks.win
---@field buf number
---@alias snacks.image.transform fun(buf:number, src:string, anchor: TSNode, image: TSNode): string
---@alias TSMatch {node:TSNode, meta:vim.treesitter.query.TSMetadata}
---@alias snacks.image.ctx {buf:number, pos?: TSMatch, src?: TSMatch, content?: TSMatch}
---@alias snacks.image.match {id: string, pos: snacks.image.Pos, src?: string, content?: string, ext?: string}
---@alias snacks.image.transform fun(match: snacks.image.match, ctx: snacks.image.ctx)
---@type table<string, snacks.image.transform>
M.transforms = {
---@param anchor TSNode
---@param img TSNode
norg = function(buf, _, anchor, img)
local row, col = img:start()
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, false)[1]
return line:sub(col + 1)
norg = function(img, ctx)
local row, col = ctx.src.node:start()
local line = vim.api.nvim_buf_get_lines(ctx.buf, row, row + 1, false)[1]
img.src = line:sub(col + 1)
end,
latex = function(img, ctx)
local fg = Snacks.util.color({ "@markup.math.latex", "Special", "Normal" }) or "#000000"
img.ext = "tex"
local content = vim.trim(img.content or "")
content = content:gsub("^%$+`?", ""):gsub("`?%$+$", "")
content = content:gsub("^\\%[", ""):gsub("\\%]$", "")
if not content:find("^\\begin") then
content = ("\\[%s\\]"):format(content)
end
img.content = ([[
\documentclass[preview,border=2pt,varwidth]{standalone}
\usepackage{xcolor, amsmath, amssymb}
\begin{document}
{ \large \color[HTML]{%s}
%s}
\end{document}
]]):format(fg:upper():sub(2), content)
end,
}
@ -62,7 +81,7 @@ function M.find(buf, from, to)
return {}
end
parser:parse(from and to and { from, to } or true)
local ret = {} ---@type {id:string, pos:snacks.image.Pos, src:string}[]
local ret = {} ---@type snacks.image.match[]
parser:for_each_tree(function(tstree, tree)
if not tstree then
return
@ -72,31 +91,56 @@ function M.find(buf, from, to)
return
end
for _, match, meta in query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to and to - 1 or nil) do
local src, pos, nid ---@type string, snacks.image.Pos, string
local anchor, image ---@type TSNode, TSNode
local ctx = {} ---@type snacks.image.ctx
local lang = meta["injection.language"] or tree:lang()
for id, nodes in pairs(match) do
nodes = type(nodes) == "userdata" and { nodes } or nodes
local name = query.captures[id]
for _, node in ipairs(nodes) do
if name == "image" then
image = node
src = vim.treesitter.get_node_text(node, buf, { metadata = meta[id] })
elseif name == "anchor" then
anchor = node
local range = { node:range() }
pos = { range[1] + 1, range[2] }
nid = node:id()
end
local field = name == "image" and "pos" or name:match("^image%.(.*)$")
if field then
ctx[field] = { node = nodes[1], meta = meta[id] or {} }
end
end
if src and pos and nid then
local transform = M.transforms[tree:lang()]
if transform then
src = transform(buf, src, anchor, image)
end
src = M.resolve(buf, src)
ret[#ret + 1] = { id = nid, pos = pos, src = src }
assert(ctx.src or ctx.content, "no image src or content")
ctx.pos = ctx.pos or ctx.src or ctx.content
assert(ctx.pos, "no image node")
local range = vim.treesitter.get_range(ctx.pos.node, buf, ctx.pos.meta)
---@type snacks.image.match
local img = {
ext = meta.ext,
id = ctx.pos.node:id(),
pos = {
range[1] == range[4] and (range[1] + 1) or (range[4] + 1),
math.min(range[2], range[5]),
},
}
img.pos[1] = math.min(img.pos[1], vim.api.nvim_buf_line_count(buf))
if ctx.src then
img.src = vim.treesitter.get_node_text(ctx.src.node, buf, { metadata = ctx.src.meta })
end
if ctx.content then
img.content = vim.treesitter.get_node_text(ctx.content.node, buf, { metadata = ctx.content.meta })
end
local transform = M.transforms[lang]
if transform then
transform(img, ctx)
end
if img.src then
img.src = M.resolve(buf, img.src)
end
if img.content and not img.src then
local root = Snacks.image.config.cache
vim.fn.mkdir(root, "p")
img.src = root .. "/" .. vim.fn.sha256(img.content):sub(1, 8) .. "-content." .. (img.ext or "png")
if vim.fn.filereadable(img.src) == 0 then
local fd = assert(io.open(img.src, "w"), "failed to open " .. img.src)
fd:write(img.content)
fd:close()
end
end
ret[#ret + 1] = img
end
end)
return ret