diff --git a/lua/markview/config/asciidoc.lua b/lua/markview/config/asciidoc.lua new file mode 100644 index 0000000..4fdd7e8 --- /dev/null +++ b/lua/markview/config/asciidoc.lua @@ -0,0 +1,87 @@ +---@type markview.config.asciidoc +return { + document_titles = { + enable = true, + + sign = "󰛓 ", + sign_hl = "MarkviewPalette7Sign", + + icon = "󰛓 ", + hl = "MarkviewPalette7", + }, + section_titles = { + enable = true, + + title_1 = { + sign = "󰌕 ", sign_hl = "MarkviewHeading1Sign", + + icon = "󰼏 ", hl = "MarkviewHeading1", + }, + title_2 = { + sign = "󰌖 ", sign_hl = "MarkviewHeading2Sign", + + icon = "󰎨 ", hl = "MarkviewHeading2", + }, + title_3 = { + + icon = "󰼑 ", hl = "MarkviewHeading3", + }, + title_4 = { + + icon = "󰎲 ", hl = "MarkviewHeading4", + }, + title_5 = { + + icon = "󰼓 ", hl = "MarkviewHeading5", + }, + title_6 = { + + icon = "󰎴 ", hl = "MarkviewHeading6", + }, + + shift_width = 1, + }, + + document_attributes = { + enable = true, + }, + + tocs = { + shift_width = 2, + hl = "MarkviewPalette2Fg", + + sign = "󰙅 ", + sign_hl = "MarkviewPalette2Sign", + + depth_1 = { + icon = "◆ ", + icon_hl = "Comment", + + hl = "MarkviewPalette5Fg", + }, + depth_2 = { + icon = "◇ ", + icon_hl = "Comment", + + hl = "MarkviewPalette5Fg", + }, + depth_3 = { + icon = "◆ ", + icon_hl = "Comment", + + hl = "MarkviewPalette5Fg", + }, + depth_4 = { + icon = "◇ ", + icon_hl = "Comment", + + hl = "MarkviewPalette5Fg", + }, + depth_5 = { + icon = "◆ ", + icon_hl = "Comment", + + hl = "MarkviewPalette5Fg", + }, + }, +}; diff --git a/lua/markview/config/asciidoc_inline.lua b/lua/markview/config/asciidoc_inline.lua new file mode 100644 index 0000000..c4ca688 --- /dev/null +++ b/lua/markview/config/asciidoc_inline.lua @@ -0,0 +1,344 @@ +local function normalize_str(str) + if type(str) ~= "string" then + return ""; + end + + return string.lower(str):gsub("^%l", string.upper); +end + +---@type markview.config.asciidoc_inline +return { + bolds = { enable = true }, + italics = { enable = true }, + + monospaces = { + enable = true, + hl = "MarkviewInlineCode", + + padding_left = " ", + padding_right = " " + }, + + highlights = { + enable = true, + + default = { + padding_left = " ", + padding_right = " ", + + hl = "MarkviewPalette3" + } + }, + + + uris = { + enable = true, + + default = { + icon = "󰌷 ", + hl = "MarkviewHyperlink", + }, + + ---|fS + + --NOTE(@OXY2DEV): Github sites. + + ["github%.com/[%a%d%-%_%.]+%/?$"] = { + --- github.com/ + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return string.match(item.destination, "github%.com/([%a%d%-%_%.]+)%/?$"); + end + }, + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+%/?$"] = { + --- github.com// + icon = "󰳐 ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)%/?$"); + end + }, + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+/tree/[%a%d%-%_%.]+%/?$"] = { + --- github.com///tree/ + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + local repo, branch = string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)/tree/([%a%d%-%_%.]+)%/?$"); + return repo .. " at " .. branch; + end + }, + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+/commits/[%a%d%-%_%.]+%/?$"] = { + --- github.com///commits/ + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+/commits/[%a%d%-%_%.]+)%/?$"); + end + }, + + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+%/releases$"] = { + --- github.com///releases + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return "Releases • " .. string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)%/releases$"); + end + }, + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+%/tags$"] = { + --- github.com///tags + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return "Tags • " .. string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)%/tags$"); + end + }, + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+%/issues$"] = { + --- github.com///issues + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return "Issues • " .. string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)%/issues$"); + end + }, + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+%/pulls$"] = { + --- github.com///pulls + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return "Pull requests • " .. string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)%/pulls$"); + end + }, + + ["github%.com/[%a%d%-%_%.]+/[%a%d%-%_%.]+%/wiki$"] = { + --- github.com///wiki + icon = " ", + hl = "MarkviewPalette0Fg", + + text = function (_, item) + return "Wiki • " .. string.match(item.destination, "github%.com/([%a%d%-%_%.]+/[%a%d%-%_%.]+)%/wiki$"); + end + }, + + --- NOTE(@OXY2DEV): Commonly used sites by programmers. + + ["developer%.mozilla%.org"] = { + priority = -9999, + + icon = "󰖟 ", + hl = "MarkviewPalette5Fg" + }, + + ["w3schools%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette4Fg" + }, + + ["stackoverflow%.com"] = { + priority = -9999, + + icon = "󰓌 ", + hl = "MarkviewPalette2Fg" + }, + + ["reddit%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette2Fg" + }, + + ["github%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette6Fg" + }, + + ["gitlab%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette2Fg" + }, + + ["dev%.to"] = { + priority = -9999, + + icon = "󱁴 ", + hl = "MarkviewPalette0Fg" + }, + + ["codepen%.io"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette6Fg" + }, + + ["replit%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette2Fg" + }, + + ["jsfiddle%.net"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette5Fg" + }, + + ["npmjs%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette0Fg" + }, + + ["pypi%.org"] = { + priority = -9999, + + icon = "󰆦 ", + hl = "MarkviewPalette0Fg" + }, + + ["mvnrepository%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette1Fg" + }, + + ["medium%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette6Fg" + }, + + ["linkedin%.com"] = { + priority = -9999, + + icon = "󰌻 ", + hl = "MarkviewPalette5Fg" + }, + + ["news%.ycombinator%.com"] = { + priority = -9999, + + icon = " ", + hl = "MarkviewPalette2Fg" + }, + + ["neovim%.io/doc/user/.*#%_?.*$"] = { + icon = " ", + hl = "MarkviewPalette4Fg", + + text = function (_, item) + local file, tag = string.match(item.destination, "neovim%.io/doc/user/(.*)#%_?(.*)$"); + --- The actual website seems to show + --- _ in the site name so, we won't + --- be replacing `_`s with ` `s. + file = string.gsub(file, "%.html$", ""); + + return string.format("%s(%s) - Neovim docs", normalize_str(file), tag); + end + }, + ["neovim%.io/doc/user/.*$"] = { + icon = " ", + hl = "MarkviewPalette4Fg", + + text = function (_, item) + local file = string.match(item.destination, "neovim%.io/doc/user/(.*)$"); + file = string.gsub(file, "%.html$", ""); + + return string.format("%s - Neovim docs", normalize_str(file)); + end + }, + + ["github%.com/vim/vim"] = { + priority = -100, + + icon = " ", + hl = "MarkviewPalette4Fg", + }, + + ["github%.com/neovim/neovim"] = { + priority = -100, + + icon = " ", + hl = "MarkviewPalette4Fg", + }, + + ["vim%.org"] = { + icon = " ", + hl = "MarkviewPalette4Fg", + }, + + ["luals%.github%.io/wiki/?.*$"] = { + icon = " ", + hl = "MarkviewPalette5Fg", + + text = function (_, item) + if string.match(item.destination, "luals%.github%.io/wiki/(.-)/#(.+)$") then + local page_mappings = { + annotations = { + ["as"] = "@as", + ["alias"] = "@alias", + ["async"] = "@async", + ["cast"] = "@cast", + ["class"] = "@class", + ["deprecated"] = "@deprecated", + ["diagnostic"] = "@diagnostic", + ["enum"] = "@enum", + ["field"] = "@field", + ["generic"] = "@generic", + ["meta"] = "@meta", + ["module"] = "@module", + ["nodiscard"] = "@nodiscard", + ["operator"] = "@operator", + ["overload"] = "@overload", + ["package"] = "@package", + ["param"] = "@param", + ["see"] = "@see", + ["source"] = "@source", + ["type"] = "@type", + ["vaarg"] = "@vaarg", + ["version"] = "@version" + } + }; + + local page, section = string.match(item.destination, "luals%.github%.io/wiki/(.-)/#(.+)$"); + + if page_mappings[page] and page_mappings[page][section] then + section = page_mappings[page][section]; + else + section = normalize_str(string.gsub(section, "%-", " ")); + end + + return string.format("%s(%s) | Lua Language Server", normalize_str(page), section); + elseif string.match(item.destination, "") then + local page = string.match(item.destination, "luals%.github%.io/wiki/(.-)/?$"); + + return string.format("%s | Lua Language Server", normalize_str(page)); + else + return item.destination; + end + end + }, + + ---|fE + }, +}; diff --git a/lua/markview/parser.lua b/lua/markview/parser.lua index aa7a82b..3a42be8 100644 --- a/lua/markview/parser.lua +++ b/lua/markview/parser.lua @@ -99,6 +99,8 @@ parser.init = function (buffer, from, to, cache) ---|fS local _parsers = { + asciidoc = require("markview.parsers.asciidoc"), + asciidoc_inline = require("markview.parsers.asciidoc_inline"), comment = require("markview.parsers.comment"); markdown = require("markview.parsers.markdown"); markdown_inline = require("markview.parsers.markdown_inline"); @@ -126,6 +128,18 @@ parser.init = function (buffer, from, to, cache) return parser.content, parser.sorted; end + --[[ + WARN: Recursion when parsing `asciidoc_inline` trees + + `cathaysia/tree-sitter-asciidoc` uses `#injection.include-children` for it's inline parser. + This causes the same text to be parsed multiple times, + + FIX(asciidoc_inline): Check parse range + + Check if a parser range has been parsed before. If it has, do not parse again. + ]] + _parsers.asciidoc_inline.parsed_ranges = {}; + ---|fS "chore: Announce start of parsing" ---@type integer Start time local start = vim.uv.hrtime(); diff --git a/lua/markview/parsers/asciidoc.lua b/lua/markview/parsers/asciidoc.lua new file mode 100644 index 0000000..bfd70c6 --- /dev/null +++ b/lua/markview/parsers/asciidoc.lua @@ -0,0 +1,314 @@ +--- HTML parser for `markview.nvim`. +local asciidoc = {}; + +---@type markview.parser.asciidoc.data +asciidoc.data = {}; + +--- Queried contents +---@type table[] +asciidoc.content = {}; + +--- Queried contents, but sorted +---@type { [string]: table } +asciidoc.sorted = {} + +--- Wrapper for `table.insert()`. +---@param data table +asciidoc.insert = function (data) + table.insert(asciidoc.content, data); + + if not asciidoc.sorted[data.class] then + asciidoc.sorted[data.class] = {}; + end + + table.insert(asciidoc.sorted[data.class], data); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc.doc_attr = function (buffer, TSNode, text, range) + local _name = TSNode:named_child(1) --[[@as TSNode]]; + local name = vim.treesitter.get_node_text(_name, buffer, {}); + + local _value = TSNode:named_child(3); + + if name == "toc" then + return; + elseif name == "toc-title" and _value then + asciidoc.data.toc_title = vim.treesitter.get_node_text(_value, buffer, {}); + elseif name == "toclevels" and _value then + asciidoc.data.toc_max_depth = math.max( + math.min( + tonumber( + vim.treesitter.get_node_text(_value, buffer, {}) + ) or 0, + 5 + ), + 0 + ); + end + + asciidoc.insert({ + class = "asciidoc_document_attribute", + + text = text, + range = range + }); +end + +---@param text string[] +---@param range markview.parsed.range +asciidoc.doc_title = function (_, _, text, range) + asciidoc.data.document_title = string.match(text[1] or "", "=%s+(.*)$") + + asciidoc.insert({ + class = "asciidoc_document_title", + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc.section_title = function (buffer, TSNode, text, range) + local _marker = TSNode:child(0); + + if not _marker then + return; + end + + local marker = vim.treesitter.get_node_text(_marker, buffer, {}); + local prev = TSNode:prev_named_sibling(); + + if prev then + local prev_text = vim.treesitter.get_node_text(prev, buffer, {}); + + if prev:type() == "element_attr" and prev_text == "[discrete]" then + goto dont_add_to_toc; + end + end + + if not asciidoc.data.toc_entries then + asciidoc.data.toc_entries = {}; + end + + table.insert(asciidoc.data.toc_entries, { + depth = (#marker or 1) - 1, + text = string.gsub(text[1] or "", "^[=%s]+", ""), + + range = vim.deepcopy(range, true), + } --[[@as markview.parser.asciidoc.data.toc_entry]]); + + ::dont_add_to_toc:: + + asciidoc.insert({ + class = "asciidoc_section_title", + marker = marker, + + text = text, + range = range + }); +end + +---@param text string[] +---@param range markview.parsed.range +asciidoc.toc_pos = function (_, _, text, range) + range.col_end = range.col_start + #(text[1] or ""); + asciidoc.data.toc_pos = range; +end + +---@param text string[] +---@param range markview.parsed.asciidoc.tocs.range +asciidoc.toc = function (_, _, text, range) + local validated = {}; + + for _, entry in ipairs(asciidoc.data.toc_entries or {}) do + if entry.depth < (asciidoc.data.toc_max_depth or 5) then + table.insert(validated, entry); + end + end + + range.col_end = range.col_start + #(text[1] or ""); + range.position = asciidoc.data.toc_pos; + + asciidoc.insert({ + class = "asciidoc_toc", + + title = asciidoc.data.toc_title, + max_depth = asciidoc.data.toc_max_depth, + entries = validated, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.asciidoc.tocs.range +asciidoc.unordered_list_item = function (buffer, TSNode, text, range) + local _marker = TSNode:child(0); + + if not _marker then + return; + end + + local marker = vim.treesitter.get_node_text(_marker, buffer, {}); + range.marker_end = range.col_start + #( string.match(text[1] or "", "^[%*%s]+") or "" ); + + asciidoc.insert({ + class = "asciidoc_unordered_list_item", + marker = marker, + + text = text, + range = range + }); +end + +--- HTML parser +---@param buffer integer +---@param TSTree table +---@param from integer? +---@param to integer? +---@return markview.parsed.asciidoc[] +---@return markview.parsed.asciidoc_sorted +asciidoc.parse = function (buffer, TSTree, from, to) + -- Clear the previous contents + asciidoc.data = {}; + asciidoc.sorted = {}; + asciidoc.content = {}; + + local can_scan, scanned_queries = pcall(vim.treesitter.query.parse, "asciidoc", [[ + (document_title) @asciidoc.doc_title + (document_attr) @asciidoc.doc_attr + + [ + (title1) + (title2) + (title3) + (title4) + (title5) + ] @asciidoc.section_title + + (block_macro + ( + (block_macro_name) @toc_pos_name + (#eq? @toc_pos_name "toc") + )) @asciidoc.toc_pos + + (unordered_list_item) @asciidoc.unordered_list_item + ]]); + + if not can_scan then + require("markview.health").print({ + kind = "ERR", + + from = "parsers/asciidoc.lua", + fn = "parse() -> query", + + message = { + { tostring(error), "DiagnosticError" } + } + }); + + return asciidoc.content, asciidoc.sorted; + end + + local function iter (queries) + ---|fS + + for capture_id, capture_node, _, _ in queries:iter_captures(TSTree:root(), buffer, from, to) do + local capture_name = queries.captures[capture_id]; + + if not capture_name:match("^asciidoc%.") then + goto continue; + end + + ---@type string? + local capture_text = vim.treesitter.get_node_text(capture_node, buffer); + local r_start, c_start, r_end, c_end = capture_node:range(); + + if capture_text == nil then + goto continue; + end + + if not capture_text:match("\n$") then + capture_text = capture_text .. "\n"; + end + + local lines = {}; + + for line in capture_text:gmatch("(.-)\n") do + table.insert(lines, line); + end + + ---@type boolean, string + local success, error = pcall( + asciidoc[capture_name:gsub("^asciidoc%.", "")], + + buffer, + capture_node, + lines, + { + row_start = r_start, + col_start = c_start, + + row_end = r_end, + col_end = c_end + } + ); + + if success == false then + require("markview.health").print({ + kind = "ERR", + + from = "parsers/asciidoc.lua", + fn = "parse()", + + message = { + { tostring(error), "DiagnosticError" } + } + }); + end + + ::continue:: + end + + ---|fE + end + + iter(scanned_queries); + + local can_scan_tquery, scanned_tqueries = pcall(vim.treesitter.query.parse, "asciidoc", [[ + (document_attr + ( + (attr_name) @toc_attr + (#eq? @toc_attr "toc") + )) @asciidoc.toc + ]]); + + if not can_scan_tquery then + require("markview.health").print({ + kind = "ERR", + + from = "parsers/asciidoc.lua", + fn = "parse() -> toc_query", + + message = { + { tostring(error), "DiagnosticError" } + } + }); + else + iter(scanned_tqueries); + end + + return asciidoc.content, asciidoc.sorted; +end + +return asciidoc; diff --git a/lua/markview/parsers/asciidoc_inline.lua b/lua/markview/parsers/asciidoc_inline.lua new file mode 100644 index 0000000..18cd718 --- /dev/null +++ b/lua/markview/parsers/asciidoc_inline.lua @@ -0,0 +1,340 @@ +--- HTML parser for `markview.nvim`. +local asciidoc_inline = {}; + +asciidoc_inline.parsed_ranges = {}; + +--- Queried contents +---@type table[] +asciidoc_inline.content = {}; + +--- Queried contents, but sorted +---@type { [string]: table } +asciidoc_inline.sorted = {} + +--- Wrapper for `table.insert()`. +---@param data table +asciidoc_inline.insert = function (data) + table.insert(asciidoc_inline.content, data); + + if not asciidoc_inline.sorted[data.class] then + asciidoc_inline.sorted[data.class] = {}; + end + + table.insert(asciidoc_inline.sorted[data.class], data); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc_inline.bold = function (buffer, TSNode, text, range) + local delimiters = {}; + + for child in TSNode:iter_children() do + if child:named() == false then + if delimiters[1] then + delimiters[2] = vim.treesitter.get_node_text(child, buffer, {}); + else + delimiters[1] = vim.treesitter.get_node_text(child, buffer, {}); + end + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_bold", + delimiters = delimiters, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc_inline.highlight = function (buffer, TSNode, text, range) + local delimiters = {}; + + for child in TSNode:iter_children() do + if child:named() == false then + if delimiters[1] then + delimiters[2] = vim.treesitter.get_node_text(child, buffer, {}); + else + delimiters[1] = vim.treesitter.get_node_text(child, buffer, {}); + end + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_highlight", + delimiters = delimiters, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc_inline.italic = function (buffer, TSNode, text, range) + local delimiters = {}; + + for child in TSNode:iter_children() do + if child:named() == false then + if delimiters[1] then + delimiters[2] = vim.treesitter.get_node_text(child, buffer, {}); + else + delimiters[1] = vim.treesitter.get_node_text(child, buffer, {}); + end + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_italic", + delimiters = delimiters, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc_inline.monospace = function (buffer, TSNode, text, range) + local delimiters = {}; + + for child in TSNode:iter_children() do + if child:named() == false then + if delimiters[1] then + delimiters[2] = vim.treesitter.get_node_text(child, buffer, {}); + else + delimiters[1] = vim.treesitter.get_node_text(child, buffer, {}); + end + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_monospace", + delimiters = delimiters, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.asciidoc_inline.labeled_uris.range +asciidoc_inline.labeled_uri = function (buffer, TSNode, text, range) + local destination; + + for child in TSNode:iter_children() do + if child:type() == "uri_label" then + _, range.label_col_start, _, range.label_col_end = child:range(); + elseif child:type() == "uri" then + destination = vim.treesitter.get_node_text(child, buffer, {}); + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_labeled_uri", + destination = destination, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.range +asciidoc_inline.uri = function (buffer, TSNode, text, range) + local delimiters = {}; + local destination; + + for child in TSNode:iter_children() do + if child:named() == false then + if delimiters[1] then + delimiters[2] = vim.treesitter.get_node_text(child, buffer, {}); + else + delimiters[1] = vim.treesitter.get_node_text(child, buffer, {}); + end + else + destination = vim.treesitter.get_node_text(child, buffer, {}); + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_uri", + delimiters = delimiters, + destination = destination, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSNode TSNode +---@param text string[] +---@param range markview.parsed.asciidoc_inline.labeled_uris.range +asciidoc_inline.uri_macro = function (buffer, TSNode, text, range) + local kind, destination; + + for child in TSNode:iter_children() do + if child:type() == "macro_name" then + kind = vim.treesitter.get_node_text(child, buffer, {}); + elseif child:type() == "target" then + destination = vim.treesitter.get_node_text(child, buffer, {}); + elseif child:type() == "attr" then + local attr = vim.treesitter.get_node_text(child, buffer, {}); + + if string.match(attr, '^".*"$') or not string.match(attr, "[,=]") then + _, range.label_col_start, _, range.label_col_end = child:range(); + else + local R = { child:range() }; + local target = string.match(attr, '^"[^"]*"') or string.match(attr, "^[^,]*") or ""; + target = string.gsub(target, "%s+$", ""); + + local spaces_before = #string.match(target, "^%s*"); + + range.label_col_start = R[2] + spaces_before; + range.label_col_end = R[2] + spaces_before + #target; + end + end + end + + asciidoc_inline.insert({ + class = "asciidoc_inline_labeled_uri", + kind = kind, + destination = destination, + + text = text, + range = range + }); +end + +---@param buffer integer +---@param TSTree TSTree +---@param from integer? +---@param to integer? +---@return markview.parsed.asciidoc_inline[] +---@return markview.parsed.asciidoc_inline_sorted +asciidoc_inline.parse = function (buffer, TSTree, from, to) + -- Clear the previous contents + asciidoc_inline.sorted = {}; + asciidoc_inline.content = {}; + + local root = TSTree:root(); + local r_range = { root:range() }; + + for _, entry in ipairs(asciidoc_inline.parsed_ranges) do + if vim.deep_equal(entry, r_range) then + return asciidoc_inline.content, asciidoc_inline.sorted; + end + end + + table.insert(asciidoc_inline.parsed_ranges, r_range); + + local can_scan, scanned_queries = pcall(vim.treesitter.query.parse, "asciidoc_inline", [[ + (emphasis) @asciidoc_inline.bold + (ltalic) @asciidoc_inline.italic + (monospace) @asciidoc_inline.monospace + (highlight) @asciidoc_inline.highlight + + (autolink + (uri)) @asciidoc_inline.uri + + (labled_uri + (uri)) @asciidoc_inline.labeled_uri + + (inline_macro + ((macro_name) @uri_macro_name + (#any-of? @uri_macro_name "link" "mailto")) + (target) + (attr)) @asciidoc_inline.uri_macro + ]]); + + if not can_scan then + require("markview.health").print({ + kind = "ERR", + + from = "parsers/asciidoc_inline.lua", + fn = "parse() -> query", + + message = { + { tostring(error), "DiagnosticError" } + } + }); + + return asciidoc_inline.content, asciidoc_inline.sorted; + end + + for capture_id, capture_node, _, _ in scanned_queries:iter_captures(TSTree:root(), buffer, from, to) do + local capture_name = scanned_queries.captures[capture_id]; + + if not capture_name:match("^asciidoc_inline%.") then + goto continue; + end + + ---@type string? + local capture_text = vim.treesitter.get_node_text(capture_node, buffer); + local r_start, c_start, r_end, c_end = capture_node:range(); + + if capture_text == nil then + goto continue; + end + + if not capture_text:match("\n$") then + capture_text = capture_text .. "\n"; + end + + local lines = {}; + + for line in capture_text:gmatch("(.-)\n") do + table.insert(lines, line); + end + + ---@type boolean, string + local success, error = pcall( + asciidoc_inline[capture_name:gsub("^asciidoc_inline%.", "")], + + buffer, + capture_node, + lines, + { + row_start = r_start, + col_start = c_start, + + row_end = r_end, + col_end = c_end + } + ); + + if success == false then + require("markview.health").print({ + kind = "ERR", + + from = "parsers/asciidoc_inline.lua", + fn = "parse()", + + message = { + { tostring(error), "DiagnosticError" } + } + }); + end + + ::continue:: + end + + return asciidoc_inline.content, asciidoc_inline.sorted; +end + +return asciidoc_inline; diff --git a/lua/markview/renderer.lua b/lua/markview/renderer.lua index 3beaf54..d6c73e1 100644 --- a/lua/markview/renderer.lua +++ b/lua/markview/renderer.lua @@ -15,6 +15,11 @@ renderer.__filter_cache = { renderer.option_maps = { ---|fS + asciidoc = { + document_title = { "asciidoc_document_title" }, + }, + asciidoc_inline = { + }, comment = { autolinks = { "comment_autolink" }, code_blocks = { "comment_code_block" }, @@ -366,6 +371,8 @@ renderer.render = function (buffer, parsed_content) ---|fS local _renderers = { + asciidoc = require("markview.renderers.asciidoc"), + asciidoc_inline = require("markview.renderers.asciidoc_inline"), comment = require("markview.renderers.comment"), html = require("markview.renderers.html"), markdown = require("markview.renderers.markdown"), @@ -470,6 +477,8 @@ renderer.clear = function (buffer, from, to, hybrid_mode) ---|fS local _renderers = { + asciidoc = require("markview.renderers.asciidoc"), + asciidoc_inline = require("markview.renderers.asciidoc_inline"), comment = require("markview.renderers.comment"); html = require("markview.renderers.html"); markdown = require("markview.renderers.markdown"); diff --git a/lua/markview/renderers/asciidoc.lua b/lua/markview/renderers/asciidoc.lua new file mode 100644 index 0000000..8445087 --- /dev/null +++ b/lua/markview/renderers/asciidoc.lua @@ -0,0 +1,215 @@ +local asciidoc = {}; + +local utils = require("markview.utils"); +local spec = require("markview.spec"); + +asciidoc.ns = vim.api.nvim_create_namespace("markview/asciidoc"); + +---@param buffer integer +---@param item markview.parsed.asciidoc.document_titles +asciidoc.document_attribute = function (buffer, item) + ---@type markview.config.asciidoc.document_titles? + local config = spec.get({ "asciidoc", "document_attributes" }, { eval_args = { buffer, item } }); + + if not config then + return; + end + + local range = item.range; + + utils.set_extmark(buffer, asciidoc.ns, range.row_start, range.col_start, { + end_row = range.row_end - 1, + conceal_lines = "", + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc.document_titles +asciidoc.document_title = function (buffer, item) + ---@type markview.config.asciidoc.document_titles? + local config = spec.get({ "asciidoc", "document_titles" }, { eval_args = { buffer, item } }); + + if not config then + return; + end + + local range = item.range; + + utils.set_extmark(buffer, asciidoc.ns, range.row_start, range.col_start, { + -- Remove `=%s*` amount of characters. + end_col = range.col_start + #string.match(item.text[1] or "", "=+%s*"), + conceal = "", + + sign_text = tostring(config.sign or ""), + sign_hl_group = utils.set_hl(config.sign_hl), + + virt_text = { + { config.icon, config.icon_hl or config.hl }, + }, + line_hl_group = utils.set_hl(config.hl), + }); +end + +--- Renders atx headings. +---@param buffer integer +---@param item markview.parsed.asciidoc.section_titles +asciidoc.section_title = function (buffer, item) + ---@type markview.config.asciidoc.section_titles? + local main_config = spec.get({ "asciidoc", "section_titles" }, { fallback = nil, eval_args = { buffer, item } }); + + if not main_config then + return; + end + + ---@type markview.config.asciidoc.section_titles.opts? + local config = spec.get({ "title_" .. (#item.marker - 1) }, { source = main_config, eval_args = { buffer, item } }); + + if not config then + return; + end + + local shift_width = spec.get({ "shift_width" }, { source = main_config, fallback = 1, eval_args = { buffer, item } }); + + local range = item.range; + + utils.set_extmark(buffer, asciidoc.ns, range.row_start, range.col_start, { + end_col = range.col_start + #item.marker, + conceal = "", + + sign_text = tostring(config.sign or ""), + sign_hl_group = utils.set_hl(config.sign_hl), + + virt_text = { + { string.rep(" ", (#item.marker - 1) * shift_width) }, + { config.icon, config.icon_hl or config.hl }, + }, + line_hl_group = utils.set_hl(config.hl), + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc.tocs +asciidoc.toc = function (buffer, item) + ---@type markview.config.asciidoc.tocs? + local main_config = spec.get({ "asciidoc", "tocs" }, { fallback = nil, eval_args = { buffer, item } }); + + if not main_config then + return; + end + + local range = item.range; + local lines = {}; + + table.insert(lines, { + { main_config.icon or "", main_config.icon_hl or main_config.hl }, + { item.title or "Table of contents", main_config.hl }, + }); + + if item.entries and #item.entries > 0 then + table.insert(lines, { { "" } }); + end + + for _, entry in ipairs(item.entries or {}) do + ---@type markview.config.asciidoc.tocs.opts? + local config = spec.get({ "depth_" .. (entry.depth or 1) }, { source = main_config, eval_args = { buffer, item } }); + + if config then + local text = require("markview.renderers.asciidoc.tostring").tostring(buffer, entry.text, config.hl); + local shift_by = (main_config.shift_width or 1) * ( (entry.depth or 1) - 1 ); + + local line = { + { string.rep(config.shift_char or " ", shift_by), config.hl }, + { config.icon or "", config.icon_hl or config.hl }, + }; + + vim.list_extend(line, text); + table.insert(lines, line); + end + end + + local title = table.remove(lines, 1); + + if range.position then + utils.set_extmark(buffer, asciidoc.ns, range.row_start, range.col_start, { + end_row = range.row_end - 1, + conceal_lines = "", + }); + + local r_pos = range.position --[[@as markview.parsed.range]]; + + utils.set_extmark(buffer, asciidoc.ns, r_pos.row_start, r_pos.col_start, { + end_col = r_pos.col_end, + conceal = "", + + virt_text = title, + virt_text_pos = "inline", + + sign_text = main_config.sign or "", + sign_hl_group = utils.set_hl(main_config.sign_hl), + + virt_lines = lines, + hl_mode = "combine", + }); + else + utils.set_extmark(buffer, asciidoc.ns, range.row_start, range.col_start, { + end_col = range.col_end, + conceal = "", + + virt_text = title, + virt_text_pos = "inline", + + sign_text = main_config.sign or "", + sign_hl_group = utils.set_hl(main_config.sign_hl), + + virt_lines = lines, + hl_mode = "combine", + }); + end +end + +---@param buffer integer +---@param content markview.parsed.asciidoc[] +asciidoc.render = function (buffer, content) + asciidoc.cache = { + font_regions = {}, + style_regions = { + superscripts = {}, + subscripts = {} + }, + }; + + local custom = spec.get({ "renderers" }, { fallback = {} }); + + for _, item in ipairs(content or {}) do + local success, err; + + if custom[item.class] then + success, err = pcall(custom[item.class], asciidoc.ns, buffer, item); + else + success, err = pcall(asciidoc[item.class:gsub("^asciidoc_", "")], buffer, item); + end + + if success == false then + require("markview.health").print({ + kind = "ERR", + + from = "renderers/asciidoc.lua", + fn = "render() -> " .. item.class, + + message = { + { tostring(err), "DiagnosticError" } + } + }); + end + end +end + +--- Clears decorations of HTML elements +---@param buffer integer +---@param from integer +---@param to integer +asciidoc.clear = function (buffer, from, to) + vim.api.nvim_buf_clear_namespace(buffer, asciidoc.ns, from or 0, to or -1); +end + +return asciidoc; diff --git a/lua/markview/renderers/asciidoc/tostring.lua b/lua/markview/renderers/asciidoc/tostring.lua new file mode 100644 index 0000000..510d214 --- /dev/null +++ b/lua/markview/renderers/asciidoc/tostring.lua @@ -0,0 +1,241 @@ +local adoc_str = {}; + +local utils = require("markview.utils"); +local spec = require("markview.spec"); + +adoc_str.buffer = -1; + +---@param match string +adoc_str.bold = function (match) + ---|fS + + ---@type markview.config.asciidoc_inline.bolds? + local config = spec.get({ + "asciidoc_inline", "bolds" + }, { + eval_args = { + adoc_str.buffer, { + class = "asciidoc_inline_bold", + delimiters = string.match(match, "^%*%*") and { "**", "**" } or { "*", "*" }, + + text = { match }, + range = { + row_start = -1, + col_start = -1, + + row_end = -1, + col_end = -1, + } + } --[[@as markview.parsed.asciidoc_inline.bolds]] + } + }); + + if not config then + return { match }; + else + local removed = string.gsub(match, "^%*+", ""):gsub("%*+$", ""); + return { removed }; + end + + ---|fE +end + +---@param match string +adoc_str.italic = function (match) + ---|fS + + ---@type markview.config.asciidoc_inline.italics? + local config = spec.get({ + "asciidoc_inline", "italics" + }, { + eval_args = { + adoc_str.buffer, { + class = "asciidoc_inline_italic", + delimiters = string.match(match, "^__") and { "__", "__" } or { "_", "_" }, + + text = { match }, + range = { + row_start = -1, + col_start = -1, + + row_end = -1, + col_end = -1, + } + } --[[@as markview.parsed.asciidoc_inline.italics]] + } + }); + + if not config then + return { match }; + else + local removed = string.gsub(match, "^[%*_]+", ""):gsub("[%*_]+$", ""); + return { removed }; + end + + ---|fE +end + +---@param match string +adoc_str.monospace = function (match) + ---|fS + + local delimiter = string.match(match, "^`+"); + + ---@type markview.config.asciidoc_inline.monospaces? + local config = spec.get({ + "asciidoc_inline", "monospaces" + }, { + eval_args = { + adoc_str.buffer, { + class = "asciidoc_inline_monospace", + delimiters = { delimiter, delimiter }, + + text = { match }, + range = { + row_start = -1, + col_start = -1, + + row_end = -1, + col_end = -1, + } + } --[[@as markview.parsed.asciidoc_inline.monospaces]] + } + }); + + if not config then + return { match }; + else + local removed = string.gsub(match, "^`+", ""):gsub("`+$", ""); + local output = {}; + + if config.corner_left then table.insert(output, { config.corner_left, config.corner_left_hl or config.hl }) end + if config.padding_left then table.insert(output, { config.padding_left, config.padding_left_hl or config.hl }) end + if config.icon then table.insert(output, { config.icon, config.icon_hl or config.hl }) end + + table.insert(output, { removed, config.hl }); + + if config.padding_right then table.insert(output, { config.padding_right, config.padding_right_hl or config.hl }) end + if config.corner_right then table.insert(output, { config.corner_right, config.corner_right_hl or config.hl }) end + + return output; + end + + ---|fE +end + +---@param match string +adoc_str.highlight = function (match) + ---|fS + + local delimiter = string.match(match, "#+"); + + ---@type markview.config.asciidoc_inline.highlights? + local main_config = spec.get({ "asciidoc_inline", "highlights" }, { fallback = nil }); + + if not main_config then + return { match }; + end + + ---@type markview.parsed.asciidoc_inline.highlights + local item = { + class = "asciidoc_inline_highlight", + delimiters = { delimiter, delimiter }, + + text = { match }, + range = { + row_start = -1, + col_start = -1, + + row_end = -1, + col_end = -1, + } + }; + + if not main_config then + return; + end + + ---@type markview.config.__inline? + local config = utils.match( + main_config, + table.concat(item.text, "\n"), + { + eval_args = { adoc_str.buffer, item } + } + ); + + if not config then + return { match }; + else + local removed = string.gsub(match, "^.-#+", ""):gsub("#+$", ""); + local output = {}; + + if config.corner_left then table.insert(output, { config.corner_left, config.corner_left_hl or config.hl }) end + if config.padding_left then table.insert(output, { config.padding_left, config.padding_left_hl or config.hl }) end + if config.icon then table.insert(output, { config.icon, config.icon_hl or config.hl }) end + + table.insert(output, { removed, config.hl }); + + if config.padding_right then table.insert(output, { config.padding_right, config.padding_right_hl or config.hl }) end + if config.corner_right then table.insert(output, { config.corner_right, config.corner_right_hl or config.hl }) end + + return output; + end + + ---|fE +end + +---@param char string +adoc_str.char = function (char) + return { char }; +end + +---@param buffer integer +---@param text string +---@param base_hl string +---@return [ string, string ][] +adoc_str.tostring = function (buffer, text, base_hl) + ---|fS + + local lpeg = vim.lpeg; + adoc_str.buffer = buffer; + + local strong_content = ( 1 - lpeg.P("*") ) + lpeg.P("\\*"); + local strong = lpeg.C( lpeg.P("*") * strong_content^1 * lpeg.P("*") ) / adoc_str.bold; + local ustrong = lpeg.C( lpeg.P("**") * strong_content^1 * lpeg.P("**") ) / adoc_str.bold; + + local italic_content = ( 1 - lpeg.P("_") ) + lpeg.P("\\_"); + local italic = lpeg.C( lpeg.P("_") * italic_content^1 * lpeg.P("_") ) / adoc_str.italic; + local uitalic = lpeg.C( lpeg.P("__") * italic_content^1 * lpeg.P("__") ) / adoc_str.italic; + + local mono_content = ( 1 - lpeg.P("`") ) + lpeg.P("\\`"); + local mono = lpeg.C( lpeg.P("`")^1 * mono_content^1 * lpeg.P("`")^1 ) / adoc_str.monospace; + + local hl_content = ( 1 - lpeg.P("##") ) + lpeg.P("\\#"); + local hl = lpeg.C( lpeg.P("##") * hl_content^1 * lpeg.P("##") ) / adoc_str.highlight; + local role_content = 1 - lpeg.P("]"); + local chl = lpeg.C( lpeg.P("[") * role_content^1 * lpeg.P("]") * lpeg.P("##") * hl_content^1 * lpeg.P("##") ) / adoc_str.highlight; + + local any = lpeg.C( lpeg.P(1) ) / adoc_str.char; + local token = ustrong + strong + uitalic + italic + mono + chl + hl + any; + + local inline = lpeg.Ct( token^0 ); + local result = {}; + + for _, item in ipairs(lpeg.match(inline, text or "")) do + local last = result[#result]; + item[2] = item[2] or base_hl; + + if (not item[2] or item[2] == base_hl) and last and (not last[2] or last[2] == base_hl) then + last[1] = last[1] .. item[1]; + else + table.insert(result, item) + end + end + + return result; + + ---|fE +end + +return adoc_str; diff --git a/lua/markview/renderers/asciidoc_inline.lua b/lua/markview/renderers/asciidoc_inline.lua new file mode 100644 index 0000000..8fae022 --- /dev/null +++ b/lua/markview/renderers/asciidoc_inline.lua @@ -0,0 +1,327 @@ +local asciidoc_inline = {}; + +local utils = require("markview.utils"); +local spec = require("markview.spec"); + +asciidoc_inline.ns = vim.api.nvim_create_namespace("markview/asciidoc_inline"); + +---@param buffer integer +---@param item markview.parsed.asciidoc_inline.bolds +asciidoc_inline.bold = function (buffer, item) + ---@type markview.config.asciidoc_inline.bolds? + local config = spec.get({ "asciidoc_inline", "bolds" }, { eval_args = { buffer, item } }); + + if not config then + return; + end + + local range = item.range; + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_start + #(item.delimiters[1] or ""), + conceal = "", + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_end, range.col_end - #(item.delimiters[2] or ""), { + end_col = range.col_end, + conceal = "", + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc_inline.highlights +asciidoc_inline.highlight = function (buffer, item) + ---@type markview.config.asciidoc_inline.highlights? + local main_config = spec.get({ "asciidoc_inline", "highlights" }, { fallback = nil }); + local range = item.range; + + if not main_config then + return; + end + + ---@type markview.config.__inline? + local config = utils.match( + main_config, + table.concat(item.text, "\n"), + { + eval_args = { buffer, item } + } + ); + + if config == nil then + return; + end + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_start + #(item.delimiters[1] or ""), + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_left or "", utils.set_hl(config.corner_left_hl or config.hl) }, + { config.padding_left or "", utils.set_hl(config.padding_left_hl or config.hl) }, + { config.icon or "", utils.set_hl(config.icon_hl or config.hl) } + }, + + hl_mode = "combine" + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_end, end_row = range.row_end, + + hl_group = utils.set_hl(config.hl), + hl_mode = "combine" + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_end, range.col_end - #(item.delimiters[2] or ""), { + end_col = range.col_end, + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) }, + { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) } + }, + + hl_mode = "combine" + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc_inline.italics +asciidoc_inline.italic = function (buffer, item) + ---@type markview.config.asciidoc_inline.italics? + local config = spec.get({ "asciidoc_inline", "italics" }, { eval_args = { buffer, item } }); + + if not config then + return; + end + + local range = item.range; + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_start + #(item.delimiters[1] or ""), + conceal = "", + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_end, range.col_end - #(item.delimiters[2] or ""), { + end_col = range.col_end, + conceal = "", + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc_inline.monospaces +asciidoc_inline.monospace = function (buffer, item) + ---@type markview.config.asciidoc_inline.monospaces? + local config = spec.get({ "asciidoc_inline", "monospaces" }, { eval_args = { buffer, item } }); + + if not config then + return; + end + + local range = item.range; + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_start + #(item.delimiters[1] or ""), + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_left or "", utils.set_hl(config.corner_left_hl or config.hl) }, + { config.padding_left or "", utils.set_hl(config.padding_left_hl or config.hl) }, + { config.icon or "", utils.set_hl(config.icon_hl or config.hl) } + }, + + hl_mode = "combine" + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_end, end_row = range.row_end, + + hl_group = utils.set_hl(config.hl), + hl_mode = "combine" + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_end, range.col_end - #(item.delimiters[2] or ""), { + end_col = range.col_end, + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) }, + { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) } + }, + + hl_mode = "combine" + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc_inline.labeled_uris +asciidoc_inline.labeled_uri = function (buffer, item) + ---@type markview.config.asciidoc_inline.uris? + local main_config = spec.get({ "asciidoc_inline", "uris" }, { fallback = nil }); + local range = item.range; + + if not main_config then + return; + end + + ---@type markview.config.asciidoc_inline.uris.opts? + local config = utils.match( + main_config, + item.destination or "", + { + eval_args = { buffer, item } + } + ); + + if config == nil then + return; + end + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.label_col_start or range.col_start, + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_left or "", utils.set_hl(config.corner_left_hl or config.hl) }, + { config.padding_left or "", utils.set_hl(config.padding_left_hl or config.hl) }, + { config.icon or "", utils.set_hl(config.icon_hl or config.hl) } + }, + + hl_mode = "combine" + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.label_col_start or range.col_start, { + end_col = range.label_col_end or range.col_end, end_row = range.row_end, + + hl_group = utils.set_hl(config.hl), + hl_mode = "combine" + }); + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_end, range.label_col_end or range.col_end, { + end_col = range.col_end, + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) }, + { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) } + }, + + hl_mode = "combine" + }); +end + +---@param buffer integer +---@param item markview.parsed.asciidoc_inline.uris +asciidoc_inline.uri = function (buffer, item) + ---@type markview.config.asciidoc_inline.uris? + local main_config = spec.get({ "asciidoc_inline", "uris" }, { fallback = nil }); + local range = item.range; + + if not main_config then + return; + end + + ---@type markview.config.asciidoc_inline.uris.opts? + local config = utils.match( + main_config, + item.destination or "", + { + eval_args = { buffer, item } + } + ); + + if config == nil then + return; + end + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_start + #(item.delimiters[1] or ""), + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_left or "", utils.set_hl(config.corner_left_hl or config.hl) }, + { config.padding_left or "", utils.set_hl(config.padding_left_hl or config.hl) }, + { config.icon or "", utils.set_hl(config.icon_hl or config.hl) } + }, + + hl_mode = "combine" + }); + + if config.text then + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start + #(item.delimiters[1] or ""), { + end_col = range.col_end - #(item.delimiters[2] or ""), end_row = range.row_end, + + virt_text = { + { config.text or "", utils.set_hl(config.text_hl or config.hl) } + }, + + hl_mode = "combine" + }); + else + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_start, range.col_start, { + end_col = range.col_end, end_row = range.row_end, + + hl_group = utils.set_hl(config.hl), + hl_mode = "combine" + }); + end + + utils.set_extmark(buffer, asciidoc_inline.ns, range.row_end, range.col_end - #(item.delimiters[2] or ""), { + end_col = range.col_end, + conceal = "", + + virt_text_pos = "inline", + virt_text = { + { config.corner_right or "", utils.set_hl(config.corner_right_hl or config.hl) }, + { config.padding_right or "", utils.set_hl(config.padding_right_hl or config.hl) } + }, + + hl_mode = "combine" + }); +end + +---@param buffer integer +---@param content markview.parsed.asciidoc_inline[] +asciidoc_inline.render = function (buffer, content) + local custom = spec.get({ "renderers" }, { fallback = {} }); + + for _, item in ipairs(content or {}) do + local success, err; + + if custom[item.class] then + success, err = pcall(custom[item.class], asciidoc_inline.ns, buffer, item); + else + success, err = pcall(asciidoc_inline[item.class:gsub("^asciidoc_inline_", "")], buffer, item); + end + + if success == false then + require("markview.health").print({ + kind = "ERR", + + from = "renderers/asciidoc_inline.lua", + fn = "render() -> " .. item.class, + + message = { + { tostring(err), "DiagnosticError" } + } + }); + end + end +end + +--- Clears decorations of HTML elements +---@param buffer integer +---@param from integer +---@param to integer +asciidoc_inline.clear = function (buffer, from, to) + vim.api.nvim_buf_clear_namespace(buffer, asciidoc_inline.ns, from or 0, to or -1); +end + +return asciidoc_inline; diff --git a/lua/markview/spec.lua b/lua/markview/spec.lua index d35690f..91ed55f 100644 --- a/lua/markview/spec.lua +++ b/lua/markview/spec.lua @@ -189,7 +189,7 @@ spec.default = { debounce = 150, icon_provider = "internal", - filetypes = { "markdown", "quarto", "rmd", "typst", }, + filetypes = { "markdown", "quarto", "rmd", "typst", "asciidoc", }, ignore_buftypes = { "nofile" }, condition = function (buffer) local is_enabled = spec.get({ "experimental", "fancy_comments" }, { @@ -227,6 +227,8 @@ spec.default = { ---@type string[] Properties that should be sourced *externally*. spec.__external_config = { + "asciidoc", + "asciidoc_inline", "comment", "html", "markdown", diff --git a/lua/markview/types/parsers/asciidoc.lua b/lua/markview/types/parsers/asciidoc.lua new file mode 100644 index 0000000..2e4854c --- /dev/null +++ b/lua/markview/types/parsers/asciidoc.lua @@ -0,0 +1,86 @@ +---@meta + +------------------------------------------------------------------------------ + +---@class markview.parsed.asciidoc.document_attributes +--- +---@field class "asciidoc_document_attribute" +--- +---@field text string[] +---@field range markview.parsed.range + +------------------------------------------------------------------------------ + +---@class markview.parsed.asciidoc.document_titles +--- +---@field class "asciidoc_document_title" +--- +---@field text string[] +---@field range markview.parsed.range + +------------------------------------------------------------------------------ + +---@class markview.parsed.asciidoc.section_titles +--- +---@field class "asciidoc_section_title" +---@field marker string The `=` part of the title. +--- +---@field text string[] +---@field range markview.parsed.range + +------------------------------------------------------------------------------ + +---@class markview.parsed.asciidoc.tocs +--- +---@field class "asciidoc_toc" +---@field title? string +---@field max_depth? integer +---@field entries markview.parser.asciidoc.data.toc_entry[] +--- +---@field text string[] +---@field range markview.parsed.asciidoc.tocs.range + + +---@class markview.parsed.asciidoc.tocs.range +--- +---@field row_start integer +---@field col_start integer +--- +---@field row_end integer +---@field col_end integer +--- +---@field position? markview.parsed.range + +------------------------------------------------------------------------------ + +---@alias markview.parsed.asciidoc +---| markview.parsed.asciidoc.document_attributes +---| markview.parsed.asciidoc.document_titles +---| markview.parsed.asciidoc.section_titles + +------------------------------------------------------------------------------ + +---@class markview.parsed.asciidoc_sorted +--- +---@field document_attributes markview.parsed.asciidoc.document_attributes[] +---@field document_titles markview.parsed.asciidoc.document_titles[] +---@field section_titles markview.parsed.asciidoc.section_titles[] + +------------------------------------------------------------------------------ + +---@class markview.parser.asciidoc.data +--- +---@field document_title? string +---@field toc_title? string +---@field toc_max_depth? integer +---@field toc_entries? markview.parser.asciidoc.data.toc_entry[] +---@field toc_pos? markview.parsed.range + + +---@class markview.parser.asciidoc.data.toc_entry +--- +---@field depth integer +--- +---@field text string +---@field range markview.parsed.range + diff --git a/lua/markview/types/parsers/asciidoc_inline.lua b/lua/markview/types/parsers/asciidoc_inline.lua new file mode 100644 index 0000000..0f0ef30 --- /dev/null +++ b/lua/markview/types/parsers/asciidoc_inline.lua @@ -0,0 +1,89 @@ +---@meta + + +---@class markview.parsed.asciidoc_inline.bolds +--- +---@field class "asciidoc_inline_bold" +---@field delimiters [ string?, string? ] Delimiters +--- +---@field text string[] +---@field range markview.parsed.range + + +---@class markview.parsed.asciidoc_inline.highlights +--- +---@field class "asciidoc_inline_highlight" +---@field delimiters [ string?, string? ] Delimiters +--- +---@field text string[] +---@field range markview.parsed.range + + +---@class markview.parsed.asciidoc_inline.italics +--- +---@field class "asciidoc_inline_italic" +---@field delimiters [ string?, string? ] Delimiters +--- +---@field text string[] +---@field range markview.parsed.range + + +---@class markview.parsed.asciidoc_inline.monospaces +--- +---@field class "asciidoc_inline_monospace" +---@field delimiters [ string?, string? ] Delimiters +--- +---@field text string[] +---@field range markview.parsed.range + + +---@class markview.parsed.asciidoc_inline.labeled_uris +--- +---@field class "asciidoc_inline_labeled_uri" +---@field kind? string URI type(e.g. `mailto`). Only if the node is a **macro**. +---@field destination string URL the node is pointing to. +--- +---@field text string[] +---@field range markview.parsed.asciidoc_inline.labeled_uris.range + + +---@class markview.parsed.asciidoc_inline.labeled_uris.range +--- +---@field row_start integer +---@field col_start integer +--- +---@field row_end integer +---@field col_end integer +--- +---@field label_col_start? integer Start column of the **label** of an URI(e.g. `foo` in `https://example.com[foo]` or `https://example.com[foo,bar]`). +---@field label_col_end? integer End column of the **label** of an URI. + + +---@class markview.parsed.asciidoc_inline.uris +--- +---@field class "asciidoc_inline_uri" +---@field delimiters [ string?, string? ] Delimiters +---@field destination string URL the node is pointing to. +--- +---@field text string[] +---@field range markview.parsed.range + + +---@alias markview.parsed.asciidoc_inline +---| markview.parsed.asciidoc_inline.bolds +---| markview.parsed.asciidoc_inline.highlights +---| markview.parsed.asciidoc_inline.italics +---| markview.parsed.asciidoc_inline.monospaces +---| markview.parsed.asciidoc_inline.labeled_uris +---| markview.parsed.asciidoc_inline.uris + + +---@class markview.parsed.asciidoc_inline_sorted +--- +---@field bolds markview.parsed.asciidoc_inline.bolds +---@field italics markview.parsed.asciidoc_inline.italics +---@field monospaces markview.parsed.asciidoc_inline.monospaces +---@field highlights markview.parsed.asciidoc_inline.highlights +---@field labeled_uris markview.parsed.asciidoc_inline.labeled_uris +---@field uris markview.parsed.asciidoc_inline.uris + diff --git a/lua/markview/types/renderers/asciidoc.lua b/lua/markview/types/renderers/asciidoc.lua new file mode 100644 index 0000000..1b5ce87 --- /dev/null +++ b/lua/markview/types/renderers/asciidoc.lua @@ -0,0 +1,84 @@ +---@meta + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc.document_attributes +--- +---@field enable boolean + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc.document_titles +--- +---@field enable boolean +--- +---@field sign? string +---@field sign_hl? string +--- +---@field icon? string +---@field icon_hl? string +--- +---@field hl? string + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc.section_titles +--- +---@field enable boolean +---@field shift_width integer +--- +---@field title_1 markview.config.asciidoc.section_titles.opts +---@field title_2 markview.config.asciidoc.section_titles.opts +---@field title_3 markview.config.asciidoc.section_titles.opts +---@field title_4 markview.config.asciidoc.section_titles.opts +---@field title_5 markview.config.asciidoc.section_titles.opts + + +---@class markview.config.asciidoc.section_titles.opts +--- +---@field icon? string +---@field icon_hl? string +--- +---@field sign? string +---@field sign_hl? string +--- +---@field hl? string + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc.tocs +--- +---@field enable boolean +---@field shift_width integer +--- +---@field icon? string Icon for the TOC title. +---@field icon_hl? string Highlight group for `icon`. +--- +---@field sign? string Sign for the TOC title. +---@field sign_hl? string Highlight group for `sign`. +--- +---@field hl? string +--- +---@field depth_1 markview.config.asciidoc.tocs.opts +---@field depth_2 markview.config.asciidoc.tocs.opts +---@field depth_3 markview.config.asciidoc.tocs.opts +---@field depth_4 markview.config.asciidoc.tocs.opts +---@field depth_5 markview.config.asciidoc.tocs.opts + + +---@class markview.config.asciidoc.tocs.opts +--- +---@field shift_char? string +---@field hl? string +--- +---@field icon? string Icon for the TOC title. +---@field icon_hl? string Highlight group for `icon`. + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc +--- +---@field document_attributes markview.config.asciidoc.document_attributes +---@field document_titles markview.config.asciidoc.document_titles +---@field section_titles markview.config.asciidoc.section_titles + diff --git a/lua/markview/types/renderers/asciidoc_inline.lua b/lua/markview/types/renderers/asciidoc_inline.lua new file mode 100644 index 0000000..f761df8 --- /dev/null +++ b/lua/markview/types/renderers/asciidoc_inline.lua @@ -0,0 +1,77 @@ +---@meta + + +---@class markview.config.asciidoc_inline.bolds +--- +---@field enable boolean + +------------------------------------------------------------------------------ + +--- Configuration for Obsidian-style highlighted texts. +---@class markview.config.asciidoc_inline.highlights +--- +---@field enable boolean Enable rendering of highlighted text. +--- +---@field default markview.config.asciidoc_inline.highlights.opts Default configuration for highlighted text. +---@field [string] markview.config.asciidoc_inline.highlights.opts Configuration for highlighted text that matches `string`. + +------------------------------------------------------------------------------ + +--[[ Options for a specific footnote type. ]] +---@alias markview.config.asciidoc_inline.highlights.opts markview.config.__inline + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc_inline.italics +--- +---@field enable boolean + +------------------------------------------------------------------------------ + +---@alias markview.config.asciidoc_inline.monospaces markview.config.__inline + +------------------------------------------------------------------------------ + +---@class markview.config.asciidoc_inline.uris +--- +---@field enable boolean Enable rendering of unlabeled URIs. +--- +---@field default markview.config.asciidoc_inline.uris.opts Default configuration for URIs. +---@field [string] markview.config.asciidoc_inline.uris.opts Configuration for URIs that matches `string`. + + +---@class markview.config.asciidoc_inline.uris.opts +--- +---@field corner_left? string Left corner. +---@field corner_left_hl? string Highlight group for the left corner. +--- +---@field padding_left? string Left padding(added after `corner_left`). +---@field padding_left_hl? string Highlight group for the left padding. +--- +---@field icon? string Icon(added after `padding_left`). +---@field icon_hl? string Highlight group for the icon. +--- +--[[ Text to show instead of the `URL`.]] +---@field text? +---| string +---| function(buffer: integer, item: markview.parsed.asciidoc_inline.uris): string +---@field text_hl? string Highlight group for the text. +--- +---@field hl? string Default highlight group(used by `*_hl` options when they are not set). +--- +---@field padding_right? string Right padding. +---@field padding_right_hl? string Highlight group for the right padding. +--- +---@field corner_right? string Right corner(added after `padding_right`). +---@field corner_right_hl? string Highlight group for the right corner. + + +------------------------------------------------------------------------------ +---@class markview.config.asciidoc_inline +--- +---@field bolds markview.config.asciidoc_inline.bolds +---@field highlights markview.config.asciidoc_inline.highlights Highlighted text configuration. +---@field italics markview.config.asciidoc_inline.italics +---@field monospaces markview.config.asciidoc_inline.monospaces +---@field uris markview.config.asciidoc_inline.uris + diff --git a/test/asciidoc.adoc b/test/asciidoc.adoc new file mode 100644 index 0000000..98017e0 --- /dev/null +++ b/test/asciidoc.adoc @@ -0,0 +1,72 @@ += Hello world + +:toc: +:toc-title: Hello + +[discrete] +== hii + + +== hii + +=== bye + +:bye: +hi +:hi: 123 + +toc::[] + +It has *strong* significance to me. + +I _cannot_ stress this enough. + +Type `OK` to accept. + +That *_really_* has to go. + +Can't pick one? Let's use them `*_all_*`. + + +**C**reate, **R**ead, **U**pdate, and **D**elete (CRUD) + +That's fan__freakin__tastic! + +Don't pass generic ``Object``s to methods that accept ``String``s! + +It was Beatle**__mania__**! + + +Mark my words, #automation is essential#. + +##Mark##up refers to text that contains formatting ##mark##s. + +Where did all the [.underline]#cores# go? + +We need [.line-through]#ten# twenty VMs. + +A [.myrole]#custom role# must be fulfilled by the theme. + + +^super^script + +~sub~script + +https://asciidoctor.org - automatic! +"https://asciidoctor.org" - automatic! + +https://asciidoctor.org[Asciidoctor] + +devel@discuss.example.org + +mailto:devel@discuss.example.org[Discuss] + +mailto:join@discuss.example.org[Subscribe,Subscribe me,I want to join!] + +* List item +** Nested list item +*** Deeper nested list item +* List item +*** Another nested list item +* List item +