diff --git a/src/chat_sdk/adapters/google_chat/cards.py b/src/chat_sdk/adapters/google_chat/cards.py index c8205cf..820d7df 100644 --- a/src/chat_sdk/adapters/google_chat/cards.py +++ b/src/chat_sdk/adapters/google_chat/cards.py @@ -10,6 +10,7 @@ from typing import Any, cast +from chat_sdk.adapters.google_chat.format_converter import GoogleChatFormatConverter from chat_sdk.cards import ( CardChild, CardElement, @@ -18,10 +19,18 @@ ) from chat_sdk.shared import card_to_fallback_text as shared_card_to_fallback_text from chat_sdk.shared import create_emoji_converter +from chat_sdk.shared.base_format_converter import parse_markdown # Convert emoji placeholders in text to GChat format (Unicode). convert_emoji = create_emoji_converter("gchat") +_gchat_converter = GoogleChatFormatConverter() + + +def _render_markdown_as_gchat(text: str) -> str: + """Parse standard markdown and render as Google Chat formatted text.""" + return _gchat_converter.from_ast(parse_markdown(text)) + def card_to_google_card( card: CardElement, @@ -148,25 +157,13 @@ def _convert_child_to_widgets( return [] -def _markdown_to_gchat(text: str) -> str: - """Convert standard Markdown formatting to Google Chat formatting. - - **bold** -> *bold* - """ - import re - - return re.sub(r"\*\*(.+?)\*\*", r"*\1*", text) - - def _convert_text_to_widget(element: dict[str, Any]) -> dict[str, Any]: """Convert a text element to a widget.""" - text = _markdown_to_gchat(convert_emoji(element.get("content", ""))) + text = _render_markdown_as_gchat(convert_emoji(element.get("content", ""))) - style = element.get("style") - if style == "bold": + if element.get("style") == "bold": text = f"*{text}*" - elif style == "muted": - # GChat doesn't have muted, use regular text + elif element.get("style") == "muted": text = convert_emoji(element.get("content", "")) return {"textParagraph": {"text": text}} @@ -302,8 +299,8 @@ def _convert_fields_to_widgets( return [ { "decoratedText": { - "topLabel": _markdown_to_gchat(convert_emoji(field.get("label", ""))), - "text": _markdown_to_gchat(convert_emoji(field.get("value", ""))), + "topLabel": _render_markdown_as_gchat(convert_emoji(field.get("label", ""))), + "text": _render_markdown_as_gchat(convert_emoji(field.get("value", ""))), }, } for field in element.get("children", []) diff --git a/tests/test_gchat_cards.py b/tests/test_gchat_cards.py index e1a4dc7..a2cd11d 100644 --- a/tests/test_gchat_cards.py +++ b/tests/test_gchat_cards.py @@ -304,11 +304,13 @@ def test_converts_multiple_bold_segments(self): widgets = gchat_card["card"]["sections"][0]["widgets"] assert widgets[0]["textParagraph"]["text"] == "*Project*: my-app, *Status*: active" - def test_preserves_single_asterisk(self): - card = Card(children=[CardText("Already *bold* in GChat format")]) + def test_single_asterisk_markdown_italic_becomes_gchat_italic(self): + # *text* is markdown italic; the full converter renders it as _text_ (GChat italic), + # not *text* (GChat bold). Card content is always treated as markdown. + card = Card(children=[CardText("The *italic* word")]) gchat_card = card_to_google_card(card) widgets = gchat_card["card"]["sections"][0]["widgets"] - assert widgets[0]["textParagraph"]["text"] == "Already *bold* in GChat format" + assert widgets[0]["textParagraph"]["text"] == "The _italic_ word" def test_converts_bold_in_field_values(self): card = Card(children=[Fields([Field(label="Status", value="**Active**")])]) @@ -330,6 +332,21 @@ def test_plain_text_no_change(self): widgets = gchat_card["card"]["sections"][0]["widgets"] assert widgets[0]["textParagraph"]["text"] == "Plain text" + def test_markdown_bullet_list_with_bold_labels_renders_without_raw_markers(self): + # Agent responses use markdown lists with bold labels. The naive **→* regex + # left list markers and asterisks literal in textParagraph, rendering as "* *Jira:*". + card = Card(children=[CardText("- **Jira:** create issues\n- **Zendesk:** manage tickets")]) + text = card_to_google_card(card)["card"]["sections"][0]["widgets"][0]["textParagraph"]["text"] + assert "• " in text, "markdown list items must render as • bullets, not raw '- item'" + assert "**" not in text, "markdown **bold** must be converted to GChat *bold*" + + def test_muted_style_skips_markdown_conversion(self): + # Upstream TS explicitly reverts muted elements to plain emoji-only text. + # The full markdown converter must not run on muted content. + card = Card(children=[CardText("**bold text**", style="muted")]) + widgets = card_to_google_card(card)["card"]["sections"][0]["widgets"] + assert widgets[0]["textParagraph"]["text"] == "**bold text**" + # --------------------------------------------------------------------------- # CardLink