Files
ofdgo/ofdgo_structure_xml.go
T
2026-10-16 23:25:33 +08:00

222 lines
6.9 KiB
Go

// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"slices"
"strconv"
"strings"
"unicode/utf8"
)
// encodeStructureNode 编码有界标引子树,引用由ObjectRef节点表达,不重复编码派生References字段
// 入参: node 根节点
// 返回: []byte XML片段, error 名称、属性或编码错误
func encodeStructureNode(node StructureNode) ([]byte, error) {
var output bytes.Buffer
encoder := xml.NewEncoder(&output)
count := 0
var write func(StructureNode, int) error
write = func(node StructureNode, depth int) error {
count++
if depth > 128 || count > 100000 {
return fmt.Errorf("custom tag structure limit exceeded")
}
if err := validateStructureNode(node); err != nil {
return err
}
start := xml.StartElement{Name: xml.Name{Space: node.Namespace, Local: node.Name}}
switch node.Namespace {
case "":
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns"}, Value: ""})
case XMLNamespace:
start.Name = xml.Name{Local: "xml:" + node.Name}
}
for _, attr := range node.Attributes {
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Space: attr.Namespace, Local: attr.Name}, Value: attr.Value})
}
if err := encoder.EncodeToken(start); err != nil {
return err
}
if err := encoder.EncodeToken(xml.CharData(node.Text)); err != nil {
return err
}
for _, child := range node.Children {
if err := write(child, depth+1); err != nil {
return err
}
}
return encoder.EncodeToken(start.End())
}
if err := write(node, 1); err != nil {
return nil, err
}
if err := encoder.Close(); err != nil {
return nil, err
}
return output.Bytes(), nil
}
// validateStructureNode 校验标引节点名称、属性和文本
// 入参: node 节点字段
// 返回: error 字段校验错误
func validateStructureNode(node StructureNode) error {
if !ofdXMLIDValid(node.Name) || node.Name == "xmlns" {
return fmt.Errorf("invalid custom tag node name: %s", node.Name)
}
if !structureXMLText(node.Text) || !structureXMLText(node.Namespace) || node.Namespace == xmlDeclarationNamespace {
return fmt.Errorf("invalid custom tag text or namespace")
}
seen := make(map[xml.Name]bool)
for _, attr := range node.Attributes {
name := xml.Name{Space: attr.Namespace, Local: attr.Name}
if !ofdXMLIDValid(attr.Name) || attr.Name == "xmlns" || attr.Namespace == xmlDeclarationNamespace || !structureXMLText(attr.Namespace) || !structureXMLText(attr.Value) || seen[name] {
return fmt.Errorf("invalid or duplicate custom tag attribute: %s", attr.Name)
}
seen[name] = true
}
return nil
}
// structureXMLText 检查XML 1.0允许的字符,不以替换字符掩盖无效输入
// 入参: value 文本
// 返回: bool 是否可无损编码
func structureXMLText(value string) bool {
if !utf8.ValidString(value) {
return false
}
for _, r := range value {
if r != '\t' && r != '\n' && r != '\r' && (r < 0x20 || r == 0xfffe || r == 0xffff) {
return false
}
}
return true
}
// updateStructureNodeXML 替换节点字段,保留子树继承的命名空间和非文本XML内容
// 入参: data 原XML, source 原节点, node 新字段
// 返回: []byte 独立节点片段, error 校验或编码错误
func updateStructureNodeXML(data []byte, source *editorXML, node StructureNode) ([]byte, error) {
if err := validateStructureNode(node); err != nil {
return nil, err
}
fragment, err := editorXMLStandalone(data[source.start:source.end], source)
if err != nil {
return nil, err
}
before, err := parseStructure(context.Background(), fragment, CustomTag{})
if err != nil {
return nil, err
}
if node.Name == before.Name && node.Namespace == before.Namespace && node.Text == before.Text && slices.Equal(node.Attributes, before.Attributes) {
return data[source.start:source.end], nil
}
root, err := parseEditorXML(fragment)
if err != nil {
return nil, err
}
if before.Text != node.Text {
var text bytes.Buffer
if err := xml.EscapeText(&text, []byte(node.Text)); err != nil {
return nil, err
}
fragment, err = editorXMLDirectText(fragment, root, text.Bytes())
if err != nil {
return nil, err
}
}
root, err = parseEditorXML(fragment)
if err != nil {
return nil, err
}
var attrs []xml.Attr
used := make(map[string]bool)
for _, attr := range root.attrs {
if attr.Name.Space == "xmlns" {
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + attr.Name.Local}, Value: attr.Value})
used[attr.Name.Local] = true
} else if attr.Name.Space == "" && attr.Name.Local == "xmlns" {
attrs = append(attrs, attr)
}
}
qualify := func(name, namespace string) string {
if namespace == "" {
return name
}
if namespace == XMLNamespace {
return "xml:" + name
}
for _, attr := range attrs {
if strings.HasPrefix(attr.Name.Local, "xmlns:") && attr.Value == namespace {
return strings.TrimPrefix(attr.Name.Local, "xmlns:") + ":" + name
}
}
prefix := "tag"
for i := 1; used[prefix]; i++ {
prefix = "tag" + strconv.Itoa(i)
}
used[prefix] = true
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: "xmlns:" + prefix}, Value: namespace})
return prefix + ":" + name
}
name := qualify(node.Name, node.Namespace)
if node.Namespace == "" {
for i := range attrs {
if attrs[i].Name.Local == "xmlns" {
attrs[i].Value = ""
}
}
}
for _, attr := range node.Attributes {
key := qualify(attr.Name, attr.Namespace)
attrs = append(attrs, xml.Attr{Name: xml.Name{Local: key}, Value: attr.Value})
}
var output bytes.Buffer
encoder := xml.NewEncoder(&output)
start := xml.StartElement{Name: xml.Name{Local: name}, Attr: attrs}
if err := encoder.EncodeToken(start); err != nil {
return nil, err
}
if err := encoder.Flush(); err != nil {
return nil, err
}
if root.open != root.end {
if node.Namespace == "" && before.Namespace != "" {
var patches []editorXMLPatch
for _, child := range root.children {
standalone, err := editorXMLStandalone(fragment[child.start:child.end], child)
if err != nil {
return nil, err
}
patches = append(patches, editorXMLPatch{child.start - root.open, child.end - root.open, standalone})
}
output.Write(editorPatchXML(fragment[root.open:root.close], patches))
} else {
output.Write(fragment[root.open:root.close])
}
}
if err := encoder.EncodeToken(start.End()); err != nil {
return nil, err
}
if err := encoder.Close(); err != nil {
return nil, err
}
return output.Bytes(), nil
}