This is an automated email from the ASF dual-hosted git repository. hsluoyz pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/casbin-gateway.git
commit f401d4d729e4898bf5b14b109508378855da8030 Author: Yang Luo <[email protected]> AuthorDate: Sun Sep 6 18:07:56 2026 +0800 feat(channels): strip Markdown for WeChat, which shows none --- imbridge/{telegram_html.go => markdown.go} | 120 ++++++++++++++++++++--------- imbridge/weixin.go | 4 +- 2 files changed, 86 insertions(+), 38 deletions(-) diff --git a/imbridge/telegram_html.go b/imbridge/markdown.go similarity index 62% rename from imbridge/telegram_html.go rename to imbridge/markdown.go index a2bdf45..5209778 100644 --- a/imbridge/telegram_html.go +++ b/imbridge/markdown.go @@ -18,12 +18,24 @@ import ( "strings" ) -// telegramHtml renders the Markdown an agent writes into the small HTML subset -// Telegram parses. HTML is chosen over MarkdownV2 because only three characters -// ever need escaping and the tags are written here rather than by the agent, so -// a marker left open halfway through a streamed answer comes out as literal text -// instead of a refused message. -func telegramHtml(text string) string { +// markdownStyle is what one platform makes of the Markdown an agent writes. The +// reading of it is the same everywhere; only what comes out differs, so a +// platform with rich text and one without share a parser. +type markdownStyle struct { + escape func(string) string + bold func(string) string + italic func(string) string + strike func(string) string + code func(string) string + block func(language, body string) string + quote func(lines []string) string + link func(label, url string) string + rule string +} + +// renderMarkdown walks the block shapes: a fenced block, a run of quoted lines, +// or a single line of its own. +func renderMarkdown(text string, style markdownStyle) string { out := &strings.Builder{} lines := strings.Split(text, "\n") @@ -35,25 +47,72 @@ func telegramHtml(text string) string { for index++; index < len(lines) && !isFence(lines[index]); index++ { body = append(body, lines[index]) } - writeCodeBlock(out, language, strings.Join(body, "\n")) + out.WriteString(style.block(language, strings.Join(body, "\n")) + "\n") continue } if isQuote(line) { quoted := []string{} for ; index < len(lines) && isQuote(lines[index]); index++ { - quoted = append(quoted, inlineHtml(trimQuote(lines[index]))) + quoted = append(quoted, inlineMarkdown(trimQuote(lines[index]), style)) } index-- - out.WriteString("<blockquote>" + strings.Join(quoted, "\n") + "</blockquote>\n") + out.WriteString(style.quote(quoted) + "\n") continue } - out.WriteString(blockLine(line) + "\n") + out.WriteString(blockLine(line, style) + "\n") } return strings.TrimRight(out.String(), "\n") } +// telegramHtml renders into the small HTML subset Telegram parses. HTML is +// chosen over MarkdownV2 because only three characters ever need escaping and +// the tags are written here rather than by the agent, so a marker left open +// halfway through a streamed answer comes out as literal text instead of a +// refused message. +func telegramHtml(text string) string { + return renderMarkdown(text, markdownStyle{ + escape: escapeHtml, + bold: func(inner string) string { return "<b>" + inner + "</b>" }, + italic: func(inner string) string { return "<i>" + inner + "</i>" }, + strike: func(inner string) string { return "<s>" + inner + "</s>" }, + code: func(inner string) string { return "<code>" + escapeHtml(inner) + "</code>" }, + block: func(language, body string) string { + if !isPlainWord(language) { + return "<pre>" + escapeHtml(body) + "</pre>" + } + return `<pre><code class="language-` + language + `">` + escapeHtml(body) + "</code></pre>" + }, + quote: func(lines []string) string { return "<blockquote>" + strings.Join(lines, "\n") + "</blockquote>" }, + link: func(label, url string) string { return `<a href="` + escapeHtml(url) + `">` + label + "</a>" }, + rule: "──────────", + }) +} + +// plainText drops the markup for a platform that has none, so a chat is not +// shown the asterisks around a word that was meant to be bold. A link keeps its +// address, since there is nothing else left to carry it. +func plainText(text string) string { + keep := func(inner string) string { return inner } + return renderMarkdown(text, markdownStyle{ + escape: keep, + bold: keep, + italic: keep, + strike: keep, + code: keep, + block: func(language, body string) string { return body }, + quote: func(lines []string) string { return strings.Join(lines, "\n") }, + link: func(label, url string) string { + if label == "" || label == url { + return url + } + return label + " " + url + }, + rule: "──────────", + }) +} + func fenceStart(line string) (string, bool) { trimmed := strings.TrimSpace(line) if !strings.HasPrefix(trimmed, "```") { @@ -66,19 +125,6 @@ func isFence(line string) bool { return strings.HasPrefix(strings.TrimSpace(line), "```") } -func writeCodeBlock(out *strings.Builder, language, body string) { - tagged := isPlainWord(language) - out.WriteString("<pre>") - if tagged { - out.WriteString(`<code class="language-` + language + `">`) - } - out.WriteString(escapeHtml(body)) - if tagged { - out.WriteString("</code>") - } - out.WriteString("</pre>\n") -} - func isQuote(line string) bool { return strings.HasPrefix(strings.TrimSpace(line), ">") } @@ -89,24 +135,24 @@ func trimQuote(line string) string { // blockLine renders the one-line shapes: a heading becomes bold, a list marker // becomes a bullet, and a rule becomes a line somebody can actually see. -func blockLine(line string) string { +func blockLine(line string, style markdownStyle) string { if trimmed := strings.TrimSpace(line); isRule(trimmed) { - return "──────────" + return style.rule } indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))] rest := line[len(indent):] if hashes := len(rest) - len(strings.TrimLeft(rest, "#")); hashes > 0 && hashes <= 6 && strings.HasPrefix(rest[hashes:], " ") { - return indent + "<b>" + inlineHtml(strings.TrimSpace(rest[hashes:])) + "</b>" + return indent + style.bold(inlineMarkdown(strings.TrimSpace(rest[hashes:]), style)) } for _, marker := range []string{"- ", "* ", "+ "} { if strings.HasPrefix(rest, marker) { - return indent + "• " + inlineHtml(rest[len(marker):]) + return indent + "• " + inlineMarkdown(rest[len(marker):], style) } } - return indent + inlineHtml(rest) + return indent + inlineMarkdown(rest, style) } func isRule(line string) bool { @@ -116,9 +162,9 @@ func isRule(line string) bool { return strings.Trim(line, "-") == "" || strings.Trim(line, "*") == "" || strings.Trim(line, "_") == "" } -// inlineHtml walks one line and turns the markers it recognises into tags. A -// marker with nothing closing it is written out as the character it is. -func inlineHtml(text string) string { +// inlineMarkdown walks one line and hands each marker it recognises to the +// style. A marker with nothing closing it is written out as the character it is. +func inlineMarkdown(text string, style markdownStyle) string { out := &strings.Builder{} runes := []rune(text) @@ -127,7 +173,7 @@ func inlineHtml(text string) string { case runes[index] == '`': width := runLength(runes, index, '`') if end := findRun(runes, index+width, '`', width); end >= 0 { - out.WriteString("<code>" + escapeHtml(strings.Trim(string(runes[index+width:end]), " ")) + "</code>") + out.WriteString(style.code(strings.Trim(string(runes[index+width:end]), " "))) index = end + width continue } @@ -135,34 +181,34 @@ func inlineHtml(text string) string { case hasMarker(runes, index, "**"), hasMarker(runes, index, "__"): marker := string(runes[index : index+2]) if end := findMarker(runes, index+2, marker); end >= 0 { - out.WriteString("<b>" + inlineHtml(string(runes[index+2:end])) + "</b>") + out.WriteString(style.bold(inlineMarkdown(string(runes[index+2:end]), style))) index = end + 2 continue } case hasMarker(runes, index, "~~"): if end := findMarker(runes, index+2, "~~"); end >= 0 { - out.WriteString("<s>" + inlineHtml(string(runes[index+2:end])) + "</s>") + out.WriteString(style.strike(inlineMarkdown(string(runes[index+2:end]), style))) index = end + 2 continue } case opensEmphasis(runes, index): if end := findEmphasis(runes, index+1, runes[index]); end >= 0 { - out.WriteString("<i>" + inlineHtml(string(runes[index+1:end])) + "</i>") + out.WriteString(style.italic(inlineMarkdown(string(runes[index+1:end]), style))) index = end + 1 continue } case runes[index] == '[': if label, url, width := linkAt(runes, index); width > 0 { - out.WriteString(`<a href="` + escapeHtml(url) + `">` + inlineHtml(label) + "</a>") + out.WriteString(style.link(inlineMarkdown(label, style), url)) index += width continue } } - out.WriteString(escapeHtml(string(runes[index]))) + out.WriteString(style.escape(string(runes[index]))) index++ } return out.String() diff --git a/imbridge/weixin.go b/imbridge/weixin.go index 5bcaeaf..a1734bd 100644 --- a/imbridge/weixin.go +++ b/imbridge/weixin.go @@ -180,7 +180,9 @@ func (w *Weixin) Send(message Message, reply Reply) (string, error) { return "", nil } - for _, chunk := range splitForChat(reply.Text, weixinMessageLimit) { + // WeChat shows a message exactly as it is written, so the markup comes off + // rather than being read out as asterisks and backticks. + for _, chunk := range splitForChat(plainText(reply.Text), weixinMessageLimit) { body := map[string]any{ "msg": map[string]any{ "from_user_id": "", --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
