131 lines
4.1 KiB
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
|
|
}
|
|
}
|
|
}
|