tohtml.lua (44642B)
1 --- @brief 2 ---<pre>help 3 ---:[range]TOhtml {file} *:TOhtml* 4 ---Converts the buffer shown in the current window to HTML, opens the generated 5 ---HTML in a new split window, and saves its contents to {file}. If {file} is not 6 ---given, a temporary file (created by |tempname()|) is used. 7 ---</pre> 8 9 -- The HTML conversion script is different from Vim's one. If you want to use 10 -- Vim's TOhtml converter, download it from the vim GitHub repo. 11 -- Here are the Vim files related to this functionality: 12 -- - https://github.com/vim/vim/blob/master/runtime/syntax/2html.vim 13 -- - https://github.com/vim/vim/blob/master/runtime/autoload/tohtml.vim 14 -- - https://github.com/vim/vim/blob/master/runtime/plugin/tohtml.vim 15 -- 16 -- Main differences between this and the vim version: 17 -- - No "ignore some visual thing" settings (just set the right Vim option) 18 -- - No support for legacy web engines 19 -- - No support for legacy encoding (supports only UTF-8) 20 -- - No interactive webpage 21 -- - No specifying the internal HTML (no XHTML, no use_css=false) 22 -- - No multiwindow diffs 23 -- - No ranges 24 -- 25 -- Remarks: 26 -- - Not all visuals are supported, so it may differ. 27 28 --- @class (private) vim.tohtml.state.global 29 --- @field background string 30 --- @field foreground string 31 --- @field title string|false 32 --- @field font string 33 --- @field highlights_name table<integer,string> 34 --- @field conf vim.tohtml.opt 35 36 --- @class (private) vim.tohtml.state:vim.tohtml.state.global 37 --- @field style vim.tohtml.styletable 38 --- @field tabstop string|false 39 --- @field opt vim.wo 40 --- @field winid integer 41 --- @field bufnr integer 42 --- @field width integer 43 --- @field start integer 44 --- @field end_ integer 45 46 --- @class (private) vim.tohtml.styletable 47 --- @field [integer] vim.tohtml.line (integer: (1-index, exclusive)) 48 49 --- @class (private) vim.tohtml.line 50 --- @field virt_lines {[integer]:[string,integer][]} 51 --- @field pre_text [string, integer?][] 52 --- @field hide? boolean 53 --- @field [integer] vim.tohtml.cell? (integer: (1-index, exclusive)) 54 55 --- @class (private) vim.tohtml.cell 56 --- @field [1] integer[] start 57 --- @field [2] integer[] close 58 --- @field [3] any[][] virt_text 59 --- @field [4] any[][] overlay_text 60 61 --- @type string[] 62 local notifications = {} 63 64 ---@param msg string 65 local function notify(msg) 66 if #notifications == 0 then 67 vim.schedule(function() 68 if #notifications > 1 then 69 vim.notify(('TOhtml: %s (+ %d more warnings)'):format(notifications[1], #notifications - 1)) 70 elseif #notifications == 1 then 71 vim.notify('TOhtml: ' .. notifications[1]) 72 end 73 notifications = {} 74 end) 75 end 76 table.insert(notifications, msg) 77 end 78 79 local HIDE_ID = -1 80 -- stylua: ignore start 81 local cterm_8_to_hex={ 82 [0] = "#808080", "#ff6060", "#00ff00", "#ffff00", 83 "#8080ff", "#ff40ff", "#00ffff", "#ffffff", 84 } 85 local cterm_16_to_hex={ 86 [0] = "#000000", "#c00000", "#008000", "#804000", 87 "#0000c0", "#c000c0", "#008080", "#c0c0c0", 88 "#808080", "#ff6060", "#00ff00", "#ffff00", 89 "#8080ff", "#ff40ff", "#00ffff", "#ffffff", 90 } 91 local cterm_88_to_hex={ 92 [0] = "#000000", "#c00000", "#008000", "#804000", 93 "#0000c0", "#c000c0", "#008080", "#c0c0c0", 94 "#808080", "#ff6060", "#00ff00", "#ffff00", 95 "#8080ff", "#ff40ff", "#00ffff", "#ffffff", 96 "#000000", "#00008b", "#0000cd", "#0000ff", 97 "#008b00", "#008b8b", "#008bcd", "#008bff", 98 "#00cd00", "#00cd8b", "#00cdcd", "#00cdff", 99 "#00ff00", "#00ff8b", "#00ffcd", "#00ffff", 100 "#8b0000", "#8b008b", "#8b00cd", "#8b00ff", 101 "#8b8b00", "#8b8b8b", "#8b8bcd", "#8b8bff", 102 "#8bcd00", "#8bcd8b", "#8bcdcd", "#8bcdff", 103 "#8bff00", "#8bff8b", "#8bffcd", "#8bffff", 104 "#cd0000", "#cd008b", "#cd00cd", "#cd00ff", 105 "#cd8b00", "#cd8b8b", "#cd8bcd", "#cd8bff", 106 "#cdcd00", "#cdcd8b", "#cdcdcd", "#cdcdff", 107 "#cdff00", "#cdff8b", "#cdffcd", "#cdffff", 108 "#ff0000", "#ff008b", "#ff00cd", "#ff00ff", 109 "#ff8b00", "#ff8b8b", "#ff8bcd", "#ff8bff", 110 "#ffcd00", "#ffcd8b", "#ffcdcd", "#ffcdff", 111 "#ffff00", "#ffff8b", "#ffffcd", "#ffffff", 112 "#2e2e2e", "#5c5c5c", "#737373", "#8b8b8b", 113 "#a2a2a2", "#b9b9b9", "#d0d0d0", "#e7e7e7", 114 } 115 local cterm_256_to_hex={ 116 [0] = "#000000", "#c00000", "#008000", "#804000", 117 "#0000c0", "#c000c0", "#008080", "#c0c0c0", 118 "#808080", "#ff6060", "#00ff00", "#ffff00", 119 "#8080ff", "#ff40ff", "#00ffff", "#ffffff", 120 "#000000", "#00005f", "#000087", "#0000af", 121 "#0000d7", "#0000ff", "#005f00", "#005f5f", 122 "#005f87", "#005faf", "#005fd7", "#005fff", 123 "#008700", "#00875f", "#008787", "#0087af", 124 "#0087d7", "#0087ff", "#00af00", "#00af5f", 125 "#00af87", "#00afaf", "#00afd7", "#00afff", 126 "#00d700", "#00d75f", "#00d787", "#00d7af", 127 "#00d7d7", "#00d7ff", "#00ff00", "#00ff5f", 128 "#00ff87", "#00ffaf", "#00ffd7", "#00ffff", 129 "#5f0000", "#5f005f", "#5f0087", "#5f00af", 130 "#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f", 131 "#5f5f87", "#5f5faf", "#5f5fd7", "#5f5fff", 132 "#5f8700", "#5f875f", "#5f8787", "#5f87af", 133 "#5f87d7", "#5f87ff", "#5faf00", "#5faf5f", 134 "#5faf87", "#5fafaf", "#5fafd7", "#5fafff", 135 "#5fd700", "#5fd75f", "#5fd787", "#5fd7af", 136 "#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f", 137 "#5fff87", "#5fffaf", "#5fffd7", "#5fffff", 138 "#870000", "#87005f", "#870087", "#8700af", 139 "#8700d7", "#8700ff", "#875f00", "#875f5f", 140 "#875f87", "#875faf", "#875fd7", "#875fff", 141 "#878700", "#87875f", "#878787", "#8787af", 142 "#8787d7", "#8787ff", "#87af00", "#87af5f", 143 "#87af87", "#87afaf", "#87afd7", "#87afff", 144 "#87d700", "#87d75f", "#87d787", "#87d7af", 145 "#87d7d7", "#87d7ff", "#87ff00", "#87ff5f", 146 "#87ff87", "#87ffaf", "#87ffd7", "#87ffff", 147 "#af0000", "#af005f", "#af0087", "#af00af", 148 "#af00d7", "#af00ff", "#af5f00", "#af5f5f", 149 "#af5f87", "#af5faf", "#af5fd7", "#af5fff", 150 "#af8700", "#af875f", "#af8787", "#af87af", 151 "#af87d7", "#af87ff", "#afaf00", "#afaf5f", 152 "#afaf87", "#afafaf", "#afafd7", "#afafff", 153 "#afd700", "#afd75f", "#afd787", "#afd7af", 154 "#afd7d7", "#afd7ff", "#afff00", "#afff5f", 155 "#afff87", "#afffaf", "#afffd7", "#afffff", 156 "#d70000", "#d7005f", "#d70087", "#d700af", 157 "#d700d7", "#d700ff", "#d75f00", "#d75f5f", 158 "#d75f87", "#d75faf", "#d75fd7", "#d75fff", 159 "#d78700", "#d7875f", "#d78787", "#d787af", 160 "#d787d7", "#d787ff", "#d7af00", "#d7af5f", 161 "#d7af87", "#d7afaf", "#d7afd7", "#d7afff", 162 "#d7d700", "#d7d75f", "#d7d787", "#d7d7af", 163 "#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f", 164 "#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff", 165 "#ff0000", "#ff005f", "#ff0087", "#ff00af", 166 "#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f", 167 "#ff5f87", "#ff5faf", "#ff5fd7", "#ff5fff", 168 "#ff8700", "#ff875f", "#ff8787", "#ff87af", 169 "#ff87d7", "#ff87ff", "#ffaf00", "#ffaf5f", 170 "#ffaf87", "#ffafaf", "#ffafd7", "#ffafff", 171 "#ffd700", "#ffd75f", "#ffd787", "#ffd7af", 172 "#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f", 173 "#ffff87", "#ffffaf", "#ffffd7", "#ffffff", 174 "#080808", "#121212", "#1c1c1c", "#262626", 175 "#303030", "#3a3a3a", "#444444", "#4e4e4e", 176 "#585858", "#626262", "#6c6c6c", "#767676", 177 "#808080", "#8a8a8a", "#949494", "#9e9e9e", 178 "#a8a8a8", "#b2b2b2", "#bcbcbc", "#c6c6c6", 179 "#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee", 180 } 181 -- stylua: ignore end 182 183 --- @type table<integer,string> 184 local cterm_color_cache = {} 185 --- @type string? 186 local background_color_cache = nil 187 --- @type string? 188 local foreground_color_cache = nil 189 190 local len = vim.api.nvim_strwidth 191 192 --- @see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands 193 --- @param color "background"|"foreground"|integer 194 --- @return string? 195 local function try_query_terminal_color(color) 196 local parameter = 4 197 if color == 'foreground' then 198 parameter = 10 199 elseif color == 'background' then 200 parameter = 11 201 end 202 --- @type string? 203 local hex = nil 204 local au = vim.api.nvim_create_autocmd('TermResponse', { 205 once = true, 206 callback = function(args) 207 hex = '#' 208 .. table.concat({ 209 args.data.sequence:match('\027%]%d+;%d*;?rgb:(%w%w)%w%w/(%w%w)%w%w/(%w%w)%w%w'), 210 }) 211 end, 212 }) 213 if type(color) == 'number' then 214 vim.api.nvim_ui_send(('\027]%s;%s;?\027\\'):format(parameter, color)) 215 else 216 vim.api.nvim_ui_send(('\027]%s;?\027\\'):format(parameter)) 217 end 218 vim.wait(100, function() 219 return hex and true or false 220 end) 221 pcall(vim.api.nvim_del_autocmd, au) 222 return hex 223 end 224 225 --- @param colorstr string 226 --- @return string 227 local function cterm_to_hex(colorstr) 228 if colorstr:sub(1, 1) == '#' then 229 return colorstr 230 end 231 assert(colorstr ~= '') 232 local color = tonumber(colorstr) --[[@as integer]] 233 assert(color and 0 <= color and color <= 255) 234 if cterm_color_cache[color] then 235 return cterm_color_cache[color] 236 end 237 local hex = try_query_terminal_color(color) 238 if hex then 239 cterm_color_cache[color] = hex 240 else 241 notify("Couldn't get terminal colors, using fallback") 242 local t_Co = tonumber(vim.api.nvim_eval('&t_Co')) 243 if t_Co <= 8 then 244 cterm_color_cache = cterm_8_to_hex 245 elseif t_Co == 88 then 246 cterm_color_cache = cterm_88_to_hex 247 elseif t_Co == 256 then 248 cterm_color_cache = cterm_256_to_hex 249 else 250 cterm_color_cache = cterm_16_to_hex 251 end 252 end 253 return cterm_color_cache[color] 254 end 255 256 --- @return string 257 local function get_background_color() 258 local bg = vim.fn.synIDattr(vim.fn.hlID('Normal'), 'bg#') 259 if bg ~= '' then 260 return cterm_to_hex(bg) 261 end 262 if background_color_cache then 263 return background_color_cache 264 end 265 local hex = try_query_terminal_color('background') 266 if not hex or not hex:match('#%x%x%x%x%x%x') then 267 notify("Couldn't get terminal background colors, using fallback") 268 hex = vim.o.background == 'light' and '#ffffff' or '#000000' 269 end 270 background_color_cache = hex 271 return hex 272 end 273 274 --- @return string 275 local function get_foreground_color() 276 local fg = vim.fn.synIDattr(vim.fn.hlID('Normal'), 'fg#') 277 if fg ~= '' then 278 return cterm_to_hex(fg) 279 end 280 if foreground_color_cache then 281 return foreground_color_cache 282 end 283 local hex = try_query_terminal_color('foreground') 284 if not hex or not hex:match('#%x%x%x%x%x%x') then 285 notify("Couldn't get terminal foreground colors, using fallback") 286 hex = vim.o.background == 'light' and '#000000' or '#ffffff' 287 end 288 foreground_color_cache = hex 289 return hex 290 end 291 292 --- @param style_line vim.tohtml.line 293 --- @param col integer (1-index) 294 --- @param field integer 295 --- @param val any 296 local function _style_line_insert(style_line, col, field, val) 297 if style_line[col] == nil then 298 style_line[col] = { {}, {}, {}, {} } 299 end 300 table.insert(style_line[col][field], val) 301 end 302 303 --- @param style_line vim.tohtml.line 304 --- @param col integer (1-index) 305 --- @param val any[] 306 local function style_line_insert_overlay_char(style_line, col, val) 307 _style_line_insert(style_line, col, 4, val) 308 end 309 310 --- @param style_line vim.tohtml.line 311 --- @param col integer (1-index) 312 --- @param val any[] 313 local function style_line_insert_virt_text(style_line, col, val) 314 _style_line_insert(style_line, col, 3, val) 315 end 316 317 --- @param state vim.tohtml.state 318 --- @param hl string|integer|string[]|integer[]? 319 --- @return nil|integer 320 local function register_hl(state, hl) 321 if type(hl) == 'table' then 322 hl = hl[#hl] --- @type string|integer 323 end 324 if type(hl) == 'nil' then 325 return 326 elseif type(hl) == 'string' then 327 hl = vim.fn.hlID(hl) 328 assert(hl ~= 0) 329 end 330 hl = vim.fn.synIDtrans(hl) 331 if not state.highlights_name[hl] then 332 local name = vim.fn.synIDattr(hl, 'name') 333 assert(name ~= '') 334 state.highlights_name[hl] = name 335 end 336 return hl 337 end 338 339 --- @param state vim.tohtml.state 340 --- @param start_row integer (1-index) 341 --- @param start_col integer (1-index) 342 --- @param end_row integer (1-index) 343 --- @param end_col integer (1-index) 344 --- @param conceal_text string 345 --- @param hl_group string|integer? 346 local function styletable_insert_conceal( 347 state, 348 start_row, 349 start_col, 350 end_row, 351 end_col, 352 conceal_text, 353 hl_group 354 ) 355 assert(state.opt.conceallevel > 0) 356 local styletable = state.style 357 if start_col == end_col and start_row == end_row then 358 return 359 end 360 if state.opt.conceallevel == 1 and conceal_text == '' then 361 conceal_text = vim.opt_local.listchars:get().conceal or ' ' 362 end 363 local hlid = register_hl(state, hl_group) 364 if vim.wo[state.winid].conceallevel ~= 3 then 365 _style_line_insert(styletable[start_row], start_col, 3, { conceal_text, hlid }) 366 end 367 _style_line_insert(styletable[start_row], start_col, 1, HIDE_ID) 368 _style_line_insert(styletable[end_row], end_col, 2, HIDE_ID) 369 end 370 371 --- @param state vim.tohtml.state 372 --- @param start_row integer (1-index) 373 --- @param start_col integer (1-index) 374 --- @param end_row integer (1-index) 375 --- @param end_col integer (1-index) 376 --- @param hl_group string|integer|nil 377 local function styletable_insert_range(state, start_row, start_col, end_row, end_col, hl_group) 378 if start_col == end_col and start_row == end_row or not hl_group then 379 return 380 end 381 local styletable = state.style 382 _style_line_insert(styletable[start_row], start_col, 1, hl_group) 383 _style_line_insert(styletable[end_row], end_col, 2, hl_group) 384 end 385 386 --- @param bufnr integer 387 --- @return vim.tohtml.styletable 388 local function generate_styletable(bufnr) 389 --- @type vim.tohtml.styletable 390 local styletable = {} 391 for row = 1, vim.api.nvim_buf_line_count(bufnr) + 1 do 392 styletable[row] = { virt_lines = {}, pre_text = {} } 393 end 394 return styletable 395 end 396 397 --- @param state vim.tohtml.state 398 local function styletable_syntax(state) 399 for row = state.start, state.end_ do 400 local prev_id = 0 401 local prev_col --- @type integer? 402 for col = 1, #vim.fn.getline(row) + 1 do 403 local hlid = vim.fn.synID(row, col, 1) 404 hlid = hlid == 0 and 0 or assert(register_hl(state, hlid)) 405 if hlid ~= prev_id then 406 if prev_id ~= 0 then 407 styletable_insert_range(state, row, assert(prev_col), row, col, prev_id) 408 end 409 prev_col = col 410 prev_id = hlid 411 end 412 end 413 end 414 end 415 416 --- @param state vim.tohtml.state 417 local function styletable_diff(state) 418 local styletable = state.style 419 for row = state.start, state.end_ do 420 local style_line = styletable[row] 421 local filler = vim.fn.diff_filler(row) 422 if filler ~= 0 then 423 local fill = (vim.opt_local.fillchars:get().diff or '-') 424 table.insert( 425 style_line.virt_lines, 426 { { fill:rep(state.width), register_hl(state, 'DiffDelete') } } 427 ) 428 end 429 if row == state.end_ + 1 then 430 break 431 end 432 local prev_id = 0 433 local prev_col --- @type integer? 434 for col = 1, #vim.fn.getline(row) do 435 local hlid = vim.fn.diff_hlID(row, col) 436 hlid = hlid == 0 and 0 or assert(register_hl(state, hlid)) 437 if hlid ~= prev_id then 438 if prev_id ~= 0 then 439 styletable_insert_range(state, row, assert(prev_col), row, col, prev_id) 440 end 441 prev_col = col 442 prev_id = hlid 443 end 444 end 445 if prev_id ~= 0 then 446 styletable_insert_range(state, row, assert(prev_col), row, #vim.fn.getline(row) + 1, prev_id) 447 end 448 end 449 end 450 451 --- @param state vim.tohtml.state 452 local function styletable_treesitter(state) 453 local bufnr = state.bufnr 454 local buf_highlighter = vim.treesitter.highlighter.active[bufnr] 455 if not buf_highlighter then 456 return 457 end 458 buf_highlighter.tree:parse(true) 459 buf_highlighter.tree:for_each_tree(function(tstree, tree) 460 --- @cast tree vim.treesitter.LanguageTree 461 if not tstree then 462 return 463 end 464 local root = tstree:root() 465 local q = buf_highlighter:get_query(tree:lang()) 466 --- @type vim.treesitter.Query? 467 local query = q:query() 468 if not query then 469 return 470 end 471 for capture, node, metadata in 472 query:iter_captures(root, buf_highlighter.bufnr, state.start - 1, state.end_) 473 do 474 local srow, scol, erow, ecol = node:range() 475 --- @diagnostic disable-next-line: invisible 476 local c = q._query.captures[capture] 477 if c ~= nil then 478 local hlid = register_hl(state, '@' .. c .. '.' .. tree:lang()) 479 if metadata.conceal and state.opt.conceallevel ~= 0 then 480 styletable_insert_conceal(state, srow + 1, scol + 1, erow + 1, ecol + 1, metadata.conceal) 481 end 482 styletable_insert_range(state, srow + 1, scol + 1, erow + 1, ecol + 1, hlid) 483 end 484 end 485 end) 486 end 487 488 --- @param state vim.tohtml.state 489 --- @param extmark [integer, integer, integer, vim.api.keyset.extmark_details] 490 --- @param namespaces table<integer,string> 491 local function _styletable_extmarks_highlight(state, extmark, namespaces) 492 if not extmark[4].hl_group then 493 return 494 end 495 ---TODO(altermo) LSP semantic tokens (and some other extmarks) are only 496 ---generated in visible lines, and not in the whole buffer. 497 if (namespaces[extmark[4].ns_id] or ''):find('nvim.lsp.semantic_tokens') then 498 notify('lsp semantic tokens are not supported, HTML may be incorrect') 499 return 500 end 501 local srow, scol, erow, ecol = 502 extmark[2], extmark[3], extmark[4].end_row or extmark[2], extmark[4].end_col or extmark[3] 503 if scol == ecol and srow == erow then 504 return 505 end 506 local hlid = register_hl(state, extmark[4].hl_group) 507 styletable_insert_range(state, srow + 1, scol + 1, erow + 1, ecol + 1, hlid) 508 end 509 510 --- @param state vim.tohtml.state 511 --- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any] 512 --- @param namespaces table<integer,string> 513 local function _styletable_extmarks_virt_text(state, extmark, namespaces) 514 if not extmark[4].virt_text then 515 return 516 end 517 ---TODO(altermo) LSP semantic tokens (and some other extmarks) are only 518 ---generated in visible lines, and not in the whole buffer. 519 if (namespaces[extmark[4].ns_id] or ''):find('nvim.lsp.inlayhint') then 520 notify('lsp inlay hints are not supported, HTML may be incorrect') 521 return 522 end 523 local styletable = state.style 524 --- @type integer,integer 525 local row, col = extmark[2], extmark[3] 526 if 527 row < vim.api.nvim_buf_line_count(state.bufnr) 528 and ( 529 extmark[4].virt_text_pos == 'inline' 530 or extmark[4].virt_text_pos == 'eol' 531 or extmark[4].virt_text_pos == 'overlay' 532 ) 533 then 534 if extmark[4].virt_text_pos == 'eol' then 535 style_line_insert_virt_text(styletable[row + 1], #vim.fn.getline(row + 1) + 1, { ' ' }) 536 end 537 local virt_text_len = 0 538 for _, i in 539 ipairs(extmark[4].virt_text --[[@as (string[][])]]) 540 do 541 local hlid = register_hl(state, i[2]) 542 if extmark[4].virt_text_pos == 'eol' then 543 style_line_insert_virt_text( 544 styletable[row + 1], 545 #vim.fn.getline(row + 1) + 1, 546 { i[1], hlid } 547 ) 548 else 549 style_line_insert_virt_text(styletable[row + 1], col + 1, { i[1], hlid }) 550 end 551 virt_text_len = virt_text_len + len(assert(i[1])) 552 end 553 if extmark[4].virt_text_pos == 'overlay' then 554 styletable_insert_range(state, row + 1, col + 1, row + 1, col + virt_text_len + 1, HIDE_ID) 555 end 556 end 557 local not_supported = { 558 virt_text_pos = 'right_align', 559 hl_mode = 'blend', 560 hl_group = 'combine', 561 } 562 for opt, val in pairs(not_supported) do 563 if extmark[4][opt] == val then 564 notify(('extmark.%s="%s" is not supported, HTML may be incorrect'):format(opt, val)) 565 end 566 end 567 end 568 569 --- @param state vim.tohtml.state 570 --- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any] 571 local function _styletable_extmarks_virt_lines(state, extmark) 572 ---TODO(altermo) if the fold start is equal to virt_line start then the fold hides the virt_line 573 if not extmark[4].virt_lines then 574 return 575 end 576 --- @type integer 577 local row = extmark[2] + (extmark[4].virt_lines_above and 1 or 2) 578 for _, line in 579 ipairs(extmark[4].virt_lines --[[@as (string[][][])]]) 580 do 581 local virt_line = {} 582 for _, i in ipairs(line) do 583 local hlid = register_hl(state, i[2]) 584 table.insert(virt_line, { i[1], hlid }) 585 end 586 table.insert(state.style[row].virt_lines, virt_line) 587 end 588 end 589 590 --- @param state vim.tohtml.state 591 --- @param extmark [integer, integer, integer, vim.api.keyset.extmark_details] 592 local function _styletable_extmarks_conceal(state, extmark) 593 if not extmark[4].conceal or state.opt.conceallevel == 0 then 594 return 595 end 596 local srow, scol, erow, ecol = 597 extmark[2], extmark[3], extmark[4].end_row or extmark[2], extmark[4].end_col or extmark[3] 598 styletable_insert_conceal( 599 state, 600 srow + 1, 601 scol + 1, 602 erow + 1, 603 ecol + 1, 604 extmark[4].conceal, 605 extmark[4].hl_group or 'Conceal' 606 ) 607 end 608 609 --- @param state vim.tohtml.state 610 local function styletable_extmarks(state) 611 --TODO(altermo) extmarks may have col/row which is outside of the buffer, which could cause an error 612 local bufnr = state.bufnr 613 local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, -1, 0, -1, { details = true }) 614 --- @cast extmarks [integer,integer,integer,vim.api.keyset.extmark_details][] 615 616 local namespaces = {} --- @type table<integer, string> 617 for ns, ns_id in pairs(vim.api.nvim_get_namespaces()) do 618 namespaces[ns_id] = ns 619 end 620 for _, v in ipairs(extmarks) do 621 _styletable_extmarks_highlight(state, v, namespaces) 622 end 623 for _, v in ipairs(extmarks) do 624 _styletable_extmarks_conceal(state, v) 625 end 626 for _, v in ipairs(extmarks) do 627 _styletable_extmarks_virt_text(state, v, namespaces) 628 end 629 for _, v in ipairs(extmarks) do 630 _styletable_extmarks_virt_lines(state, v) 631 end 632 end 633 634 --- @param state vim.tohtml.state 635 local function styletable_folds(state) 636 local styletable = state.style 637 local has_folded = false 638 for row = state.start, state.end_ do 639 if vim.fn.foldclosed(row) > 0 then 640 has_folded = true 641 styletable[row].hide = true 642 end 643 if vim.fn.foldclosed(row) == row then 644 local hlid = register_hl(state, 'Folded') 645 ---TODO(altermo): Is there a way to get highlighted foldtext? 646 local foldtext = vim.fn.foldtextresult(row) 647 foldtext = foldtext .. (vim.opt.fillchars:get().fold or '·'):rep(state.width - #foldtext) 648 table.insert(styletable[row].virt_lines, { { foldtext, hlid } }) 649 end 650 end 651 if has_folded and type(({ pcall(vim.api.nvim_eval, vim.o.foldtext) })[2]) == 'table' then 652 notify('foldtext returning a table with highlights is not supported, HTML may be incorrect') 653 end 654 end 655 656 --- @param state vim.tohtml.state 657 local function styletable_conceal(state) 658 local bufnr = state.bufnr 659 vim._with({ buf = bufnr }, function() 660 for row = state.start, state.end_ do 661 --- @type table<integer,[integer,integer,string]> 662 local conceals = {} 663 local line_len_exclusive = #vim.fn.getline(row) + 1 664 for col = 1, line_len_exclusive do 665 --- @type integer,string,integer 666 local is_concealed, conceal, hlid = unpack(vim.fn.synconcealed(row, col) --[[@as table]]) 667 if is_concealed ~= 0 then 668 if not conceals[hlid] then 669 conceals[hlid] = { col, math.min(col + 1, line_len_exclusive), conceal } 670 else 671 conceals[hlid][2] = math.min(col + 1, line_len_exclusive) 672 end 673 end 674 end 675 for _, v in pairs(conceals) do 676 styletable_insert_conceal(state, row, v[1], row, v[2], v[3], 'Conceal') 677 end 678 end 679 end) 680 end 681 682 --- @param state vim.tohtml.state 683 local function styletable_match(state) 684 for _, match in ipairs(vim.fn.getmatches(state.winid)) do 685 local hlid = register_hl(state, match.group) 686 local function range(srow, scol, erow, ecol) 687 if match.group == 'Conceal' and state.opt.conceallevel ~= 0 then 688 styletable_insert_conceal(state, srow, scol, erow, ecol, match.conceal or '', hlid) 689 else 690 styletable_insert_range(state, srow, scol, erow, ecol, hlid) 691 end 692 end 693 if match.pos1 then 694 for key, v in 695 pairs(match --[[@as table<string,[integer,integer,integer]>]]) 696 do 697 if key:match('^pos(%d+)$') then 698 if #v == 1 then 699 range(v[1], 1, v[1], #vim.fn.getline(v[1]) + 1) 700 else 701 range(v[1], v[2], v[1], v[3] + v[2]) 702 end 703 end 704 end 705 else 706 for _, v in 707 ipairs(vim.fn.matchbufline(state.bufnr, assert(match.pattern), 1, '$') --[[@as (table[])]]) 708 do 709 range(v.lnum, v.byteidx + 1, v.lnum, v.byteidx + 1 + #v.text) 710 end 711 end 712 end 713 end 714 715 --- Requires state.conf.number_lines to be set to true 716 --- @param state vim.tohtml.state 717 local function styletable_statuscolumn(state) 718 if not state.conf.number_lines then 719 return 720 end 721 local statuscolumn = state.opt.statuscolumn 722 723 if statuscolumn == '' then 724 if state.opt.relativenumber then 725 if state.opt.number then 726 statuscolumn = '%C%s%{%v:lnum!=line(".")?"%=".v:relnum." ":v:lnum%}' 727 else 728 statuscolumn = '%C%s%{%"%=".v:relnum." "%}' 729 end 730 else 731 statuscolumn = '%C%s%{%"%=".v:lnum." "%}' 732 end 733 end 734 local minwidth = 0 735 736 local signcolumn = state.opt.signcolumn 737 if state.opt.number or state.opt.relativenumber then 738 minwidth = minwidth + state.opt.numberwidth 739 if signcolumn == 'number' then 740 signcolumn = 'no' 741 end 742 end 743 if signcolumn == 'number' then 744 signcolumn = 'auto' 745 end 746 if signcolumn ~= 'no' then 747 local max = tonumber(signcolumn:match('^%w-:(%d)')) --[[@as integer?]] 748 or 1 749 if signcolumn:match('^auto') then 750 --- @type table<integer,integer> 751 local signcount = {} 752 for _, extmark in 753 ipairs(vim.api.nvim_buf_get_extmarks(state.bufnr, -1, 0, -1, { details = true })) 754 do 755 --- @cast extmark [integer, integer, integer, vim.api.keyset.extmark_details] 756 if extmark[4].sign_text then 757 signcount[extmark[2]] = (signcount[extmark[2]] or 0) + 1 758 end 759 end 760 local maxsigns = 0 761 for _, v in pairs(signcount) do 762 if v > maxsigns then 763 maxsigns = v 764 end 765 end 766 minwidth = minwidth + math.min(maxsigns, max) * 2 767 else 768 minwidth = minwidth + max * 2 769 end 770 end 771 772 local foldcolumn = state.opt.foldcolumn 773 if foldcolumn ~= '0' then 774 if foldcolumn:match('^auto') then 775 local max = tonumber(foldcolumn:match('^%w-:(%d)')) --[[@as integer?]] 776 or 1 777 local maxfold = 0 778 vim._with({ buf = state.bufnr }, function() 779 for row = state.start, state.end_ do 780 local foldlevel = vim.fn.foldlevel(row) 781 if foldlevel > maxfold then 782 maxfold = foldlevel 783 end 784 end 785 end) 786 minwidth = minwidth + math.min(maxfold, max) 787 else 788 minwidth = minwidth + tonumber(foldcolumn) --[[@as integer]] 789 end 790 end 791 792 --- @type table<integer,vim.api.keyset.eval_statusline_ret> 793 local statuses = {} 794 for row = state.start, state.end_ do 795 local status = vim.api.nvim_eval_statusline( 796 statuscolumn, 797 { winid = state.winid, use_statuscol_lnum = row, highlights = true } 798 ) 799 local width = len(status.str) 800 if width > minwidth then 801 minwidth = width 802 end 803 table.insert(statuses, status) 804 end 805 for row, status in pairs(statuses) do 806 local str = status.str 807 local hls = status.highlights 808 for k, v in ipairs(hls) do 809 local hlsk1 = hls[k + 1] 810 local text = str:sub(v.start + 1, hlsk1 and hlsk1.start or nil) 811 if k == #hls then 812 text = text .. (' '):rep(minwidth - len(str)) 813 end 814 if text ~= '' then 815 local hlid = register_hl(state, v.group) 816 local virt_text = { text, hlid } 817 table.insert(state.style[row].pre_text, virt_text) 818 end 819 end 820 end 821 end 822 823 --- @param state vim.tohtml.state 824 local function styletable_listchars(state) 825 if not state.opt.list then 826 return 827 end 828 --- @return string 829 local function utf8_sub(str, i, j) 830 return vim.fn.strcharpart(str, i - 1, j and j - i + 1 or nil) 831 end 832 --- @type table<string,string> 833 local listchars = vim.opt_local.listchars:get() 834 local ids = setmetatable({}, { 835 __index = function(t, k) 836 rawset(t, k, register_hl(state, k)) 837 return rawget(t, k) 838 end, 839 }) 840 841 if listchars.eol then 842 for row = state.start, state.end_ do 843 local style_line = state.style[row] 844 style_line_insert_overlay_char( 845 style_line, 846 #vim.fn.getline(row) + 1, 847 { listchars.eol, ids.NonText } 848 ) 849 end 850 end 851 852 if listchars.tab and state.tabstop then 853 for _, match in 854 ipairs(vim.fn.matchbufline(state.bufnr, '\t', 1, '$') --[[@as (table[])]]) 855 do 856 local vcol = vim.fn.virtcol({ match.lnum, match.byteidx }, false, state.winid) --[[@as integer]] 857 local tablen = #state.tabstop - (vcol % #state.tabstop) 858 local text --- @type string 859 if len(listchars.tab) == 3 then 860 if tablen == 1 then 861 text = utf8_sub(listchars.tab, 3, 3) 862 else 863 text = utf8_sub(listchars.tab, 1, 1) 864 .. utf8_sub(listchars.tab, 2, 2):rep(tablen - 2) 865 .. utf8_sub(listchars.tab, 3, 3) 866 end 867 else 868 text = utf8_sub(listchars.tab, 1, 1) .. utf8_sub(listchars.tab, 2, 2):rep(tablen - 1) 869 end 870 style_line_insert_overlay_char( 871 state.style[match.lnum], 872 match.byteidx + 1, 873 { text, ids.Whitespace } 874 ) 875 end 876 end 877 878 if listchars.space then 879 for _, match in 880 ipairs(vim.fn.matchbufline(state.bufnr, ' ', 1, '$') --[[@as (table[])]]) 881 do 882 style_line_insert_overlay_char( 883 state.style[match.lnum], 884 match.byteidx + 1, 885 { listchars.space, ids.Whitespace } 886 ) 887 end 888 end 889 890 if listchars.multispace then 891 for _, match in 892 ipairs(vim.fn.matchbufline(state.bufnr, [[ \+]], 1, '$') --[[@as (table[])]]) 893 do 894 local text = utf8_sub(listchars.multispace:rep(len(match.text)), 1, len(match.text)) 895 for i = 1, len(text) do 896 style_line_insert_overlay_char( 897 state.style[match.lnum], 898 match.byteidx + i, 899 { utf8_sub(text, i, i), ids.Whitespace } 900 ) 901 end 902 end 903 end 904 905 if listchars.lead or listchars.leadmultispace then 906 for _, match in 907 ipairs(vim.fn.matchbufline(state.bufnr, [[^ \+]], 1, '$') --[[@as (table[])]]) 908 do 909 local text = '' 910 if len(match.text) == 1 or not listchars.leadmultispace then 911 if listchars.lead then 912 text = listchars.lead:rep(len(match.text)) 913 end 914 elseif listchars.leadmultispace then 915 text = utf8_sub(listchars.leadmultispace:rep(len(match.text)), 1, len(match.text)) 916 end 917 for i = 1, len(text) do 918 style_line_insert_overlay_char( 919 state.style[match.lnum], 920 match.byteidx + i, 921 { utf8_sub(text, i, i), ids.Whitespace } 922 ) 923 end 924 end 925 end 926 927 if listchars.trail then 928 for _, match in 929 ipairs(vim.fn.matchbufline(state.bufnr, [[ \+$]], 1, '$') --[[@as (table[])]]) 930 do 931 local text = listchars.trail:rep(len(match.text)) 932 for i = 1, len(text) do 933 style_line_insert_overlay_char( 934 state.style[match.lnum], 935 match.byteidx + i, 936 { utf8_sub(text, i, i), ids.Whitespace } 937 ) 938 end 939 end 940 end 941 942 if listchars.nbsp then 943 for _, match in 944 ipairs( 945 vim.fn.matchbufline(state.bufnr, '\226\128\175\\|\194\160', 1, '$') --[[@as (table[])]] 946 ) 947 do 948 style_line_insert_overlay_char( 949 state.style[match.lnum], 950 match.byteidx + 1, 951 { listchars.nbsp, ids.Whitespace } 952 ) 953 for i = 2, #match.text do 954 style_line_insert_overlay_char( 955 state.style[match.lnum], 956 match.byteidx + i, 957 { '', ids.Whitespace } 958 ) 959 end 960 end 961 end 962 end 963 964 --- @param name string 965 --- @return string 966 local function highlight_name_to_class_name(name) 967 return (name:gsub('%.', '-'):gsub('@', '-')) 968 end 969 970 --- @param name string 971 --- @return string 972 local function name_to_tag(name) 973 return '<span class="' .. highlight_name_to_class_name(name) .. '">' 974 end 975 976 --- @param _ string 977 --- @return string 978 local function name_to_closetag(_) 979 return '</span>' 980 end 981 982 --- @param str string 983 --- @param tabstop string|false? 984 --- @return string 985 local function html_escape(str, tabstop) 986 str = str:gsub('&', '&'):gsub('<', '<'):gsub('>', '>'):gsub('"', '"') 987 if tabstop then 988 --- @type string 989 str = str:gsub('\t', tabstop) 990 end 991 return str 992 end 993 994 --- @param out string[] 995 --- @param state vim.tohtml.state.global 996 local function extend_style(out, state) 997 table.insert(out, '<style>') 998 table.insert(out, ('* {font-family: %s}'):format(state.font)) 999 table.insert( 1000 out, 1001 ('body {background-color: %s; color: %s}'):format(state.background, state.foreground) 1002 ) 1003 for hlid, name in pairs(state.highlights_name) do 1004 --TODO(altermo) use local namespace (instead of global 0) 1005 local fg = vim.fn.synIDattr(hlid, 'fg#') 1006 local bg = vim.fn.synIDattr(hlid, 'bg#') 1007 local sp = vim.fn.synIDattr(hlid, 'sp#') 1008 local decor_line = {} 1009 if vim.fn.synIDattr(hlid, 'underline') ~= '' then 1010 table.insert(decor_line, 'underline') 1011 end 1012 if vim.fn.synIDattr(hlid, 'strikethrough') ~= '' then 1013 table.insert(decor_line, 'line-through') 1014 end 1015 if vim.fn.synIDattr(hlid, 'undercurl') ~= '' then 1016 table.insert(decor_line, 'underline') 1017 end 1018 local c = { 1019 color = fg ~= '' and cterm_to_hex(fg) or nil, 1020 ['background-color'] = bg ~= '' and cterm_to_hex(bg) or nil, 1021 ['font-style'] = vim.fn.synIDattr(hlid, 'italic') ~= '' and 'italic' or nil, 1022 ['font-weight'] = vim.fn.synIDattr(hlid, 'bold') ~= '' and 'bold' or nil, 1023 ['text-decoration-line'] = not vim.tbl_isempty(decor_line) and table.concat(decor_line, ' ') 1024 or nil, 1025 -- TODO(ribru17): fallback to displayed text color if sp not set 1026 ['text-decoration-color'] = sp ~= '' and cterm_to_hex(sp) or nil, 1027 --TODO(altermo) if strikethrough and undercurl then the strikethrough becomes wavy 1028 ['text-decoration-style'] = vim.fn.synIDattr(hlid, 'undercurl') ~= '' and 'wavy' or nil, 1029 } 1030 local attrs = {} 1031 for attr, val in pairs(c) do 1032 table.insert(attrs, attr .. ': ' .. val) 1033 end 1034 table.insert( 1035 out, 1036 '.' .. highlight_name_to_class_name(name) .. ' {' .. table.concat(attrs, '; ') .. '}' 1037 ) 1038 end 1039 table.insert(out, '</style>') 1040 end 1041 1042 --- @param out string[] 1043 --- @param state vim.tohtml.state.global 1044 local function extend_head(out, state) 1045 table.insert(out, '<head>') 1046 table.insert(out, '<meta charset="UTF-8">') 1047 if state.title ~= false then 1048 table.insert(out, ('<title>%s</title>'):format(state.title)) 1049 end 1050 local colorscheme = vim.api.nvim_exec2('colorscheme', { output = true }).output 1051 table.insert( 1052 out, 1053 ('<meta name="colorscheme" content="%s"></meta>'):format(html_escape(colorscheme)) 1054 ) 1055 extend_style(out, state) 1056 table.insert(out, '</head>') 1057 end 1058 1059 --- @param out string[] 1060 --- @param state vim.tohtml.state 1061 --- @param row integer 1062 local function _extend_virt_lines(out, state, row) 1063 local style_line = state.style[row] 1064 for _, virt_line in ipairs(style_line.virt_lines) do 1065 local virt_s = '' 1066 for _, v in ipairs(virt_line) do 1067 if v[2] then 1068 virt_s = virt_s .. (name_to_tag(state.highlights_name[v[2]])) 1069 end 1070 virt_s = virt_s .. v[1] 1071 if v[2] then 1072 --- @type string 1073 virt_s = virt_s .. (name_to_closetag(state.highlights_name[v[2]])) 1074 end 1075 end 1076 table.insert(out, virt_s) 1077 end 1078 end 1079 1080 --- @param state vim.tohtml.state 1081 --- @param row integer 1082 --- @return string 1083 local function _pre_text_to_html(state, row) 1084 local style_line = state.style[row] 1085 local s = '' 1086 for _, pre_text in ipairs(style_line.pre_text) do 1087 if pre_text[2] then 1088 s = s .. (name_to_tag(state.highlights_name[pre_text[2]])) 1089 end 1090 s = s .. (html_escape(pre_text[1], state.tabstop)) 1091 if pre_text[2] then 1092 --- @type string 1093 s = s .. (name_to_closetag(state.highlights_name[pre_text[2]])) 1094 end 1095 end 1096 return s 1097 end 1098 1099 --- @param state vim.tohtml.state 1100 --- @param char table 1101 --- @return string 1102 local function _char_to_html(state, char) 1103 local s = '' 1104 if char[2] then 1105 s = s .. name_to_tag(state.highlights_name[char[2]]) 1106 end 1107 s = s .. html_escape(char[1], state.tabstop) 1108 if char[2] then 1109 s = s .. name_to_closetag(state.highlights_name[char[2]]) 1110 end 1111 return s 1112 end 1113 1114 --- @param state vim.tohtml.state 1115 --- @param cell vim.tohtml.cell 1116 --- @return string 1117 local function _virt_text_to_html(state, cell) 1118 local s = '' 1119 for _, v in ipairs(cell[3]) do 1120 if v[2] then 1121 s = s .. (name_to_tag(state.highlights_name[v[2]])) 1122 end 1123 --- @type string 1124 s = s .. html_escape(v[1], state.tabstop) 1125 if v[2] then 1126 s = s .. name_to_closetag(state.highlights_name[v[2]]) 1127 end 1128 end 1129 return s 1130 end 1131 1132 --- @param out string[] 1133 --- @param state vim.tohtml.state 1134 local function extend_pre(out, state) 1135 local styletable = state.style 1136 table.insert(out, '<pre>') 1137 local out_start = #out 1138 local hide_count = 0 1139 --- @type integer[] 1140 local stack = {} 1141 1142 local before = '' 1143 local after = '' 1144 --- @param row integer 1145 local function loop(row) 1146 local inside = row <= state.end_ and row >= state.start 1147 local style_line = styletable[row] 1148 if style_line.hide and (styletable[row - 1] or {}).hide then 1149 return 1150 end 1151 if inside then 1152 _extend_virt_lines(out, state, row) 1153 end 1154 --Possible improvement (altermo): 1155 --Instead of looping over all the buffer characters per line, 1156 --why not loop over all the style_line cells, 1157 --and then calculating the amount of text. 1158 if style_line.hide then 1159 return 1160 end 1161 local line = vim.api.nvim_buf_get_lines(state.bufnr, row - 1, row, false)[1] or '' 1162 local s = '' 1163 if inside then 1164 s = s .. _pre_text_to_html(state, row) 1165 end 1166 local true_line_len = #line + 1 1167 for k in 1168 pairs(style_line --[[@as table<string,any>]]) 1169 do 1170 if type(k) == 'number' and k > true_line_len then 1171 true_line_len = k --[[@as integer]] 1172 end 1173 end 1174 for col = 1, true_line_len do 1175 local cell = style_line[col] 1176 --- @type table? 1177 local char 1178 if cell then 1179 for i = #cell[2], 1, -1 do 1180 local hlid = cell[2][i] 1181 if hlid < 0 then 1182 if hlid == HIDE_ID then 1183 hide_count = hide_count - 1 1184 end 1185 else 1186 --- @type integer? 1187 local index 1188 for idx = #stack, 1, -1 do 1189 s = s .. (name_to_closetag(state.highlights_name[stack[idx]])) 1190 if stack[idx] == hlid then 1191 index = idx 1192 break 1193 end 1194 end 1195 assert(index, 'a coles tag which has no corresponding open tag') 1196 for idx = index + 1, #stack do 1197 s = s .. (name_to_tag(state.highlights_name[stack[idx]])) 1198 end 1199 table.remove(stack, index) 1200 end 1201 end 1202 1203 for _, hlid in ipairs(cell[1]) do 1204 if hlid < 0 then 1205 if hlid == HIDE_ID then 1206 hide_count = hide_count + 1 1207 end 1208 else 1209 table.insert(stack, hlid) 1210 s = s .. (name_to_tag(state.highlights_name[hlid])) 1211 end 1212 end 1213 1214 if cell[3] and inside then 1215 s = s .. _virt_text_to_html(state, cell) 1216 end 1217 1218 char = cell[4][#cell[4]] 1219 end 1220 1221 if col == true_line_len and not char then 1222 break 1223 end 1224 1225 if hide_count == 0 and inside then 1226 s = s 1227 .. _char_to_html( 1228 state, 1229 char 1230 or { vim.api.nvim_buf_get_text(state.bufnr, row - 1, col - 1, row - 1, col, {})[1] } 1231 ) 1232 end 1233 end 1234 if row > state.end_ + 1 then 1235 after = after .. s 1236 elseif row < state.start then 1237 before = s .. before 1238 else 1239 table.insert(out, s) 1240 end 1241 end 1242 1243 for row = 1, vim.api.nvim_buf_line_count(state.bufnr) + 1 do 1244 loop(row) 1245 end 1246 out[out_start] = out[out_start] .. before 1247 out[#out] = out[#out] .. after 1248 assert(#stack == 0, 'an open HTML tag was never closed') 1249 table.insert(out, '</pre>') 1250 end 1251 1252 --- @param out string[] 1253 --- @param fn fun() 1254 local function extend_body(out, fn) 1255 table.insert(out, '<body style="display: flex">') 1256 fn() 1257 table.insert(out, '</body>') 1258 end 1259 1260 --- @param out string[] 1261 --- @param fn fun() 1262 local function extend_html(out, fn) 1263 table.insert(out, '<!DOCTYPE html>') 1264 table.insert(out, '<html>') 1265 fn() 1266 table.insert(out, '</html>') 1267 end 1268 1269 --- @param winid integer 1270 --- @param global_state vim.tohtml.state.global 1271 --- @return vim.tohtml.state 1272 local function global_state_to_state(winid, global_state) 1273 local bufnr = vim.api.nvim_win_get_buf(winid) 1274 local opt = global_state.conf 1275 local width = opt.width or vim.bo[bufnr].textwidth 1276 if not width or width < 1 then 1277 width = vim.api.nvim_win_get_width(winid) 1278 end 1279 local range = opt.range or { 1, vim.api.nvim_buf_line_count(bufnr) } 1280 local state = setmetatable({ 1281 winid = winid == 0 and vim.api.nvim_get_current_win() or winid, 1282 opt = vim.wo[winid], 1283 style = generate_styletable(bufnr), 1284 bufnr = bufnr, 1285 tabstop = (' '):rep(vim.bo[bufnr].tabstop), 1286 width = width, 1287 start = range[1], 1288 end_ = range[2], 1289 }, { __index = global_state }) 1290 return state --[[@as vim.tohtml.state]] 1291 end 1292 1293 --- @param opt vim.tohtml.opt 1294 --- @param title? string 1295 --- @return vim.tohtml.state.global 1296 local function opt_to_global_state(opt, title) 1297 local fonts = {} 1298 if opt.font then 1299 fonts = type(opt.font) == 'string' and { opt.font } or opt.font --[[@as (string[])]] 1300 for i, v in pairs(fonts) do 1301 fonts[i] = ('"%s"'):format(v) 1302 end 1303 elseif vim.o.guifont:match('^[^:]+') then 1304 -- Example: 1305 -- Input: "Font,Escape\,comma, Ignore space after comma" 1306 -- Output: { "Font","Escape,comma","Ignore space after comma" } 1307 local prev = '' 1308 for name in vim.gsplit(assert(vim.o.guifont:match('^[^:]+')), ',', { trimempty = true }) do 1309 if vim.endswith(name, '\\') then 1310 prev = prev .. vim.trim(name:sub(1, -2) .. ',') 1311 elseif vim.trim(name) ~= '' then 1312 table.insert(fonts, ('"%s%s"'):format(prev, vim.trim(name))) 1313 prev = '' 1314 end 1315 end 1316 end 1317 -- Generic family names (monospace here) must not be quoted 1318 -- because the browser recognizes them as font families. 1319 table.insert(fonts, 'monospace') 1320 --- @type vim.tohtml.state.global 1321 local state = { 1322 background = get_background_color(), 1323 foreground = get_foreground_color(), 1324 title = opt.title or title or false, 1325 font = table.concat(fonts, ','), 1326 highlights_name = {}, 1327 conf = opt, 1328 } 1329 return state 1330 end 1331 1332 --- @type fun(state: vim.tohtml.state)[] 1333 local styletable_funcs = { 1334 styletable_syntax, 1335 styletable_diff, 1336 styletable_treesitter, 1337 styletable_match, 1338 styletable_extmarks, 1339 styletable_conceal, 1340 styletable_listchars, 1341 styletable_folds, 1342 styletable_statuscolumn, 1343 } 1344 1345 --- @param state vim.tohtml.state 1346 local function state_generate_style(state) 1347 vim._with({ win = state.winid }, function() 1348 for _, fn in ipairs(styletable_funcs) do 1349 --- @type string? 1350 local cond 1351 if type(fn) == 'table' then 1352 cond = fn[2] --[[@as string]] 1353 --- @type function 1354 fn = fn[1] 1355 end 1356 if not cond or cond(state) then 1357 fn(state) 1358 end 1359 end 1360 end) 1361 end 1362 1363 --- @param winid integer 1364 --- @param opt? vim.tohtml.opt 1365 --- @return string[] 1366 local function win_to_html(winid, opt) 1367 opt = opt or {} 1368 local title = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(winid)) 1369 1370 local global_state = opt_to_global_state(opt, title) 1371 local state = global_state_to_state(winid, global_state) 1372 state_generate_style(state) 1373 1374 local html = {} 1375 table.insert(html, '<!-- vim: set nomodeline: -->') 1376 extend_html(html, function() 1377 extend_head(html, global_state) 1378 extend_body(html, function() 1379 extend_pre(html, state) 1380 end) 1381 end) 1382 return html 1383 end 1384 1385 local M = {} 1386 1387 --- @class vim.tohtml.opt 1388 --- @inlinedoc 1389 --- 1390 --- Title tag to set in the generated HTML code. 1391 --- (default: buffer name) 1392 --- @field title? string|false 1393 --- 1394 --- Show line numbers. 1395 --- (default: `false`) 1396 --- @field number_lines? boolean 1397 --- 1398 --- Fonts to use. 1399 --- (default: `guifont`) 1400 --- @field font? string[]|string 1401 --- 1402 --- Width used for items which are either right aligned or repeat a character 1403 --- infinitely. 1404 --- (default: 'textwidth' if non-zero or window width otherwise) 1405 --- @field width? integer 1406 --- 1407 --- Range of rows to use. 1408 --- (default: entire buffer) 1409 --- @field range? integer[] 1410 1411 --- Converts the buffer shown in the window {winid} to HTML and returns the output as a list of string. 1412 --- @param winid? integer Window to convert (defaults to current window) 1413 --- @param opt? vim.tohtml.opt Optional parameters. 1414 --- @return string[] 1415 function M.tohtml(winid, opt) 1416 return win_to_html(winid or 0, opt) 1417 end 1418 1419 return M