--- @brief ---
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.
---
-- 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')
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) 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, '')
end
--- @param out string[]
--- @param fn fun()
local function extend_body(out, fn)
table.insert(out, '')
fn()
table.insert(out, '')
end
--- @param out string[]
--- @param fn fun()
local function extend_html(out, fn)
table.insert(out, '')
table.insert(out, '')
fn()
table.insert(out, '')
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