diff --git a/strix/interface/viewer/report_pdf.py b/strix/interface/viewer/report_pdf.py index 358bd424..91436095 100644 --- a/strix/interface/viewer/report_pdf.py +++ b/strix/interface/viewer/report_pdf.py @@ -417,14 +417,15 @@ def _inline_md(text: str) -> str: codes.append(match.group(1)) return f"\x00{len(codes) - 1}\x00" - # Emphasis content may not contain its own delimiter or a tag, so the three passes can - # never interleave into crossed markup (``..``), which reportlab rejects. - # A run of asterisks such as a masked secret (``******``) therefore stays literal. + # Bold spans must start and end with a non-delimiter, so a run of asterisks such as a masked + # secret (``******``) stays literal. Italic content is plain text or complete ``..`` + # spans, so the passes can nest (``**a *b* c**``, ``*a **b** c*``) but can never interleave + # into crossed markup (``..``), which reportlab rejects. seg = html.escape(re.sub(r"`([^`]+)`", _stash, text)) - seg = re.sub(r"\*\*\*([^*<>\n]+?)\*\*\*", r"\1", seg) - seg = re.sub(r"\*\*([^*<>\n]+?)\*\*", r"\1", seg) - seg = re.sub(r"__([^_<>\n]+?)__", r"\1", seg) - seg = re.sub(r"\*([^*<>\n]+?)\*", r"\1", seg) + seg = re.sub(r"\*\*\*(?=[^*])(.+?)(?<=[^*])\*\*\*", r"\1", seg) + seg = re.sub(r"\*\*(?=[^*])(.+?)(?<=[^*])\*\*", r"\1", seg) + seg = re.sub(r"__(?=[^_])(.+?)(?<=[^_])__", r"\1", seg) + seg = re.sub(r"\*((?:[^*<>\n]|[^<>*\n]*)+?)\*", r"\1", seg) def _restore(match: re.Match[str]) -> str: inner = html.escape(codes[int(match.group(1))]) diff --git a/tests/test_report_pdf.py b/tests/test_report_pdf.py index c25b486b..35b103b0 100644 --- a/tests/test_report_pdf.py +++ b/tests/test_report_pdf.py @@ -156,3 +156,9 @@ def test_generate_report_pdf_with_masked_secret_in_summary(tmp_path: Path) -> No def test_para_falls_back_to_plain_text_on_crossed_markup() -> None: para = _para("x & y", ParagraphStyle("t")) assert para.getPlainText() == "x & y" + + +def test_inline_md_keeps_balanced_nested_emphasis() -> None: + assert _inline_md("**bold with *italic* inside**") == "bold with italic inside" + assert _inline_md("*outer **bold** inner*") == "outer bold inner" + assert "******" in _inline_md("(observed as '******')") # a masked secret stays literal