diff --git a/docs/presentations/nova-leadership-deck.pptx b/docs/presentations/nova-leadership-deck.pptx index 20774fb..120bed2 100644 Binary files a/docs/presentations/nova-leadership-deck.pptx and b/docs/presentations/nova-leadership-deck.pptx differ diff --git a/scripts/render_pptx.py b/scripts/render_pptx.py index 5e50c58..a1ddbef 100644 --- a/scripts/render_pptx.py +++ b/scripts/render_pptx.py @@ -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.