tohtml.lua 43 KB

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