diff --git a/nanobot/channels/feishu/runtime.py b/nanobot/channels/feishu/runtime.py index dcd8c9e9..02473e66 100644 --- a/nanobot/channels/feishu/runtime.py +++ b/nanobot/channels/feishu/runtime.py @@ -1388,18 +1388,30 @@ class FeishuChannel(BaseChannel): def _build_card_elements(self, content: str) -> list[dict]: """Split content into div/markdown + table elements for Feishu card.""" + protected = content + code_blocks: list[str] = [] + for m in self._CODE_BLOCK_RE.finditer(content): + code_blocks.append(m.group(1)) + protected = protected.replace(m.group(1), f"\x00CODE{len(code_blocks) - 1}\x00", 1) + elements, last_end = [], 0 - for m in self._TABLE_RE.finditer(content): - before = content[last_end : m.start()] + for m in self._TABLE_RE.finditer(protected): + before = protected[last_end : m.start()] if before.strip(): elements.extend(self._split_headings(before)) elements.append( self._parse_md_table(m.group(1)) or {"tag": "markdown", "content": m.group(1)} ) last_end = m.end() - remaining = content[last_end:] + remaining = protected[last_end:] if remaining.strip(): elements.extend(self._split_headings(remaining)) + + for i, cb in enumerate(code_blocks): + for el in elements: + if el.get("tag") == "markdown": + el["content"] = el["content"].replace(f"\x00CODE{i}\x00", cb) + return elements or [{"tag": "markdown", "content": content}] @staticmethod diff --git a/nanobot/channels/feishu/tests/test_feishu_markdown_rendering.py b/nanobot/channels/feishu/tests/test_feishu_markdown_rendering.py index bf165a81..5c971ab7 100644 --- a/nanobot/channels/feishu/tests/test_feishu_markdown_rendering.py +++ b/nanobot/channels/feishu/tests/test_feishu_markdown_rendering.py @@ -1,7 +1,6 @@ # Check optional Feishu dependencies before running tests try: - from nanobot.channels import feishu - FEISHU_AVAILABLE = getattr(feishu, "FEISHU_AVAILABLE", False) + from nanobot.channels.feishu.runtime import FEISHU_AVAILABLE except ImportError: FEISHU_AVAILABLE = False @@ -66,3 +65,23 @@ def test_split_headings_keeps_markdown_body_and_code_blocks_intact() -> None: assert elements[1]["tag"] == "markdown" assert "Body with **bold** text." in elements[1]["content"] assert "```python\nprint('hi')\n```" in elements[1]["content"] + + +def test_build_card_elements_keeps_fenced_markdown_tables_intact() -> None: + channel = FeishuChannel.__new__(FeishuChannel) + text = "Before\n\n```\n| a | b |\n| - | - |\n| 1 | 2 |\n```\n\nAfter" + + elements = channel._build_card_elements(text) + + assert all(el.get("tag") != "table" for el in elements) + joined = "\n".join(el["content"] for el in elements if el.get("tag") == "markdown") + assert "```\n| a | b |\n| - | - |\n| 1 | 2 |\n```" in joined + + +def test_build_card_elements_still_parses_unfenced_markdown_tables() -> None: + channel = FeishuChannel.__new__(FeishuChannel) + text = "Before\n\n| a | b |\n| - | - |\n| 1 | 2 |\n\nAfter" + + elements = channel._build_card_elements(text) + + assert any(el.get("tag") == "table" for el in elements)