diff --git a/rendering/common.py b/rendering/common.py index 95f893c..bdaceab 100644 --- a/rendering/common.py +++ b/rendering/common.py @@ -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) diff --git a/rendering/docx_brief.py b/rendering/docx_brief.py index 6c3d04d..e09fa5a 100644 --- a/rendering/docx_brief.py +++ b/rendering/docx_brief.py @@ -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) # ───────────────────────── 入口 ───────────────────────── diff --git a/rendering/docx_manuscript.py b/rendering/docx_manuscript.py index 11e3db7..8a51426 100644 --- a/rendering/docx_manuscript.py +++ b/rendering/docx_manuscript.py @@ -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) # ───────────────────────── 入口 ─────────────────────────