diff --git a/shortcuts/doc/docs_create.go b/shortcuts/doc/docs_create.go index 69ec15c8..7f7b94b1 100644 --- a/shortcuts/doc/docs_create.go +++ b/shortcuts/doc/docs_create.go @@ -68,7 +68,7 @@ var DocsCreate = common.Shortcut{ func buildDocsCreateArgs(runtime *common.RuntimeContext) map[string]interface{} { args := map[string]interface{}{ - "markdown": runtime.Str("markdown"), + "markdown": prepareMarkdownForCreate(runtime.Str("markdown")), } if v := runtime.Str("title"); v != "" { args["title"] = v diff --git a/shortcuts/doc/docs_update.go b/shortcuts/doc/docs_update.go index ea80550e..13d7b0ba 100644 --- a/shortcuts/doc/docs_update.go +++ b/shortcuts/doc/docs_update.go @@ -71,7 +71,7 @@ var DocsUpdate = common.Shortcut{ "mode": runtime.Str("mode"), } if v := runtime.Str("markdown"); v != "" { - args["markdown"] = v + args["markdown"] = prepareMarkdownForCreate(v) } if v := runtime.Str("selection-with-ellipsis"); v != "" { args["selection_with_ellipsis"] = v @@ -94,7 +94,7 @@ var DocsUpdate = common.Shortcut{ "mode": runtime.Str("mode"), } if v := runtime.Str("markdown"); v != "" { - args["markdown"] = v + args["markdown"] = prepareMarkdownForCreate(v) } if v := runtime.Str("selection-with-ellipsis"); v != "" { args["selection_with_ellipsis"] = v diff --git a/shortcuts/doc/markdown_fix.go b/shortcuts/doc/markdown_fix.go index f9b28dcc..2ac98b69 100644 --- a/shortcuts/doc/markdown_fix.go +++ b/shortcuts/doc/markdown_fix.go @@ -31,12 +31,21 @@ import ( // 5. fixCalloutEmoji: replaces named emoji aliases (e.g. emoji="warning") with // actual Unicode emoji characters that create-doc understands. Applied only // outside fenced code blocks. +// +// prepareMarkdownForCreate applies fixes that should run on any Markdown before +// it is sent to the create-doc / update-doc MCP tools, regardless of whether +// the content originated from a Lark export or was written by hand. +func prepareMarkdownForCreate(md string) string { + return applyOutsideCodeFences(md, fixCalloutType) +} + func fixExportedMarkdown(md string) string { md = applyOutsideCodeFences(md, fixBoldSpacing) md = applyOutsideCodeFences(md, fixSetextAmbiguity) md = applyOutsideCodeFences(md, fixBlockquoteHardBreaks) md = fixTopLevelSoftbreaks(md) md = applyOutsideCodeFences(md, fixCalloutEmoji) + md = applyOutsideCodeFences(md, fixCalloutType) // Collapse runs of 3+ consecutive newlines into exactly 2 (one blank line), // but only outside fenced code blocks to preserve intentional blank lines in code. md = applyOutsideCodeFences(md, func(s string) string { @@ -220,6 +229,60 @@ func fixSetextAmbiguity(md string) string { return setextRe.ReplaceAllString(md, "$1\n\n$2") } +// calloutTypeColors maps callout type="" to a [background-color, border-color] pair. +// When a callout tag has type= but no background-color=, these defaults are applied. +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"}, +} + +// calloutTypeRe matches a opening tag so individual attributes can +// be inspected and patched. +var calloutTypeRe = regexp.MustCompile(`]*)?>`) + +// fixCalloutType expands the semantic type="" shorthand on callout tags +// into explicit background-color= (and border-color= when absent). When a +// background-color is already present the tag is left unchanged so that an +// explicit color always wins. +func fixCalloutType(md string) string { + return calloutTypeRe.ReplaceAllStringFunc(md, func(tag string) string { + attrs := "" + if m := calloutTypeRe.FindStringSubmatch(tag); len(m) == 2 { + attrs = m[1] + } + // Extract type value. + typeRe := regexp.MustCompile(`\btype="([^"]*)"`) + typeParts := typeRe.FindStringSubmatch(attrs) + if len(typeParts) != 2 { + return tag // no type= attribute + } + typeName := typeParts[1] + colors, ok := calloutTypeColors[typeName] + if !ok { + return tag // unknown type — leave as-is + } + // Only inject background-color when it is absent. + if strings.Contains(attrs, "background-color=") { + return tag + } + // Inject background-color (and border-color when absent) before the + // closing >. + extra := ` background-color="` + colors[0] + `"` + if !strings.Contains(attrs, "border-color=") { + extra += ` border-color="` + colors[1] + `"` + } + return tag[:len(tag)-1] + extra + ">" + }) +} + // calloutEmojiAliases maps named emoji strings that fetch-doc emits to actual // Unicode emoji characters that create-doc accepts. var calloutEmojiAliases = map[string]string{ diff --git a/shortcuts/doc/markdown_fix_test.go b/shortcuts/doc/markdown_fix_test.go index b47ab778..b76b181e 100644 --- a/shortcuts/doc/markdown_fix_test.go +++ b/shortcuts/doc/markdown_fix_test.go @@ -252,6 +252,78 @@ func TestFixExportedMarkdown(t *testing.T) { } } +func TestFixCalloutType(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "warning type gets light-yellow background and border", + input: ``, + want: ``, + }, + { + name: "info type gets light-blue", + input: ``, + want: ``, + }, + { + name: "tip type gets light-green", + input: ``, + want: ``, + }, + { + name: "error type gets light-red", + input: ``, + want: ``, + }, + { + name: "important type gets light-purple", + input: ``, + want: ``, + }, + { + name: "caution type gets light-orange", + input: ``, + want: ``, + }, + { + name: "explicit background-color is preserved", + input: ``, + want: ``, + }, + { + name: "explicit border-color is preserved when background-color absent", + input: ``, + want: ``, + }, + { + name: "unknown type left unchanged", + input: ``, + want: ``, + }, + { + name: "no type attribute left unchanged", + input: ``, + want: ``, + }, + { + name: "non-callout tag unchanged", + input: `
`, + want: `
`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fixCalloutType(tt.input) + if got != tt.want { + t.Errorf("fixCalloutType(%q)\n got %q\n want %q", tt.input, got, tt.want) + } + }) + } +} + func TestFixCalloutEmoji(t *testing.T) { tests := []struct { name string