Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions lua/md-render/display_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,40 @@ function M.reset_osc8_cache()
_osc8_supported = nil
end

-- Common info-string aliases that don't match a treesitter parser name directly.
-- nvim-treesitter "main" branch and bare Neovim register no defaults, so without
-- this table fenced blocks tagged ```sh / ```js / ... silently lose highlighting.
-- Users can add more via vim.treesitter.language.register() — that path is tried
-- first via vim.treesitter.language.get_lang().
local LANG_ALIASES = {
sh = "bash",
shell = "bash",
shellscript = "bash",
zsh = "bash",
js = "javascript",
jsx = "javascript",
ts = "typescript",
py = "python",
rb = "ruby",
rs = "rust",
yml = "yaml",
md = "markdown",
ps1 = "powershell",
}

--- Resolve a fenced-block info string to a treesitter parser name.
---@param name string
---@return string
local function resolve_lang(name)
local lower = name:lower()
local registered = vim.treesitter.language.get_lang(lower)
if registered and registered ~= lower then return registered end
return LANG_ALIASES[lower] or lower
end

M._resolve_lang = resolve_lang
M._LANG_ALIASES = LANG_ALIASES

--- Apply treesitter syntax highlighting to code blocks
---@param buf integer
---@param ns integer
Expand All @@ -82,13 +116,15 @@ function M.apply_treesitter_highlights(buf, ns, content)
end
local code_text = table.concat(code_lines, "\n")

local ok, parser = pcall(vim.treesitter.get_string_parser, code_text, block.language)
local lang = resolve_lang(block.language)

local ok, parser = pcall(vim.treesitter.get_string_parser, code_text, lang)
if not ok or not parser then goto continue end

local trees = parser:parse()
if not trees or #trees == 0 then goto continue end

local query = vim.treesitter.query.get(block.language, "highlights")
local query = vim.treesitter.query.get(lang, "highlights")
if not query then goto continue end

for id, node in query:iter_captures(trees[1]:root(), code_text) do
Expand Down Expand Up @@ -117,7 +153,7 @@ function M.apply_treesitter_highlights(buf, ns, content)
pcall(vim.api.nvim_buf_set_extmark, buf, ns, buf_sr, buf_sc, {
end_row = buf_er,
end_col = buf_ec,
hl_group = "@" .. name .. "." .. block.language,
hl_group = "@" .. name .. "." .. lang,
priority = 4200,
})
::skip_capture::
Expand Down
76 changes: 76 additions & 0 deletions tests/display_utils_test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
-- display_utils tests
-- Run: nvim --headless -u NONE --noplugin -l tests/display_utils_test.lua

package.path = vim.fn.getcwd() .. "/lua/?.lua;" .. vim.fn.getcwd() .. "/lua/?/init.lua;" .. package.path

local display_utils = require "md-render.display_utils"

local pass_count = 0
local fail_count = 0

local function assert_eq(actual, expected, msg)
if actual == expected then
pass_count = pass_count + 1
else
fail_count = fail_count + 1
print("FAIL: " .. msg)
print(" expected: " .. vim.inspect(expected))
print(" actual: " .. vim.inspect(actual))
end
end

local function test(name, fn)
local ok, err = pcall(fn)
if not ok then
fail_count = fail_count + 1
print("ERROR: " .. name .. ": " .. tostring(err))
end
end

-- ============================================================================
-- resolve_lang: map fenced info-string to treesitter parser name
-- ============================================================================

test("resolve_lang maps sh-family aliases to bash", function()
assert_eq(display_utils._resolve_lang "sh", "bash", "sh -> bash")
assert_eq(display_utils._resolve_lang "zsh", "bash", "zsh -> bash")
assert_eq(display_utils._resolve_lang "shell", "bash", "shell -> bash")
assert_eq(display_utils._resolve_lang "shellscript", "bash", "shellscript -> bash")
end)

test("resolve_lang maps common short forms", function()
assert_eq(display_utils._resolve_lang "js", "javascript", "js -> javascript")
assert_eq(display_utils._resolve_lang "jsx", "javascript", "jsx -> javascript")
assert_eq(display_utils._resolve_lang "ts", "typescript", "ts -> typescript")
assert_eq(display_utils._resolve_lang "py", "python", "py -> python")
assert_eq(display_utils._resolve_lang "rb", "ruby", "rb -> ruby")
assert_eq(display_utils._resolve_lang "rs", "rust", "rs -> rust")
assert_eq(display_utils._resolve_lang "yml", "yaml", "yml -> yaml")
assert_eq(display_utils._resolve_lang "md", "markdown", "md -> markdown")
assert_eq(display_utils._resolve_lang "ps1", "powershell", "ps1 -> powershell")
end)

test("resolve_lang is case-insensitive", function()
assert_eq(display_utils._resolve_lang "SH", "bash", "SH -> bash")
assert_eq(display_utils._resolve_lang "Bash", "bash", "Bash -> bash (passthrough)")
end)

test("resolve_lang passes through names with no alias", function()
assert_eq(display_utils._resolve_lang "bash", "bash", "bash stays bash")
assert_eq(display_utils._resolve_lang "lua", "lua", "lua stays lua")
assert_eq(display_utils._resolve_lang "go", "go", "go stays go")
assert_eq(display_utils._resolve_lang "unknown_xyz", "unknown_xyz", "unknown stays unknown")
end)

test("resolve_lang honors vim.treesitter.language.register", function()
-- Simulate a user-registered alias and confirm it wins over the literal name.
vim.treesitter.language.register("markdown", "custom_md_lang")
assert_eq(
display_utils._resolve_lang "custom_md_lang",
"markdown",
"registered alias custom_md_lang -> markdown"
)
end)

print(string.format("display_utils_test: %d passed, %d failed", pass_count, fail_count))
if fail_count > 0 then os.exit(1) end
Loading