diff --git a/shortcuts/doc/docs_create_v2.go b/shortcuts/doc/docs_create_v2.go
index 413c4cbff..5a42b3709 100644
--- a/shortcuts/doc/docs_create_v2.go
+++ b/shortcuts/doc/docs_create_v2.go
@@ -27,6 +27,11 @@ func validateCreateV2(_ context.Context, runtime *common.RuntimeContext) error {
if runtime.Str("parent-token") != "" && runtime.Str("parent-position") != "" {
return common.FlagErrorf("--parent-token and --parent-position are mutually exclusive")
}
+ if runtime.Str("doc-format") == "markdown" {
+ if msg := CheckV2MarkdownCustomTags(runtime.Str("content")); msg != "" {
+ return common.FlagErrorf("%s", msg)
+ }
+ }
return nil
}
diff --git a/shortcuts/doc/docs_update_check.go b/shortcuts/doc/docs_update_check.go
index cf71c1012..9d41a5b0b 100644
--- a/shortcuts/doc/docs_update_check.go
+++ b/shortcuts/doc/docs_update_check.go
@@ -34,6 +34,11 @@ import (
// _**text**_ *__text__*
// Lark stores only one of the two emphases (usually italic), silently
// dropping the other. The user wanted both; they will get one.
+//
+// 3. Whole-paragraph style markers are not rendered by Lark. A line that
+// consists entirely of *…* or **…** (no other content) is treated as a
+// literal string rather than an italic or bold paragraph. The markers
+// appear verbatim in the document instead of applying style.
func docsUpdateWarnings(mode, markdown string) []string {
var warnings []string
if w := checkDocsUpdateReplaceMultilineMarkdown(mode, markdown); w != "" {
@@ -42,6 +47,9 @@ func docsUpdateWarnings(mode, markdown string) []string {
if w := checkDocsUpdateBoldItalic(markdown); w != "" {
warnings = append(warnings, w)
}
+ if w := checkDocsUpdateWholeParagraphStyle(markdown); w != "" {
+ warnings = append(warnings, w)
+ }
return warnings
}
@@ -279,3 +287,75 @@ func leadingRun(s string, c byte) string {
}
return s[:i]
}
+
+// wholeParagraphStyleRe matches a line whose entire content is a single
+// emphasis span: *…* or **…** (at least one non-whitespace char inside).
+// CJK and ASCII content both match; we deliberately do NOT match ***…***
+// because that is already covered by checkDocsUpdateBoldItalic.
+// wholeParagraphStyleRe matches a line whose entire content is exactly one
+// *italic* or **bold** span. Requirements: opener and closer must be the same
+// number of asterisks (1 or 2), content has no * or newline, and at least one
+// non-whitespace non-asterisk character is present (prevents `* *` matches).
+var wholeParagraphStyleRe = regexp.MustCompile(
+ `^(?:\*[^*\n]*[^\s*\n][^*\n]*\*|\*\*[^*\n]*[^\s*\n][^*\n]*\*\*)$`,
+)
+
+// checkDocsUpdateWholeParagraphStyle warns when a markdown paragraph
+// consists entirely of *italic* or **bold** markers. Lark's markdown
+// parser treats such lines as literal strings (the markers appear verbatim)
+// rather than styled paragraphs. This is distinct from inline emphasis
+// embedded in mixed-content lines, which Lark handles correctly.
+func checkDocsUpdateWholeParagraphStyle(markdown string) string {
+ if markdown == "" {
+ return ""
+ }
+ sanitized := stripMarkdownCodeRegions(markdown)
+ for _, line := range strings.Split(sanitized, "\n") {
+ trimmed := strings.TrimSpace(line)
+ if wholeParagraphStyleRe.MatchString(trimmed) {
+ return "line consisting entirely of *…* or **…** markers will not be rendered as italic/bold by Lark; " +
+ "the markers appear as literal characters. " +
+ "Mix the styled text with surrounding prose, or use Lark XML: … / …."
+ }
+ }
+ return ""
+}
+
+// v2MarkdownUnsupportedTags lists Lark custom XML tags that the v2
+// markdown parser does not understand. When present in markdown-mode
+// content, the parser silently strips or corrupts them — collapsing grid
+// columns, discarding lark-table rows, or escaping text-color tags to
+// literal characters. Use --doc-format xml to preserve these constructs.
+var v2MarkdownUnsupportedTags = []struct {
+ prefix string
+ display string
+}{
+ {""},
+ {""},
+ {""},
+ {""},
+ {""},
+}
+
+// CheckV2MarkdownCustomTags returns a non-empty error message when content
+// contains Lark custom tags that the v2 markdown parser silently corrupts.
+// Callers in Validate should return this as an error; callers in DryRun
+// may choose to surface it as a warning instead.
+func CheckV2MarkdownCustomTags(content string) string {
+ if content == "" {
+ return ""
+ }
+ var found []string
+ for _, t := range v2MarkdownUnsupportedTags {
+ if strings.Contains(content, t.prefix) {
+ found = append(found, t.display)
+ }
+ }
+ if len(found) == 0 {
+ return ""
+ }
+ return "--doc-format markdown does not support Lark custom tags (" +
+ strings.Join(found, ", ") + "); " +
+ "the v2 API will silently strip or corrupt them. " +
+ "Switch to --doc-format xml (the default) to preserve these blocks."
+}
diff --git a/shortcuts/doc/docs_update_check_test.go b/shortcuts/doc/docs_update_check_test.go
index 50905873a..26ecba24b 100644
--- a/shortcuts/doc/docs_update_check_test.go
+++ b/shortcuts/doc/docs_update_check_test.go
@@ -364,6 +364,19 @@ func TestDocsUpdateWarningsAggregates(t *testing.T) {
}
}
+func TestDocsUpdateWarningsWholeParagraphStyle(t *testing.T) {
+ t.Parallel()
+
+ // Whole-paragraph italic should surface via docsUpdateWarnings.
+ warnings := docsUpdateWarnings("append", "*整段斜体段落*")
+ if len(warnings) != 1 {
+ t.Fatalf("expected 1 warning, got %d: %v", len(warnings), warnings)
+ }
+ if !strings.Contains(warnings[0], "*…*") {
+ t.Fatalf("unexpected warning text: %q", warnings[0])
+ }
+}
+
func TestDocsUpdateWarningsEmpty(t *testing.T) {
t.Parallel()
@@ -373,3 +386,146 @@ func TestDocsUpdateWarningsEmpty(t *testing.T) {
t.Fatalf("expected no warnings, got: %v", warnings)
}
}
+
+func TestCheckV2MarkdownCustomTags(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ content string
+ wantHint bool
+ wantTag string
+ }{
+ {
+ name: "empty content produces no warning",
+ content: "",
+ wantHint: false,
+ },
+ {
+ name: "plain markdown without custom tags is fine",
+ content: "# Title\n\nSome **bold** paragraph.",
+ wantHint: false,
+ },
+ {
+ name: "callout tag is safe (supported by v2)",
+ content: `content`,
+ wantHint: false,
+ },
+ {
+ name: "grid tag triggers warning",
+ content: `A`,
+ wantHint: true,
+ wantTag: "",
+ },
+ {
+ name: "column tag triggers warning",
+ content: `col content`,
+ wantHint: true,
+ wantTag: "",
+ },
+ {
+ name: "lark-table tag triggers warning",
+ content: "\ncell\n",
+ wantHint: true,
+ wantTag: "",
+ },
+ {
+ name: "text color tag triggers warning",
+ content: `colored text`,
+ wantHint: true,
+ wantTag: "",
+ },
+ {
+ name: "multiple custom tags all appear in message",
+ content: ``,
+ wantHint: true,
+ wantTag: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ got := CheckV2MarkdownCustomTags(tt.content)
+ hasHint := got != ""
+ if hasHint != tt.wantHint {
+ t.Fatalf("CheckV2MarkdownCustomTags(%q) = %q, wantHint=%v", tt.content, got, tt.wantHint)
+ }
+ if tt.wantTag != "" && !strings.Contains(got, tt.wantTag) {
+ t.Fatalf("expected message to contain %q, got: %q", tt.wantTag, got)
+ }
+ })
+ }
+}
+
+func TestCheckDocsUpdateWholeParagraphStyle(t *testing.T) {
+ t.Parallel()
+
+ tests := []struct {
+ name string
+ markdown string
+ wantHint bool
+ }{
+ {
+ name: "plain paragraph no warning",
+ markdown: "This is a normal paragraph.",
+ wantHint: false,
+ },
+ {
+ name: "inline bold within mixed line is fine",
+ markdown: "See **important** detail here.",
+ wantHint: false,
+ },
+ {
+ name: "whole line italic triggers warning",
+ markdown: "*为什么大家都选代码?*",
+ wantHint: true,
+ },
+ {
+ name: "whole line bold triggers warning",
+ markdown: "**回到最初的问题:要用长期的耐心去打磨。**",
+ wantHint: true,
+ },
+ {
+ name: "whole line italic inside larger doc triggers warning",
+ markdown: "# Title\n\nSome prose.\n\n*整段斜体行*\n\nMore prose.",
+ wantHint: true,
+ },
+ {
+ name: "triple asterisk not matched (covered by bold+italic check)",
+ markdown: "***text***",
+ wantHint: false,
+ },
+ {
+ name: "asymmetric markers not matched (*text**)",
+ markdown: "*text**",
+ wantHint: false,
+ },
+ {
+ name: "whitespace-only inside markers not matched",
+ markdown: "* *",
+ wantHint: false,
+ },
+ {
+ name: "whole paragraph style inside code fence is ignored",
+ markdown: "```\n*not a paragraph*\n```",
+ wantHint: false,
+ },
+ {
+ name: "empty markdown no warning",
+ markdown: "",
+ wantHint: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ got := checkDocsUpdateWholeParagraphStyle(tt.markdown)
+ hasHint := got != ""
+ if hasHint != tt.wantHint {
+ t.Fatalf("checkDocsUpdateWholeParagraphStyle(%q) = %q, wantHint=%v", tt.markdown, got, tt.wantHint)
+ }
+ })
+ }
+}
diff --git a/shortcuts/doc/docs_update_v2.go b/shortcuts/doc/docs_update_v2.go
index 62b1f0cb6..fd1d0ec73 100644
--- a/shortcuts/doc/docs_update_v2.go
+++ b/shortcuts/doc/docs_update_v2.go
@@ -103,6 +103,11 @@ func validateUpdateV2(_ context.Context, runtime *common.RuntimeContext) error {
return common.FlagErrorf("--command append requires --content")
}
}
+ if runtime.Str("doc-format") == "markdown" && content != "" {
+ if msg := CheckV2MarkdownCustomTags(content); msg != "" {
+ return common.FlagErrorf("%s", msg)
+ }
+ }
return nil
}