mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 00:55:53 +08:00
1549 lines
38 KiB
Go
1549 lines
38 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package convertlib
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// cardObj is a convenience alias for generic JSON objects.
|
|
type cardObj = map[string]interface{}
|
|
|
|
// cardMode controls output verbosity.
|
|
type cardMode int
|
|
|
|
const (
|
|
cardModeConcise cardMode = 0
|
|
cardModeDetailed cardMode = 1
|
|
)
|
|
|
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
var cardEmojiMap = map[string]string{
|
|
"OK": "👌",
|
|
"THUMBSUP": "👍",
|
|
"SMILE": "😊",
|
|
"HEART": "❤️",
|
|
"CLAP": "👏",
|
|
"FIRE": "🔥",
|
|
"PARTY": "🎉",
|
|
"THINK": "🤔",
|
|
}
|
|
|
|
var cardChartTypeNames = map[string]string{
|
|
"bar": "Bar chart",
|
|
"line": "Line chart",
|
|
"pie": "Pie chart",
|
|
"area": "Area chart",
|
|
"radar": "Radar chart",
|
|
"scatter": "Scatter plot",
|
|
}
|
|
|
|
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
|
|
type interactiveConverter struct{}
|
|
|
|
func (interactiveConverter) Convert(ctx *ConvertContext) string {
|
|
return convertCard(ctx.RawContent)
|
|
}
|
|
|
|
// convertCard converts a raw interactive/card message content JSON to human-readable string.
|
|
func convertCard(raw string) string {
|
|
var parsed cardObj
|
|
if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
|
|
return "[interactive card]"
|
|
}
|
|
|
|
// raw_card_content format: outer JSON has "json_card" string field
|
|
if jsonCard, ok := parsed["json_card"].(string); ok {
|
|
c := &cardConverter{mode: cardModeConcise}
|
|
if att, ok := parsed["json_attachment"].(string); ok && att != "" {
|
|
var attObj cardObj
|
|
if json.Unmarshal([]byte(att), &attObj) == nil {
|
|
c.attachment = attObj
|
|
}
|
|
}
|
|
schema := 0
|
|
if s, ok := parsed["card_schema"].(float64); ok {
|
|
schema = int(s)
|
|
}
|
|
result := c.convert(jsonCard, schema)
|
|
if result == "" {
|
|
return "[interactive card]"
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Legacy format
|
|
return convertLegacyCard(parsed)
|
|
}
|
|
|
|
// ── Legacy converter ──────────────────────────────────────────────────────────
|
|
|
|
func convertLegacyCard(parsed cardObj) string {
|
|
var texts []string
|
|
|
|
if header, ok := parsed["header"].(cardObj); ok {
|
|
if title, ok := header["title"].(cardObj); ok {
|
|
if content, ok := title["content"].(string); ok && content != "" {
|
|
texts = append(texts, "**"+content+"**")
|
|
}
|
|
}
|
|
}
|
|
|
|
body, _ := parsed["body"].(cardObj)
|
|
var elements []interface{}
|
|
if e, ok := parsed["elements"].([]interface{}); ok {
|
|
elements = e
|
|
} else if body != nil {
|
|
if e, ok := body["elements"].([]interface{}); ok {
|
|
elements = e
|
|
}
|
|
}
|
|
legacyExtractTexts(elements, &texts)
|
|
|
|
if len(texts) == 0 {
|
|
return "[interactive card]"
|
|
}
|
|
return strings.Join(texts, "\n")
|
|
}
|
|
|
|
func legacyExtractTexts(elements []interface{}, out *[]string) {
|
|
for _, el := range elements {
|
|
elem, ok := el.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
tag, _ := elem["tag"].(string)
|
|
|
|
if tag == "markdown" {
|
|
if content, ok := elem["content"].(string); ok {
|
|
*out = append(*out, content)
|
|
}
|
|
continue
|
|
}
|
|
if tag == "div" || tag == "plain_text" || tag == "lark_md" {
|
|
if text, ok := elem["text"].(cardObj); ok {
|
|
if content, ok := text["content"].(string); ok && content != "" {
|
|
*out = append(*out, content)
|
|
}
|
|
}
|
|
if content, ok := elem["content"].(string); ok && content != "" {
|
|
*out = append(*out, content)
|
|
}
|
|
}
|
|
if tag == "column_set" {
|
|
if cols, ok := elem["columns"].([]interface{}); ok {
|
|
for _, col := range cols {
|
|
if cm, ok := col.(cardObj); ok {
|
|
if elems, ok := cm["elements"].([]interface{}); ok {
|
|
legacyExtractTexts(elems, out)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if elems, ok := elem["elements"].([]interface{}); ok {
|
|
legacyExtractTexts(elems, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── CardConverter ─────────────────────────────────────────────────────────────
|
|
|
|
type cardConverter struct {
|
|
mode cardMode
|
|
attachment cardObj
|
|
}
|
|
|
|
func (c *cardConverter) convert(jsonCard string, hintSchema int) string {
|
|
var card cardObj
|
|
if err := json.Unmarshal([]byte(jsonCard), &card); err != nil {
|
|
return "<card>\n[Unable to parse card content]\n</card>"
|
|
}
|
|
|
|
header, _ := card["header"].(cardObj)
|
|
title := ""
|
|
if header != nil {
|
|
title = c.extractHeaderTitle(header)
|
|
}
|
|
|
|
bodyContent := ""
|
|
if body, ok := card["body"].(cardObj); ok {
|
|
bodyContent = c.convertBody(body)
|
|
}
|
|
|
|
var sb strings.Builder
|
|
if title != "" {
|
|
sb.WriteString("<card title=\"")
|
|
sb.WriteString(cardEscapeAttr(title))
|
|
sb.WriteString("\">\n")
|
|
} else {
|
|
sb.WriteString("<card>\n")
|
|
}
|
|
if bodyContent != "" {
|
|
sb.WriteString(bodyContent)
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("</card>")
|
|
return sb.String()
|
|
}
|
|
|
|
func (c *cardConverter) extractHeaderTitle(header cardObj) string {
|
|
if prop, ok := header["property"].(cardObj); ok {
|
|
if titleElem, ok := prop["title"]; ok {
|
|
return c.extractTextContent(titleElem)
|
|
}
|
|
}
|
|
if titleElem, ok := header["title"]; ok {
|
|
return c.extractTextContent(titleElem)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *cardConverter) convertBody(body cardObj) string {
|
|
var elements []interface{}
|
|
|
|
if prop, ok := body["property"].(cardObj); ok {
|
|
if e, ok := prop["elements"].([]interface{}); ok && len(e) > 0 {
|
|
elements = e
|
|
}
|
|
}
|
|
if len(elements) == 0 {
|
|
if e, ok := body["elements"].([]interface{}); ok {
|
|
elements = e
|
|
}
|
|
}
|
|
|
|
if len(elements) == 0 {
|
|
return ""
|
|
}
|
|
return c.convertElements(elements, 0)
|
|
}
|
|
|
|
func (c *cardConverter) convertElements(elements []interface{}, depth int) string {
|
|
var results []string
|
|
for _, el := range elements {
|
|
elem, ok := el.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if result := c.convertElement(elem, depth); result != "" {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
return strings.Join(results, "\n")
|
|
}
|
|
|
|
func (c *cardConverter) extractProperty(elem cardObj) cardObj {
|
|
if prop, ok := elem["property"].(cardObj); ok {
|
|
return prop
|
|
}
|
|
return elem
|
|
}
|
|
|
|
func (c *cardConverter) convertElement(elem cardObj, depth int) string {
|
|
tag, _ := elem["tag"].(string)
|
|
id, _ := elem["id"].(string)
|
|
prop := c.extractProperty(elem)
|
|
|
|
switch tag {
|
|
case "plain_text", "text":
|
|
return c.convertPlainText(prop)
|
|
case "markdown":
|
|
return c.convertMarkdown(prop)
|
|
case "markdown_v1":
|
|
return c.convertMarkdownV1(elem, prop)
|
|
case "div":
|
|
return c.convertDiv(prop, id)
|
|
case "note":
|
|
return c.convertNote(prop)
|
|
case "hr":
|
|
return "---"
|
|
case "br":
|
|
return "\n"
|
|
case "column_set":
|
|
return c.convertColumnSet(prop, depth)
|
|
case "column":
|
|
return c.convertColumn(prop, depth)
|
|
case "person":
|
|
return c.convertPerson(prop, id)
|
|
case "person_v1":
|
|
return c.convertPersonV1(prop, id)
|
|
case "person_list":
|
|
return c.convertPersonList(prop)
|
|
case "avatar":
|
|
return c.convertAvatar(prop, id)
|
|
case "at":
|
|
return c.convertAt(prop)
|
|
case "at_all":
|
|
return "@everyone"
|
|
case "button":
|
|
return c.convertButton(prop, id)
|
|
case "actions", "action":
|
|
return c.convertActions(prop)
|
|
case "overflow":
|
|
return c.convertOverflow(prop)
|
|
case "select_static", "select_person":
|
|
return c.convertSelect(prop, id, false)
|
|
case "multi_select_static", "multi_select_person":
|
|
return c.convertSelect(prop, id, true)
|
|
case "select_img":
|
|
return c.convertSelectImg(prop, id)
|
|
case "input":
|
|
return c.convertInput(prop, id)
|
|
case "date_picker":
|
|
return c.convertDatePicker(prop, id, "date")
|
|
case "picker_time":
|
|
return c.convertDatePicker(prop, id, "time")
|
|
case "picker_datetime":
|
|
return c.convertDatePicker(prop, id, "datetime")
|
|
case "checker":
|
|
return c.convertChecker(prop, id)
|
|
case "img", "image":
|
|
return c.convertImage(prop, id)
|
|
case "img_combination":
|
|
return c.convertImgCombination(prop)
|
|
case "table":
|
|
return c.convertTable(prop)
|
|
case "chart":
|
|
return c.convertChart(prop, id)
|
|
case "audio":
|
|
return c.convertAudio(prop, id)
|
|
case "video":
|
|
return c.convertVideo(prop, id)
|
|
case "collapsible_panel":
|
|
return c.convertCollapsiblePanel(prop, id)
|
|
case "form":
|
|
return c.convertForm(prop, id)
|
|
case "interactive_container":
|
|
return c.convertInteractiveContainer(prop, id)
|
|
case "text_tag":
|
|
return c.convertTextTag(prop)
|
|
case "number_tag":
|
|
return c.convertNumberTag(prop)
|
|
case "link":
|
|
return c.convertLink(prop)
|
|
case "emoji":
|
|
return c.convertEmoji(prop)
|
|
case "local_datetime":
|
|
return c.convertLocalDatetime(prop)
|
|
case "list":
|
|
return c.convertList(prop)
|
|
case "blockquote":
|
|
return c.convertBlockquote(prop)
|
|
case "code_block":
|
|
return c.convertCodeBlock(prop)
|
|
case "code_span":
|
|
return c.convertCodeSpan(prop)
|
|
case "heading":
|
|
return c.convertHeading(prop)
|
|
case "fallback_text":
|
|
return c.convertFallbackText(prop)
|
|
case "repeat":
|
|
return c.convertRepeat(prop)
|
|
case "card_header", "custom_icon", "standard_icon":
|
|
return ""
|
|
default:
|
|
return c.convertUnknown(prop, tag)
|
|
}
|
|
}
|
|
|
|
// ── Text extraction ───────────────────────────────────────────────────────────
|
|
|
|
func (c *cardConverter) extractTextContent(v interface{}) string {
|
|
if v == nil {
|
|
return ""
|
|
}
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
m, ok := v.(cardObj)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
if prop, ok := m["property"].(cardObj); ok {
|
|
return c.extractTextFromProperty(prop)
|
|
}
|
|
return c.extractTextFromProperty(m)
|
|
}
|
|
|
|
func (c *cardConverter) extractTextFromProperty(prop cardObj) string {
|
|
// i18n content
|
|
if i18n, ok := prop["i18nContent"].(cardObj); ok {
|
|
for _, lang := range []string{"zh_cn", "en_us", "ja_jp"} {
|
|
if t, ok := i18n[lang].(string); ok && t != "" {
|
|
return t
|
|
}
|
|
}
|
|
}
|
|
if content, ok := prop["content"].(string); ok {
|
|
return content
|
|
}
|
|
if elements, ok := prop["elements"].([]interface{}); ok && len(elements) > 0 {
|
|
var texts []string
|
|
for _, el := range elements {
|
|
if t := c.extractTextContent(el); t != "" {
|
|
texts = append(texts, t)
|
|
}
|
|
}
|
|
return strings.Join(texts, "")
|
|
}
|
|
if text, ok := prop["text"].(string); ok {
|
|
return text
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ── Element converters ────────────────────────────────────────────────────────
|
|
|
|
func (c *cardConverter) convertPlainText(prop cardObj) string {
|
|
content, _ := prop["content"].(string)
|
|
if content == "" {
|
|
return ""
|
|
}
|
|
return c.applyTextStyle(content, prop)
|
|
}
|
|
|
|
func (c *cardConverter) convertMarkdown(prop cardObj) string {
|
|
if elements, ok := prop["elements"].([]interface{}); ok && len(elements) > 0 {
|
|
return c.convertMarkdownElements(elements)
|
|
}
|
|
if content, ok := prop["content"].(string); ok {
|
|
return content
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *cardConverter) convertMarkdownV1(elem, prop cardObj) string {
|
|
if elements, ok := prop["elements"].([]interface{}); ok && len(elements) > 0 {
|
|
return c.convertMarkdownElements(elements)
|
|
}
|
|
if fallback, ok := elem["fallback"].(cardObj); ok {
|
|
return c.convertElement(fallback, 0)
|
|
}
|
|
if content, ok := prop["content"].(string); ok {
|
|
return content
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *cardConverter) convertMarkdownElements(elements []interface{}) string {
|
|
var parts []string
|
|
for _, el := range elements {
|
|
elem, ok := el.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if result := c.convertElement(elem, 0); result != "" {
|
|
parts = append(parts, result)
|
|
}
|
|
}
|
|
return strings.Join(parts, "")
|
|
}
|
|
|
|
func (c *cardConverter) convertDiv(prop cardObj, _ string) string {
|
|
var results []string
|
|
|
|
if textElem, ok := prop["text"].(cardObj); ok {
|
|
if text := c.convertElement(textElem, 0); text != "" {
|
|
if textSize, _ := textElem["text_size"].(string); textSize == "notation" {
|
|
text = "📝 " + text
|
|
}
|
|
results = append(results, text)
|
|
}
|
|
}
|
|
|
|
if fields, ok := prop["fields"].([]interface{}); ok {
|
|
var fieldTexts []string
|
|
for _, field := range fields {
|
|
fm, ok := field.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if te, ok := fm["text"].(cardObj); ok {
|
|
if ft := c.convertElement(te, 0); ft != "" {
|
|
fieldTexts = append(fieldTexts, ft)
|
|
}
|
|
}
|
|
}
|
|
if len(fieldTexts) > 0 {
|
|
results = append(results, strings.Join(fieldTexts, "\n"))
|
|
}
|
|
}
|
|
|
|
if extraElem, ok := prop["extra"].(cardObj); ok {
|
|
if extra := c.convertElement(extraElem, 0); extra != "" {
|
|
results = append(results, extra)
|
|
}
|
|
}
|
|
|
|
return strings.Join(results, "\n")
|
|
}
|
|
|
|
func (c *cardConverter) convertNote(prop cardObj) string {
|
|
elements, _ := prop["elements"].([]interface{})
|
|
if len(elements) == 0 {
|
|
return ""
|
|
}
|
|
var texts []string
|
|
for _, el := range elements {
|
|
elem, ok := el.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if text := c.convertElement(elem, 0); text != "" {
|
|
texts = append(texts, text)
|
|
}
|
|
}
|
|
if len(texts) == 0 {
|
|
return ""
|
|
}
|
|
return "📝 " + strings.Join(texts, " ")
|
|
}
|
|
|
|
func (c *cardConverter) convertLink(prop cardObj) string {
|
|
content, _ := prop["content"].(string)
|
|
if content == "" {
|
|
content = "Link"
|
|
}
|
|
urlStr := ""
|
|
if urlObj, ok := prop["url"].(cardObj); ok {
|
|
urlStr, _ = urlObj["url"].(string)
|
|
}
|
|
if urlStr != "" {
|
|
return fmt.Sprintf("[%s](%s)", escapeMDLinkText(content), urlStr)
|
|
}
|
|
return content
|
|
}
|
|
|
|
func (c *cardConverter) convertEmoji(prop cardObj) string {
|
|
key, _ := prop["key"].(string)
|
|
if emoji, ok := cardEmojiMap[key]; ok {
|
|
return emoji
|
|
}
|
|
return ":" + key + ":"
|
|
}
|
|
|
|
func (c *cardConverter) convertLocalDatetime(prop cardObj) string {
|
|
if ms, ok := prop["milliseconds"].(string); ok && ms != "" {
|
|
if formatted := cardFormatMillisToISO8601(ms); formatted != "" {
|
|
return formatted
|
|
}
|
|
}
|
|
fallback, _ := prop["fallbackText"].(string)
|
|
return fallback
|
|
}
|
|
|
|
func (c *cardConverter) convertList(prop cardObj) string {
|
|
items, _ := prop["items"].([]interface{})
|
|
if len(items) == 0 {
|
|
return ""
|
|
}
|
|
var lines []string
|
|
for _, item := range items {
|
|
im, ok := item.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
level := 0
|
|
if l, ok := im["level"].(float64); ok {
|
|
level = int(l)
|
|
}
|
|
listType, _ := im["type"].(string)
|
|
order := 0
|
|
if o, ok := im["order"].(float64); ok {
|
|
order = int(math.Floor(float64(o)))
|
|
}
|
|
indent := strings.Repeat(" ", level)
|
|
marker := "-"
|
|
if listType == "ol" {
|
|
marker = fmt.Sprintf("%d.", order)
|
|
}
|
|
if elements, ok := im["elements"].([]interface{}); ok {
|
|
content := c.convertMarkdownElements(elements)
|
|
lines = append(lines, fmt.Sprintf("%s%s %s", indent, marker, content))
|
|
}
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (c *cardConverter) convertBlockquote(prop cardObj) string {
|
|
content := ""
|
|
if s, ok := prop["content"].(string); ok {
|
|
content = s
|
|
} else if elements, ok := prop["elements"].([]interface{}); ok {
|
|
content = c.convertMarkdownElements(elements)
|
|
}
|
|
if content == "" {
|
|
return ""
|
|
}
|
|
lines := strings.Split(content, "\n")
|
|
for i, line := range lines {
|
|
lines[i] = "> " + line
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (c *cardConverter) convertCodeBlock(prop cardObj) string {
|
|
language, _ := prop["language"].(string)
|
|
if language == "" {
|
|
language = "plaintext"
|
|
}
|
|
var code strings.Builder
|
|
if contents, ok := prop["contents"].([]interface{}); ok {
|
|
for _, line := range contents {
|
|
lm, ok := line.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if lineContents, ok := lm["contents"].([]interface{}); ok {
|
|
for _, lc := range lineContents {
|
|
cm, ok := lc.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if s, ok := cm["content"].(string); ok {
|
|
code.WriteString(s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return fmt.Sprintf("```%s\n%s```", language, code.String())
|
|
}
|
|
|
|
func (c *cardConverter) convertCodeSpan(prop cardObj) string {
|
|
content, _ := prop["content"].(string)
|
|
return "`" + content + "`"
|
|
}
|
|
|
|
func (c *cardConverter) convertHeading(prop cardObj) string {
|
|
level := 1
|
|
if l, ok := prop["level"].(float64); ok {
|
|
level = int(l)
|
|
if level < 1 {
|
|
level = 1
|
|
}
|
|
if level > 6 {
|
|
level = 6
|
|
}
|
|
}
|
|
content := ""
|
|
if s, ok := prop["content"].(string); ok {
|
|
content = s
|
|
} else if elements, ok := prop["elements"].([]interface{}); ok {
|
|
content = c.convertMarkdownElements(elements)
|
|
}
|
|
return strings.Repeat("#", level) + " " + content
|
|
}
|
|
|
|
func (c *cardConverter) convertFallbackText(prop cardObj) string {
|
|
if textElem, ok := prop["text"].(cardObj); ok {
|
|
return c.extractTextContent(textElem)
|
|
}
|
|
if elements, ok := prop["elements"].([]interface{}); ok {
|
|
return c.convertMarkdownElements(elements)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *cardConverter) convertTextTag(prop cardObj) string {
|
|
textElem := prop["text"]
|
|
text := c.extractTextContent(textElem)
|
|
if text == "" {
|
|
return ""
|
|
}
|
|
return "「" + text + "」"
|
|
}
|
|
|
|
func (c *cardConverter) convertNumberTag(prop cardObj) string {
|
|
textElem := prop["text"]
|
|
text := c.extractTextContent(textElem)
|
|
if text == "" {
|
|
return ""
|
|
}
|
|
if urlObj, ok := prop["url"].(cardObj); ok {
|
|
if urlStr, ok := urlObj["url"].(string); ok && urlStr != "" {
|
|
return fmt.Sprintf("[%s](%s)", escapeMDLinkText(text), urlStr)
|
|
}
|
|
}
|
|
return text
|
|
}
|
|
|
|
func (c *cardConverter) convertUnknown(prop cardObj, tag string) string {
|
|
if prop != nil {
|
|
for _, path := range []string{"content", "text", "title", "label", "placeholder"} {
|
|
if v, ok := prop[path]; ok {
|
|
text := c.extractTextContent(v)
|
|
if text != "" {
|
|
return text
|
|
}
|
|
}
|
|
}
|
|
if elements, ok := prop["elements"].([]interface{}); ok && len(elements) > 0 {
|
|
return c.convertElements(elements, 0)
|
|
}
|
|
}
|
|
if c.mode == cardModeDetailed {
|
|
return fmt.Sprintf("[Unknown content](tag:%s)", tag)
|
|
}
|
|
return "[Unknown content]"
|
|
}
|
|
|
|
func (c *cardConverter) convertColumnSet(prop cardObj, depth int) string {
|
|
columns, _ := prop["columns"].([]interface{})
|
|
if len(columns) == 0 {
|
|
return ""
|
|
}
|
|
var results []string
|
|
for _, col := range columns {
|
|
elem, ok := col.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if result := c.convertElement(elem, depth+1); result != "" {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
sep := "\n\n"
|
|
if allColumnsAreButtons(results) {
|
|
sep = " "
|
|
}
|
|
return strings.Join(results, sep)
|
|
}
|
|
|
|
// allColumnsAreButtons reports whether every result looks like a button token
|
|
// (e.g. "[Text]", "[Text](url)", "[Text ✗]"). Used to decide whether
|
|
// column_set columns should be space-joined (button row) or newline-joined.
|
|
func allColumnsAreButtons(results []string) bool {
|
|
if len(results) == 0 {
|
|
return false
|
|
}
|
|
for _, r := range results {
|
|
if !strings.HasPrefix(r, "[") || strings.Contains(r, "\n") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (c *cardConverter) convertColumn(prop cardObj, depth int) string {
|
|
elements, _ := prop["elements"].([]interface{})
|
|
if len(elements) == 0 {
|
|
return ""
|
|
}
|
|
return c.convertElements(elements, depth)
|
|
}
|
|
|
|
func (c *cardConverter) convertForm(prop cardObj, _ string) string {
|
|
var sb strings.Builder
|
|
sb.WriteString("<form>\n")
|
|
if elements, ok := prop["elements"].([]interface{}); ok {
|
|
sb.WriteString(c.convertElements(elements, 0))
|
|
}
|
|
sb.WriteString("\n</form>")
|
|
return sb.String()
|
|
}
|
|
|
|
func (c *cardConverter) convertCollapsiblePanel(prop cardObj, _ string) string {
|
|
expanded, _ := prop["expanded"].(bool)
|
|
title := "Details"
|
|
if header, ok := prop["header"].(cardObj); ok {
|
|
if titleElem, ok := header["title"]; ok {
|
|
if t := c.extractTextContent(titleElem); t != "" {
|
|
title = t
|
|
}
|
|
}
|
|
}
|
|
|
|
shouldExpand := expanded || c.mode == cardModeDetailed
|
|
if shouldExpand {
|
|
var sb strings.Builder
|
|
sb.WriteString("▼ " + title + "\n")
|
|
if elements, ok := prop["elements"].([]interface{}); ok {
|
|
content := c.convertElements(elements, 1)
|
|
for _, line := range strings.Split(content, "\n") {
|
|
if line != "" {
|
|
sb.WriteString(" " + line + "\n")
|
|
}
|
|
}
|
|
}
|
|
sb.WriteString("▲")
|
|
return sb.String()
|
|
}
|
|
return "▶ " + title
|
|
}
|
|
|
|
func (c *cardConverter) convertInteractiveContainer(prop cardObj, id string) string {
|
|
urlStr := ""
|
|
if actions, ok := prop["actions"].([]interface{}); ok && len(actions) > 0 {
|
|
if action, ok := actions[0].(cardObj); ok {
|
|
if actionType, _ := action["type"].(string); actionType == "open_url" {
|
|
if actionData, ok := action["action"].(cardObj); ok {
|
|
urlStr, _ = actionData["url"].(string)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("<clickable")
|
|
if urlStr != "" {
|
|
sb.WriteString(fmt.Sprintf(" url=\"%s\"", cardEscapeAttr(urlStr)))
|
|
}
|
|
if c.mode == cardModeDetailed && id != "" {
|
|
sb.WriteString(fmt.Sprintf(" id=\"%s\"", id))
|
|
}
|
|
sb.WriteString(">\n")
|
|
if elements, ok := prop["elements"].([]interface{}); ok {
|
|
sb.WriteString(c.convertElements(elements, 0))
|
|
}
|
|
sb.WriteString("\n</clickable>")
|
|
return sb.String()
|
|
}
|
|
|
|
func (c *cardConverter) convertRepeat(prop cardObj) string {
|
|
if elements, ok := prop["elements"].([]interface{}); ok {
|
|
return c.convertElements(elements, 0)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *cardConverter) convertButton(prop cardObj, _ string) string {
|
|
buttonText := ""
|
|
if textElem, ok := prop["text"].(cardObj); ok {
|
|
buttonText = c.extractTextContent(textElem)
|
|
}
|
|
if buttonText == "" {
|
|
buttonText = "Button"
|
|
}
|
|
|
|
disabled, _ := prop["disabled"].(bool)
|
|
if disabled && c.mode == cardModeConcise {
|
|
return fmt.Sprintf("[%s ✗]", buttonText)
|
|
}
|
|
|
|
if actions, ok := prop["actions"].([]interface{}); ok {
|
|
for _, action := range actions {
|
|
am, ok := action.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if am["type"] == "open_url" {
|
|
if ad, ok := am["action"].(cardObj); ok {
|
|
if urlStr, ok := ad["url"].(string); ok && urlStr != "" {
|
|
return fmt.Sprintf("[%s](%s)", escapeMDLinkText(buttonText), urlStr)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if disabled && c.mode == cardModeDetailed {
|
|
result := fmt.Sprintf("[%s ✗]", buttonText)
|
|
if tips, ok := prop["disabledTips"].(cardObj); ok {
|
|
if tipsText := c.extractTextContent(tips); tipsText != "" {
|
|
result += fmt.Sprintf("(tips:\"%s\")", tipsText)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
return fmt.Sprintf("[%s]", buttonText)
|
|
}
|
|
|
|
func (c *cardConverter) convertActions(prop cardObj) string {
|
|
actions, _ := prop["actions"].([]interface{})
|
|
if len(actions) == 0 {
|
|
return ""
|
|
}
|
|
var results []string
|
|
for _, action := range actions {
|
|
elem, ok := action.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if result := c.convertElement(elem, 0); result != "" {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
return strings.Join(results, " ")
|
|
}
|
|
|
|
func (c *cardConverter) convertOverflow(prop cardObj) string {
|
|
options, _ := prop["options"].([]interface{})
|
|
if len(options) == 0 {
|
|
return ""
|
|
}
|
|
var optTexts []string
|
|
for _, opt := range options {
|
|
om, ok := opt.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if textElem, ok := om["text"].(cardObj); ok {
|
|
if text := c.extractTextContent(textElem); text != "" {
|
|
optTexts = append(optTexts, text)
|
|
}
|
|
}
|
|
}
|
|
return "⋮ " + strings.Join(optTexts, ", ")
|
|
}
|
|
|
|
func (c *cardConverter) convertSelect(prop cardObj, id string, isMulti bool) string {
|
|
options, _ := prop["options"].([]interface{})
|
|
|
|
selectedValues := map[string]bool{}
|
|
if isMulti {
|
|
if vals, ok := prop["selectedValues"].([]interface{}); ok {
|
|
for _, v := range vals {
|
|
if s, ok := v.(string); ok {
|
|
selectedValues[s] = true
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if init, ok := prop["initialOption"].(string); ok {
|
|
selectedValues[init] = true
|
|
}
|
|
if idx, ok := prop["initialIndex"].(float64); ok {
|
|
i := int(idx)
|
|
if i >= 0 && i < len(options) {
|
|
if opt, ok := options[i].(cardObj); ok {
|
|
if val, ok := opt["value"].(string); ok {
|
|
selectedValues[val] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var optionTexts []string
|
|
hasSelected := false
|
|
for _, opt := range options {
|
|
om, ok := opt.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
optText := ""
|
|
if textElem, ok := om["text"].(cardObj); ok {
|
|
optText = c.extractTextContent(textElem)
|
|
}
|
|
if optText == "" {
|
|
optText, _ = om["value"].(string)
|
|
}
|
|
if optText == "" {
|
|
continue
|
|
}
|
|
value, _ := om["value"].(string)
|
|
if selectedValues[value] {
|
|
optText = "✓" + optText
|
|
hasSelected = true
|
|
}
|
|
optionTexts = append(optionTexts, optText)
|
|
}
|
|
|
|
if len(optionTexts) == 0 {
|
|
placeholder := "Please select"
|
|
if phElem, ok := prop["placeholder"].(cardObj); ok {
|
|
if ph := c.extractTextContent(phElem); ph != "" {
|
|
placeholder = ph
|
|
}
|
|
}
|
|
optionTexts = append(optionTexts, placeholder+" ▼")
|
|
} else if !hasSelected {
|
|
optionTexts[len(optionTexts)-1] += " ▼"
|
|
}
|
|
|
|
result := "{" + strings.Join(optionTexts, " / ") + "}"
|
|
if c.mode == cardModeDetailed {
|
|
var attrs []string
|
|
if isMulti {
|
|
attrs = append(attrs, "multi")
|
|
}
|
|
if strings.Contains(id, "person") {
|
|
attrs = append(attrs, "type:person")
|
|
}
|
|
if len(attrs) > 0 {
|
|
result += "(" + strings.Join(attrs, " ") + ")"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertSelectImg(prop cardObj, _ string) string {
|
|
options, _ := prop["options"].([]interface{})
|
|
if len(options) == 0 {
|
|
return ""
|
|
}
|
|
selectedValues := map[string]bool{}
|
|
if vals, ok := prop["selectedValues"].([]interface{}); ok {
|
|
for _, v := range vals {
|
|
if s, ok := v.(string); ok {
|
|
selectedValues[s] = true
|
|
}
|
|
}
|
|
}
|
|
var optTexts []string
|
|
for i, opt := range options {
|
|
om, ok := opt.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
value, _ := om["value"].(string)
|
|
text := fmt.Sprintf("🖼️ Image %d", i+1)
|
|
if selectedValues[value] {
|
|
text = "✓" + text
|
|
}
|
|
optTexts = append(optTexts, text)
|
|
}
|
|
return "{" + strings.Join(optTexts, " / ") + "}"
|
|
}
|
|
|
|
func (c *cardConverter) convertInput(prop cardObj, _ string) string {
|
|
label := ""
|
|
if labelElem, ok := prop["label"].(cardObj); ok {
|
|
label = c.extractTextContent(labelElem)
|
|
}
|
|
|
|
defaultValue, _ := prop["defaultValue"].(string)
|
|
placeholder := ""
|
|
if phElem, ok := prop["placeholder"].(cardObj); ok {
|
|
placeholder = c.extractTextContent(phElem)
|
|
}
|
|
|
|
var result string
|
|
switch {
|
|
case defaultValue != "":
|
|
result = defaultValue + "___"
|
|
case placeholder != "":
|
|
result = placeholder + "_____"
|
|
default:
|
|
result = "_____"
|
|
}
|
|
|
|
if label != "" {
|
|
result = label + ": " + result
|
|
}
|
|
|
|
if inputType, _ := prop["inputType"].(string); inputType == "multiline_text" {
|
|
result = strings.ReplaceAll(result, "_____", "...")
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertDatePicker(prop cardObj, _ string, pickerType string) string {
|
|
var emoji, value string
|
|
switch pickerType {
|
|
case "date":
|
|
emoji = "📅"
|
|
value, _ = prop["initialDate"].(string)
|
|
case "time":
|
|
emoji = "🕐"
|
|
value, _ = prop["initialTime"].(string)
|
|
case "datetime":
|
|
emoji = "📅"
|
|
value, _ = prop["initialDatetime"].(string)
|
|
default:
|
|
emoji = "📅"
|
|
}
|
|
|
|
if value != "" {
|
|
value = cardNormalizeTimeFormat(value)
|
|
}
|
|
if value == "" {
|
|
placeholder := "Select"
|
|
if phElem, ok := prop["placeholder"].(cardObj); ok {
|
|
if ph := c.extractTextContent(phElem); ph != "" {
|
|
placeholder = ph
|
|
}
|
|
}
|
|
value = placeholder
|
|
}
|
|
return emoji + " " + value
|
|
}
|
|
|
|
func (c *cardConverter) convertChecker(prop cardObj, id string) string {
|
|
checked, _ := prop["checked"].(bool)
|
|
checkMark := "[ ]"
|
|
if checked {
|
|
checkMark = "[x]"
|
|
}
|
|
text := ""
|
|
if textElem, ok := prop["text"].(cardObj); ok {
|
|
text = c.extractTextContent(textElem)
|
|
}
|
|
result := checkMark + " " + text
|
|
if c.mode == cardModeDetailed && id != "" {
|
|
result += "(id:" + id + ")"
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertImage(prop cardObj, _ string) string {
|
|
alt := "Image"
|
|
if altElem, ok := prop["alt"].(cardObj); ok {
|
|
if altText := c.extractTextContent(altElem); altText != "" {
|
|
alt = altText
|
|
}
|
|
}
|
|
if titleElem, ok := prop["title"].(cardObj); ok {
|
|
if titleText := c.extractTextContent(titleElem); titleText != "" {
|
|
alt = titleText
|
|
}
|
|
}
|
|
|
|
result := "🖼️ " + alt
|
|
if c.mode == cardModeDetailed {
|
|
if imageID, ok := prop["imageID"].(string); ok && imageID != "" {
|
|
if token := c.getImageToken(imageID); token != "" {
|
|
result += "(img_token:" + token + ")"
|
|
} else {
|
|
result += "(img_key:" + imageID + ")"
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertImgCombination(prop cardObj) string {
|
|
imgList, _ := prop["imgList"].([]interface{})
|
|
if len(imgList) == 0 {
|
|
return ""
|
|
}
|
|
result := fmt.Sprintf("🖼️ %d image(s)", len(imgList))
|
|
if c.mode == cardModeDetailed {
|
|
var keys []string
|
|
for _, img := range imgList {
|
|
im, ok := img.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if imageID, ok := im["imageID"].(string); ok && imageID != "" {
|
|
keys = append(keys, imageID)
|
|
}
|
|
}
|
|
if len(keys) > 0 {
|
|
result += "(keys:" + strings.Join(keys, ",") + ")"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertChart(prop cardObj, _ string) string {
|
|
title := "Chart"
|
|
chartType := ""
|
|
|
|
if chartSpec, ok := prop["chartSpec"].(cardObj); ok {
|
|
if titleObj, ok := chartSpec["title"].(cardObj); ok {
|
|
if text, ok := titleObj["text"].(string); ok && text != "" {
|
|
title = text
|
|
}
|
|
}
|
|
if ct, ok := chartSpec["type"].(string); ok && ct != "" {
|
|
chartType = ct
|
|
if typeName, ok := cardChartTypeNames[ct]; ok {
|
|
title += typeName
|
|
}
|
|
}
|
|
}
|
|
|
|
summary := c.extractChartSummary(prop, chartType)
|
|
result := "📊 " + title
|
|
if summary != "" {
|
|
result += "\nSummary: " + summary
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) extractChartSummary(prop cardObj, chartType string) string {
|
|
chartSpec, ok := prop["chartSpec"].(cardObj)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
dataObj, ok := chartSpec["data"].(cardObj)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
values, ok := dataObj["values"].([]interface{})
|
|
if !ok || len(values) == 0 {
|
|
return ""
|
|
}
|
|
|
|
switch chartType {
|
|
case "line", "bar", "area":
|
|
xField, _ := chartSpec["xField"].(string)
|
|
yField, _ := chartSpec["yField"].(string)
|
|
if xField == "" || yField == "" {
|
|
return fmt.Sprintf("%d data point(s)", len(values))
|
|
}
|
|
var parts []string
|
|
for _, v := range values {
|
|
vm, ok := v.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%v:%v", vm[xField], vm[yField]))
|
|
}
|
|
if len(parts) > 0 {
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
case "pie":
|
|
catField, _ := chartSpec["categoryField"].(string)
|
|
valField, _ := chartSpec["valueField"].(string)
|
|
if catField == "" || valField == "" {
|
|
return fmt.Sprintf("%d data point(s)", len(values))
|
|
}
|
|
var parts []string
|
|
for _, v := range values {
|
|
vm, ok := v.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
parts = append(parts, fmt.Sprintf("%v:%v", vm[catField], vm[valField]))
|
|
}
|
|
if len(parts) > 0 {
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
}
|
|
return fmt.Sprintf("%d data point(s)", len(values))
|
|
}
|
|
|
|
func (c *cardConverter) convertAudio(prop cardObj, _ string) string {
|
|
result := "🎵 Audio"
|
|
if c.mode == cardModeDetailed {
|
|
fileID, _ := prop["fileID"].(string)
|
|
if fileID == "" {
|
|
fileID, _ = prop["audioID"].(string)
|
|
}
|
|
if fileID != "" {
|
|
result += "(key:" + fileID + ")"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertVideo(prop cardObj, _ string) string {
|
|
result := "🎬 Video"
|
|
if c.mode == cardModeDetailed {
|
|
fileID, _ := prop["fileID"].(string)
|
|
if fileID == "" {
|
|
fileID, _ = prop["videoID"].(string)
|
|
}
|
|
if fileID != "" {
|
|
result += "(key:" + fileID + ")"
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertTable(prop cardObj) string {
|
|
columns, _ := prop["columns"].([]interface{})
|
|
if len(columns) == 0 {
|
|
return ""
|
|
}
|
|
rows, _ := prop["rows"].([]interface{})
|
|
|
|
var colNames, colKeys []string
|
|
for _, col := range columns {
|
|
cm, ok := col.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
displayName, _ := cm["displayName"].(string)
|
|
name, _ := cm["name"].(string)
|
|
if displayName == "" {
|
|
displayName = name
|
|
}
|
|
colNames = append(colNames, displayName)
|
|
colKeys = append(colKeys, name)
|
|
}
|
|
|
|
var lines []string
|
|
lines = append(lines, "| "+strings.Join(colNames, " | ")+" |")
|
|
separator := "|"
|
|
for range colNames {
|
|
separator += "------|"
|
|
}
|
|
lines = append(lines, separator)
|
|
|
|
for _, row := range rows {
|
|
rm, ok := row.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
var cells []string
|
|
for _, key := range colKeys {
|
|
cellValue := ""
|
|
if cellData, ok := rm[key].(cardObj); ok {
|
|
if cellData["data"] != nil {
|
|
cellValue = c.extractTableCellValue(cellData["data"])
|
|
}
|
|
}
|
|
cells = append(cells, cellValue)
|
|
}
|
|
lines = append(lines, "| "+strings.Join(cells, " | ")+" |")
|
|
}
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func (c *cardConverter) extractTableCellValue(data interface{}) string {
|
|
switch v := data.(type) {
|
|
case string:
|
|
return v
|
|
case float64:
|
|
return strconv.FormatFloat(v, 'f', 2, 64)
|
|
case []interface{}:
|
|
var texts []string
|
|
for _, item := range v {
|
|
im, ok := item.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if text, ok := im["text"].(string); ok {
|
|
texts = append(texts, "「"+text+"」")
|
|
}
|
|
}
|
|
return strings.Join(texts, " ")
|
|
default:
|
|
if m, ok := data.(cardObj); ok {
|
|
return c.extractTextContent(m)
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (c *cardConverter) convertPerson(prop cardObj, _ string) string {
|
|
userID, _ := prop["userID"].(string)
|
|
if userID == "" {
|
|
return ""
|
|
}
|
|
personName := c.lookupPersonName(userID)
|
|
if personName == "" {
|
|
if notation, ok := prop["notation"].(cardObj); ok {
|
|
personName = c.extractTextContent(notation)
|
|
}
|
|
}
|
|
if personName != "" {
|
|
if c.mode == cardModeDetailed {
|
|
return fmt.Sprintf("@%s(open_id:%s)", personName, userID)
|
|
}
|
|
return "@" + personName
|
|
}
|
|
if c.mode == cardModeDetailed {
|
|
return fmt.Sprintf("@user(open_id:%s)", userID)
|
|
}
|
|
return "@" + userID
|
|
}
|
|
|
|
// convertPersonV1 handles the v1 card schema person element.
|
|
// [#20] NOTE: this function duplicates ~20 lines from convertPerson with the only difference
|
|
// being the absence of the `notation` fallback block. Ideally it should delegate to
|
|
// convertPerson, but doing so would introduce the notation fallback for v1 schema elements
|
|
// (subtle behavior change). Not merged to preserve identical output behavior.
|
|
func (c *cardConverter) convertPersonV1(prop cardObj, _ string) string {
|
|
userID, _ := prop["userID"].(string)
|
|
if userID == "" {
|
|
return ""
|
|
}
|
|
personName := c.lookupPersonName(userID)
|
|
if personName != "" {
|
|
if c.mode == cardModeDetailed {
|
|
return fmt.Sprintf("@%s(open_id:%s)", personName, userID)
|
|
}
|
|
return "@" + personName
|
|
}
|
|
if c.mode == cardModeDetailed {
|
|
return fmt.Sprintf("@user(open_id:%s)", userID)
|
|
}
|
|
return "@" + userID
|
|
}
|
|
|
|
func (c *cardConverter) convertPersonList(prop cardObj) string {
|
|
persons, _ := prop["persons"].([]interface{})
|
|
if len(persons) == 0 {
|
|
return ""
|
|
}
|
|
var names []string
|
|
for _, person := range persons {
|
|
pm, ok := person.(cardObj)
|
|
if !ok {
|
|
continue
|
|
}
|
|
personID, _ := pm["id"].(string)
|
|
if c.mode == cardModeDetailed && personID != "" {
|
|
names = append(names, fmt.Sprintf("@user(id:%s)", personID))
|
|
} else {
|
|
names = append(names, "@user")
|
|
}
|
|
}
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
func (c *cardConverter) convertAvatar(prop cardObj, _ string) string {
|
|
userID, _ := prop["userID"].(string)
|
|
result := "👤"
|
|
if c.mode == cardModeDetailed && userID != "" {
|
|
result += "(id:" + userID + ")"
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (c *cardConverter) convertAt(prop cardObj) string {
|
|
userID, _ := prop["userID"].(string)
|
|
if userID == "" {
|
|
return ""
|
|
}
|
|
userName := ""
|
|
actualUserID := ""
|
|
if c.attachment != nil {
|
|
if atUsers, ok := c.attachment["at_users"].(cardObj); ok {
|
|
if userInfo, ok := atUsers[userID].(cardObj); ok {
|
|
userName, _ = userInfo["content"].(string)
|
|
actualUserID, _ = userInfo["user_id"].(string)
|
|
}
|
|
}
|
|
}
|
|
if userName != "" {
|
|
if c.mode == cardModeDetailed {
|
|
if actualUserID != "" {
|
|
return fmt.Sprintf("@%s(user_id:%s)", userName, actualUserID)
|
|
}
|
|
return fmt.Sprintf("@%s(open_id:%s)", userName, userID)
|
|
}
|
|
return "@" + userName
|
|
}
|
|
if c.mode == cardModeDetailed {
|
|
if actualUserID != "" {
|
|
return fmt.Sprintf("@user(user_id:%s)", actualUserID)
|
|
}
|
|
return fmt.Sprintf("@user(open_id:%s)", userID)
|
|
}
|
|
return "@" + userID
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
func (c *cardConverter) lookupPersonName(userID string) string {
|
|
if c.attachment == nil {
|
|
return ""
|
|
}
|
|
if persons, ok := c.attachment["persons"].(cardObj); ok {
|
|
if person, ok := persons[userID].(cardObj); ok {
|
|
if content, ok := person["content"].(string); ok {
|
|
return content
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (c *cardConverter) getImageToken(imageID string) string {
|
|
if c.attachment == nil {
|
|
return ""
|
|
}
|
|
if images, ok := c.attachment["images"].(cardObj); ok {
|
|
if imageInfo, ok := images[imageID].(cardObj); ok {
|
|
if token, ok := imageInfo["token"].(string); ok {
|
|
return token
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type cardTextStyle struct {
|
|
bold bool
|
|
italic bool
|
|
strikethrough bool
|
|
}
|
|
|
|
func (c *cardConverter) extractTextStyle(prop cardObj) cardTextStyle {
|
|
style := cardTextStyle{}
|
|
textStyle, ok := prop["textStyle"].(cardObj)
|
|
if !ok {
|
|
return style
|
|
}
|
|
attrs, _ := textStyle["attributes"].([]interface{})
|
|
for _, attr := range attrs {
|
|
s, ok := attr.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
switch s {
|
|
case "bold":
|
|
style.bold = true
|
|
case "italic":
|
|
style.italic = true
|
|
case "strikethrough":
|
|
style.strikethrough = true
|
|
}
|
|
}
|
|
return style
|
|
}
|
|
|
|
func (c *cardConverter) applyTextStyle(content string, prop cardObj) string {
|
|
if content == "" {
|
|
return content
|
|
}
|
|
style := c.extractTextStyle(prop)
|
|
if style.strikethrough {
|
|
content = "~~" + content + "~~"
|
|
}
|
|
if style.italic {
|
|
content = "*" + content + "*"
|
|
}
|
|
if style.bold {
|
|
content = "**" + content + "**"
|
|
}
|
|
return content
|
|
}
|
|
|
|
// ── Utility functions ─────────────────────────────────────────────────────────
|
|
|
|
func cardEscapeAttr(s string) string {
|
|
return cardAttrEscaper.Replace(s)
|
|
}
|
|
|
|
var cardAttrEscaper = strings.NewReplacer(
|
|
`\`, `\\`,
|
|
`"`, `\"`,
|
|
"\n", `\n`,
|
|
"\r", `\r`,
|
|
"\t", `\t`,
|
|
)
|
|
|
|
func cardFormatMillisToISO8601(ms string) string {
|
|
n, err := strconv.ParseInt(ms, 10, 64)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
t := time.Unix(n/1000, (n%1000)*int64(time.Millisecond)).UTC()
|
|
return t.Format(time.RFC3339)
|
|
}
|
|
|
|
func cardNormalizeTimeFormat(input string) string {
|
|
if input == "" {
|
|
return ""
|
|
}
|
|
n, err := strconv.ParseInt(input, 10, 64)
|
|
if err == nil {
|
|
if len(input) >= 13 {
|
|
t := time.Unix(n/1000, (n%1000)*int64(time.Millisecond)).UTC()
|
|
return t.Format(time.RFC3339)
|
|
} else if len(input) >= 10 {
|
|
t := time.Unix(n, 0).UTC()
|
|
return t.Format(time.RFC3339)
|
|
}
|
|
}
|
|
// Already ISO8601 or date/time string
|
|
return input
|
|
}
|