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
33 changes: 33 additions & 0 deletions lua/md-render/content_builder.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1116,6 +1116,38 @@ local function list_content_column(line)
return #ws + #marker + (#gap <= 4 and #gap or 1)
end

--- Expand tabs in each line's leading whitespace to spaces, with tab stops
--- every four columns as CommonMark specifies.
---
--- Everything that measures indentation counts spaces, so a list item
--- indented with a tab was one column deep instead of four: its continuation
--- lines hung from the wrong column, and the tab itself reached the buffer,
--- where 'tabstop' decided how wide it was. Fenced code is left alone, since
--- its tabs are content.
---@param lines string[]
---@return string[]
local function expand_leading_tabs(lines)
local result = {}
local in_code = false
for i, line in ipairs(lines) do
result[i] = line
local fence = line:match "^%s*```" or line:match "^%s*~~~"
if fence then
in_code = not in_code
elseif not in_code then
local ws = line:match "^[ \t]*"
if ws:find("\t", 1, true) then
local col = 0
for c in ws:gmatch "." do
col = c == "\t" and (col + 4 - col % 4) or (col + 1)
end
result[i] = string.rep(" ", col) .. line:sub(#ws + 1)
end
end
end
return result
end

--- Move the content of a block container to column 0 and report the indent
--- that was taken off each line.
---
Expand Down Expand Up @@ -1429,6 +1461,7 @@ function ContentBuilder:render_document(lines, opts)
-- container_indents keeps the indent per *original* line so the loop below
-- can restore it on output.
local container_indents
lines = expand_leading_tabs(lines)
lines, container_indents = strip_container_indent(lines)
lines, src_indices = preprocess_multiline_html(lines, src_indices)
lines, src_indices = join_paragraph_continuations(lines, src_indices, container_indents)
Expand Down
24 changes: 24 additions & 0 deletions tests/list_indent_test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,29 @@ do
)
end

-- Test 10: a tab in the indentation advances to the next multiple of four,
-- so a tab-indented item nests exactly as a four-space one does, wraps under
-- its own text, and no tab reaches the output
do
local long = string.rep("あ", 30)
local tabbed = render({ "- 外側", "\t- " .. long }, 24)
assert_eq(tabbed, render({ "- 外側", " - " .. long }, 24), "a tab should indent like four spaces")
assert_eq(
#tabbed[3]:match "^ *",
vim.api.nvim_strwidth(tabbed[2]:match "^ *[^ ]+ "),
"a tab-indented item should hang under its text"
)
assert_eq(table.concat(tabbed):find("\t", 1, true), nil, "no tab should reach the output")

assert_eq(
render { "- 外側", " \t- 内側" },
render { "- 外側", " - 内側" },
"a tab after spaces should stop at column four"
)

local out = render { "- 項目", "", " ```", " \tx = 1", " ```" }
assert_eq(out[2]:find("\t", 1, true) ~= nil, true, "a tab in fenced code is content and must stay")
end

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