diff --git a/lua/snacks/image/doc.lua b/lua/snacks/image/doc.lua index 51f5c9ce..2c737a14 100644 --- a/lua/snacks/image/doc.lua +++ b/lua/snacks/image/doc.lua @@ -6,160 +6,22 @@ local M = {} ---@field win snacks.win ---@field buf number ----@class snacks.image.Query ----@field setup fun():vim.treesitter.Query ----@field query? vim.treesitter.Query|false ----@field transform? fun(buf:number, src:string, anchor: TSNode, image: TSNode): string +---@alias snacks.image.transform fun(buf:number, src:string, anchor: TSNode, image: TSNode): string ----@type table -M._queries = { - markdown = { - setup = function() - return vim.treesitter.query.parse( - "markdown_inline", - [[ - (image - [ - (link_destination) @image - (image_description (shortcut_link (link_text) @image)) - ]) @anchor - ]] - ) - end, - transform = function(_, src) - return src:gsub("|.*", ""):gsub("^<", ""):gsub(">$", "") - end, - }, - html = { - setup = function() - return vim.treesitter.query.parse( - "html", - [[ - (element - (start_tag - (tag_name) @tag (#eq? @tag "img") - (attribute - (attribute_name) @attr_name (#eq? @attr_name "src") - (quoted_attribute_value (attribute_value) @image) - ) - ) - ) @anchor - (self_closing_tag - (tag_name) @tag (#eq? @tag "img") - (attribute - (attribute_name) @attr_name (#eq? @attr_name "src") - (quoted_attribute_value (attribute_value) @image) - ) - ) @anchor - ]] - ) - end, - }, - tsx = { - setup = function() - return vim.treesitter.query.parse( - "tsx", - [[ - (jsx_element - (jsx_opening_element - (identifier) @tag (#eq? @tag "img") - (jsx_attribute - (property_identifier) @attr_name (#eq? @attr_name "src") - (string (string_fragment) @image) - ) - ) - ) @anchor - - (jsx_self_closing_element - (identifier) @tag (#eq? @tag "img") - (jsx_attribute - (property_identifier) @attr_name (#eq? @attr_name "src") - (string (string_fragment) @image) - ) - ) @anchor - ]] - ) - end, - }, - javascript = { - setup = function() - return vim.treesitter.query.parse( - "javascript", - [[ - (jsx_element - (jsx_opening_element - (identifier) @tag (#eq? @tag "img") - (jsx_attribute - (property_identifier) @attr_name (#eq? @attr_name "src") - (string (string_fragment) @image) - ) - ) - ) @anchor - - (jsx_self_closing_element - (identifier) @tag (#eq? @tag "img") - (jsx_attribute - (property_identifier) @attr_name (#eq? @attr_name "src") - (string (string_fragment) @image) - ) - ) @anchor - ]] - ) - end, - }, - css = { - setup = function() - return vim.treesitter.query.parse( - "css", - [[ - (declaration - (call_expression - (function_name) @fn (#eq? @fn "url") - (arguments [(plain_value) @image (string_value (string_content) @image)])) - ) @anchor - ]] - ) - end, - }, - norg = { - setup = function() - return vim.treesitter.query.parse( - "norg", - [[ - (infirm_tag - (tag_name) @tag (#eq? @tag "image") - (tag_parameters (tag_param) @image) - ) @anchor - ]] - ) - end, - ---@param anchor TSNode - ---@param img TSNode - transform = 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) - end, - }, +---@type table +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) + end, } local hover ---@type snacks.image.Hover? local uv = vim.uv or vim.loop -function M.queries() - local ret = {} ---@type snacks.image.Query[] - for _, query in pairs(M._queries) do - if query.query == nil then - local ok, q = pcall(query.setup) - query.query = ok and q or false - end - if query.query then - table.insert(ret, query) - end - end - return ret -end - ---@param str string function M.url_decode(str) return str:gsub("+", " "):gsub("%%(%x%x)", function(hex) @@ -201,36 +63,39 @@ function M.find(buf, from, to) end parser:parse(from and to and { from, to } or true) local ret = {} ---@type {id:string, pos:snacks.image.Pos, src:string}[] - parser:for_each_tree(function(tstree) + parser:for_each_tree(function(tstree, tree) if not tstree then return end - for _, query in ipairs(M.queries()) do - for _, match in query.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 - for id, nodes in pairs(match) do - nodes = type(nodes) == "userdata" and { nodes } or nodes - local name = query.query.captures[id] - for _, node in ipairs(nodes) do - if name == "image" then - image = node - src = vim.treesitter.get_node_text(node, buf) - elseif name == "anchor" then - anchor = node - local range = { node:range() } - pos = { range[1] + 1, range[2] } - nid = node:id() - end + local query = vim.treesitter.query.get(tree:lang(), "images") + if not query then + 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 + 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 end - if src and pos and nid then - if query.transform then - src = query.transform(buf, src, anchor, image) - end - src = M.resolve(buf, src) - ret[#ret + 1] = { id = nid, pos = pos, src = src } + 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 } end end end) diff --git a/lua/snacks/image/init.lua b/lua/snacks/image/init.lua index 244f2cee..514b4a3b 100644 --- a/lua/snacks/image/init.lua +++ b/lua/snacks/image/init.lua @@ -51,7 +51,6 @@ local defaults = { -- a treesitter parser must be available for the enabled languages. -- supported language injections: markdown, html enabled = true, - lang = { "markdown", "html", "norg", "tsx", "javascript", "css", "vue", "angular" }, -- render the image inline in the buffer -- if your env doesn't support unicode placeholders, this will be disabled -- takes precedence over `opts.float` on supported terminals @@ -127,6 +126,14 @@ function M.hover() M.doc.hover() end +---@return string[] +function M.langs() + local queries = vim.api.nvim_get_runtime_file("queries/*/images.scm", true) + return vim.tbl_map(function(q) + return q:match("queries/(.-)/images%.scm") + end, queries) +end + ---@private ---@param ev? vim.api.keyset.create_autocmd.callback_args function M.setup(ev) @@ -156,12 +163,13 @@ function M.setup(ev) }) end if M.config.enabled and M.config.doc.enabled then + local langs = M.langs() vim.api.nvim_create_autocmd("FileType", { group = group, callback = function(e) local ft = vim.bo[e.buf].filetype local lang = vim.treesitter.language.get_lang(ft) - if vim.tbl_contains(M.config.doc.lang, lang) then + if vim.tbl_contains(langs, lang) then vim.schedule(function() M.doc.attach(e.buf) end) @@ -208,14 +216,15 @@ function M.health() ) ) - M.doc.queries() - for lang, q in pairs(M.doc._queries) do - if q.query then - Snacks.health.ok("Images rendering for `" .. lang .. "` is available") + for _, lang in ipairs(M.langs()) do + local ok, parser = pcall(vim.treesitter.get_string_parser, "", lang) + if ok and parser then + Snacks.health.ok("Image rendering for `" .. lang .. "` is available") else - Snacks.health.warn("Images rendering for `" .. lang .. "` is not available.\nMissing treesitter parser.") + Snacks.health.error("Image rendering for `" .. lang .. "` is not available") end end + if env.supported then Snacks.health.ok("your terminal supports the kitty graphics protocol") elseif M.config.force then diff --git a/queries/css/images.scm b/queries/css/images.scm new file mode 100644 index 00000000..bfcfde54 --- /dev/null +++ b/queries/css/images.scm @@ -0,0 +1,6 @@ + +(declaration + (call_expression + (function_name) @fn (#eq? @fn "url") + (arguments [(plain_value) @image (string_value (string_content) @image)])) +) @anchor diff --git a/queries/html/images.scm b/queries/html/images.scm new file mode 100644 index 00000000..dd45239f --- /dev/null +++ b/queries/html/images.scm @@ -0,0 +1,17 @@ + +(element + (start_tag + (tag_name) @tag (#eq? @tag "img") + (attribute + (attribute_name) @attr_name (#eq? @attr_name "src") + (quoted_attribute_value (attribute_value) @image) + ) + ) +) @anchor +(self_closing_tag + (tag_name) @tag (#eq? @tag "img") + (attribute + (attribute_name) @attr_name (#eq? @attr_name "src") + (quoted_attribute_value (attribute_value) @image) + ) +) @anchor diff --git a/queries/javascript/images.scm b/queries/javascript/images.scm new file mode 100644 index 00000000..3216bb13 --- /dev/null +++ b/queries/javascript/images.scm @@ -0,0 +1,18 @@ + +(jsx_element + (jsx_opening_element + (identifier) @tag (#eq? @tag "img") + (jsx_attribute + (property_identifier) @attr_name (#eq? @attr_name "src") + (string (string_fragment) @image) + ) + ) +) @anchor + +(jsx_self_closing_element + (identifier) @tag (#eq? @tag "img") + (jsx_attribute + (property_identifier) @attr_name (#eq? @attr_name "src") + (string (string_fragment) @image) + ) +) @anchor diff --git a/queries/markdown/images.scm b/queries/markdown/images.scm new file mode 100644 index 00000000..a78cf02a --- /dev/null +++ b/queries/markdown/images.scm @@ -0,0 +1 @@ +; extends diff --git a/queries/markdown_inline/images.scm b/queries/markdown_inline/images.scm new file mode 100644 index 00000000..2cabcf97 --- /dev/null +++ b/queries/markdown_inline/images.scm @@ -0,0 +1,9 @@ + +(image + [ + (link_destination) @image + (image_description (shortcut_link (link_text) @image)) + (#gsub! @image "|.*" "") ; remove wikilink image options + (#gsub! @image "^<" "") ; remove bracket link + (#gsub! @image "^>" "") + ]) @anchor diff --git a/queries/norg/images.scm b/queries/norg/images.scm new file mode 100644 index 00000000..4aee5108 --- /dev/null +++ b/queries/norg/images.scm @@ -0,0 +1,5 @@ + +(infirm_tag + (tag_name) @tag (#eq? @tag "image") + (tag_parameters (tag_param) @image) +) @anchor diff --git a/queries/scss/images.scm b/queries/scss/images.scm new file mode 100644 index 00000000..f8d00dc2 --- /dev/null +++ b/queries/scss/images.scm @@ -0,0 +1,11 @@ +(declaration + (call_expression + (function_name) @fn (#eq? @fn "url") + (arguments [ + (plain_value) @image + (string_value) @image + ; Remove quotes from the image URL + (#gsub! @image "^['\"]" "") + (#gsub! @image "['\"]$" "") + ])) +) @anchor diff --git a/queries/tsx/images.scm b/queries/tsx/images.scm new file mode 100644 index 00000000..3216bb13 --- /dev/null +++ b/queries/tsx/images.scm @@ -0,0 +1,18 @@ + +(jsx_element + (jsx_opening_element + (identifier) @tag (#eq? @tag "img") + (jsx_attribute + (property_identifier) @attr_name (#eq? @attr_name "src") + (string (string_fragment) @image) + ) + ) +) @anchor + +(jsx_self_closing_element + (identifier) @tag (#eq? @tag "img") + (jsx_attribute + (property_identifier) @attr_name (#eq? @attr_name "src") + (string (string_fragment) @image) + ) +) @anchor diff --git a/queries/vue/images.scm b/queries/vue/images.scm new file mode 100644 index 00000000..afd761a5 --- /dev/null +++ b/queries/vue/images.scm @@ -0,0 +1,2 @@ +; inherits: html +; extends diff --git a/tests/image/test.css b/tests/image/test.css new file mode 100644 index 00000000..f8c7f7b2 --- /dev/null +++ b/tests/image/test.css @@ -0,0 +1,4 @@ +.foo { + background: lightblue url("./test.png") no-repeat fixed center; + content: url(test.png); +} diff --git a/tests/image/test.html b/tests/image/test.html new file mode 100644 index 00000000..00dc1790 --- /dev/null +++ b/tests/image/test.html @@ -0,0 +1,19 @@ + + + Latest release + + + Last commit + + + + diff --git a/tests/image/test.jpg b/tests/image/test.jpg new file mode 100644 index 00000000..caafaf0b Binary files /dev/null and b/tests/image/test.jpg differ diff --git a/tests/image/test.jsx b/tests/image/test.jsx new file mode 100644 index 00000000..e55304ec --- /dev/null +++ b/tests/image/test.jsx @@ -0,0 +1,10 @@ + +export const Modal = (props) => { + + return ( + + + + + ) +} diff --git a/tests/image/test.md b/tests/image/test.md new file mode 100644 index 00000000..24ef84a9 --- /dev/null +++ b/tests/image/test.md @@ -0,0 +1,25 @@ +# test + +## Wikilinks + +!![[test.png]] + +!![[test.png|options]] + +## Injected HTML + +png + + + Latest release + + +## Markdown Links + +![small](https://picsum.photos/200/30) + +![relative png](./test.png) + +![png](test.png) + +![jpg](test.jpg) diff --git a/tests/image/test.norg b/tests/image/test.norg new file mode 100644 index 00000000..9a660bed --- /dev/null +++ b/tests/image/test.norg @@ -0,0 +1,7 @@ +# Test + +- foo + +.image ./test.png + +.image test.png diff --git a/tests/image/test.png b/tests/image/test.png new file mode 100644 index 00000000..97e62722 Binary files /dev/null and b/tests/image/test.png differ diff --git a/tests/image/test.scss b/tests/image/test.scss new file mode 100644 index 00000000..944f5b1d --- /dev/null +++ b/tests/image/test.scss @@ -0,0 +1,5 @@ +.foo { + background: lightblue url("./test.png") no-repeat fixed center; + background: lightblue url("./test.png") no-repeat fixed center; + content: url(test.png); +} diff --git a/tests/image/test.tsx b/tests/image/test.tsx new file mode 100644 index 00000000..69e90e86 --- /dev/null +++ b/tests/image/test.tsx @@ -0,0 +1,9 @@ +export const Modal = (props) => { + + return ( + + + + + ) +} diff --git a/tests/image/test.vue b/tests/image/test.vue new file mode 100644 index 00000000..1e2665ef --- /dev/null +++ b/tests/image/test.vue @@ -0,0 +1,17 @@ + + + + +