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 @@ -75,6 +75,7 @@
"mode": runtime.Str("mode"),
}
if v := runtime.Str("markdown"); v != "" {
WarnCalloutType(v, runtime.IO().ErrOut)

Check warning on line 78 in shortcuts/doc/docs_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update.go#L78

Added line #L78 was not covered by tests
args["markdown"] = v
}
if v := runtime.Str("selection-with-ellipsis"); v != "" {
Expand Down Expand Up @@ -108,6 +109,7 @@
"mode": mode,
}
if markdown != "" {
WarnCalloutType(markdown, runtime.IO().ErrOut)

Check warning on line 112 in shortcuts/doc/docs_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update.go#L112

Added line #L112 was not covered by tests
args["markdown"] = markdown
}
if v := runtime.Str("selection-with-ellipsis"); v != "" {
Expand Down
67 changes: 67 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"
"unicode"
Expand Down Expand Up @@ -306,6 +308,71 @@
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=(?:"([^"]*)"|'([^']*)')`)

// calloutBackgroundColorAttrRe matches a background-color= attribute name
// with optional whitespace around the equals sign, so forms like
// `background-color="..."` and `background-color = "..."` are both accepted.
var calloutBackgroundColorAttrRe = regexp.MustCompile(`\bbackground-color\s*=`)

// 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.
Comment thread
herbertliu marked this conversation as resolved.
func WarnCalloutType(md string, w io.Writer) {
Comment thread
herbertliu marked this conversation as resolved.
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 calloutBackgroundColorAttrRe.MatchString(attrs) {
return tag
}
parts := calloutTypeAttrRe.FindStringSubmatch(attrs)
if len(parts) < 2 {
return tag // no type= attribute
}

Check warning on line 359 in shortcuts/doc/markdown_fix.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/markdown_fix.go#L358-L359

Added lines #L358 - L359 were not covered by tests
// 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
79 changes: 79 additions & 0 deletions shortcuts/doc/markdown_fix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,85 @@ 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: "whitespace around equals is tolerated in background-color",
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