Files
dingtalk-workspace-cli/internal/helpers/doc_format_guard.go

101 lines
3.2 KiB
Go

package helpers
import (
"fmt"
"strings"
"github.com/spf13/cobra"
)
// validateDocFormat enforces a case-sensitive whitelist on --content-format.
//
// allowed must include the empty string "" if the flag is optional;
// the first non-empty entry is rendered as "<value> (默认)" in error text.
// commandLabel is the human-readable command path (e.g. "doc create").
// example is a copy-paste-ready invocation shown to the user.
//
// raw=="json" is special-cased: agents pass it because the global persistent
// `-f/--format` (CLI output format, default json) shadows the per-command
// --content-format flag in their mental model. The error names the collision
// instead of just rejecting, so the agent doesn't loop on the same value.
func validateDocFormat(cmd *cobra.Command, allowed []string, commandLabel, example string) error {
raw, _ := cmd.Flags().GetString("content-format")
for _, a := range allowed {
if raw == a {
return nil
}
}
var msg string
if raw == "json" {
msg = fmt.Sprintf(
"--content-format json 在 %s 上无效。这里的 --content-format 指**文档内容**格式,不是 CLI 输出格式;CLI 输出格式由顶层 -f/--format 控制(默认即为 json,无需重复传)。本命令接受: %s",
commandLabel, renderAllowedFormats(allowed))
} else {
msg = fmt.Sprintf("--content-format 取值 %q 无效,%s 仅接受: %s", raw, commandLabel, renderAllowedFormats(allowed))
}
return &CLIError{
Code: CodeInvalidParam,
Message: msg,
Suggestion: "示例: " + example,
Operation: commandLabel,
}
}
// renderAllowedFormats turns ["", "markdown", "jsonml"] into "markdown (默认), jsonml".
// When `allowed` contains the empty string "" anywhere, the first non-empty entry
// is rendered with the "(默认)" suffix. Other non-empty entries are rendered
// verbatim. The "" entry itself is never rendered.
func renderAllowedFormats(allowed []string) string {
hasEmpty := false
for _, a := range allowed {
if a == "" {
hasEmpty = true
break
}
}
var parts []string
defaultMarked := false
for _, a := range allowed {
if a == "" {
continue
}
if hasEmpty && !defaultMarked {
parts = append(parts, a+" (默认)")
defaultMarked = true
continue
}
parts = append(parts, a)
}
return strings.Join(parts, ", ")
}
// sniffJsonMLLike is a cheap (no JSON parse) heuristic for "looks like JSONML".
//
// Triggers on:
// - leading "[" followed (within 64 bytes, after whitespace) by a JSON string literal
// - leading "{" followed (within 64 bytes) by "\"jsonml\"" key
//
// Does NOT trigger on markdown link "[text](url)" — the first non-whitespace
// after "[" must be a double-quote, not a letter.
func sniffJsonMLLike(content string) bool {
const lookahead = 64
s := strings.TrimLeft(content, " \t\r\n")
if s == "" {
return false
}
scan := s
if len(scan) > lookahead {
scan = scan[:lookahead]
}
switch s[0] {
case '[':
rest := strings.TrimLeft(scan[1:], " \t\r\n")
// ["tag"...] = single node; [["tag"...], ...] = body-level node list
return strings.HasPrefix(rest, `"`) || strings.HasPrefix(rest, `[`)
case '{':
rest := strings.TrimLeft(scan[1:], " \t\r\n")
return strings.HasPrefix(rest, `"jsonml"`)
}
return false
}