yao/integrations/dingtalk/format.go
Max 3eb43a59ab Enhance message handling for integrations with improved formatting and localization
- Add locale support for DingTalk and Discord integrations to ensure messages are sent in the correct language.
- Update message formatting logic in DingTalk and Discord to utilize dedicated formatting functions for Markdown.
- Implement image and file handling improvements in Feishu integration, allowing for better attachment processing and error handling.
- Refactor message sending methods to streamline the process and enhance code clarity across integrations.
2026-03-02 09:00:56 +08:00

170 lines
3.6 KiB
Go

package dingtalk
import (
"regexp"
"strings"
)
// FormatDingTalkMarkdown converts standard Markdown to DingTalk's Markdown subset.
//
// DingTalk webhook Markdown supports:
// - # headings (1-6)
// - **bold**, *italic*
// - > blockquote
// - - unordered list
// - [link](url)
// - ![image](url)
// - --- divider
//
// NOT supported (must be degraded):
// - ~~strikethrough~~ → plain text
// - ``` code blocks → indented text
// - `inline code` → plain text
// - tables → pre-formatted text
// - ordered lists → "N. " text (passthrough, may not render)
func FormatDingTalkMarkdown(md string) string {
md = strings.ReplaceAll(md, "\r\n", "\n")
var out strings.Builder
lines := strings.Split(md, "\n")
inCodeBlock := false
var codeLines []string
inTable := false
var tableRows [][]string
for i := 0; i < len(lines); i++ {
line := lines[i]
if strings.HasPrefix(line, "```") {
if !inCodeBlock {
inCodeBlock = true
codeLines = nil
} else {
inCodeBlock = false
out.WriteString("\n")
for _, cl := range codeLines {
out.WriteString(" " + cl + "\n")
}
out.WriteString("\n")
}
continue
}
if inCodeBlock {
codeLines = append(codeLines, line)
continue
}
if dtIsTableRow(line) {
if !inTable {
inTable = true
tableRows = nil
}
if dtIsTableSep(line) {
continue
}
tableRows = append(tableRows, dtParseTableRow(line))
continue
}
if inTable {
dtFlushTable(&out, tableRows)
inTable = false
tableRows = nil
}
line = dtReStrikethrough.ReplaceAllString(line, "$1")
line = dtReInlineCode.ReplaceAllString(line, "$1")
out.WriteString(line + "\n")
}
if inCodeBlock && len(codeLines) > 0 {
out.WriteString("\n")
for _, cl := range codeLines {
out.WriteString(" " + cl + "\n")
}
out.WriteString("\n")
}
if inTable {
dtFlushTable(&out, tableRows)
}
return strings.TrimRight(out.String(), "\n")
}
var (
dtReStrikethrough = regexp.MustCompile(`~~(.+?)~~`)
dtReInlineCode = regexp.MustCompile("`([^`]+)`")
dtReTableRow = regexp.MustCompile(`^\|.*\|$`)
dtReTableSep = regexp.MustCompile(`^\|[\s\-:|]+\|$`)
)
func dtIsTableRow(line string) bool {
return dtReTableRow.MatchString(strings.TrimSpace(line))
}
func dtIsTableSep(line string) bool {
return dtReTableSep.MatchString(strings.TrimSpace(line))
}
func dtParseTableRow(line string) []string {
line = strings.TrimSpace(line)
line = strings.TrimPrefix(line, "|")
line = strings.TrimSuffix(line, "|")
cells := strings.Split(line, "|")
for i := range cells {
cells[i] = strings.TrimSpace(cells[i])
}
return cells
}
func dtFlushTable(out *strings.Builder, rows [][]string) {
if len(rows) == 0 {
return
}
colWidths := make([]int, len(rows[0]))
for _, row := range rows {
for i, cell := range row {
if i < len(colWidths) && len([]rune(cell)) > colWidths[i] {
colWidths[i] = len([]rune(cell))
}
}
}
out.WriteString("\n")
for ri, row := range rows {
for ci, cell := range row {
if ci > 0 {
out.WriteString(" | ")
}
w := 0
if ci < len(colWidths) {
w = colWidths[ci]
}
out.WriteString(dtPadRight(cell, w))
}
out.WriteString("\n")
if ri == 0 && len(rows) > 1 {
for ci := range row {
if ci > 0 {
out.WriteString("-+-")
}
w := 0
if ci < len(colWidths) {
w = colWidths[ci]
}
out.WriteString(strings.Repeat("-", w))
}
out.WriteString("\n")
}
}
out.WriteString("\n")
}
func dtPadRight(s string, width int) string {
runes := []rune(s)
if len(runes) >= width {
return s
}
return s + strings.Repeat(" ", width-len(runes))
}