Skip to content

Commit c71442a

Browse files
fix(gchat): use full markdown parser for card text rendering (#92)
* fix(gchat): use full markdown parser for card text rendering The _markdown_to_gchat helper only converted **bold** to *bold* via a naive regex, leaving markdown bullet lists and italic syntax as raw literals in textParagraph widgets. Google Chat does not render markdown list syntax (- item, * item), so agent responses with bullet lists appeared with literal * characters instead of formatted bullets. Replace _markdown_to_gchat with _render_markdown_as_gchat which round-trips content through parse_markdown + GoogleChatFormatConverter, the same path used for plain text messages. This correctly converts: - markdown lists -> bullet characters - **bold** -> *bold* (GChat bold) - *italic* -> _italic_ (GChat italic) Apply the fix to both _convert_text_to_widget and _convert_fields_to_widgets. Update the test that asserted old pass-through behavior for *italic* to reflect the correct output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(gchat): restore muted style handling dropped in markdown converter refactor Upstream TS convertTextToWidget explicitly reverts muted elements to plain emoji-only text, bypassing markdown conversion. The AST-converter refactor dropped the elif branch, causing muted card text to go through parse_markdown + GoogleChatFormatConverter.from_ast instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a3133c7 commit c71442a

2 files changed

Lines changed: 34 additions & 20 deletions

File tree

src/chat_sdk/adapters/google_chat/cards.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from typing import Any, cast
1212

13+
from chat_sdk.adapters.google_chat.format_converter import GoogleChatFormatConverter
1314
from chat_sdk.cards import (
1415
CardChild,
1516
CardElement,
@@ -18,10 +19,18 @@
1819
)
1920
from chat_sdk.shared import card_to_fallback_text as shared_card_to_fallback_text
2021
from chat_sdk.shared import create_emoji_converter
22+
from chat_sdk.shared.base_format_converter import parse_markdown
2123

2224
# Convert emoji placeholders in text to GChat format (Unicode).
2325
convert_emoji = create_emoji_converter("gchat")
2426

27+
_gchat_converter = GoogleChatFormatConverter()
28+
29+
30+
def _render_markdown_as_gchat(text: str) -> str:
31+
"""Parse standard markdown and render as Google Chat formatted text."""
32+
return _gchat_converter.from_ast(parse_markdown(text))
33+
2534

2635
def card_to_google_card(
2736
card: CardElement,
@@ -148,25 +157,13 @@ def _convert_child_to_widgets(
148157
return []
149158

150159

151-
def _markdown_to_gchat(text: str) -> str:
152-
"""Convert standard Markdown formatting to Google Chat formatting.
153-
154-
**bold** -> *bold*
155-
"""
156-
import re
157-
158-
return re.sub(r"\*\*(.+?)\*\*", r"*\1*", text)
159-
160-
161160
def _convert_text_to_widget(element: dict[str, Any]) -> dict[str, Any]:
162161
"""Convert a text element to a widget."""
163-
text = _markdown_to_gchat(convert_emoji(element.get("content", "")))
162+
text = _render_markdown_as_gchat(convert_emoji(element.get("content", "")))
164163

165-
style = element.get("style")
166-
if style == "bold":
164+
if element.get("style") == "bold":
167165
text = f"*{text}*"
168-
elif style == "muted":
169-
# GChat doesn't have muted, use regular text
166+
elif element.get("style") == "muted":
170167
text = convert_emoji(element.get("content", ""))
171168

172169
return {"textParagraph": {"text": text}}
@@ -302,8 +299,8 @@ def _convert_fields_to_widgets(
302299
return [
303300
{
304301
"decoratedText": {
305-
"topLabel": _markdown_to_gchat(convert_emoji(field.get("label", ""))),
306-
"text": _markdown_to_gchat(convert_emoji(field.get("value", ""))),
302+
"topLabel": _render_markdown_as_gchat(convert_emoji(field.get("label", ""))),
303+
"text": _render_markdown_as_gchat(convert_emoji(field.get("value", ""))),
307304
},
308305
}
309306
for field in element.get("children", [])

tests/test_gchat_cards.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,13 @@ def test_converts_multiple_bold_segments(self):
304304
widgets = gchat_card["card"]["sections"][0]["widgets"]
305305
assert widgets[0]["textParagraph"]["text"] == "*Project*: my-app, *Status*: active"
306306

307-
def test_preserves_single_asterisk(self):
308-
card = Card(children=[CardText("Already *bold* in GChat format")])
307+
def test_single_asterisk_markdown_italic_becomes_gchat_italic(self):
308+
# *text* is markdown italic; the full converter renders it as _text_ (GChat italic),
309+
# not *text* (GChat bold). Card content is always treated as markdown.
310+
card = Card(children=[CardText("The *italic* word")])
309311
gchat_card = card_to_google_card(card)
310312
widgets = gchat_card["card"]["sections"][0]["widgets"]
311-
assert widgets[0]["textParagraph"]["text"] == "Already *bold* in GChat format"
313+
assert widgets[0]["textParagraph"]["text"] == "The _italic_ word"
312314

313315
def test_converts_bold_in_field_values(self):
314316
card = Card(children=[Fields([Field(label="Status", value="**Active**")])])
@@ -330,6 +332,21 @@ def test_plain_text_no_change(self):
330332
widgets = gchat_card["card"]["sections"][0]["widgets"]
331333
assert widgets[0]["textParagraph"]["text"] == "Plain text"
332334

335+
def test_markdown_bullet_list_with_bold_labels_renders_without_raw_markers(self):
336+
# Agent responses use markdown lists with bold labels. The naive **→* regex
337+
# left list markers and asterisks literal in textParagraph, rendering as "* *Jira:*".
338+
card = Card(children=[CardText("- **Jira:** create issues\n- **Zendesk:** manage tickets")])
339+
text = card_to_google_card(card)["card"]["sections"][0]["widgets"][0]["textParagraph"]["text"]
340+
assert "• " in text, "markdown list items must render as • bullets, not raw '- item'"
341+
assert "**" not in text, "markdown **bold** must be converted to GChat *bold*"
342+
343+
def test_muted_style_skips_markdown_conversion(self):
344+
# Upstream TS explicitly reverts muted elements to plain emoji-only text.
345+
# The full markdown converter must not run on muted content.
346+
card = Card(children=[CardText("**bold text**", style="muted")])
347+
widgets = card_to_google_card(card)["card"]["sections"][0]["widgets"]
348+
assert widgets[0]["textParagraph"]["text"] == "**bold text**"
349+
333350

334351
# ---------------------------------------------------------------------------
335352
# CardLink

0 commit comments

Comments
 (0)