feat(P03): render PPTX + F5(b) inline bold de-emphasize — all checks pass

---ci---
project: acdl
phase: 3
milestone: v1.30
status: verify
wave: 4
persona: backend-engineer
---/ci---
This commit is contained in:
CIAgent
2026-08-20 14:25:53 +00:00
parent b85da0471b
commit 0c4f5582f3
2 changed files with 34 additions and 13 deletions
Binary file not shown.
+34 -13
View File
@@ -310,9 +310,15 @@ def _strip_inline_emphasis(text: str) -> str:
return text
def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT):
"""Add inline runs to paragraph `p`, rendering **bold** as red strong,
`code` as monospace, *italic* as italic. Other text is plain."""
def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TEXT,
bold_color: RGBColor = None):
"""Add inline runs to paragraph `p`, rendering **bold** as strong,
`code` as monospace, *italic* as italic. Other text is plain.
F5(b): bold_color defaults to RED (legacy behavior) but can be
overridden to base_color to de-emphasize bold on slides with many
bold sections (avoids a red wall)."""
if bold_color is None:
bold_color = RED
# Tokenize on `**...**`, `*...*`, `` `...` ``
tokens = re.split(r"(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)", text)
for tok in tokens:
@@ -324,7 +330,7 @@ def _inline_runs(p, text: str, *, size: int = 18, base_color: RGBColor = BODY_TE
r.font.name = FONT_NAME
r.font.size = Pt(size)
r.font.bold = True
r.font.color.rgb = RED
r.font.color.rgb = bold_color
elif tok.startswith("`") and tok.endswith("`"):
r = p.add_run()
r.text = tok[1:-1]
@@ -588,7 +594,7 @@ def parse_slide(slide_src: str):
}
def render_title_slide(prs, slide_data, footer_text: str = ""):
def render_title_slide(prs, slide_data, deck_dir: Path, footer_text: str = ""):
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
_set_bg(slide, BLACK)
# red top bar
@@ -637,6 +643,11 @@ def render_title_slide(prs, slide_data, footer_text: str = ""):
r_b.font.italic = True
r_b.font.color.rgb = WHITE
cur_top += Inches(0.85)
elif kind == "image":
# image on title slide (D-246 diagrams on cover)
img_path = deck_dir / item[1]
_add_picture(slide, img_path, top=cur_top)
cur_top += Inches(3.6)
_add_footer(slide, footer_text, dark_bg=True)
@@ -646,11 +657,19 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "")
_add_title_bar(slide)
_add_title_text(slide, slide_data["title"], color=RED, size=28, top=0.25,
bold=True, height=0.7)
# F5(b): if a slide has >=4 lead blocks, de-emphasize them to
# black+bold (section labels) instead of red+bold (avoids a red
# wall on milestone-timeline slides like slide 6).
lead_count = sum(1 for item in slide_data["body"] if item[0] == "lead")
lead_color = BODY_TEXT if lead_count >= 4 else RED
# F5(b): if a slide has >=4 lead/bold-prefixed blocks, de-emphasize
# them to black+bold (section labels) instead of red+bold (avoids a
# red wall on milestone-timeline slides like slide 6). Count both
# explicit lead blocks (**...** on own line) and plain blocks that
# start with **bold** inline.
def _is_bold_block(item):
if item[0] == "lead":
return True
if item[0] == "plain" and item[1].lstrip().startswith("**"):
return True
return False
bold_count = sum(1 for item in slide_data["body"] if _is_bold_block(item))
lead_color = BODY_TEXT if bold_count >= 4 else RED
# F1: vertical balance — pre-compute the content height to center
# the block between the title (bottom ~0.95") and the footer
# (top ~7.12"). Content starts at 1.05" by default; if the total
@@ -675,7 +694,8 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "")
tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
_inline_runs(p, item[1], size=18, base_color=lead_color)
_inline_runs(p, item[1], size=18, base_color=lead_color,
bold_color=lead_color)
# make the whole lead bold
for r in p.runs:
r.font.bold = True
@@ -688,7 +708,8 @@ def render_content_slide(prs, slide_data, deck_dir: Path, footer_text: str = "")
tf.auto_size = MSO_AUTO_SIZE.SHAPE_TO_FIT_TEXT
p = tf.paragraphs[0]
p.alignment = PP_ALIGN.LEFT
_inline_runs(p, item[1], size=18, base_color=BODY_TEXT)
_inline_runs(p, item[1], size=18, base_color=BODY_TEXT,
bold_color=lead_color)
cur_top += Inches(0.45)
elif kind == "quote":
tb = slide.shapes.add_textbox(
@@ -783,7 +804,7 @@ def render_deck(md_path: Path, pptx_path: Path):
is_title = (idx == 0) or data["is_title_class"] or data["title_is_h1"]
# The appendix is a content slide (rendered normally)
if idx == 0 and (data["title_is_h1"] or data["is_title_class"]):
render_title_slide(prs, data, footer_text=footer_text)
render_title_slide(prs, data, deck_dir, footer_text=footer_text)
elif data["is_title_class"] and not data["title_is_h1"] and idx != 0:
# Marp _class: title on a non-H1 slide (e.g., appendix) — render as
# content but with a title-style bar. Keep it simple: content slide.