mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 21:25:11 +00:00
feat(image): added support for a bunch of aditional languages
This commit is contained in:
parent
332f4278b0
commit
a596f8a9ea
21 changed files with 236 additions and 179 deletions
|
@ -6,160 +6,22 @@ local M = {}
|
||||||
---@field win snacks.win
|
---@field win snacks.win
|
||||||
---@field buf number
|
---@field buf number
|
||||||
|
|
||||||
---@class snacks.image.Query
|
---@alias snacks.image.transform fun(buf:number, src:string, anchor: TSNode, image: TSNode): string
|
||||||
---@field setup fun():vim.treesitter.Query
|
|
||||||
---@field query? vim.treesitter.Query|false
|
|
||||||
---@field transform? fun(buf:number, src:string, anchor: TSNode, image: TSNode): string
|
|
||||||
|
|
||||||
---@type table<string, {setup:(fun():vim.treesitter.Query), query?:vim.treesitter.Query|false}>
|
---@type table<string, snacks.image.transform>
|
||||||
M._queries = {
|
M.transforms = {
|
||||||
markdown = {
|
---@param anchor TSNode
|
||||||
setup = function()
|
---@param img TSNode
|
||||||
return vim.treesitter.query.parse(
|
norg = function(buf, _, anchor, img)
|
||||||
"markdown_inline",
|
local row, col = img:start()
|
||||||
[[
|
local line = vim.api.nvim_buf_get_lines(buf, row, row + 1, false)[1]
|
||||||
(image
|
return line:sub(col + 1)
|
||||||
[
|
end,
|
||||||
(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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local hover ---@type snacks.image.Hover?
|
local hover ---@type snacks.image.Hover?
|
||||||
local uv = vim.uv or vim.loop
|
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
|
---@param str string
|
||||||
function M.url_decode(str)
|
function M.url_decode(str)
|
||||||
return str:gsub("+", " "):gsub("%%(%x%x)", function(hex)
|
return str:gsub("+", " "):gsub("%%(%x%x)", function(hex)
|
||||||
|
@ -201,36 +63,39 @@ function M.find(buf, from, to)
|
||||||
end
|
end
|
||||||
parser:parse(from and to and { from, to } or true)
|
parser:parse(from and to and { from, to } or true)
|
||||||
local ret = {} ---@type {id:string, pos:snacks.image.Pos, src:string}[]
|
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
|
if not tstree then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, query in ipairs(M.queries()) do
|
local query = vim.treesitter.query.get(tree:lang(), "images")
|
||||||
for _, match in query.query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to and to - 1 or nil) do
|
if not query then
|
||||||
local src, pos, nid ---@type string, snacks.image.Pos, string
|
return
|
||||||
local anchor, image ---@type TSNode, TSNode
|
end
|
||||||
for id, nodes in pairs(match) do
|
for _, match, meta in query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to and to - 1 or nil) do
|
||||||
nodes = type(nodes) == "userdata" and { nodes } or nodes
|
local src, pos, nid ---@type string, snacks.image.Pos, string
|
||||||
local name = query.query.captures[id]
|
local anchor, image ---@type TSNode, TSNode
|
||||||
for _, node in ipairs(nodes) do
|
for id, nodes in pairs(match) do
|
||||||
if name == "image" then
|
nodes = type(nodes) == "userdata" and { nodes } or nodes
|
||||||
image = node
|
local name = query.captures[id]
|
||||||
src = vim.treesitter.get_node_text(node, buf)
|
for _, node in ipairs(nodes) do
|
||||||
elseif name == "anchor" then
|
if name == "image" then
|
||||||
anchor = node
|
image = node
|
||||||
local range = { node:range() }
|
src = vim.treesitter.get_node_text(node, buf, { metadata = meta[id] })
|
||||||
pos = { range[1] + 1, range[2] }
|
elseif name == "anchor" then
|
||||||
nid = node:id()
|
anchor = node
|
||||||
end
|
local range = { node:range() }
|
||||||
|
pos = { range[1] + 1, range[2] }
|
||||||
|
nid = node:id()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if src and pos and nid then
|
end
|
||||||
if query.transform then
|
if src and pos and nid then
|
||||||
src = query.transform(buf, src, anchor, image)
|
local transform = M.transforms[tree:lang()]
|
||||||
end
|
if transform then
|
||||||
src = M.resolve(buf, src)
|
src = transform(buf, src, anchor, image)
|
||||||
ret[#ret + 1] = { id = nid, pos = pos, src = src }
|
|
||||||
end
|
end
|
||||||
|
src = M.resolve(buf, src)
|
||||||
|
ret[#ret + 1] = { id = nid, pos = pos, src = src }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -51,7 +51,6 @@ local defaults = {
|
||||||
-- a treesitter parser must be available for the enabled languages.
|
-- a treesitter parser must be available for the enabled languages.
|
||||||
-- supported language injections: markdown, html
|
-- supported language injections: markdown, html
|
||||||
enabled = true,
|
enabled = true,
|
||||||
lang = { "markdown", "html", "norg", "tsx", "javascript", "css", "vue", "angular" },
|
|
||||||
-- render the image inline in the buffer
|
-- render the image inline in the buffer
|
||||||
-- if your env doesn't support unicode placeholders, this will be disabled
|
-- if your env doesn't support unicode placeholders, this will be disabled
|
||||||
-- takes precedence over `opts.float` on supported terminals
|
-- takes precedence over `opts.float` on supported terminals
|
||||||
|
@ -127,6 +126,14 @@ function M.hover()
|
||||||
M.doc.hover()
|
M.doc.hover()
|
||||||
end
|
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
|
---@private
|
||||||
---@param ev? vim.api.keyset.create_autocmd.callback_args
|
---@param ev? vim.api.keyset.create_autocmd.callback_args
|
||||||
function M.setup(ev)
|
function M.setup(ev)
|
||||||
|
@ -156,12 +163,13 @@ function M.setup(ev)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
if M.config.enabled and M.config.doc.enabled then
|
if M.config.enabled and M.config.doc.enabled then
|
||||||
|
local langs = M.langs()
|
||||||
vim.api.nvim_create_autocmd("FileType", {
|
vim.api.nvim_create_autocmd("FileType", {
|
||||||
group = group,
|
group = group,
|
||||||
callback = function(e)
|
callback = function(e)
|
||||||
local ft = vim.bo[e.buf].filetype
|
local ft = vim.bo[e.buf].filetype
|
||||||
local lang = vim.treesitter.language.get_lang(ft)
|
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()
|
vim.schedule(function()
|
||||||
M.doc.attach(e.buf)
|
M.doc.attach(e.buf)
|
||||||
end)
|
end)
|
||||||
|
@ -208,14 +216,15 @@ function M.health()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
M.doc.queries()
|
for _, lang in ipairs(M.langs()) do
|
||||||
for lang, q in pairs(M.doc._queries) do
|
local ok, parser = pcall(vim.treesitter.get_string_parser, "", lang)
|
||||||
if q.query then
|
if ok and parser then
|
||||||
Snacks.health.ok("Images rendering for `" .. lang .. "` is available")
|
Snacks.health.ok("Image rendering for `" .. lang .. "` is available")
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
if env.supported then
|
if env.supported then
|
||||||
Snacks.health.ok("your terminal supports the kitty graphics protocol")
|
Snacks.health.ok("your terminal supports the kitty graphics protocol")
|
||||||
elseif M.config.force then
|
elseif M.config.force then
|
||||||
|
|
6
queries/css/images.scm
Normal file
6
queries/css/images.scm
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
(declaration
|
||||||
|
(call_expression
|
||||||
|
(function_name) @fn (#eq? @fn "url")
|
||||||
|
(arguments [(plain_value) @image (string_value (string_content) @image)]))
|
||||||
|
) @anchor
|
17
queries/html/images.scm
Normal file
17
queries/html/images.scm
Normal file
|
@ -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
|
18
queries/javascript/images.scm
Normal file
18
queries/javascript/images.scm
Normal file
|
@ -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
|
1
queries/markdown/images.scm
Normal file
1
queries/markdown/images.scm
Normal file
|
@ -0,0 +1 @@
|
||||||
|
; extends
|
9
queries/markdown_inline/images.scm
Normal file
9
queries/markdown_inline/images.scm
Normal file
|
@ -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
|
5
queries/norg/images.scm
Normal file
5
queries/norg/images.scm
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
(infirm_tag
|
||||||
|
(tag_name) @tag (#eq? @tag "image")
|
||||||
|
(tag_parameters (tag_param) @image)
|
||||||
|
) @anchor
|
11
queries/scss/images.scm
Normal file
11
queries/scss/images.scm
Normal file
|
@ -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
|
18
queries/tsx/images.scm
Normal file
18
queries/tsx/images.scm
Normal file
|
@ -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
|
2
queries/vue/images.scm
Normal file
2
queries/vue/images.scm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
; inherits: html
|
||||||
|
; extends
|
4
tests/image/test.css
Normal file
4
tests/image/test.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.foo {
|
||||||
|
background: lightblue url("./test.png") no-repeat fixed center;
|
||||||
|
content: url(test.png);
|
||||||
|
}
|
19
tests/image/test.html
Normal file
19
tests/image/test.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<body>
|
||||||
|
<a href="https://github.com/folke/lazy.nvim/releases/latest">
|
||||||
|
<img
|
||||||
|
alt="Latest release"
|
||||||
|
src="https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/folke/lazy.nvim/pulse">
|
||||||
|
<img
|
||||||
|
alt="Last commit"
|
||||||
|
src="https://img.shields.io/github/last-commit/folke/lazy.nvim?style=for-the-badge&logo=starship&color=8bd5ca&logoColor=D9E0EE&labelColor=302D41"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
background: lightblue url("./test.png") no-repeat fixed center;
|
||||||
|
content: url(test.png);
|
||||||
|
</style>
|
BIN
tests/image/test.jpg
Normal file
BIN
tests/image/test.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
10
tests/image/test.jsx
Normal file
10
tests/image/test.jsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export const Modal = (props) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show>
|
||||||
|
<img src="test.png" />
|
||||||
|
<img src="https://picsum.photos/200/300"></img>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
25
tests/image/test.md
Normal file
25
tests/image/test.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# test
|
||||||
|
|
||||||
|
## Wikilinks
|
||||||
|
|
||||||
|
!![[test.png]]
|
||||||
|
|
||||||
|
!![[test.png|options]]
|
||||||
|
|
||||||
|
## Injected HTML
|
||||||
|
|
||||||
|
<img src="test.png" alt="png" width="200" height="30" />
|
||||||
|
|
||||||
|
<a href="https://github.com/folke/lazy.nvim/releases/latest">
|
||||||
|
<img alt="Latest release" src="https://img.shields.io/github/v/release/folke/lazy.nvim?style=for-the-badge&logo=starship&color=C9CBFF&logoColor=D9E0EE&labelColor=302D41&include_prerelease&sort=semver" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Markdown Links
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
7
tests/image/test.norg
Normal file
7
tests/image/test.norg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Test
|
||||||
|
|
||||||
|
- foo
|
||||||
|
|
||||||
|
.image ./test.png
|
||||||
|
|
||||||
|
.image test.png
|
BIN
tests/image/test.png
Normal file
BIN
tests/image/test.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.4 MiB |
5
tests/image/test.scss
Normal file
5
tests/image/test.scss
Normal file
|
@ -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);
|
||||||
|
}
|
9
tests/image/test.tsx
Normal file
9
tests/image/test.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export const Modal = (props) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show>
|
||||||
|
<img src="test.png" />
|
||||||
|
<img src="https://picsum.photos/200/300"></img>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
17
tests/image/test.vue
Normal file
17
tests/image/test.vue
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
const greeting = ref("Hello World!");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p class="greeting">{{ greeting }}</p>
|
||||||
|
<img src="test.png" alt="test" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.greeting {
|
||||||
|
color: red;
|
||||||
|
font-weight: bold;
|
||||||
|
background: url("test.png");
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Add table
Add a link
Reference in a new issue