Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion shortcuts/doc/docs_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ var DocsCreate = common.Shortcut{
}

func buildDocsCreateArgs(runtime *common.RuntimeContext) map[string]interface{} {
md := runtime.Str("markdown")
WarnCalloutType(md, runtime.IO().ErrOut)
args := map[string]interface{}{
"markdown": runtime.Str("markdown"),
"markdown": md,
}
if v := runtime.Str("title"); v != "" {
args["title"] = v
Expand Down
2 changes: 2 additions & 0 deletions shortcuts/doc/docs_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ var DocsUpdate = common.Shortcut{
"mode": runtime.Str("mode"),
}
if v := runtime.Str("markdown"); v != "" {
WarnCalloutType(v, runtime.IO().ErrOut)
args["markdown"] = v
}
if v := runtime.Str("selection-with-ellipsis"); v != "" {
Expand All @@ -94,6 +95,7 @@ var DocsUpdate = common.Shortcut{
"mode": runtime.Str("mode"),
}
if v := runtime.Str("markdown"); v != "" {
WarnCalloutType(v, runtime.IO().ErrOut)
args["markdown"] = v
}
if v := runtime.Str("selection-with-ellipsis"); v != "" {
Expand Down
62 changes: 62 additions & 0 deletions shortcuts/doc/markdown_fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package doc

import (
"fmt"
"io"
"regexp"
"strings"
)
Expand Down Expand Up @@ -220,6 +222,66 @@ func fixSetextAmbiguity(md string) string {
return setextRe.ReplaceAllString(md, "$1\n\n$2")
}

// calloutTypeColors maps the semantic type= shorthand to a recommended
// [background-color, border-color] pair for Feishu callout blocks.
// Used only for hint messages β€” the Markdown itself is never rewritten.
var calloutTypeColors = map[string][2]string{
"warning": {"light-yellow", "yellow"},
"caution": {"light-orange", "orange"},
"note": {"light-blue", "blue"},
"info": {"light-blue", "blue"},
"tip": {"light-green", "green"},
"success": {"light-green", "green"},
"check": {"light-green", "green"},
"error": {"light-red", "red"},
"danger": {"light-red", "red"},
"important": {"light-purple", "purple"},
}

// calloutOpenTagRe matches a <callout …> opening tag.
var calloutOpenTagRe = regexp.MustCompile(`<callout(\s[^>]*)?>`)

// calloutTypeAttrRe extracts the value of a type= attribute (single or double
// quoted) from a callout opening tag's attribute string.
var calloutTypeAttrRe = regexp.MustCompile(`\btype=(?:"([^"]*)"|'([^']*)')`)

// WarnCalloutType scans md for callout tags that carry a type= attribute but
// no background-color= attribute, then writes a hint line to w for each one
// suggesting the explicit Feishu color attributes to use instead.
//
// The Markdown is not modified β€” the caller is responsible for acting on the
// hints or ignoring them. This keeps the create/update path transparent: user
// input reaches create-doc exactly as written.
func WarnCalloutType(md string, w io.Writer) {
calloutOpenTagRe.ReplaceAllStringFunc(md, func(tag string) string {
attrs := ""
if m := calloutOpenTagRe.FindStringSubmatch(tag); len(m) == 2 {
attrs = m[1]
}
// Skip tags that already carry an explicit background-color.
if strings.Contains(attrs, "background-color=") {
return tag
Comment on lines +261 to +263
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟑 Minor

Harden background-color detection to avoid false hints.

Line 262 only checks the exact substring background-color=. It misses valid forms like background-color = "light-yellow" and can emit incorrect hints.

πŸ”§ Suggested patch
 var calloutTypeAttrRe = regexp.MustCompile(`\btype=(?:"([^"]*)"|'([^']*)')`)
+var calloutBackgroundColorAttrRe = regexp.MustCompile(`\bbackground-color\s*=`)

 func WarnCalloutType(md string, w io.Writer) {
 	calloutOpenTagRe.ReplaceAllStringFunc(md, func(tag string) string {
 		attrs := ""
 		if m := calloutOpenTagRe.FindStringSubmatch(tag); len(m) == 2 {
 			attrs = m[1]
 		}
 		// Skip tags that already carry an explicit background-color.
-		if strings.Contains(attrs, "background-color=") {
+		if calloutBackgroundColorAttrRe.MatchString(attrs) {
 			return tag
 		}
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/markdown_fix.go` around lines 261 - 263, The check that skips
tags with an explicit background color is too strict: it only looks for the
literal substring "background-color=" using strings.Contains(attrs,
"background-color="), so forms like 'background-color = "light-yellow"' or with
varied spacing/quotes are missed; update the detection in the markdown_fix.go
logic that currently inspects attrs (the variable checked and returns tag) to
use a whitespace-tolerant match (e.g., a regex or normalize attrs by collapsing
whitespace) to detect "background-color" followed by optional spaces and an '='
(background-color\s*=?), and then skip returning the hint when that match is
present. Ensure the change targets the same check that returns tag and continues
to accept other attributes unchanged.

}
parts := calloutTypeAttrRe.FindStringSubmatch(attrs)
if len(parts) < 2 {
return tag // no type= attribute
}
// parts[1] is the double-quoted capture, parts[2] is single-quoted.
typeName := parts[1]
if typeName == "" {
typeName = parts[2]
}
colors, ok := calloutTypeColors[typeName]
if !ok {
return tag // unknown type β€” no hint to give
}
fmt.Fprintf(w,
"hint: callout type=%q has no background-color; consider: background-color=%q border-color=%q\n",
typeName, colors[0], colors[1])
return tag
})
}

// calloutEmojiAliases maps named emoji strings that fetch-doc emits to actual
// Unicode emoji characters that create-doc accepts.
var calloutEmojiAliases = map[string]string{
Expand Down
74 changes: 74 additions & 0 deletions shortcuts/doc/markdown_fix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,80 @@ func TestFixExportedMarkdown(t *testing.T) {
}
}

func TestWarnCalloutType(t *testing.T) {
tests := []struct {
name string
input string
wantHint bool // whether a hint line is expected
hintContains string // substring the hint must contain
}{
{
name: "warning type without background-color emits hint",
input: `<callout type="warning" emoji="πŸ“">`,
wantHint: true,
hintContains: `background-color="light-yellow"`,
},
{
name: "info type without background-color emits hint",
input: `<callout type="info" emoji="ℹ️">`,
wantHint: true,
hintContains: `background-color="light-blue"`,
},
{
name: "single-quoted type attribute emits hint",
input: `<callout type='warning' emoji="πŸ“">`,
wantHint: true,
hintContains: `background-color="light-yellow"`,
},
{
name: "explicit background-color suppresses hint",
input: `<callout type="warning" emoji="πŸ“" background-color="light-red">`,
wantHint: false,
},
{
name: "unknown type emits no hint",
input: `<callout type="custom" emoji="πŸ”₯">`,
wantHint: false,
},
{
name: "no type attribute emits no hint",
input: `<callout emoji="πŸ’‘" background-color="light-green">`,
wantHint: false,
},
{
name: "non-callout tag emits no hint",
input: `<div type="warning">`,
wantHint: false,
},
{
name: "hint includes border-color suggestion",
input: `<callout type="error" emoji="❌">`,
wantHint: true,
hintContains: `border-color="red"`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf strings.Builder
WarnCalloutType(tt.input, &buf)
got := buf.String()
if tt.wantHint {
if got == "" {
t.Errorf("WarnCalloutType(%q): expected hint, got no output", tt.input)
return
}
if tt.hintContains != "" && !strings.Contains(got, tt.hintContains) {
t.Errorf("WarnCalloutType(%q): hint %q missing %q", tt.input, got, tt.hintContains)
}
} else {
if got != "" {
t.Errorf("WarnCalloutType(%q): expected no output, got %q", tt.input, got)
}
}
})
}
}

func TestFixCalloutEmoji(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading