fix(export): 修复列表项行内语义丢失与混合嵌套顺序重排
- pdf.py `_block_list_item` 改为按 AST 顺序逐段输出:正文暂存为行内标记文本, 遇嵌套列表先 flush 再递归,之后继续后续正文,保持「父段—子列表—后续段」原始顺序 - pdf.py/docx.py 列表项直接行内节点改走 `_render_inline_node`,保留加粗/链接语义, 不再只渲染 children 而丢掉格式(PDF 链接以 /URI 注解保留,DOCX 写入 w:hyperlink) - docx.py `_render_inline_node` 增加 bold/italic 默认值,便于列表项直接调用 - 契约文档 StaticRenderer 状态「计划新增」→「已实现」 - 新增回归测试:混合嵌套顺序、PDF/DOCX 列表项行内语义 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -176,12 +176,13 @@ class DocxExporter:
|
||||
if first:
|
||||
self._add_run(p, marker)
|
||||
first = False
|
||||
if child.children:
|
||||
# 段落或行内容器(strong/link 等):渲染其行内子节点
|
||||
if child.type == "paragraph":
|
||||
# 块级容器:展开其行内子节点
|
||||
self._render_inline(p, child.children, warnings)
|
||||
else:
|
||||
# 直接行内叶子节点(text 等):拼进段落,不能交给块级渲染器(会丢弃正文)
|
||||
self._add_run(p, child.text or "")
|
||||
# 直接行内节点(text/strong/emphasis/link/codespan 等):走行内渲染保留
|
||||
# 语义(加粗/斜体/超链接),不能只渲染其 children 而丢掉格式。
|
||||
self._render_inline_node(p, child, warnings)
|
||||
if color is not None:
|
||||
for run in p.runs:
|
||||
run.font.color.rgb = color
|
||||
@@ -259,7 +260,12 @@ class DocxExporter:
|
||||
self._render_inline_node(paragraph, child, warnings, bold, italic)
|
||||
|
||||
def _render_inline_node(
|
||||
self, paragraph, node: DocumentNode, warnings: list[str], bold: bool, italic: bool
|
||||
self,
|
||||
paragraph,
|
||||
node: DocumentNode,
|
||||
warnings: list[str],
|
||||
bold: bool = False,
|
||||
italic: bool = False,
|
||||
) -> None:
|
||||
t = node.type
|
||||
if t == "text":
|
||||
|
||||
@@ -220,22 +220,32 @@ class PdfExporter:
|
||||
if color:
|
||||
style_kwargs["textColor"] = color
|
||||
style = ParagraphStyle(f"pdf-li-{indent}-{color or 'normal'}", **style_kwargs)
|
||||
# 先收集父级正文、后处理嵌套列表:保证「父级文字在前、子列表在后」的阅读顺序,
|
||||
# 而不是在循环里遇到嵌套列表就立刻递归输出(那会把子列表排到父级前面)。
|
||||
# 按 AST 顺序逐段输出:正文暂存为行内标记文本,遇到嵌套列表先 flush 再递归、
|
||||
# 之后继续后续正文,保持「父段—子列表—后续段」的原始顺序(而不是把所有正文
|
||||
# 都挤到子列表之前)。直接行内节点(text/strong/link 等)走 _render_inline_node,
|
||||
# 保留加粗/链接等语义,不能只渲染其 children 而丢掉格式。
|
||||
parts: list[str] = []
|
||||
nested: list[DocumentNode] = []
|
||||
first = True
|
||||
|
||||
def flush() -> None:
|
||||
nonlocal first
|
||||
text = "<br/>".join(parts)
|
||||
if first:
|
||||
text = marker + text
|
||||
first = False
|
||||
if text:
|
||||
story.append(Paragraph(text, style))
|
||||
parts.clear()
|
||||
|
||||
for child in item.children:
|
||||
if child.type == "list":
|
||||
nested.append(child)
|
||||
flush()
|
||||
self._block_list(child, story, warnings, indent + 14, color)
|
||||
elif child.type == "paragraph":
|
||||
parts.append(self._render_inline(child.children, warnings))
|
||||
elif child.children:
|
||||
parts.append(self._render_inline(child.children, warnings))
|
||||
else:
|
||||
parts.append(_html.escape(child.text))
|
||||
story.append(Paragraph(marker + "<br/>".join(parts), style))
|
||||
for child_list in nested:
|
||||
self._block_list(child_list, story, warnings, indent + 14, color)
|
||||
parts.append(self._render_inline_node(child, warnings))
|
||||
flush()
|
||||
|
||||
def _block_table(self, node: DocumentNode, story: list, warnings: list[str]) -> None:
|
||||
rows = node.children
|
||||
|
||||
Reference in New Issue
Block a user