Files
dingtalk-workspace-cli/internal/helpers/doc_jsonml_fragment_strip.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
}