From bc1b0b7ec2ecd68638899aa286666ec310af56b7 Mon Sep 17 00:00:00 2001 From: Adrian <1917353+apetcu@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:01:40 +0300 Subject: [PATCH] fix(viewer): keep balanced nested emphasis working Restricting emphasis content to non-delimiters made `**bold with *italic* inside**` render its outer delimiters literally. Bold spans now only require a non-delimiter at each end (so `******` still stays literal) and italic content may contain complete .. spans, which keeps nesting working while still ruling out crossed tags. --- strix/interface/viewer/report_pdf.py | 15 ++++++++------- tests/test_report_pdf.py | 6 ++++++ 2 files changed, 14 insertions(+), 7 deletions(-) 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