feat: 完成 DOCX 通用结构映射与严格收口

This commit is contained in:
SkyJourney
2026-07-31 15:19:34 +08:00
parent 6e40374200
commit f3847f3e4b
31 changed files with 3175 additions and 64 deletions
@@ -1,18 +1,29 @@
local map_path = os.getenv("MD_TO_PDF_DOCX_MEDIA_MAP")
local structure_plan_path =
os.getenv("MD_TO_PDF_DOCX_STRUCTURE_PLAN")
if map_path == nil or map_path == "" then
error("DOCX media map path is missing")
end
local map_file, open_error = io.open(map_path, "rb")
if map_file == nil then
error("DOCX media map cannot be opened: " .. tostring(open_error))
if structure_plan_path == nil or structure_plan_path == "" then
error("DOCX structure plan path is missing")
end
local map_content = map_file:read("*a")
map_file:close()
local function read_json(path, description)
local file, open_error = io.open(path, "rb")
if file == nil then
error(
description .. " cannot be opened: " .. tostring(open_error)
)
end
local content = file:read("*a")
file:close()
return pandoc.json.decode(content, false)
end
local media_map = pandoc.json.decode(map_content, false)
local media_map = read_json(map_path, "DOCX media map")
local structure_plan =
read_json(structure_plan_path, "DOCX structure plan")
local counters = {
image = 0,
mermaid = 0,
@@ -86,6 +97,69 @@ local function replace_code_block(block)
return pandoc.Para { image }
end
local function append_inlines(target, value)
target:extend(pandoc.Inlines(value))
end
local function structure_paragraph(block)
local inlines = pandoc.Inlines {}
for index, segment in ipairs(block.segments) do
if index > 1 then
if block.separator == "tab" then
inlines:insert(
pandoc.RawInline(
"openxml",
"<w:r><w:tab/></w:r>"
)
)
else
inlines:insert(pandoc.Space())
end
end
append_inlines(inlines, segment)
end
local paragraph = pandoc.Para(inlines)
if block.styleId == nil or block.styleId == "" then
return paragraph
end
return pandoc.Div(
{ paragraph },
pandoc.Attr("", {}, { ["custom-style"] = block.styleId })
)
end
local function marker_paragraph(marker)
return pandoc.Para { pandoc.Str(marker) }
end
local function structure_blocks(blocks)
local result = pandoc.Blocks {}
for _, block in ipairs(blocks) do
if block.kind == "paragraph" then
result:insert(structure_paragraph(block))
elseif block.kind == "container" then
local children = structure_blocks(block.blocks)
result:insert(marker_paragraph(block.startMarker))
result:extend(children)
result:insert(marker_paragraph(block.endMarker))
elseif block.kind == "section-break" then
result:insert(marker_paragraph(block.marker))
else
error("Unsupported DOCX structure block: " .. tostring(block.kind))
end
end
return result
end
local function suppress_first_body_heading(blocks)
for index, block in ipairs(blocks) do
if block.t == "Header" and block.level == 1 then
blocks:remove(index)
return
end
end
end
local function validate_counts()
for _, kind in ipairs({ "image", "mermaid", "echarts" }) do
if counters[kind] ~= #media_map[kind] then
@@ -104,6 +178,16 @@ return {
Image = replace_image,
CodeBlock = replace_code_block
}
if structure_plan.titlePolicy.metadataTitle == "suppress" then
transformed.meta.title = nil
end
if structure_plan.titlePolicy.firstBodyHeading == "suppress" then
suppress_first_body_heading(transformed.blocks)
end
local blocks = structure_blocks(structure_plan.prefix)
blocks:extend(transformed.blocks)
blocks:extend(structure_blocks(structure_plan.suffix))
transformed.blocks = blocks
validate_counts()
return transformed
end