Files
dingtalk-workspace-cli/internal/helpers/doc/jsonml_custom_validators.go

131 lines
4.1 KiB
Go

//go:build goexperiment.jsonv2
package doc
import "fmt"
// CustomValidator is a custom validation function invoked after declarative
// schema validation. It handles cross-field relationships, conditional
// requirements, and other logic that cannot be expressed declaratively.
type CustomValidator func(node *JsonMLNode, attrs map[string]any, schema *TagSchema, emit func(Diagnostic))
// customValidators is the per-tag custom validator registry.
var customValidators = map[string][]CustomValidator{}
// RegisterCustomValidator registers a custom validator for a specific tag.
// Validators are invoked in registration order after standard schema checks.
func RegisterCustomValidator(tag string, v CustomValidator) {
customValidators[tag] = append(customValidators[tag], v)
}
// p/h1-h6: list.start is only meaningful for ordered lists
func validateListStart(node *JsonMLNode, attrs map[string]any, _ *TagSchema, emit func(Diagnostic)) {
if attrs == nil {
return
}
listObj, ok := attrs["list"].(map[string]any)
if !ok {
return
}
if listObj["start"] == nil {
return
}
isOrdered, _ := listObj["isOrdered"].(bool)
if !isOrdered {
emit(Diagnostic{
Range: node.Range,
Severity: SeverityWarning,
Code: CodeJSONMLSchemaListStartInvalid,
Source: SourceJSONMLSchema,
Message: `"start" is only meaningful when list.isOrdered is true`,
})
}
}
func init() {
// table: sr != true 时 colsWidth 必填
// Note: attrs can be typed-nil map when JSON has {}, so we check nil OR missing key
RegisterCustomValidator("table", func(node *JsonMLNode, attrs map[string]any, _ *TagSchema, emit func(Diagnostic)) {
sr, _ := attrs["sr"].(bool)
if sr {
return // server-rendered table, colsWidth not needed
}
if attrs == nil || attrs["colsWidth"] == nil {
emit(Diagnostic{
Range: node.Range,
Severity: SeverityWarning,
Code: CodeJSONMLSchemaTableMissingColsWidth,
Source: SourceJSONMLSchema,
Message: `table without sr=true should have "colsWidth" attribute`,
Suggestion: `"colsWidth": [100, 100, 100]`,
})
}
})
// table: colsWidth 长度应与 tr 子节点中 tc 数量一致
RegisterCustomValidator("table", func(node *JsonMLNode, attrs map[string]any, _ *TagSchema, emit func(Diagnostic)) {
if attrs == nil {
return
}
colsWidth, ok := attrs["colsWidth"].([]any)
if !ok || len(colsWidth) == 0 {
return
}
// Find first tr child and count tc cells
for _, child := range node.Children {
if child.Tag == "tr" {
cellCount := 0
for _, cell := range child.Children {
if cell.Tag == "tc" {
cellCount++
}
}
if cellCount > 0 && len(colsWidth) != cellCount {
emit(Diagnostic{
Range: node.Range,
Severity: SeverityWarning,
Code: CodeJSONMLSchemaTableColsWidthMismatch,
Source: SourceJSONMLSchema,
Message: fmt.Sprintf("colsWidth length (%d) doesn't match column count (%d)", len(colsWidth), cellCount),
})
}
break // only check first row
}
}
})
// p/h1-h6: list validators
for _, tag := range []string{"p", "h1", "h2", "h3", "h4", "h5", "h6"} {
RegisterCustomValidator(tag, validateListStart)
}
// toc: content must be an array of objects (directory entries)
RegisterCustomValidator("toc", validateTocContentShape)
}
// validateTocContentShape checks that toc.content is an array where each
// element is an object (a directory entry). Primitives or nested arrays
// are likely malformed.
func validateTocContentShape(node *JsonMLNode, attrs map[string]any, _ *TagSchema, emit func(Diagnostic)) {
if attrs == nil {
return
}
content, ok := attrs["content"].([]any)
if !ok {
return // type mismatch already reported by schema validator
}
for i, item := range content {
if _, isObj := item.(map[string]any); !isObj {
emit(Diagnostic{
Range: node.Range,
Severity: SeverityWarning,
Code: CodeJSONMLSchemaTocContentShape,
Source: SourceJSONMLSchema,
Message: fmt.Sprintf("toc.content[%d] should be an object (directory entry), got %T", i, item),
Suggestion: `{"heading": "Title", "level": 1}`,
})
break // report only the first violation to reduce noise
}
}
}