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
2 changes: 1 addition & 1 deletion shortcuts/doc/docs_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions shortcuts/doc/docs_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
63 changes: 63 additions & 0 deletions shortcuts/doc/markdown_fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -220,6 +229,60 @@ func fixSetextAmbiguity(md string) string {
return setextRe.ReplaceAllString(md, "$1\n\n$2")
}

// calloutTypeColors maps callout type="<name>" 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 <callout …> opening tag so individual attributes can
// be inspected and patched.
var calloutTypeRe = regexp.MustCompile(`<callout(\s[^>]*)?>`)

// fixCalloutType expands the semantic type="<name>" 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
Comment on lines +262 to +265
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

fixCalloutType misses single-quoted type attributes.

Line 262 only matches type="...". Tags like <callout type='warning'> are left unchanged, so shorthand expansion silently fails.

[sraise_minor_fix_note]

πŸ”§ Proposed fix
 var calloutTypeRe = regexp.MustCompile(`<callout(\s[^>]*)?>`)
+var calloutTypeAttrRe = regexp.MustCompile(`\btype=(?:"([^"]*)"|'([^']*)')`)

 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 {
+		typeParts := calloutTypeAttrRe.FindStringSubmatch(attrs)
+		if len(typeParts) != 3 {
 			return tag // no type= attribute
 		}
-		typeName := typeParts[1]
+		typeName := typeParts[1]
+		if typeName == "" {
+			typeName = typeParts[2]
+		}
 		colors, ok := calloutTypeColors[typeName]
 		if !ok {
 			return tag // unknown type β€” leave as-is
 		}
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
typeRe := regexp.MustCompile(`\btype="([^"]*)"`)
typeParts := typeRe.FindStringSubmatch(attrs)
if len(typeParts) != 2 {
return tag // no type= attribute
var calloutTypeRe = regexp.MustCompile(`<callout(\s[^>]*)?>`)
var calloutTypeAttrRe = regexp.MustCompile(`\btype=(?:"([^"]*)"|'([^']*)')`)
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]
}
typeParts := calloutTypeAttrRe.FindStringSubmatch(attrs)
if len(typeParts) != 3 {
return tag // no type= attribute
}
typeName := typeParts[1]
if typeName == "" {
typeName = typeParts[2]
}
colors, ok := calloutTypeColors[typeName]
if !ok {
return tag // unknown type β€” leave as-is
}
πŸ€– 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 262 - 265, fixCalloutType
currently uses typeRe := regexp.MustCompile(`\btype="([^"]*)"`) which only
matches double-quoted attributes and leaves single-quoted callouts (e.g.,
<callout type='warning'>) unchanged; update the regex used in fixCalloutType
(variable typeRe and its use with attrs and tag) to accept either single or
double quotes (for example use a pattern that captures quote with backreference
or a pattern like \btype=(?:'|")([^'"]*)(?:'|")) and adjust how you read the
capture groups from typeParts accordingly so single-quoted and double-quoted
type attributes are both handled.

}
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{
Expand Down
72 changes: 72 additions & 0 deletions shortcuts/doc/markdown_fix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: `<callout type="warning" emoji="πŸ“">`,
want: `<callout type="warning" emoji="πŸ“" background-color="light-yellow" border-color="yellow">`,
},
{
name: "info type gets light-blue",
input: `<callout type="info" emoji="ℹ️">`,
want: `<callout type="info" emoji="ℹ️" background-color="light-blue" border-color="blue">`,
},
{
name: "tip type gets light-green",
input: `<callout type="tip" emoji="πŸ’‘">`,
want: `<callout type="tip" emoji="πŸ’‘" background-color="light-green" border-color="green">`,
},
{
name: "error type gets light-red",
input: `<callout type="error" emoji="❌">`,
want: `<callout type="error" emoji="❌" background-color="light-red" border-color="red">`,
},
{
name: "important type gets light-purple",
input: `<callout type="important" emoji="❗">`,
want: `<callout type="important" emoji="❗" background-color="light-purple" border-color="purple">`,
},
{
name: "caution type gets light-orange",
input: `<callout type="caution" emoji="⚠️">`,
want: `<callout type="caution" emoji="⚠️" background-color="light-orange" border-color="orange">`,
},
{
name: "explicit background-color is preserved",
input: `<callout type="warning" emoji="πŸ“" background-color="light-red">`,
want: `<callout type="warning" emoji="πŸ“" background-color="light-red">`,
},
{
name: "explicit border-color is preserved when background-color absent",
input: `<callout type="info" emoji="ℹ️" border-color="red">`,
want: `<callout type="info" emoji="ℹ️" border-color="red" background-color="light-blue">`,
},
{
name: "unknown type left unchanged",
input: `<callout type="custom" emoji="πŸ”₯">`,
want: `<callout type="custom" emoji="πŸ”₯">`,
},
{
name: "no type attribute left unchanged",
input: `<callout emoji="πŸ’‘" background-color="light-green">`,
want: `<callout emoji="πŸ’‘" background-color="light-green">`,
},
{
name: "non-callout tag unchanged",
input: `<div type="warning">`,
want: `<div type="warning">`,
},
}
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
Expand Down
Loading