mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00

When a plugin registers a TermRequest handler there is currently no way for the handler to know where the terminal's cursor position was when the sequence was received. This is often useful information, e.g. for OSC 133 sequences which are used to annotate shell prompts. Modify the event data for the TermRequest autocommand to be a table instead of just a string. The "sequence" field of the table contains the sequence string and the "cursor" field contains the cursor position when the sequence was received. To maintain consistency between TermRequest and TermResponse (and to future proof the latter), TermResponse's event data is also updated to be a table with a "sequence" field. BREAKING CHANGE: event data for TermRequest and TermResponse is now a table
1419 lines
43 KiB
Lua
1419 lines
43 KiB
Lua
--- @brief
|
|
---<pre>help
|
|
---:[range]TOhtml {file} *:TOhtml*
|
|
---Converts the buffer shown in the current window to HTML, opens the generated
|
|
---HTML in a new split window, and saves its contents to {file}. If {file} is not
|
|
---given, a temporary file (created by |tempname()|) is used.
|
|
---</pre>
|
|
|
|
-- The HTML conversion script is different from Vim's one. If you want to use
|
|
-- Vim's TOhtml converter, download it from the vim GitHub repo.
|
|
-- Here are the Vim files related to this functionality:
|
|
-- - https://github.com/vim/vim/blob/master/runtime/syntax/2html.vim
|
|
-- - https://github.com/vim/vim/blob/master/runtime/autoload/tohtml.vim
|
|
-- - https://github.com/vim/vim/blob/master/runtime/plugin/tohtml.vim
|
|
--
|
|
-- Main differences between this and the vim version:
|
|
-- - No "ignore some visual thing" settings (just set the right Vim option)
|
|
-- - No support for legacy web engines
|
|
-- - No support for legacy encoding (supports only UTF-8)
|
|
-- - No interactive webpage
|
|
-- - No specifying the internal HTML (no XHTML, no use_css=false)
|
|
-- - No multiwindow diffs
|
|
-- - No ranges
|
|
--
|
|
-- Remarks:
|
|
-- - Not all visuals are supported, so it may differ.
|
|
|
|
--- @class (private) vim.tohtml.state.global
|
|
--- @field background string
|
|
--- @field foreground string
|
|
--- @field title string|false
|
|
--- @field font string
|
|
--- @field highlights_name table<integer,string>
|
|
--- @field conf vim.tohtml.opt
|
|
|
|
--- @class (private) vim.tohtml.state:vim.tohtml.state.global
|
|
--- @field style vim.tohtml.styletable
|
|
--- @field tabstop string|false
|
|
--- @field opt vim.wo
|
|
--- @field winid integer
|
|
--- @field bufnr integer
|
|
--- @field width integer
|
|
--- @field start integer
|
|
--- @field end_ integer
|
|
|
|
--- @class (private) vim.tohtml.styletable
|
|
--- @field [integer] vim.tohtml.line (integer: (1-index, exclusive))
|
|
|
|
--- @class (private) vim.tohtml.line
|
|
--- @field virt_lines {[integer]:[string,integer][]}
|
|
--- @field pre_text string[][]
|
|
--- @field hide? boolean
|
|
--- @field [integer] vim.tohtml.cell? (integer: (1-index, exclusive))
|
|
|
|
--- @class (private) vim.tohtml.cell
|
|
--- @field [1] integer[] start
|
|
--- @field [2] integer[] close
|
|
--- @field [3] any[][] virt_text
|
|
--- @field [4] any[][] overlay_text
|
|
|
|
--- @type string[]
|
|
local notifications = {}
|
|
|
|
---@param msg string
|
|
local function notify(msg)
|
|
if #notifications == 0 then
|
|
vim.schedule(function()
|
|
if #notifications > 1 then
|
|
vim.notify(('TOhtml: %s (+ %d more warnings)'):format(notifications[1], #notifications - 1))
|
|
elseif #notifications == 1 then
|
|
vim.notify('TOhtml: ' .. notifications[1])
|
|
end
|
|
notifications = {}
|
|
end)
|
|
end
|
|
table.insert(notifications, msg)
|
|
end
|
|
|
|
local HIDE_ID = -1
|
|
-- stylua: ignore start
|
|
local cterm_8_to_hex={
|
|
[0] = "#808080", "#ff6060", "#00ff00", "#ffff00",
|
|
"#8080ff", "#ff40ff", "#00ffff", "#ffffff",
|
|
}
|
|
local cterm_16_to_hex={
|
|
[0] = "#000000", "#c00000", "#008000", "#804000",
|
|
"#0000c0", "#c000c0", "#008080", "#c0c0c0",
|
|
"#808080", "#ff6060", "#00ff00", "#ffff00",
|
|
"#8080ff", "#ff40ff", "#00ffff", "#ffffff",
|
|
}
|
|
local cterm_88_to_hex={
|
|
[0] = "#000000", "#c00000", "#008000", "#804000",
|
|
"#0000c0", "#c000c0", "#008080", "#c0c0c0",
|
|
"#808080", "#ff6060", "#00ff00", "#ffff00",
|
|
"#8080ff", "#ff40ff", "#00ffff", "#ffffff",
|
|
"#000000", "#00008b", "#0000cd", "#0000ff",
|
|
"#008b00", "#008b8b", "#008bcd", "#008bff",
|
|
"#00cd00", "#00cd8b", "#00cdcd", "#00cdff",
|
|
"#00ff00", "#00ff8b", "#00ffcd", "#00ffff",
|
|
"#8b0000", "#8b008b", "#8b00cd", "#8b00ff",
|
|
"#8b8b00", "#8b8b8b", "#8b8bcd", "#8b8bff",
|
|
"#8bcd00", "#8bcd8b", "#8bcdcd", "#8bcdff",
|
|
"#8bff00", "#8bff8b", "#8bffcd", "#8bffff",
|
|
"#cd0000", "#cd008b", "#cd00cd", "#cd00ff",
|
|
"#cd8b00", "#cd8b8b", "#cd8bcd", "#cd8bff",
|
|
"#cdcd00", "#cdcd8b", "#cdcdcd", "#cdcdff",
|
|
"#cdff00", "#cdff8b", "#cdffcd", "#cdffff",
|
|
"#ff0000", "#ff008b", "#ff00cd", "#ff00ff",
|
|
"#ff8b00", "#ff8b8b", "#ff8bcd", "#ff8bff",
|
|
"#ffcd00", "#ffcd8b", "#ffcdcd", "#ffcdff",
|
|
"#ffff00", "#ffff8b", "#ffffcd", "#ffffff",
|
|
"#2e2e2e", "#5c5c5c", "#737373", "#8b8b8b",
|
|
"#a2a2a2", "#b9b9b9", "#d0d0d0", "#e7e7e7",
|
|
}
|
|
local cterm_256_to_hex={
|
|
[0] = "#000000", "#c00000", "#008000", "#804000",
|
|
"#0000c0", "#c000c0", "#008080", "#c0c0c0",
|
|
"#808080", "#ff6060", "#00ff00", "#ffff00",
|
|
"#8080ff", "#ff40ff", "#00ffff", "#ffffff",
|
|
"#000000", "#00005f", "#000087", "#0000af",
|
|
"#0000d7", "#0000ff", "#005f00", "#005f5f",
|
|
"#005f87", "#005faf", "#005fd7", "#005fff",
|
|
"#008700", "#00875f", "#008787", "#0087af",
|
|
"#0087d7", "#0087ff", "#00af00", "#00af5f",
|
|
"#00af87", "#00afaf", "#00afd7", "#00afff",
|
|
"#00d700", "#00d75f", "#00d787", "#00d7af",
|
|
"#00d7d7", "#00d7ff", "#00ff00", "#00ff5f",
|
|
"#00ff87", "#00ffaf", "#00ffd7", "#00ffff",
|
|
"#5f0000", "#5f005f", "#5f0087", "#5f00af",
|
|
"#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f",
|
|
"#5f5f87", "#5f5faf", "#5f5fd7", "#5f5fff",
|
|
"#5f8700", "#5f875f", "#5f8787", "#5f87af",
|
|
"#5f87d7", "#5f87ff", "#5faf00", "#5faf5f",
|
|
"#5faf87", "#5fafaf", "#5fafd7", "#5fafff",
|
|
"#5fd700", "#5fd75f", "#5fd787", "#5fd7af",
|
|
"#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f",
|
|
"#5fff87", "#5fffaf", "#5fffd7", "#5fffff",
|
|
"#870000", "#87005f", "#870087", "#8700af",
|
|
"#8700d7", "#8700ff", "#875f00", "#875f5f",
|
|
"#875f87", "#875faf", "#875fd7", "#875fff",
|
|
"#878700", "#87875f", "#878787", "#8787af",
|
|
"#8787d7", "#8787ff", "#87af00", "#87af5f",
|
|
"#87af87", "#87afaf", "#87afd7", "#87afff",
|
|
"#87d700", "#87d75f", "#87d787", "#87d7af",
|
|
"#87d7d7", "#87d7ff", "#87ff00", "#87ff5f",
|
|
"#87ff87", "#87ffaf", "#87ffd7", "#87ffff",
|
|
"#af0000", "#af005f", "#af0087", "#af00af",
|
|
"#af00d7", "#af00ff", "#af5f00", "#af5f5f",
|
|
"#af5f87", "#af5faf", "#af5fd7", "#af5fff",
|
|
"#af8700", "#af875f", "#af8787", "#af87af",
|
|
"#af87d7", "#af87ff", "#afaf00", "#afaf5f",
|
|
"#afaf87", "#afafaf", "#afafd7", "#afafff",
|
|
"#afd700", "#afd75f", "#afd787", "#afd7af",
|
|
"#afd7d7", "#afd7ff", "#afff00", "#afff5f",
|
|
"#afff87", "#afffaf", "#afffd7", "#afffff",
|
|
"#d70000", "#d7005f", "#d70087", "#d700af",
|
|
"#d700d7", "#d700ff", "#d75f00", "#d75f5f",
|
|
"#d75f87", "#d75faf", "#d75fd7", "#d75fff",
|
|
"#d78700", "#d7875f", "#d78787", "#d787af",
|
|
"#d787d7", "#d787ff", "#d7af00", "#d7af5f",
|
|
"#d7af87", "#d7afaf", "#d7afd7", "#d7afff",
|
|
"#d7d700", "#d7d75f", "#d7d787", "#d7d7af",
|
|
"#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f",
|
|
"#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff",
|
|
"#ff0000", "#ff005f", "#ff0087", "#ff00af",
|
|
"#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f",
|
|
"#ff5f87", "#ff5faf", "#ff5fd7", "#ff5fff",
|
|
"#ff8700", "#ff875f", "#ff8787", "#ff87af",
|
|
"#ff87d7", "#ff87ff", "#ffaf00", "#ffaf5f",
|
|
"#ffaf87", "#ffafaf", "#ffafd7", "#ffafff",
|
|
"#ffd700", "#ffd75f", "#ffd787", "#ffd7af",
|
|
"#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f",
|
|
"#ffff87", "#ffffaf", "#ffffd7", "#ffffff",
|
|
"#080808", "#121212", "#1c1c1c", "#262626",
|
|
"#303030", "#3a3a3a", "#444444", "#4e4e4e",
|
|
"#585858", "#626262", "#6c6c6c", "#767676",
|
|
"#808080", "#8a8a8a", "#949494", "#9e9e9e",
|
|
"#a8a8a8", "#b2b2b2", "#bcbcbc", "#c6c6c6",
|
|
"#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee",
|
|
}
|
|
-- stylua: ignore end
|
|
|
|
--- @type table<integer,string>
|
|
local cterm_color_cache = {}
|
|
--- @type string?
|
|
local background_color_cache = nil
|
|
--- @type string?
|
|
local foreground_color_cache = nil
|
|
|
|
local len = vim.api.nvim_strwidth
|
|
|
|
--- @see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
|
|
--- @param color "background"|"foreground"|integer
|
|
--- @return string?
|
|
local function try_query_terminal_color(color)
|
|
local parameter = 4
|
|
if color == 'foreground' then
|
|
parameter = 10
|
|
elseif color == 'background' then
|
|
parameter = 11
|
|
end
|
|
--- @type string?
|
|
local hex = nil
|
|
local au = vim.api.nvim_create_autocmd('TermResponse', {
|
|
once = true,
|
|
callback = function(args)
|
|
hex = '#'
|
|
.. table.concat({
|
|
args.data.sequence:match('\027%]%d+;%d*;?rgb:(%w%w)%w%w/(%w%w)%w%w/(%w%w)%w%w'),
|
|
})
|
|
end,
|
|
})
|
|
if type(color) == 'number' then
|
|
io.stdout:write(('\027]%s;%s;?\027\\'):format(parameter, color))
|
|
else
|
|
io.stdout:write(('\027]%s;?\027\\'):format(parameter))
|
|
end
|
|
vim.wait(100, function()
|
|
return hex and true or false
|
|
end)
|
|
pcall(vim.api.nvim_del_autocmd, au)
|
|
return hex
|
|
end
|
|
|
|
--- @param colorstr string
|
|
--- @return string
|
|
local function cterm_to_hex(colorstr)
|
|
if colorstr:sub(1, 1) == '#' then
|
|
return colorstr
|
|
end
|
|
assert(colorstr ~= '')
|
|
local color = tonumber(colorstr)
|
|
assert(color and 0 <= color and color <= 255)
|
|
if cterm_color_cache[color] then
|
|
return cterm_color_cache[color]
|
|
end
|
|
local hex = try_query_terminal_color(color)
|
|
if hex then
|
|
cterm_color_cache[color] = hex
|
|
else
|
|
notify("Couldn't get terminal colors, using fallback")
|
|
local t_Co = tonumber(vim.api.nvim_eval('&t_Co'))
|
|
if t_Co <= 8 then
|
|
cterm_color_cache = cterm_8_to_hex
|
|
elseif t_Co == 88 then
|
|
cterm_color_cache = cterm_88_to_hex
|
|
elseif t_Co == 256 then
|
|
cterm_color_cache = cterm_256_to_hex
|
|
else
|
|
cterm_color_cache = cterm_16_to_hex
|
|
end
|
|
end
|
|
return cterm_color_cache[color]
|
|
end
|
|
|
|
--- @return string
|
|
local function get_background_color()
|
|
local bg = vim.fn.synIDattr(vim.fn.hlID('Normal'), 'bg#')
|
|
if bg ~= '' then
|
|
return cterm_to_hex(bg)
|
|
end
|
|
if background_color_cache then
|
|
return background_color_cache
|
|
end
|
|
local hex = try_query_terminal_color('background')
|
|
if not hex or not hex:match('#%x%x%x%x%x%x') then
|
|
notify("Couldn't get terminal background colors, using fallback")
|
|
hex = vim.o.background == 'light' and '#ffffff' or '#000000'
|
|
end
|
|
background_color_cache = hex
|
|
return hex
|
|
end
|
|
|
|
--- @return string
|
|
local function get_foreground_color()
|
|
local fg = vim.fn.synIDattr(vim.fn.hlID('Normal'), 'fg#')
|
|
if fg ~= '' then
|
|
return cterm_to_hex(fg)
|
|
end
|
|
if foreground_color_cache then
|
|
return foreground_color_cache
|
|
end
|
|
local hex = try_query_terminal_color('foreground')
|
|
if not hex or not hex:match('#%x%x%x%x%x%x') then
|
|
notify("Couldn't get terminal foreground colors, using fallback")
|
|
hex = vim.o.background == 'light' and '#000000' or '#ffffff'
|
|
end
|
|
foreground_color_cache = hex
|
|
return hex
|
|
end
|
|
|
|
--- @param style_line vim.tohtml.line
|
|
--- @param col integer (1-index)
|
|
--- @param field integer
|
|
--- @param val any
|
|
local function _style_line_insert(style_line, col, field, val)
|
|
if style_line[col] == nil then
|
|
style_line[col] = { {}, {}, {}, {} }
|
|
end
|
|
table.insert(style_line[col][field], val)
|
|
end
|
|
|
|
--- @param style_line vim.tohtml.line
|
|
--- @param col integer (1-index)
|
|
--- @param val any[]
|
|
local function style_line_insert_overlay_char(style_line, col, val)
|
|
_style_line_insert(style_line, col, 4, val)
|
|
end
|
|
|
|
--- @param style_line vim.tohtml.line
|
|
--- @param col integer (1-index)
|
|
--- @param val any[]
|
|
local function style_line_insert_virt_text(style_line, col, val)
|
|
_style_line_insert(style_line, col, 3, val)
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param hl string|integer|string[]|integer[]?
|
|
--- @return nil|integer
|
|
local function register_hl(state, hl)
|
|
if type(hl) == 'table' then
|
|
hl = hl[#hl] --- @type string|integer
|
|
end
|
|
if type(hl) == 'nil' then
|
|
return
|
|
elseif type(hl) == 'string' then
|
|
hl = vim.fn.hlID(hl)
|
|
assert(hl ~= 0)
|
|
end
|
|
hl = vim.fn.synIDtrans(hl)
|
|
if not state.highlights_name[hl] then
|
|
local name = vim.fn.synIDattr(hl, 'name')
|
|
assert(name ~= '')
|
|
state.highlights_name[hl] = name
|
|
end
|
|
return hl
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param start_row integer (1-index)
|
|
--- @param start_col integer (1-index)
|
|
--- @param end_row integer (1-index)
|
|
--- @param end_col integer (1-index)
|
|
--- @param conceal_text string
|
|
--- @param hl_group string|integer?
|
|
local function styletable_insert_conceal(
|
|
state,
|
|
start_row,
|
|
start_col,
|
|
end_row,
|
|
end_col,
|
|
conceal_text,
|
|
hl_group
|
|
)
|
|
assert(state.opt.conceallevel > 0)
|
|
local styletable = state.style
|
|
if start_col == end_col and start_row == end_row then
|
|
return
|
|
end
|
|
if state.opt.conceallevel == 1 and conceal_text == '' then
|
|
conceal_text = vim.opt_local.listchars:get().conceal or ' '
|
|
end
|
|
local hlid = register_hl(state, hl_group)
|
|
if vim.wo[state.winid].conceallevel ~= 3 then
|
|
_style_line_insert(styletable[start_row], start_col, 3, { conceal_text, hlid })
|
|
end
|
|
_style_line_insert(styletable[start_row], start_col, 1, HIDE_ID)
|
|
_style_line_insert(styletable[end_row], end_col, 2, HIDE_ID)
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param start_row integer (1-index)
|
|
--- @param start_col integer (1-index)
|
|
--- @param end_row integer (1-index)
|
|
--- @param end_col integer (1-index)
|
|
--- @param hl_group string|integer|nil
|
|
local function styletable_insert_range(state, start_row, start_col, end_row, end_col, hl_group)
|
|
if start_col == end_col and start_row == end_row or not hl_group then
|
|
return
|
|
end
|
|
local styletable = state.style
|
|
_style_line_insert(styletable[start_row], start_col, 1, hl_group)
|
|
_style_line_insert(styletable[end_row], end_col, 2, hl_group)
|
|
end
|
|
|
|
--- @param bufnr integer
|
|
--- @return vim.tohtml.styletable
|
|
local function generate_styletable(bufnr)
|
|
--- @type vim.tohtml.styletable
|
|
local styletable = {}
|
|
for row = 1, vim.api.nvim_buf_line_count(bufnr) + 1 do
|
|
styletable[row] = { virt_lines = {}, pre_text = {} }
|
|
end
|
|
return styletable
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_syntax(state)
|
|
for row = state.start, state.end_ do
|
|
local prev_id = 0
|
|
local prev_col = nil
|
|
for col = 1, #vim.fn.getline(row) + 1 do
|
|
local hlid = vim.fn.synID(row, col, 1)
|
|
hlid = hlid == 0 and 0 or assert(register_hl(state, hlid))
|
|
if hlid ~= prev_id then
|
|
if prev_id ~= 0 then
|
|
styletable_insert_range(state, row, assert(prev_col), row, col, prev_id)
|
|
end
|
|
prev_col = col
|
|
prev_id = hlid
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_diff(state)
|
|
local styletable = state.style
|
|
for row = state.start, state.end_ do
|
|
local style_line = styletable[row]
|
|
local filler = vim.fn.diff_filler(row)
|
|
if filler ~= 0 then
|
|
local fill = (vim.opt_local.fillchars:get().diff or '-')
|
|
table.insert(
|
|
style_line.virt_lines,
|
|
{ { fill:rep(state.width), register_hl(state, 'DiffDelete') } }
|
|
)
|
|
end
|
|
if row == state.end_ + 1 then
|
|
break
|
|
end
|
|
local prev_id = 0
|
|
local prev_col = nil
|
|
for col = 1, #vim.fn.getline(row) do
|
|
local hlid = vim.fn.diff_hlID(row, col)
|
|
hlid = hlid == 0 and 0 or assert(register_hl(state, hlid))
|
|
if hlid ~= prev_id then
|
|
if prev_id ~= 0 then
|
|
styletable_insert_range(state, row, assert(prev_col), row, col, prev_id)
|
|
end
|
|
prev_col = col
|
|
prev_id = hlid
|
|
end
|
|
end
|
|
if prev_id ~= 0 then
|
|
styletable_insert_range(state, row, assert(prev_col), row, #vim.fn.getline(row) + 1, prev_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_treesitter(state)
|
|
local bufnr = state.bufnr
|
|
local buf_highlighter = vim.treesitter.highlighter.active[bufnr]
|
|
if not buf_highlighter then
|
|
return
|
|
end
|
|
buf_highlighter.tree:parse(true)
|
|
buf_highlighter.tree:for_each_tree(function(tstree, tree)
|
|
--- @cast tree vim.treesitter.LanguageTree
|
|
if not tstree then
|
|
return
|
|
end
|
|
local root = tstree:root()
|
|
local q = buf_highlighter:get_query(tree:lang())
|
|
--- @type vim.treesitter.Query?
|
|
local query = q:query()
|
|
if not query then
|
|
return
|
|
end
|
|
for capture, node, metadata in
|
|
query:iter_captures(root, buf_highlighter.bufnr, state.start - 1, state.end_)
|
|
do
|
|
local srow, scol, erow, ecol = node:range()
|
|
--- @diagnostic disable-next-line: invisible
|
|
local c = q._query.captures[capture]
|
|
if c ~= nil then
|
|
local hlid = register_hl(state, '@' .. c .. '.' .. tree:lang())
|
|
if metadata.conceal and state.opt.conceallevel ~= 0 then
|
|
styletable_insert_conceal(state, srow + 1, scol + 1, erow + 1, ecol + 1, metadata.conceal)
|
|
end
|
|
styletable_insert_range(state, srow + 1, scol + 1, erow + 1, ecol + 1, hlid)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
|
|
--- @param namespaces table<integer,string>
|
|
local function _styletable_extmarks_highlight(state, extmark, namespaces)
|
|
if not extmark[4].hl_group then
|
|
return
|
|
end
|
|
---TODO(altermo) LSP semantic tokens (and some other extmarks) are only
|
|
---generated in visible lines, and not in the whole buffer.
|
|
if (namespaces[extmark[4].ns_id] or ''):find('nvim.lsp.semantic_tokens') then
|
|
notify('lsp semantic tokens are not supported, HTML may be incorrect')
|
|
return
|
|
end
|
|
local srow, scol, erow, ecol =
|
|
extmark[2], extmark[3], extmark[4].end_row or extmark[2], extmark[4].end_col or extmark[3]
|
|
if scol == ecol and srow == erow then
|
|
return
|
|
end
|
|
local hlid = register_hl(state, extmark[4].hl_group)
|
|
styletable_insert_range(state, srow + 1, scol + 1, erow + 1, ecol + 1, hlid)
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
|
|
--- @param namespaces table<integer,string>
|
|
local function _styletable_extmarks_virt_text(state, extmark, namespaces)
|
|
if not extmark[4].virt_text then
|
|
return
|
|
end
|
|
---TODO(altermo) LSP semantic tokens (and some other extmarks) are only
|
|
---generated in visible lines, and not in the whole buffer.
|
|
if (namespaces[extmark[4].ns_id] or ''):find('nvim.lsp.inlayhint') then
|
|
notify('lsp inlay hints are not supported, HTML may be incorrect')
|
|
return
|
|
end
|
|
local styletable = state.style
|
|
--- @type integer,integer
|
|
local row, col = extmark[2], extmark[3]
|
|
if
|
|
row < vim.api.nvim_buf_line_count(state.bufnr)
|
|
and (
|
|
extmark[4].virt_text_pos == 'inline'
|
|
or extmark[4].virt_text_pos == 'eol'
|
|
or extmark[4].virt_text_pos == 'overlay'
|
|
)
|
|
then
|
|
if extmark[4].virt_text_pos == 'eol' then
|
|
style_line_insert_virt_text(styletable[row + 1], #vim.fn.getline(row + 1) + 1, { ' ' })
|
|
end
|
|
local virt_text_len = 0
|
|
for _, i in
|
|
ipairs(extmark[4].virt_text --[[@as (string[][])]])
|
|
do
|
|
local hlid = register_hl(state, i[2])
|
|
if extmark[4].virt_text_pos == 'eol' then
|
|
style_line_insert_virt_text(
|
|
styletable[row + 1],
|
|
#vim.fn.getline(row + 1) + 1,
|
|
{ i[1], hlid }
|
|
)
|
|
else
|
|
style_line_insert_virt_text(styletable[row + 1], col + 1, { i[1], hlid })
|
|
end
|
|
virt_text_len = virt_text_len + len(i[1])
|
|
end
|
|
if extmark[4].virt_text_pos == 'overlay' then
|
|
styletable_insert_range(state, row + 1, col + 1, row + 1, col + virt_text_len + 1, HIDE_ID)
|
|
end
|
|
end
|
|
local not_supported = {
|
|
virt_text_pos = 'right_align',
|
|
hl_mode = 'blend',
|
|
hl_group = 'combine',
|
|
}
|
|
for opt, val in pairs(not_supported) do
|
|
if extmark[4][opt] == val then
|
|
notify(('extmark.%s="%s" is not supported, HTML may be incorrect'):format(opt, val))
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
|
|
local function _styletable_extmarks_virt_lines(state, extmark)
|
|
---TODO(altermo) if the fold start is equal to virt_line start then the fold hides the virt_line
|
|
if not extmark[4].virt_lines then
|
|
return
|
|
end
|
|
--- @type integer
|
|
local row = extmark[2] + (extmark[4].virt_lines_above and 1 or 2)
|
|
for _, line in
|
|
ipairs(extmark[4].virt_lines --[[@as (string[][][])]])
|
|
do
|
|
local virt_line = {}
|
|
for _, i in ipairs(line) do
|
|
local hlid = register_hl(state, i[2])
|
|
table.insert(virt_line, { i[1], hlid })
|
|
end
|
|
table.insert(state.style[row].virt_lines, virt_line)
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
|
|
local function _styletable_extmarks_conceal(state, extmark)
|
|
if not extmark[4].conceal or state.opt.conceallevel == 0 then
|
|
return
|
|
end
|
|
local srow, scol, erow, ecol =
|
|
extmark[2], extmark[3], extmark[4].end_row or extmark[2], extmark[4].end_col or extmark[3]
|
|
styletable_insert_conceal(
|
|
state,
|
|
srow + 1,
|
|
scol + 1,
|
|
erow + 1,
|
|
ecol + 1,
|
|
extmark[4].conceal,
|
|
extmark[4].hl_group or 'Conceal'
|
|
)
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_extmarks(state)
|
|
--TODO(altermo) extmarks may have col/row which is outside of the buffer, which could cause an error
|
|
local bufnr = state.bufnr
|
|
local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true })
|
|
local namespaces = {} --- @type table<integer, string>
|
|
for ns, ns_id in pairs(vim.api.nvim_get_namespaces()) do
|
|
namespaces[ns_id] = ns
|
|
end
|
|
for _, v in ipairs(extmarks) do
|
|
_styletable_extmarks_highlight(state, v, namespaces)
|
|
end
|
|
for _, v in ipairs(extmarks) do
|
|
_styletable_extmarks_conceal(state, v)
|
|
end
|
|
for _, v in ipairs(extmarks) do
|
|
_styletable_extmarks_virt_text(state, v, namespaces)
|
|
end
|
|
for _, v in ipairs(extmarks) do
|
|
_styletable_extmarks_virt_lines(state, v)
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_folds(state)
|
|
local styletable = state.style
|
|
local has_folded = false
|
|
for row = state.start, state.end_ do
|
|
if vim.fn.foldclosed(row) > 0 then
|
|
has_folded = true
|
|
styletable[row].hide = true
|
|
end
|
|
if vim.fn.foldclosed(row) == row then
|
|
local hlid = register_hl(state, 'Folded')
|
|
---TODO(altermo): Is there a way to get highlighted foldtext?
|
|
local foldtext = vim.fn.foldtextresult(row)
|
|
foldtext = foldtext .. (vim.opt.fillchars:get().fold or '·'):rep(state.width - #foldtext)
|
|
table.insert(styletable[row].virt_lines, { { foldtext, hlid } })
|
|
end
|
|
end
|
|
if has_folded and type(({ pcall(vim.api.nvim_eval, vim.o.foldtext) })[2]) == 'table' then
|
|
notify('foldtext returning a table with highlights is not supported, HTML may be incorrect')
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_conceal(state)
|
|
local bufnr = state.bufnr
|
|
vim._with({ buf = bufnr }, function()
|
|
for row = state.start, state.end_ do
|
|
--- @type table<integer,[integer,integer,string]>
|
|
local conceals = {}
|
|
local line_len_exclusive = #vim.fn.getline(row) + 1
|
|
for col = 1, line_len_exclusive do
|
|
--- @type integer,string,integer
|
|
local is_concealed, conceal, hlid = unpack(vim.fn.synconcealed(row, col) --[[@as table]])
|
|
if is_concealed == 0 then
|
|
assert(true)
|
|
elseif not conceals[hlid] then
|
|
conceals[hlid] = { col, math.min(col + 1, line_len_exclusive), conceal }
|
|
else
|
|
conceals[hlid][2] = math.min(col + 1, line_len_exclusive)
|
|
end
|
|
end
|
|
for _, v in pairs(conceals) do
|
|
styletable_insert_conceal(state, row, v[1], row, v[2], v[3], 'Conceal')
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_match(state)
|
|
for _, match in
|
|
ipairs(vim.fn.getmatches(state.winid) --[[@as (table[])]])
|
|
do
|
|
local hlid = register_hl(state, match.group)
|
|
local function range(srow, scol, erow, ecol)
|
|
if match.group == 'Conceal' and state.opt.conceallevel ~= 0 then
|
|
styletable_insert_conceal(state, srow, scol, erow, ecol, match.conceal or '', hlid)
|
|
else
|
|
styletable_insert_range(state, srow, scol, erow, ecol, hlid)
|
|
end
|
|
end
|
|
if match.pos1 then
|
|
for key, v in
|
|
pairs(match --[[@as (table<string,integer[]>)]])
|
|
do
|
|
if not key:match('^pos(%d+)$') then
|
|
assert(true)
|
|
elseif #v == 1 then
|
|
range(v[1], 1, v[1], #vim.fn.getline(v[1]) + 1)
|
|
else
|
|
range(v[1], v[2], v[1], v[3] + v[2])
|
|
end
|
|
end
|
|
else
|
|
for _, v in
|
|
ipairs(vim.fn.matchbufline(state.bufnr, match.pattern, 1, '$') --[[@as (table[])]])
|
|
do
|
|
range(v.lnum, v.byteidx + 1, v.lnum, v.byteidx + 1 + #v.text)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Requires state.conf.number_lines to be set to true
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_statuscolumn(state)
|
|
if not state.conf.number_lines then
|
|
return
|
|
end
|
|
local statuscolumn = state.opt.statuscolumn
|
|
|
|
if statuscolumn == '' then
|
|
if state.opt.relativenumber then
|
|
if state.opt.number then
|
|
statuscolumn = '%C%s%{%v:lnum!=line(".")?"%=".v:relnum." ":v:lnum%}'
|
|
else
|
|
statuscolumn = '%C%s%{%"%=".v:relnum." "%}'
|
|
end
|
|
else
|
|
statuscolumn = '%C%s%{%"%=".v:lnum." "%}'
|
|
end
|
|
end
|
|
local minwidth = 0
|
|
|
|
local signcolumn = state.opt.signcolumn
|
|
if state.opt.number or state.opt.relativenumber then
|
|
minwidth = minwidth + state.opt.numberwidth
|
|
if signcolumn == 'number' then
|
|
signcolumn = 'no'
|
|
end
|
|
end
|
|
if signcolumn == 'number' then
|
|
signcolumn = 'auto'
|
|
end
|
|
if signcolumn ~= 'no' then
|
|
local max = tonumber(signcolumn:match('^%w-:(%d)')) or 1
|
|
if signcolumn:match('^auto') then
|
|
--- @type table<integer,integer>
|
|
local signcount = {}
|
|
for _, extmark in
|
|
ipairs(vim.api.nvim_buf_get_extmarks(state.bufnr, -1, 0, -1, { details = true }))
|
|
do
|
|
if extmark[4].sign_text then
|
|
signcount[extmark[2]] = (signcount[extmark[2]] or 0) + 1
|
|
end
|
|
end
|
|
local maxsigns = 0
|
|
for _, v in pairs(signcount) do
|
|
if v > maxsigns then
|
|
maxsigns = v
|
|
end
|
|
end
|
|
minwidth = minwidth + math.min(maxsigns, max) * 2
|
|
else
|
|
minwidth = minwidth + max * 2
|
|
end
|
|
end
|
|
|
|
local foldcolumn = state.opt.foldcolumn
|
|
if foldcolumn ~= '0' then
|
|
if foldcolumn:match('^auto') then
|
|
local max = tonumber(foldcolumn:match('^%w-:(%d)')) or 1
|
|
local maxfold = 0
|
|
vim._with({ buf = state.bufnr }, function()
|
|
for row = state.start, state.end_ do
|
|
local foldlevel = vim.fn.foldlevel(row)
|
|
if foldlevel > maxfold then
|
|
maxfold = foldlevel
|
|
end
|
|
end
|
|
end)
|
|
minwidth = minwidth + math.min(maxfold, max)
|
|
else
|
|
minwidth = minwidth + tonumber(foldcolumn)
|
|
end
|
|
end
|
|
|
|
--- @type table<integer,any>
|
|
local statuses = {}
|
|
for row = state.start, state.end_ do
|
|
local status = vim.api.nvim_eval_statusline(
|
|
statuscolumn,
|
|
{ winid = state.winid, use_statuscol_lnum = row, highlights = true }
|
|
)
|
|
local width = len(status.str)
|
|
if width > minwidth then
|
|
minwidth = width
|
|
end
|
|
table.insert(statuses, status)
|
|
--- @type string
|
|
end
|
|
for row, status in pairs(statuses) do
|
|
--- @type string
|
|
local str = status.str
|
|
--- @type table[]
|
|
local hls = status.highlights
|
|
for k, v in ipairs(hls) do
|
|
local text = str:sub(v.start + 1, hls[k + 1] and hls[k + 1].start or nil)
|
|
if k == #hls then
|
|
text = text .. (' '):rep(minwidth - len(str))
|
|
end
|
|
if text ~= '' then
|
|
local hlid = register_hl(state, v.group)
|
|
local virt_text = { text, hlid }
|
|
table.insert(state.style[row].pre_text, virt_text)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function styletable_listchars(state)
|
|
if not state.opt.list then
|
|
return
|
|
end
|
|
--- @return string
|
|
local function utf8_sub(str, i, j)
|
|
return vim.fn.strcharpart(str, i - 1, j and j - i + 1 or nil)
|
|
end
|
|
--- @type table<string,string>
|
|
local listchars = vim.opt_local.listchars:get()
|
|
local ids = setmetatable({}, {
|
|
__index = function(t, k)
|
|
rawset(t, k, register_hl(state, k))
|
|
return rawget(t, k)
|
|
end,
|
|
})
|
|
|
|
if listchars.eol then
|
|
for row = state.start, state.end_ do
|
|
local style_line = state.style[row]
|
|
style_line_insert_overlay_char(
|
|
style_line,
|
|
#vim.fn.getline(row) + 1,
|
|
{ listchars.eol, ids.NonText }
|
|
)
|
|
end
|
|
end
|
|
|
|
if listchars.tab and state.tabstop then
|
|
for _, match in
|
|
ipairs(vim.fn.matchbufline(state.bufnr, '\t', 1, '$') --[[@as (table[])]])
|
|
do
|
|
--- @type integer
|
|
local tablen = #state.tabstop
|
|
- ((vim.fn.virtcol({ match.lnum, match.byteidx }, false, state.winid)) % #state.tabstop)
|
|
--- @type string?
|
|
local text
|
|
if len(listchars.tab) == 3 then
|
|
if tablen == 1 then
|
|
text = utf8_sub(listchars.tab, 3, 3)
|
|
else
|
|
text = utf8_sub(listchars.tab, 1, 1)
|
|
.. utf8_sub(listchars.tab, 2, 2):rep(tablen - 2)
|
|
.. utf8_sub(listchars.tab, 3, 3)
|
|
end
|
|
else
|
|
text = utf8_sub(listchars.tab, 1, 1) .. utf8_sub(listchars.tab, 2, 2):rep(tablen - 1)
|
|
end
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + 1,
|
|
{ text, ids.Whitespace }
|
|
)
|
|
end
|
|
end
|
|
|
|
if listchars.space then
|
|
for _, match in
|
|
ipairs(vim.fn.matchbufline(state.bufnr, ' ', 1, '$') --[[@as (table[])]])
|
|
do
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + 1,
|
|
{ listchars.space, ids.Whitespace }
|
|
)
|
|
end
|
|
end
|
|
|
|
if listchars.multispace then
|
|
for _, match in
|
|
ipairs(vim.fn.matchbufline(state.bufnr, [[ \+]], 1, '$') --[[@as (table[])]])
|
|
do
|
|
local text = utf8_sub(listchars.multispace:rep(len(match.text)), 1, len(match.text))
|
|
for i = 1, len(text) do
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + i,
|
|
{ utf8_sub(text, i, i), ids.Whitespace }
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
if listchars.lead or listchars.leadmultispace then
|
|
for _, match in
|
|
ipairs(vim.fn.matchbufline(state.bufnr, [[^ \+]], 1, '$') --[[@as (table[])]])
|
|
do
|
|
local text = ''
|
|
if len(match.text) == 1 or not listchars.leadmultispace then
|
|
if listchars.lead then
|
|
text = listchars.lead:rep(len(match.text))
|
|
end
|
|
elseif listchars.leadmultispace then
|
|
text = utf8_sub(listchars.leadmultispace:rep(len(match.text)), 1, len(match.text))
|
|
end
|
|
for i = 1, len(text) do
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + i,
|
|
{ utf8_sub(text, i, i), ids.Whitespace }
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
if listchars.trail then
|
|
for _, match in
|
|
ipairs(vim.fn.matchbufline(state.bufnr, [[ \+$]], 1, '$') --[[@as (table[])]])
|
|
do
|
|
local text = listchars.trail:rep(len(match.text))
|
|
for i = 1, len(text) do
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + i,
|
|
{ utf8_sub(text, i, i), ids.Whitespace }
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
if listchars.nbsp then
|
|
for _, match in
|
|
ipairs(
|
|
vim.fn.matchbufline(state.bufnr, '\226\128\175\\|\194\160', 1, '$') --[[@as (table[])]]
|
|
)
|
|
do
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + 1,
|
|
{ listchars.nbsp, ids.Whitespace }
|
|
)
|
|
for i = 2, #match.text do
|
|
style_line_insert_overlay_char(
|
|
state.style[match.lnum],
|
|
match.byteidx + i,
|
|
{ '', ids.Whitespace }
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- @param name string
|
|
--- @return string
|
|
local function highlight_name_to_class_name(name)
|
|
return (name:gsub('%.', '-'):gsub('@', '-'))
|
|
end
|
|
|
|
--- @param name string
|
|
--- @return string
|
|
local function name_to_tag(name)
|
|
return '<span class="' .. highlight_name_to_class_name(name) .. '">'
|
|
end
|
|
|
|
--- @param _ string
|
|
--- @return string
|
|
local function name_to_closetag(_)
|
|
return '</span>'
|
|
end
|
|
|
|
--- @param str string
|
|
--- @param tabstop string|false?
|
|
--- @return string
|
|
local function html_escape(str, tabstop)
|
|
str = str:gsub('&', '&'):gsub('<', '<'):gsub('>', '>'):gsub('"', '"')
|
|
if tabstop then
|
|
--- @type string
|
|
str = str:gsub('\t', tabstop)
|
|
end
|
|
return str
|
|
end
|
|
|
|
--- @param out string[]
|
|
--- @param state vim.tohtml.state.global
|
|
local function extend_style(out, state)
|
|
table.insert(out, '<style>')
|
|
table.insert(out, ('* {font-family: %s}'):format(state.font))
|
|
table.insert(
|
|
out,
|
|
('body {background-color: %s; color: %s}'):format(state.background, state.foreground)
|
|
)
|
|
for hlid, name in pairs(state.highlights_name) do
|
|
--TODO(altermo) use local namespace (instead of global 0)
|
|
local fg = vim.fn.synIDattr(hlid, 'fg#')
|
|
local bg = vim.fn.synIDattr(hlid, 'bg#')
|
|
local sp = vim.fn.synIDattr(hlid, 'sp#')
|
|
local decor_line = {}
|
|
if vim.fn.synIDattr(hlid, 'underline') ~= '' then
|
|
table.insert(decor_line, 'underline')
|
|
end
|
|
if vim.fn.synIDattr(hlid, 'strikethrough') ~= '' then
|
|
table.insert(decor_line, 'line-through')
|
|
end
|
|
if vim.fn.synIDattr(hlid, 'undercurl') ~= '' then
|
|
table.insert(decor_line, 'underline')
|
|
end
|
|
local c = {
|
|
color = fg ~= '' and cterm_to_hex(fg) or nil,
|
|
['background-color'] = bg ~= '' and cterm_to_hex(bg) or nil,
|
|
['font-style'] = vim.fn.synIDattr(hlid, 'italic') ~= '' and 'italic' or nil,
|
|
['font-weight'] = vim.fn.synIDattr(hlid, 'bold') ~= '' and 'bold' or nil,
|
|
['text-decoration-line'] = not vim.tbl_isempty(decor_line) and table.concat(decor_line, ' ')
|
|
or nil,
|
|
-- TODO(ribru17): fallback to displayed text color if sp not set
|
|
['text-decoration-color'] = sp ~= '' and cterm_to_hex(sp) or nil,
|
|
--TODO(altermo) if strikethrough and undercurl then the strikethrough becomes wavy
|
|
['text-decoration-style'] = vim.fn.synIDattr(hlid, 'undercurl') ~= '' and 'wavy' or nil,
|
|
}
|
|
local attrs = {}
|
|
for attr, val in pairs(c) do
|
|
table.insert(attrs, attr .. ': ' .. val)
|
|
end
|
|
table.insert(
|
|
out,
|
|
'.' .. highlight_name_to_class_name(name) .. ' {' .. table.concat(attrs, '; ') .. '}'
|
|
)
|
|
end
|
|
table.insert(out, '</style>')
|
|
end
|
|
|
|
--- @param out string[]
|
|
--- @param state vim.tohtml.state.global
|
|
local function extend_head(out, state)
|
|
table.insert(out, '<head>')
|
|
table.insert(out, '<meta charset="UTF-8">')
|
|
if state.title ~= false then
|
|
table.insert(out, ('<title>%s</title>'):format(state.title))
|
|
end
|
|
local colorscheme = vim.api.nvim_exec2('colorscheme', { output = true }).output
|
|
table.insert(
|
|
out,
|
|
('<meta name="colorscheme" content="%s"></meta>'):format(html_escape(colorscheme))
|
|
)
|
|
extend_style(out, state)
|
|
table.insert(out, '</head>')
|
|
end
|
|
|
|
--- @param out string[]
|
|
--- @param state vim.tohtml.state
|
|
--- @param row integer
|
|
local function _extend_virt_lines(out, state, row)
|
|
local style_line = state.style[row]
|
|
for _, virt_line in ipairs(style_line.virt_lines) do
|
|
local virt_s = ''
|
|
for _, v in ipairs(virt_line) do
|
|
if v[2] then
|
|
virt_s = virt_s .. (name_to_tag(state.highlights_name[v[2]]))
|
|
end
|
|
virt_s = virt_s .. v[1]
|
|
if v[2] then
|
|
--- @type string
|
|
virt_s = virt_s .. (name_to_closetag(state.highlights_name[v[2]]))
|
|
end
|
|
end
|
|
table.insert(out, virt_s)
|
|
end
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param row integer
|
|
--- @return string
|
|
local function _pre_text_to_html(state, row)
|
|
local style_line = state.style[row]
|
|
local s = ''
|
|
for _, pre_text in ipairs(style_line.pre_text) do
|
|
if pre_text[2] then
|
|
s = s .. (name_to_tag(state.highlights_name[pre_text[2]]))
|
|
end
|
|
s = s .. (html_escape(pre_text[1], state.tabstop))
|
|
if pre_text[2] then
|
|
--- @type string
|
|
s = s .. (name_to_closetag(state.highlights_name[pre_text[2]]))
|
|
end
|
|
end
|
|
return s
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param char table
|
|
--- @return string
|
|
local function _char_to_html(state, char)
|
|
local s = ''
|
|
if char[2] then
|
|
s = s .. name_to_tag(state.highlights_name[char[2]])
|
|
end
|
|
s = s .. html_escape(char[1], state.tabstop)
|
|
if char[2] then
|
|
s = s .. name_to_closetag(state.highlights_name[char[2]])
|
|
end
|
|
return s
|
|
end
|
|
|
|
--- @param state vim.tohtml.state
|
|
--- @param cell vim.tohtml.cell
|
|
--- @return string
|
|
local function _virt_text_to_html(state, cell)
|
|
local s = ''
|
|
for _, v in ipairs(cell[3]) do
|
|
if v[2] then
|
|
s = s .. (name_to_tag(state.highlights_name[v[2]]))
|
|
end
|
|
--- @type string
|
|
s = s .. html_escape(v[1], state.tabstop)
|
|
if v[2] then
|
|
s = s .. name_to_closetag(state.highlights_name[v[2]])
|
|
end
|
|
end
|
|
return s
|
|
end
|
|
|
|
--- @param out string[]
|
|
--- @param state vim.tohtml.state
|
|
local function extend_pre(out, state)
|
|
local styletable = state.style
|
|
table.insert(out, '<pre>')
|
|
local out_start = #out
|
|
local hide_count = 0
|
|
--- @type integer[]
|
|
local stack = {}
|
|
|
|
local before = ''
|
|
local after = ''
|
|
local function loop(row)
|
|
local inside = row <= state.end_ and row >= state.start
|
|
local style_line = styletable[row]
|
|
if style_line.hide and (styletable[row - 1] or {}).hide then
|
|
return
|
|
end
|
|
if inside then
|
|
_extend_virt_lines(out, state, row)
|
|
end
|
|
--Possible improvement (altermo):
|
|
--Instead of looping over all the buffer characters per line,
|
|
--why not loop over all the style_line cells,
|
|
--and then calculating the amount of text.
|
|
if style_line.hide then
|
|
return
|
|
end
|
|
local line = vim.api.nvim_buf_get_lines(state.bufnr, row - 1, row, false)[1] or ''
|
|
local s = ''
|
|
if inside then
|
|
s = s .. _pre_text_to_html(state, row)
|
|
end
|
|
local true_line_len = #line + 1
|
|
for k in
|
|
pairs(style_line --[[@as table<string,any>]])
|
|
do
|
|
if type(k) == 'number' and k > true_line_len then
|
|
true_line_len = k
|
|
end
|
|
end
|
|
for col = 1, true_line_len do
|
|
local cell = style_line[col]
|
|
--- @type table?
|
|
local char
|
|
if cell then
|
|
for i = #cell[2], 1, -1 do
|
|
local hlid = cell[2][i]
|
|
if hlid < 0 then
|
|
if hlid == HIDE_ID then
|
|
hide_count = hide_count - 1
|
|
end
|
|
else
|
|
--- @type integer?
|
|
local index
|
|
for idx = #stack, 1, -1 do
|
|
s = s .. (name_to_closetag(state.highlights_name[stack[idx]]))
|
|
if stack[idx] == hlid then
|
|
index = idx
|
|
break
|
|
end
|
|
end
|
|
assert(index, 'a coles tag which has no corresponding open tag')
|
|
for idx = index + 1, #stack do
|
|
s = s .. (name_to_tag(state.highlights_name[stack[idx]]))
|
|
end
|
|
table.remove(stack, index)
|
|
end
|
|
end
|
|
|
|
for _, hlid in ipairs(cell[1]) do
|
|
if hlid < 0 then
|
|
if hlid == HIDE_ID then
|
|
hide_count = hide_count + 1
|
|
end
|
|
else
|
|
table.insert(stack, hlid)
|
|
s = s .. (name_to_tag(state.highlights_name[hlid]))
|
|
end
|
|
end
|
|
|
|
if cell[3] and inside then
|
|
s = s .. _virt_text_to_html(state, cell)
|
|
end
|
|
|
|
char = cell[4][#cell[4]]
|
|
end
|
|
|
|
if col == true_line_len and not char then
|
|
break
|
|
end
|
|
|
|
if hide_count == 0 and inside then
|
|
s = s
|
|
.. _char_to_html(
|
|
state,
|
|
char
|
|
or { vim.api.nvim_buf_get_text(state.bufnr, row - 1, col - 1, row - 1, col, {})[1] }
|
|
)
|
|
end
|
|
end
|
|
if row > state.end_ + 1 then
|
|
after = after .. s
|
|
elseif row < state.start then
|
|
before = s .. before
|
|
else
|
|
table.insert(out, s)
|
|
end
|
|
end
|
|
|
|
for row = 1, vim.api.nvim_buf_line_count(state.bufnr) + 1 do
|
|
loop(row)
|
|
end
|
|
out[out_start] = out[out_start] .. before
|
|
out[#out] = out[#out] .. after
|
|
assert(#stack == 0, 'an open HTML tag was never closed')
|
|
table.insert(out, '</pre>')
|
|
end
|
|
|
|
--- @param out string[]
|
|
--- @param fn fun()
|
|
local function extend_body(out, fn)
|
|
table.insert(out, '<body style="display: flex">')
|
|
fn()
|
|
table.insert(out, '</body>')
|
|
end
|
|
|
|
--- @param out string[]
|
|
--- @param fn fun()
|
|
local function extend_html(out, fn)
|
|
table.insert(out, '<!DOCTYPE html>')
|
|
table.insert(out, '<html>')
|
|
fn()
|
|
table.insert(out, '</html>')
|
|
end
|
|
|
|
--- @param winid integer
|
|
--- @param global_state vim.tohtml.state.global
|
|
--- @return vim.tohtml.state
|
|
local function global_state_to_state(winid, global_state)
|
|
local bufnr = vim.api.nvim_win_get_buf(winid)
|
|
local opt = global_state.conf
|
|
local width = opt.width or vim.bo[bufnr].textwidth
|
|
if not width or width < 1 then
|
|
width = vim.api.nvim_win_get_width(winid)
|
|
end
|
|
local range = opt.range or { 1, vim.api.nvim_buf_line_count(bufnr) }
|
|
local state = setmetatable({
|
|
winid = winid == 0 and vim.api.nvim_get_current_win() or winid,
|
|
opt = vim.wo[winid],
|
|
style = generate_styletable(bufnr),
|
|
bufnr = bufnr,
|
|
tabstop = (' '):rep(vim.bo[bufnr].tabstop),
|
|
width = width,
|
|
start = range[1],
|
|
end_ = range[2],
|
|
}, { __index = global_state })
|
|
return state --[[@as vim.tohtml.state]]
|
|
end
|
|
|
|
--- @param opt vim.tohtml.opt
|
|
--- @param title? string
|
|
--- @return vim.tohtml.state.global
|
|
local function opt_to_global_state(opt, title)
|
|
local fonts = {}
|
|
if opt.font then
|
|
fonts = type(opt.font) == 'string' and { opt.font } or opt.font --[[@as (string[])]]
|
|
for i, v in pairs(fonts) do
|
|
fonts[i] = ('"%s"'):format(v)
|
|
end
|
|
elseif vim.o.guifont:match('^[^:]+') then
|
|
-- Example:
|
|
-- Input: "Font,Escape\,comma, Ignore space after comma"
|
|
-- Output: { "Font","Escape,comma","Ignore space after comma" }
|
|
local prev = ''
|
|
for name in vim.gsplit(vim.o.guifont:match('^[^:]+'), ',', { trimempty = true }) do
|
|
if vim.endswith(name, '\\') then
|
|
prev = prev .. vim.trim(name:sub(1, -2) .. ',')
|
|
elseif vim.trim(name) ~= '' then
|
|
table.insert(fonts, ('"%s%s"'):format(prev, vim.trim(name)))
|
|
prev = ''
|
|
end
|
|
end
|
|
end
|
|
-- Generic family names (monospace here) must not be quoted
|
|
-- because the browser recognizes them as font families.
|
|
table.insert(fonts, 'monospace')
|
|
--- @type vim.tohtml.state.global
|
|
local state = {
|
|
background = get_background_color(),
|
|
foreground = get_foreground_color(),
|
|
title = opt.title or title or false,
|
|
font = table.concat(fonts, ','),
|
|
highlights_name = {},
|
|
conf = opt,
|
|
}
|
|
return state
|
|
end
|
|
|
|
--- @type fun(state: vim.tohtml.state)[]
|
|
local styletable_funcs = {
|
|
styletable_syntax,
|
|
styletable_diff,
|
|
styletable_treesitter,
|
|
styletable_match,
|
|
styletable_extmarks,
|
|
styletable_conceal,
|
|
styletable_listchars,
|
|
styletable_folds,
|
|
styletable_statuscolumn,
|
|
}
|
|
|
|
--- @param state vim.tohtml.state
|
|
local function state_generate_style(state)
|
|
vim._with({ win = state.winid }, function()
|
|
for _, fn in ipairs(styletable_funcs) do
|
|
--- @type string?
|
|
local cond
|
|
if type(fn) == 'table' then
|
|
cond = fn[2] --[[@as string]]
|
|
--- @type function
|
|
fn = fn[1]
|
|
end
|
|
if not cond or cond(state) then
|
|
fn(state)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- @param winid integer
|
|
--- @param opt? vim.tohtml.opt
|
|
--- @return string[]
|
|
local function win_to_html(winid, opt)
|
|
opt = opt or {}
|
|
local title = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(winid))
|
|
|
|
local global_state = opt_to_global_state(opt, title)
|
|
local state = global_state_to_state(winid, global_state)
|
|
state_generate_style(state)
|
|
|
|
local html = {}
|
|
extend_html(html, function()
|
|
extend_head(html, global_state)
|
|
extend_body(html, function()
|
|
extend_pre(html, state)
|
|
end)
|
|
end)
|
|
return html
|
|
end
|
|
|
|
local M = {}
|
|
|
|
--- @class vim.tohtml.opt
|
|
--- @inlinedoc
|
|
---
|
|
--- Title tag to set in the generated HTML code.
|
|
--- (default: buffer name)
|
|
--- @field title? string|false
|
|
---
|
|
--- Show line numbers.
|
|
--- (default: `false`)
|
|
--- @field number_lines? boolean
|
|
---
|
|
--- Fonts to use.
|
|
--- (default: `guifont`)
|
|
--- @field font? string[]|string
|
|
---
|
|
--- Width used for items which are either right aligned or repeat a character
|
|
--- infinitely.
|
|
--- (default: 'textwidth' if non-zero or window width otherwise)
|
|
--- @field width? integer
|
|
---
|
|
--- Range of rows to use.
|
|
--- (default: entire buffer)
|
|
--- @field range? integer[]
|
|
|
|
--- Converts the buffer shown in the window {winid} to HTML and returns the output as a list of string.
|
|
--- @param winid? integer Window to convert (defaults to current window)
|
|
--- @param opt? vim.tohtml.opt Optional parameters.
|
|
--- @return string[]
|
|
function M.tohtml(winid, opt)
|
|
return win_to_html(winid or 0, opt)
|
|
end
|
|
|
|
return M
|