Files
ofdgo/ofdgo_editor_structure.go
2026-10-18 18:08:52 +08:00

597 lines
18 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"
"maps"
"path"
"slices"
"strings"
)
// pruneStructureReferences 迁移公文图元编号并清理已删除的引用,保留未知标引和原有悬空引用
// 改动进入输出快照,由既有文档及版本隔离流程维护共享文件
// 入参: ctx 取消上下文, parts 输出覆盖条目, generated 本次编码结果及扫描记录
// 返回: error 读取、解析或取消错误
func (e *Editor) pruneStructureReferences(ctx context.Context, parts map[string][]byte, generated map[string]editorGeneratedReferences) error {
if err := ctx.Err(); err != nil {
return err
}
if e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 {
return nil
}
tags, err := e.source.reader.CustomTagsContext(ctx)
if err != nil {
return err
}
knownTags, err := e.source.reader.structureReferenceTags(ctx, tags)
if err != nil {
return err
}
if !slices.Contains(knownTags, true) {
return nil
}
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)
pageRemaps := make(map[string]map[string][]string)
remainingObjects := make(map[string]map[string]bool)
var snapshot *Reader
var snapshotPages map[string]Page
sources := make(map[string]*editorSourcePage, len(e.source.pages)+len(e.source.structureObjects))
for id, source := range e.source.pages {
sources[editorResourceID(id)] = source
}
for id := range e.source.structureObjects {
if _, ok := sources[id]; !ok {
sources[id] = nil
}
}
for key, source := range sources {
if !pages[key] {
removedPages[key] = true
continue
}
known := e.source.structureObjects[key]
changed := false
if source != nil {
_, changed = parts[e.source.reader.ResPath(source.ref.BaseLoc)]
changed = changed && source.original != nil
}
if !changed && len(known) == 0 {
continue
}
if snapshot == nil {
reader := e.source.reader
files := make(map[string][]byte, len(reader.files)+len(parts))
maps.Copy(files, reader.files)
maps.Copy(files, parts)
snapshot = &Reader{Zip: reader.Zip, files: files, encryption: e.encryption, documentIndex: reader.documentIndex, selectedVersion: reader.selectedVersion}
if err := snapshot.initRootContext(ctx); err != nil {
return err
}
doc, err := snapshot.docStructureContext(ctx)
if err != nil {
return err
}
snapshotPages = make(map[string]Page, len(doc.Pages.Page))
for _, page := range doc.Pages.Page {
snapshotPages[editorResourceID(page.ID)] = page
}
}
original := make(map[string]bool)
if source != nil {
var err error
original, err = structurePageObjectIDs(ctx, e.source.reader, source.ref)
if err != nil {
return err
}
}
maps.Copy(original, known)
page := snapshotPages[key]
name := snapshot.ResPath(page.BaseLoc)
scanned := generated[name]
remaining := scanned.objects
if remaining == nil || !scanned.matches(parts[name]) {
var err error
remaining, err = structurePageObjectIDs(ctx, snapshot, page)
if err != nil {
return err
}
} else if snapshot.doc.Annotations != "" {
if _, err := snapshot.DocContext(ctx); err != nil {
return err
}
visitor := &structureObjectVisitor{objects: maps.Clone(remaining)}
renderer := NewRenderer(snapshot)
renderer.pageContext = ctx
for _, annotation := range snapshot.Annots[page.ID] {
if err := renderer.walkAnnotation(annotation, visitor); err != nil {
return err
}
}
remaining = visitor.objects
}
remainingObjects[key] = remaining
links, err := structureRemapLinks(ctx, e.structureRemaps[key])
if err != nil {
return err
}
related := make(map[string][]string)
for from, targets := range links {
for _, to := range targets {
if err := ctx.Err(); err != nil {
return err
}
related[from] = append(related[from], to)
related[to] = append(related[to], from)
}
}
for id := range remaining {
if len(related[id]) != 0 {
original[id] = true
}
}
pending := slices.Collect(maps.Keys(original))
for len(pending) != 0 {
if err := ctx.Err(); err != nil {
return err
}
id := pending[len(pending)-1]
pending = pending[:len(pending)-1]
for _, next := range related[id] {
if !original[next] {
original[next] = true
pending = append(pending, next)
}
}
}
for id := range original {
if err := ctx.Err(); err != nil {
return err
}
if len(links[id]) != 0 {
if pageRemaps[key] == nil {
pageRemaps[key] = make(map[string][]string)
}
pageRemaps[key][id] = links[id]
}
if !remaining[id] {
if removedObjects[key] == nil {
removedObjects[key] = make(map[string]bool)
}
removedObjects[key][id] = true
}
}
}
if len(removedPages) == 0 && len(removedObjects) == 0 && len(pageRemaps) == 0 {
return nil
}
protected := make(map[string]bool)
for index, tag := range tags {
if !knownTags[index] {
protected[e.source.reader.ResPath(tag.FileLoc)] = true
}
}
isolated := make(map[string]string)
originals := make(map[string][]byte)
redirects := make(map[int]string)
for index, tag := range tags {
if !knownTags[index] {
continue
}
name := e.source.reader.ResPath(tag.FileLoc)
data, ok := originals[name]
if !ok {
data, ok = parts[name]
if !ok {
data, err = e.source.reader.readFileViewContext(ctx, name)
if err != nil {
return err
}
}
originals[name] = data
}
remaps := pageRemaps
if starts := e.source.structureStarts[index]; len(starts) != 0 {
remaps = maps.Clone(pageRemaps)
for page, start := range starts {
links, err := structureRemapLinks(ctx, e.structureRemaps[page][start:])
if err != nil {
return err
}
remaps[page] = links
}
}
mappedObjects := make(map[string]map[string][]string)
updated, err := rewriteStructureReferences(ctx, data, tag, func(page string, ids []string) (string, []string, error) {
key := editorResourceID(page)
if removedPages[key] {
return page, nil, nil
}
kept := make([]string, 0, len(ids))
for _, id := range ids {
canonical := editorResourceID(id)
if len(remaps[key][canonical]) != 0 {
mapped, ok := mappedObjects[key][canonical]
if !ok {
var err error
mapped, err = structureMappedIDs(ctx, canonical, remaps[key], remainingObjects[key])
if err != nil {
return "", nil, err
}
if mappedObjects[key] == nil {
mappedObjects[key] = make(map[string][]string)
}
mappedObjects[key][canonical] = mapped
}
kept = append(kept, mapped...)
} else if !removedObjects[key][canonical] {
kept = append(kept, id)
}
}
return page, kept, nil
})
if err != nil {
return err
}
if !bytes.Equal(data, updated) {
if protected[name] {
target := isolated[name]
if target == "" {
target = packageAvailableName(e.source.reader, parts, path.Join(e.packageDirectory(), "Tags", "Tag.xml"))
isolated[name] = target
}
redirects[index] = target
name = target
}
parts[name] = updated
}
}
if len(redirects) != 0 {
return e.redirectStructureEntries(parts, redirects)
}
return nil
}
// redirectStructureEntries 一次改写输出快照中的标引路径,保留同次页面及索引改动
// 入参: parts 输出覆盖条目, targets 标引入口序号及新内容路径
// 返回: error 索引读取或路径改写错误
func (e *Editor) redirectStructureEntries(parts map[string][]byte, targets map[int]string) error {
name, data, _, entries, err := editorStructureIndex(e.source, parts)
if err != nil {
return err
}
patches := make([]editorXMLPatch, 0, len(targets))
for index, target := range targets {
if index < 0 || index >= len(entries) {
return fmt.Errorf("custom tag structure not found: %d", index)
}
loc := entries[index].child("FileLoc")
if loc == nil {
return fmt.Errorf("custom tag requires FileLoc")
}
var value bytes.Buffer
if err := xml.EscapeText(&value, []byte("/"+target)); err != nil {
return err
}
patches = append(patches, editorXMLContent(data, loc, value.Bytes()))
}
parts[name] = editorPatchXML(data, patches)
return nil
}
// remapStructureObjects 将图元修改和标引编号迁移作为一次操作提交,映射只作用于当前页
// 入参: page 页面索引, remaps 按修改顺序记录的编号映射, edit 图元修改
// 返回: error 图元修改错误
func (e *Editor) remapStructureObjects(page int, remaps []map[string]string, edit func() error) error {
if len(remaps) == 0 || e.source == nil || e.source.document.CustomTags.Path == "" && len(e.source.document.CustomTags.CustomTag) == 0 {
return edit()
}
return e.Transaction(func(editor *Editor) error {
if err := edit(); err != nil {
return err
}
if editor.structureRemaps == nil {
editor.structureRemaps = make(map[string][]map[string]string)
}
key := editorResourceID(editor.pages[page].ID)
next := slices.Clone(editor.structureRemaps[key])
for _, ids := range remaps {
if len(ids) != 0 {
next = append(next, maps.Clone(ids))
}
}
editor.structureRemaps[key] = next
return nil
})
}
// structureRemapStarts 记录各页面已应用的编号迁移位置,不与后续编辑共享可变状态
// 返回: map[string]int 页面编号及已应用的迁移记录数
func (e *Editor) structureRemapStarts() map[string]int {
starts := make(map[string]int, len(e.structureRemaps))
for page, remaps := range e.structureRemaps {
starts[page] = len(remaps)
}
return starts
}
// structureRemapLinks 合并按操作顺序记录的编号迁移,保留一对多关系并排除重复边
// 入参: ctx 取消上下文, remaps 图元编号迁移记录
// 返回: map[string][]string 原编号及其迁移目标, error 取消错误
func structureRemapLinks(ctx context.Context, remaps []map[string]string) (map[string][]string, error) {
links := make(map[string][]string)
seen := make(map[[2]string]bool)
for _, ids := range remaps {
if err := ctx.Err(); err != nil {
return nil, err
}
for from, to := range ids {
if err := ctx.Err(); err != nil {
return nil, err
}
key := [2]string{from, to}
if from == to || seen[key] {
continue
}
seen[key] = true
links[from] = append(links[from], to)
}
}
return links, ctx.Err()
}
// structureMappedIDs 沿编号迁移关系收集当前仍可达的图元,同一来源的结果不重复
// 入参: ctx 取消上下文, id 原编号, links 编号迁移关系, remaining 当前页面可达图元
// 返回: []string 保留的原编号及迁移后的编号, error 取消错误
func structureMappedIDs(ctx context.Context, id string, links map[string][]string, remaining map[string]bool) ([]string, error) {
var result []string
pending := []string{id}
seen := make(map[string]bool)
for len(pending) != 0 {
if err := ctx.Err(); err != nil {
return nil, err
}
id = pending[len(pending)-1]
pending = pending[:len(pending)-1]
if seen[id] {
continue
}
seen[id] = true
if remaining[id] {
result = append(result, id)
}
for i := len(links[id]) - 1; i >= 0; i-- {
pending = append(pending, links[id][i])
}
}
return result, nil
}
// structurePageObjectIDs 收集页面、模板、复合资源及注释外观中的图元标识,不加载字体或图像
// 入参: ctx 取消上下文, reader 资源已加载的阅读器, page 页面引用
// 返回: map[string]bool 可达图元标识, error 页面读取或遍历错误
func structurePageObjectIDs(ctx context.Context, reader *Reader, page Page) (map[string]bool, error) {
if _, err := reader.DocContext(ctx); err != nil {
return nil, err
}
content, err := reader.PageContentContext(ctx, page)
if err != nil {
return nil, err
}
renderer := NewRenderer(reader, WithAnnotations(true))
renderer.pageContext = ctx
visitor := &structureObjectVisitor{objects: make(map[string]bool)}
if err := renderer.WalkPage(content, visitor); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
return visitor.objects, nil
}
// annotationStructureSource 记录被修改注释原有的外观图元,供保存时清理失效标引
// 入参: page 页面索引, wanted 待修改注释标识
// 返回: *editorSource 保留已知图元的包快照, error 读取或遍历错误
func (e *Editor) annotationStructureSource(page int, wanted map[string]bool) (*editorSource, error) {
base := e.source
if base.document.CustomTags.Path == "" && len(base.document.CustomTags.CustomTag) == 0 {
return base, nil
}
tags, err := base.reader.CustomTags()
if err != nil {
return nil, err
}
knownTags, err := base.reader.structureReferenceTags(context.Background(), tags)
if err != nil {
return nil, err
}
if !slices.Contains(knownTags, true) {
return base, nil
}
if _, err := base.reader.Doc(); err != nil {
return nil, err
}
if source := base.pages[e.pages[page].ID]; source != nil {
if _, err := base.reader.PageContent(source.ref); err != nil {
return nil, err
}
}
visitor := &structureObjectVisitor{objects: make(map[string]bool)}
renderer := NewRenderer(base.reader)
for _, annotation := range base.reader.Annots[e.pages[page].ID] {
if wanted[editorResourceID(annotation.ID)] {
if err := renderer.walkAnnotation(annotation, visitor); err != nil {
return nil, err
}
}
}
next := *base
next.structureObjects = maps.Clone(base.structureObjects)
if next.structureObjects == nil {
next.structureObjects = make(map[string]map[string]bool)
}
key := editorResourceID(e.pages[page].ID)
maps.Copy(visitor.objects, next.structureObjects[key])
next.structureObjects[key] = visitor.objects
return &next, nil
}
// rewriteStructureReferences 仅改写已识别公文命名空间的叶子引用,保留其他XML字节
// 入参: ctx 取消上下文, data 原始标引, tag 标引入口, rewrite 引用映射,空标识列表表示删除引用
// 返回: []byte 修改后的标引, error 解析或取消错误
func rewriteStructureReferences(ctx context.Context, data []byte, tag CustomTag, rewrite func(string, []string) (string, []string, error)) ([]byte, error) {
root, err := parseEditorXMLContext(ctx, data)
if err != nil {
return nil, err
}
if !structureOfficial(tag, root.name.Space) {
structure, err := parseStructure(ctx, data, tag)
if err != nil {
return nil, err
}
if !structureInvoiceReferences(structure) {
return data, nil
}
}
var patches []editorXMLPatch
var walk func(*editorXML, int) error
walk = func(node *editorXML, depth int) error {
if err := ctx.Err(); err != nil {
return err
}
if depth > 128 {
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, err := rewrite(node.attr("PageRef"), ids)
if err != nil {
return err
}
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.CustomTagsContext(m.ctx)
if err != nil {
return nil, err
}
knownTags, err := m.reader.structureReferenceTags(m.ctx, tags)
if err != nil {
return nil, err
}
var entries []byte
for i, tag := range tags {
if !knownTags[i] {
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, error) {
key := editorImportKey(page)
if !m.pages[key] {
return page, nil, 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, nil
})
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
}