103 lines
3.3 KiB
Go
103 lines
3.3 KiB
Go
package helpers
|
|
|
|
import "fmt"
|
|
|
|
// A fragment is the read-only result container returned by doc read
|
|
// --scope/--tags. It is not part of the writable JSONML schema, so write paths
|
|
// remove fragment wrappers and preserve their children before validation.
|
|
|
|
// jsonmlChildStart returns the index of the first child in a JSONML node array
|
|
// [tag, attrs?, ...children].
|
|
func jsonmlChildStart(arr []any) int {
|
|
if len(arr) > 1 {
|
|
if _, ok := arr[1].(map[string]any); ok {
|
|
return 2
|
|
}
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// stripFragmentChildren recursively unwraps fragment nodes in a children
|
|
// slice, splicing each fragment's children in place.
|
|
func stripFragmentChildren(children []any) ([]any, int) {
|
|
out := make([]any, 0, len(children))
|
|
count := 0
|
|
for _, child := range children {
|
|
arr, ok := child.([]any)
|
|
if !ok || len(arr) == 0 {
|
|
out = append(out, child)
|
|
continue
|
|
}
|
|
if tag, _ := arr[0].(string); tag == "fragment" {
|
|
kids, nestedCount := stripFragmentChildren(arr[jsonmlChildStart(arr):])
|
|
out = append(out, kids...)
|
|
count += 1 + nestedCount
|
|
continue
|
|
}
|
|
newNode, nestedCount := stripFragmentInNode(arr)
|
|
out = append(out, newNode)
|
|
count += nestedCount
|
|
}
|
|
return out, count
|
|
}
|
|
|
|
// stripFragmentInNode keeps a node's tag and attributes while recursively
|
|
// unwrapping fragments among its children.
|
|
func stripFragmentInNode(node []any) ([]any, int) {
|
|
start := jsonmlChildStart(node)
|
|
if start >= len(node) {
|
|
return node, 0
|
|
}
|
|
kids, count := stripFragmentChildren(node[start:])
|
|
if count == 0 {
|
|
return node, 0
|
|
}
|
|
newNode := make([]any, 0, start+len(kids))
|
|
newNode = append(newNode, node[:start]...)
|
|
newNode = append(newNode, kids...)
|
|
return newNode, count
|
|
}
|
|
|
|
// stripBodyFragments removes fragment wrappers from a document body. A
|
|
// top-level fragment is promoted to a root body so create/update can consume a
|
|
// scoped read result directly.
|
|
func stripBodyFragments(body []any) ([]any, int) {
|
|
if len(body) == 0 {
|
|
return body, 0
|
|
}
|
|
if tag, _ := body[0].(string); tag == "fragment" {
|
|
kids, nestedCount := stripFragmentChildren(body[jsonmlChildStart(body):])
|
|
root := make([]any, 0, 2+len(kids))
|
|
root = append(root, "root", map[string]any{})
|
|
root = append(root, kids...)
|
|
return root, 1 + nestedCount
|
|
}
|
|
return stripFragmentInNode(body)
|
|
}
|
|
|
|
// stripNodeFragments removes fragment wrappers from a single block element.
|
|
// A top-level fragment must contain exactly one JSONML node because block
|
|
// insert/update accepts only one element.
|
|
func stripNodeFragments(node []any) ([]any, int, error) {
|
|
if len(node) == 0 {
|
|
return node, 0, nil
|
|
}
|
|
if tag, _ := node[0].(string); tag == "fragment" {
|
|
kids, nestedCount := stripFragmentChildren(node[jsonmlChildStart(node):])
|
|
switch len(kids) {
|
|
case 0:
|
|
return nil, 0, fmt.Errorf("fragment 只读容器为空,无可写回的节点;fragment 是 doc read --scope 的查询结果,不能写回")
|
|
case 1:
|
|
inner, ok := kids[0].([]any)
|
|
if !ok {
|
|
return nil, 0, fmt.Errorf("fragment 只读容器的子节点不是合法 JSONML 节点")
|
|
}
|
|
return inner, 1 + nestedCount, nil
|
|
default:
|
|
return nil, 0, fmt.Errorf("fragment 只读容器含 %d 个节点,block insert/update 一次只能写一个;请分多次调用,或用 doc update --content-format jsonml 整篇覆盖", len(kids))
|
|
}
|
|
}
|
|
cleaned, count := stripFragmentInNode(node)
|
|
return cleaned, count, nil
|
|
}
|