|
| 1 | +local list = require('render-markdown.list') |
| 2 | +local state = require('render-markdown.state') |
| 3 | + |
| 4 | +local M = {} |
| 5 | + |
| 6 | +---@param namespace number |
| 7 | +---@param root TSNode |
| 8 | +M.render = function(namespace, root) |
| 9 | + local highlights = state.config.highlights |
| 10 | + ---@diagnostic disable-next-line: missing-parameter |
| 11 | + for id, node in state.markdown_query:iter_captures(root, 0) do |
| 12 | + local capture = state.markdown_query.captures[id] |
| 13 | + local value = vim.treesitter.get_node_text(node, 0) |
| 14 | + local start_row, start_col, end_row, end_col = node:range() |
| 15 | + |
| 16 | + if capture == 'heading' then |
| 17 | + local level = #value |
| 18 | + local heading = list.cycle(state.config.headings, level) |
| 19 | + local background = list.clamp_last(highlights.heading.backgrounds, level) |
| 20 | + local foreground = list.clamp_last(highlights.heading.foregrounds, level) |
| 21 | + |
| 22 | + local virt_text = { string.rep(' ', level - 1) .. heading, { foreground, background } } |
| 23 | + vim.api.nvim_buf_set_extmark(0, namespace, start_row, 0, { |
| 24 | + end_row = end_row + 1, |
| 25 | + end_col = 0, |
| 26 | + hl_group = background, |
| 27 | + virt_text = { virt_text }, |
| 28 | + virt_text_pos = 'overlay', |
| 29 | + hl_eol = true, |
| 30 | + }) |
| 31 | + elseif capture == 'code' then |
| 32 | + vim.api.nvim_buf_set_extmark(0, namespace, start_row, 0, { |
| 33 | + end_row = end_row, |
| 34 | + end_col = 0, |
| 35 | + hl_group = highlights.code, |
| 36 | + hl_eol = true, |
| 37 | + }) |
| 38 | + elseif capture == 'list_marker' then |
| 39 | + -- List markers from tree-sitter should have leading spaces removed, however there are known |
| 40 | + -- edge cases in the parser: https://github.com/tree-sitter-grammars/tree-sitter-markdown/issues/127 |
| 41 | + -- As a result we handle leading spaces here, can remove if this gets fixed upstream |
| 42 | + local _, leading_spaces = value:find('^%s*') |
| 43 | + local level = M.calculate_list_level(node) |
| 44 | + local bullet = list.cycle(state.config.bullets, level) |
| 45 | + |
| 46 | + local virt_text = { string.rep(' ', leading_spaces or 0) .. bullet, highlights.bullet } |
| 47 | + vim.api.nvim_buf_set_extmark(0, namespace, start_row, start_col, { |
| 48 | + end_row = end_row, |
| 49 | + end_col = end_col, |
| 50 | + virt_text = { virt_text }, |
| 51 | + virt_text_pos = 'overlay', |
| 52 | + }) |
| 53 | + elseif capture == 'quote_marker' then |
| 54 | + local virt_text = { value:gsub('>', state.config.quote), highlights.quote } |
| 55 | + vim.api.nvim_buf_set_extmark(0, namespace, start_row, start_col, { |
| 56 | + end_row = end_row, |
| 57 | + end_col = end_col, |
| 58 | + virt_text = { virt_text }, |
| 59 | + virt_text_pos = 'overlay', |
| 60 | + }) |
| 61 | + elseif capture == 'table' then |
| 62 | + if state.config.fat_tables then |
| 63 | + local lines = vim.api.nvim_buf_get_lines(0, start_row, end_row, false) |
| 64 | + local table_head = list.first(lines) |
| 65 | + local table_tail = list.last(lines) |
| 66 | + if #table_head == #table_tail then |
| 67 | + local headings = vim.split(table_head, '|', { plain = true, trimempty = true }) |
| 68 | + local sections = vim.tbl_map(function(part) |
| 69 | + return string.rep('─', #part) |
| 70 | + end, headings) |
| 71 | + |
| 72 | + local line_above = { { '┌' .. table.concat(sections, '┬') .. '┐', highlights.table.head } } |
| 73 | + vim.api.nvim_buf_set_extmark(0, namespace, start_row, start_col, { |
| 74 | + virt_lines_above = true, |
| 75 | + virt_lines = { line_above }, |
| 76 | + }) |
| 77 | + |
| 78 | + local line_below = { { '└' .. table.concat(sections, '┴') .. '┘', highlights.table.row } } |
| 79 | + vim.api.nvim_buf_set_extmark(0, namespace, end_row, start_col, { |
| 80 | + virt_lines_above = true, |
| 81 | + virt_lines = { line_below }, |
| 82 | + }) |
| 83 | + end |
| 84 | + end |
| 85 | + elseif vim.tbl_contains({ 'table_head', 'table_delim', 'table_row' }, capture) then |
| 86 | + local row = value:gsub('|', '│') |
| 87 | + if capture == 'table_delim' then |
| 88 | + -- Order matters here, in particular handling inner intersections before left & right |
| 89 | + row = row:gsub('-', '─') |
| 90 | + :gsub(' ', '─') |
| 91 | + :gsub('─│─', '─┼─') |
| 92 | + :gsub('│─', '├─') |
| 93 | + :gsub('─│', '─┤') |
| 94 | + end |
| 95 | + |
| 96 | + local highlight = highlights.table.head |
| 97 | + if capture == 'table_row' then |
| 98 | + highlight = highlights.table.row |
| 99 | + end |
| 100 | + |
| 101 | + local virt_text = { row, highlight } |
| 102 | + vim.api.nvim_buf_set_extmark(0, namespace, start_row, start_col, { |
| 103 | + end_row = end_row, |
| 104 | + end_col = end_col, |
| 105 | + virt_text = { virt_text }, |
| 106 | + virt_text_pos = 'overlay', |
| 107 | + }) |
| 108 | + else |
| 109 | + -- Should only get here if user provides custom capture, currently unhandled |
| 110 | + vim.print('Unhandled markdown capture: ' .. capture) |
| 111 | + end |
| 112 | + end |
| 113 | +end |
| 114 | + |
| 115 | +--- Walk through all parent nodes and count the number of nodes with type list |
| 116 | +--- to calculate the level of the given node |
| 117 | +---@param node TSNode |
| 118 | +---@return integer |
| 119 | +M.calculate_list_level = function(node) |
| 120 | + local level = 0 |
| 121 | + local parent = node:parent() |
| 122 | + while parent ~= nil do |
| 123 | + local parent_type = parent:type() |
| 124 | + if vim.tbl_contains({ 'section', 'document' }, parent_type) then |
| 125 | + -- when reaching a section or the document we are clearly at the |
| 126 | + -- top of the list |
| 127 | + break |
| 128 | + elseif parent_type == 'list' then |
| 129 | + -- found a list increase the level and continue |
| 130 | + level = level + 1 |
| 131 | + end |
| 132 | + parent = parent:parent() |
| 133 | + end |
| 134 | + return level |
| 135 | +end |
| 136 | + |
| 137 | +return M |
0 commit comments