diff --git a/lua/md-render/content_builder.lua b/lua/md-render/content_builder.lua index cebd7ca..ec580f2 100644 --- a/lua/md-render/content_builder.lua +++ b/lua/md-render/content_builder.lua @@ -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. --- @@ -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) diff --git a/tests/list_indent_test.lua b/tests/list_indent_test.lua index ec9e566..2019899 100644 --- a/tests/list_indent_test.lua +++ b/tests/list_indent_test.lua @@ -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