Files
ofdgo/ofdgo_editor_structure.go
T
2026-10-13 00:30:27 +08:00

249 lines
7.0 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"
"path"
"strings"
)
// pruneStructureReferences 清理本次编辑删除的公文图元引用,未知标引和原有悬空引用保持原样
// 改动进入输出快照,由既有文档及版本隔离流程维护共享文件
// 入参: parts 输出覆盖条目
// 返回: error 读取、解析或取消错误
func (e *Editor) pruneStructureReferences(parts map[string][]byte) error {
if e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 {
return nil
}
ctx := context.Background()
if e.output != nil {
ctx = e.output.ctx
}
pages := make(map[string]bool, len(e.pages))
for _, page := range e.pages {
pages[editorResourceID(page.ID)] = true
}
removedPages := make(map[string]bool)
removedObjects := make(map[string]map[string]bool)
for id, source := range e.source.pages {
key := editorResourceID(id)
if !pages[key] {
removedPages[key] = true
continue
}
data, changed := parts[e.source.reader.ResPath(source.ref.BaseLoc)]
if !changed || source.original == nil {
continue
}
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return err
}
remaining := make(map[string]bool)
var scan func(*editorXML)
scan = func(node *editorXML) {
if classifyOFDNamespace(node.name.Space) == ofdXMLUnknown {
return
}
if id := editorResourceID(node.attr("ID")); id != "" {
remaining[id] = true
}
for _, child := range node.children {
scan(child)
}
}
scan(root)
for id := range source.nodes {
id = editorResourceID(id)
if id != "" && !remaining[id] {
if removedObjects[key] == nil {
removedObjects[key] = make(map[string]bool)
}
removedObjects[key][id] = true
}
}
}
if len(removedPages) == 0 && len(removedObjects) == 0 {
return nil
}
tags, err := e.source.reader.CustomTags()
if err != nil {
return err
}
for _, tag := range tags {
if !structureOfficial(tag, tag.NameSpace) {
continue
}
name := e.source.reader.ResPath(tag.FileLoc)
data, ok := parts[name]
if !ok {
data, err = e.source.reader.readFileViewContext(ctx, name)
if err != nil {
return err
}
}
updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string) {
key := editorResourceID(page)
if removedPages[key] {
return page, nil
}
kept := make([]string, 0, len(ids))
for _, id := range ids {
if !removedObjects[key][editorResourceID(id)] {
kept = append(kept, id)
}
}
return page, kept
})
if err != nil {
return err
}
if !bytes.Equal(data, updated) {
parts[name] = updated
}
}
return nil
}
// rewriteStructureReferences 仅改写已识别公文命名空间的叶子引用,保留其他XML字节
// 入参: ctx 取消上下文, data 原始标引, tag 标引入口, rewrite 引用映射,空标识列表表示删除引用
// 返回: []byte 修改后的标引, error 解析或取消错误
func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag, rewrite func(string, []string) (string, []string)) ([]byte, error) {
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
if !structureOfficial(tag, root.name.Space) {
return data, nil
}
var patches []editorXMLPatch
count := 0
var walk func(*editorXML, int) error
walk = func(node *editorXML, depth int) error {
if err := ctx.Err(); err != nil {
return err
}
count++
if depth > 128 || count > 100000 {
return fmt.Errorf("custom tag structure limit exceeded")
}
if node.name.Space != root.name.Space {
return nil
}
if node.name.Local == "ObjectRef" && len(node.children) == 0 {
value := strings.TrimSpace(editorImportText(data, node))
ids := structureObjectIDs(value)
if len(ids) == 0 {
return nil
}
page, next := rewrite(node.attr("PageRef"), ids)
if len(next) == 0 {
patches = append(patches, editorXMLPatch{start: node.start, end: node.end})
return nil
}
if strings.Join(next, ",") != strings.Join(ids, ",") {
var text bytes.Buffer
_ = xml.EscapeText(&text, []byte(strings.Join(next, ",")))
patches = append(patches, editorXMLContent(data, node, text.Bytes()))
}
if page != node.attr("PageRef") {
patch, err := editorXMLAttributePatch(data, node, "PageRef", page)
if err != nil {
return err
}
patches = append(patches, patch)
}
return nil
}
for _, child := range node.children {
if err := walk(child, depth+1); err != nil {
return err
}
}
return nil
}
if err := walk(root, 1); err != nil {
return nil, err
}
if len(patches) == 0 {
return data, nil
}
return editorPatchXML(data, patches), nil
}
// structures 迁移所选页面的公文标引,按已迁移图元映射引用,不猜测其他行业的引用规则
// 返回: []byte 标引索引条目, error 读取、解析或取消错误
func (m *editorPageImport) structures() ([]byte, error) {
tags, err := m.reader.CustomTags()
if err != nil {
return nil, err
}
var entries []byte
for i, tag := range tags {
if !structureOfficial(tag, tag.NameSpace) {
continue
}
data, err := m.readFile(m.reader.ResPath(tag.FileLoc))
if err != nil {
return nil, err
}
count := 0
updated, err := rewriteStructureReferences(m.ctx, data, tag, func(page string, ids []string) (string, []string) {
key := editorImportKey(page)
if !m.pages[key] {
return page, nil
}
var mapped []string
for _, id := range ids {
if value := m.ids[editorImportKey(id)]; value != "" {
mapped = append(mapped, value)
}
}
count += len(mapped)
return m.ids[key], mapped
})
if err != nil {
return nil, err
}
if count == 0 {
continue
}
name := packageAvailableName(m.target, m.files, path.Join(m.directory, "Tags", fmt.Sprintf("Tag_%d.xml", i)))
m.files[name] = updated
var content []byte
if tag.SchemaLoc != "" {
schema, err := m.copyData(m.reader.ResPath(tag.SchemaLoc), path.Join(m.directory, "Tags", path.Base(tag.SchemaLoc)), false)
if err != nil {
return nil, err
}
content = append(content, editorXMLText("SchemaLoc", "/"+strings.TrimPrefix(schema, "/"))...)
}
content = append(content, editorXMLText("FileLoc", "/"+name)...)
var attrs ofdAttrs
attrs.add("TypeID", tag.TypeID)
attrs.add("NameSpace", tag.NameSpace)
entry, err := editorXMLContainer("CustomTag", attrs, content)
if err != nil {
return nil, err
}
entries = append(entries, entry...)
}
return entries, nil
}