refactor(rendering): 块收集器与 inline 切分下沉 common——golden 零 diff 验收
审查 P1:docx_brief 与 docx_manuscript 的 render_md_block 各手写一套块级 扫描,同一 md 走两条路径最易漂移。按「分派语义留 profile、边界收集归 common」 拆分: - common 新增 gather_fence(闭合判据单一事实源)/ gather_table / gather_blockquote / gather_paragraph(软换行并合,is_list 谓词注入) ——此前两份逐字拷贝的循环全部换成共享调用 - brief 的 add_inline_rich 手写 INLINE_RE 循环换 common.parse_inline 迭代 (与 manuscript 同一切分事实源);brief 差异收敛到 plain 段处理 (引文上标 + 化学式)一处 - 刻意不动:分派顺序与 brief 状态机(in_refs/expect_meta/in_tldr 会改变块 边界语义,是 profile 的产品行为不是重复)、各 profile 样式 验收:golden 基线(上一提交)逐字节零 diff;319 测试全过。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
035aec44ac
commit
21e7dad7f1
|
|
@ -173,6 +173,66 @@ def is_separator_row(cells: list[str]) -> bool:
|
|||
return all(re.match(r"^[-:\s]+$", c) for c in cells if c != "")
|
||||
|
||||
|
||||
# ───────────────────────── 块收集器(docx_brief / docx_manuscript 共用)─────────────────────────
|
||||
# render_md_block 的分派顺序与状态机(brief 的 in_refs/expect_meta/in_tldr)是各
|
||||
# profile 的语义,留在各自文件;这里只收**块边界的收集逻辑**——fence 闭合判据、
|
||||
# 表格聚块、blockquote 并合、段落软换行并合,这些此前两份逐字拷贝、最易漂移。
|
||||
|
||||
def gather_fence(lines: list[str], i: int, fence: str) -> tuple[list[str], int]:
|
||||
"""FENCE_RE 命中开栅行后调用(i 指向开栅行,fence=开栅串)。收集代码行至闭合,
|
||||
返回 (code_lines, next_i)。闭合判据:同字符(` vs ~)且长度 >= 开栅;无闭合收到文末。"""
|
||||
code: list[str] = []
|
||||
i += 1
|
||||
n = len(lines)
|
||||
while i < n:
|
||||
m_close = FENCE_RE.match(lines[i])
|
||||
if m_close and m_close.group(1)[0] == fence[0] and len(m_close.group(1)) >= len(fence):
|
||||
i += 1
|
||||
break
|
||||
code.append(lines[i])
|
||||
i += 1
|
||||
return code, i
|
||||
|
||||
|
||||
def gather_table(lines: list[str], i: int) -> tuple[list[str], int]:
|
||||
"""从 i 起收集连续表格行,返回 (table_lines, next_i)。"""
|
||||
block: list[str] = []
|
||||
n = len(lines)
|
||||
while i < n and is_table_line(lines[i]):
|
||||
block.append(lines[i])
|
||||
i += 1
|
||||
return block, i
|
||||
|
||||
|
||||
def gather_blockquote(lines: list[str], i: int) -> tuple[list[str], int]:
|
||||
"""从 i 起收集连续 `>` 行(剥掉引导符,逐行 strip),返回 (texts, next_i)。"""
|
||||
texts = [BLOCKQUOTE_RE.sub("", lines[i].rstrip()).strip()]
|
||||
i += 1
|
||||
n = len(lines)
|
||||
while i < n and is_blockquote(lines[i]):
|
||||
texts.append(BLOCKQUOTE_RE.sub("", lines[i]).strip())
|
||||
i += 1
|
||||
return texts, i
|
||||
|
||||
|
||||
def gather_paragraph(lines: list[str], i: int, is_list) -> tuple[str, int]:
|
||||
"""普通段落软换行并合:从 i 起吸后续行直到空行/标题/引用/表格/列表/HR,
|
||||
返回 (合并后的段落文本, next_i)。is_list 是 profile 各自的列表判定谓词。"""
|
||||
buf = [lines[i].rstrip().strip()]
|
||||
j = i + 1
|
||||
n = len(lines)
|
||||
while j < n:
|
||||
nxt = lines[j].rstrip()
|
||||
if not nxt.strip():
|
||||
break
|
||||
if (is_heading(nxt) or is_blockquote(nxt) or is_table_line(nxt)
|
||||
or is_list(nxt) or is_hr(nxt)):
|
||||
break
|
||||
buf.append(nxt.strip())
|
||||
j += 1
|
||||
return " ".join(buf), j
|
||||
|
||||
|
||||
# ───────────────────────── 图片 ─────────────────────────
|
||||
|
||||
MAX_IMG_WIDTH = Cm(15)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ from .common import (
|
|||
set_style_fonts as _set_style_fonts,
|
||||
set_subscript as _set_subscript,
|
||||
CHEM_RE as _CHEM_RE,
|
||||
INLINE_RE as _INLINE_RE,
|
||||
parse_inline as _parse_inline,
|
||||
HEADING_RE as _HEADING_RE,
|
||||
TABLE_LINE_RE as _TABLE_LINE_RE,
|
||||
BLOCKQUOTE_RE as _BLOCKQUOTE_RE,
|
||||
|
|
@ -35,6 +35,10 @@ from .common import (
|
|||
resolve_image_path as _resolve_image_path,
|
||||
MAX_IMG_WIDTH as _MAX_IMG_WIDTH,
|
||||
collect_md_files as _collect_md_files,
|
||||
gather_fence as _gather_fence,
|
||||
gather_table as _gather_table,
|
||||
gather_blockquote as _gather_blockquote,
|
||||
gather_paragraph as _gather_paragraph,
|
||||
)
|
||||
|
||||
# ───────────────────────── 主题色 ─────────────────────────
|
||||
|
|
@ -243,29 +247,26 @@ def _set_subscript_super(run) -> None:
|
|||
|
||||
def add_inline_rich(paragraph, text: str, *, size_pt=12.0, cn_font="宋体",
|
||||
make_citations=True) -> None:
|
||||
pos = 0
|
||||
for m in _INLINE_RE.finditer(text):
|
||||
if m.start() > pos:
|
||||
_emit_plain_with_cites(paragraph, text[pos:m.start()], size_pt=size_pt,
|
||||
"""行内 markdown → runs:切分走 common.parse_inline(与 manuscript 同一事实源);
|
||||
brief 的差异只在 plain 段处理(引文上标超链 + 化学式下标,见 _emit_plain_with_cites)。"""
|
||||
for style, seg in _parse_inline(text):
|
||||
if style == "plain":
|
||||
_emit_plain_with_cites(paragraph, seg, size_pt=size_pt,
|
||||
cn_font=cn_font, make_citations=make_citations)
|
||||
if m.group("bold"):
|
||||
run = paragraph.add_run(m.group("bold_t"))
|
||||
elif style == "bold":
|
||||
run = paragraph.add_run(seg)
|
||||
run.bold = True
|
||||
run.font.size = Pt(size_pt)
|
||||
_set_run_fonts(run, cn_font=cn_font)
|
||||
elif m.group("italic"):
|
||||
run = paragraph.add_run(m.group("italic_t"))
|
||||
elif style == "italic":
|
||||
run = paragraph.add_run(seg)
|
||||
run.italic = True
|
||||
run.font.size = Pt(size_pt)
|
||||
_set_run_fonts(run, cn_font=cn_font)
|
||||
elif m.group("code"):
|
||||
run = paragraph.add_run(m.group("code_t"))
|
||||
elif style == "code":
|
||||
run = paragraph.add_run(seg)
|
||||
run.font.size = Pt(size_pt)
|
||||
_set_run_fonts(run, cn_font=cn_font, en_font="Consolas")
|
||||
pos = m.end()
|
||||
if pos < len(text):
|
||||
_emit_plain_with_cites(paragraph, text[pos:], size_pt=size_pt,
|
||||
cn_font=cn_font, make_citations=make_citations)
|
||||
|
||||
|
||||
# ───────────────────────── 标题 / 段落 ─────────────────────────
|
||||
|
|
@ -526,16 +527,7 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
|
||||
m_fence = _FENCE_RE.match(line)
|
||||
if m_fence:
|
||||
fence = m_fence.group(1)
|
||||
code = []
|
||||
i += 1
|
||||
while i < n:
|
||||
mc = _FENCE_RE.match(lines[i])
|
||||
if mc and mc.group(1)[0] == fence[0] and len(mc.group(1)) >= len(fence):
|
||||
i += 1
|
||||
break
|
||||
code.append(lines[i])
|
||||
i += 1
|
||||
code, i = _gather_fence(lines, i, m_fence.group(1))
|
||||
for ln in code:
|
||||
p = doc.add_paragraph()
|
||||
p.paragraph_format.first_line_indent = None
|
||||
|
|
@ -546,10 +538,7 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
continue
|
||||
|
||||
if _TABLE_LINE_RE.match(line):
|
||||
block = []
|
||||
while i < n and _TABLE_LINE_RE.match(lines[i]):
|
||||
block.append(lines[i])
|
||||
i += 1
|
||||
block, i = _gather_table(lines, i)
|
||||
render_table(doc, block, color)
|
||||
continue
|
||||
|
||||
|
|
@ -570,12 +559,8 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
|
||||
if _BLOCKQUOTE_RE.match(line):
|
||||
# 引用块:并合连续 > 行,做浅红 callout(说明 / 取舍纪律等)
|
||||
buf = [_BLOCKQUOTE_RE.sub("", line).strip()]
|
||||
i += 1
|
||||
while i < n and _BLOCKQUOTE_RE.match(lines[i]):
|
||||
buf.append(_BLOCKQUOTE_RE.sub("", lines[i]).strip())
|
||||
i += 1
|
||||
add_callout(doc, " ".join(buf), TLDR_FILL, color)
|
||||
texts, i = _gather_blockquote(lines, i)
|
||||
add_callout(doc, " ".join(texts), TLDR_FILL, color)
|
||||
continue
|
||||
|
||||
# 参考文献条目
|
||||
|
|
@ -609,17 +594,8 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
continue
|
||||
|
||||
# 普通段落:并合软换行
|
||||
buf = [line.strip()]
|
||||
j = i + 1
|
||||
while j < n:
|
||||
nxt = lines[j].rstrip()
|
||||
if not nxt.strip() or _HEADING_RE.match(nxt) or _BLOCKQUOTE_RE.match(nxt) \
|
||||
or _TABLE_LINE_RE.match(nxt) or is_list_item(nxt) or _HR_RE.match(nxt):
|
||||
break
|
||||
buf.append(nxt.strip())
|
||||
j += 1
|
||||
add_body_paragraph(doc, " ".join(buf), indent=True)
|
||||
i = j
|
||||
text, i = _gather_paragraph(lines, i, is_list_item)
|
||||
add_body_paragraph(doc, text, indent=True)
|
||||
|
||||
|
||||
# ───────────────────────── 入口 ─────────────────────────
|
||||
|
|
|
|||
|
|
@ -349,17 +349,8 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
|
||||
m_fence = common.FENCE_RE.match(line)
|
||||
if m_fence:
|
||||
fence = m_fence.group(1)
|
||||
lang = m_fence.group(2) or ""
|
||||
code: list[str] = []
|
||||
i += 1
|
||||
while i < n:
|
||||
m_close = common.FENCE_RE.match(lines[i])
|
||||
if m_close and m_close.group(1)[0] == fence[0] and len(m_close.group(1)) >= len(fence):
|
||||
i += 1
|
||||
break
|
||||
code.append(lines[i])
|
||||
i += 1
|
||||
code, i = common.gather_fence(lines, i, m_fence.group(1))
|
||||
|
||||
if lang.lower() == "mermaid":
|
||||
source = "\n".join(code)
|
||||
|
|
@ -375,10 +366,7 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
continue
|
||||
|
||||
if common.is_table_line(line):
|
||||
block: list[str] = []
|
||||
while i < n and common.is_table_line(lines[i]):
|
||||
block.append(lines[i])
|
||||
i += 1
|
||||
block, i = common.gather_table(lines, i)
|
||||
render_table(doc, block)
|
||||
continue
|
||||
|
||||
|
|
@ -398,19 +386,8 @@ def render_md_block(doc: Document, md_text: str, ctx: dict) -> None:
|
|||
i += 1
|
||||
continue
|
||||
|
||||
buf = [line.strip()]
|
||||
j = i + 1
|
||||
while j < n:
|
||||
nxt = lines[j].rstrip()
|
||||
if not nxt.strip():
|
||||
break
|
||||
if (common.is_heading(nxt) or common.is_blockquote(nxt) or common.is_table_line(nxt)
|
||||
or is_list_item(nxt, prof) or common.is_hr(nxt)):
|
||||
break
|
||||
buf.append(nxt.strip())
|
||||
j += 1
|
||||
add_body_paragraph(doc, " ".join(buf), indent=True)
|
||||
i = j
|
||||
text, i = common.gather_paragraph(lines, i, lambda ln: is_list_item(ln, prof))
|
||||
add_body_paragraph(doc, text, indent=True)
|
||||
|
||||
|
||||
# ───────────────────────── 入口 ─────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in New Issue