mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 14:16:55 +08:00
612 lines
19 KiB
Go
612 lines
19 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"
|
|
"time"
|
|
)
|
|
|
|
// AddAnnotation 新增标准注解并分配独立标识,外观中的基本对象使用注解局部毫米坐标
|
|
// Appearance.Objects为空时使用分类对象集合,输入对象及资源不被修改
|
|
// 入参: index 页面索引, annotation 注解内容,ID由编辑器生成,空LastModDate使用当前日期
|
|
// 返回: string 注解标识, error 错误信息
|
|
func (e *Editor) AddAnnotation(index int, annotation Annotation) (string, error) {
|
|
var id string
|
|
err := e.Transaction(func(next *Editor) error {
|
|
var data []byte
|
|
var err error
|
|
id, data, err = next.prepareAnnotation(index, annotation)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return next.appendAnnotations(index, data, true)
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// prepareAnnotation 校验注解并分配外观标识,提交由调用方事务或独立导入文档负责
|
|
// 入参: index 页面索引, annotation 注解内容
|
|
// 返回: string 注解标识, []byte 标准XML, error 校验错误
|
|
func (e *Editor) prepareAnnotation(index int, annotation Annotation) (string, []byte, error) {
|
|
if index < 0 || index >= len(e.pages) {
|
|
return "", nil, fmt.Errorf("page index %d out of range", index)
|
|
}
|
|
if !slices.Contains([]string{"Link", "Path", "Highlight", "Stamp", "Watermark"}, annotation.Type) {
|
|
return "", nil, fmt.Errorf("invalid annotation type %q", annotation.Type)
|
|
}
|
|
if _, err := creationBox(annotation.Appearance.Boundary); err != nil {
|
|
return "", nil, err
|
|
}
|
|
if err := e.prepareSourceIDs(); err != nil {
|
|
return "", nil, err
|
|
}
|
|
id := e.nextID()
|
|
objects := annotation.Appearance.Objects
|
|
if len(objects) == 0 {
|
|
objects = nil
|
|
for _, object := range annotation.Appearance.TextObject {
|
|
objects = append(objects, GraphicObject{Type: "TextObject", TextObject: object})
|
|
}
|
|
for _, object := range annotation.Appearance.PathObject {
|
|
objects = append(objects, GraphicObject{Type: "PathObject", PathObject: object})
|
|
}
|
|
for _, object := range annotation.Appearance.ImageObject {
|
|
objects = append(objects, GraphicObject{Type: "ImageObject", ImageObject: object})
|
|
}
|
|
if len(annotation.Appearance.CompositeGraphicUnit) != 0 {
|
|
return "", nil, fmt.Errorf("use composite copy to insert existing complex content")
|
|
}
|
|
}
|
|
var content []byte
|
|
for _, object := range objects {
|
|
prepared, err := e.prepareObject(e.nextID(), cloneEditorData(object))
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
data, err := editorObjectXML(prepared)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
|
}
|
|
appearance, err := editorXMLContainer("Appearance", ofdAttrs{{Name: xml.Name{Local: "Boundary"}, Value: annotation.Appearance.Boundary}}, content)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
date := annotation.LastModDate
|
|
if date == "" {
|
|
date = time.Now().Format("2006-01-02")
|
|
} else if _, err := time.Parse("2006-01-02", date); err != nil {
|
|
return "", nil, fmt.Errorf("invalid annotation date %q: %w", date, err)
|
|
}
|
|
attrs := ofdAttrs{{Name: xml.Name{Local: "ID"}, Value: id}, {Name: xml.Name{Local: "Type"}, Value: annotation.Type},
|
|
{Name: xml.Name{Local: "Creator"}, Value: annotation.Creator}, {Name: xml.Name{Local: "LastModDate"}, Value: date}}
|
|
attrs.add("Subtype", annotation.Subtype)
|
|
attrs.flag("Visible", annotation.Visible)
|
|
if annotation.NoZoom {
|
|
attrs.add("NoZoom", "true")
|
|
}
|
|
if annotation.NoRotate {
|
|
attrs.add("NoRotate", "true")
|
|
}
|
|
content = nil
|
|
if annotation.Remark != "" {
|
|
content = editorXMLText("Remark", annotation.Remark)
|
|
}
|
|
if annotation.Parameters != nil && len(annotation.Parameters.Parameter) != 0 {
|
|
var parameters []byte
|
|
for _, parameter := range annotation.Parameters.Parameter {
|
|
if parameter.Name == "" {
|
|
return "", nil, fmt.Errorf("empty annotation parameter name")
|
|
}
|
|
var value bytes.Buffer
|
|
if err := xml.EscapeText(&value, []byte(parameter.Value)); err != nil {
|
|
return "", nil, err
|
|
}
|
|
data, err := editorXMLContainer("Parameter", ofdAttrs{{Name: xml.Name{Local: "Name"}, Value: parameter.Name}}, value.Bytes())
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
parameters = append(parameters, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
|
}
|
|
data, err := editorXMLContainer("Parameters", nil, parameters)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
content = append(content, bytes.TrimPrefix(data, []byte(xml.Header))...)
|
|
}
|
|
content = append(content, bytes.TrimPrefix(appearance, []byte(xml.Header))...)
|
|
data, err := editorXMLContainer("Annot", attrs, content)
|
|
return id, data, err
|
|
}
|
|
|
|
// Annotation 获取注解的独立内容快照,修改原注解应使用局部编辑方法以保留扩展字段
|
|
// 入参: index 页面索引, id 注解标识
|
|
// 返回: Annotation 注解内容, error 错误信息
|
|
func (e *Editor) Annotation(index int, id string) (Annotation, error) {
|
|
data, err := e.annotationXML(index, id)
|
|
var annotation Annotation
|
|
if err == nil {
|
|
err = xml.Unmarshal(data, &annotation)
|
|
}
|
|
return annotation, err
|
|
}
|
|
|
|
// AnnotationTexts 按绘制顺序读取注解外观中的文字,展开复合资源并去重,不依赖字体或渲染后端
|
|
// 入参: index 页面索引, id 注解标识
|
|
// 返回: []string 文字列表, error 注解读取或资源引用错误
|
|
func (e *Editor) AnnotationTexts(index int, id string) ([]string, error) {
|
|
annotation, err := e.Annotation(index, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader, err := e.reader(nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
if _, err := reader.PageContentByIndex(index); err != nil {
|
|
return nil, err
|
|
}
|
|
texts := make([]string, 0)
|
|
seen := make(map[string]bool)
|
|
visiting := make(map[*CompositeGraphicUnit]bool)
|
|
collected := make(map[*CompositeGraphicUnit]bool)
|
|
var collect func(CompositeGraphicUnit) error
|
|
collect = func(unit CompositeGraphicUnit) error {
|
|
if unit.ResourceID != "" {
|
|
ref, _ := resourceValue(reader.compositeGraphicUnitCache, unit.ResourceID)
|
|
if ref == nil || visiting[ref] {
|
|
return fmt.Errorf("invalid composite resource %q", unit.ResourceID)
|
|
}
|
|
if !collected[ref] {
|
|
visiting[ref] = true
|
|
if err := collect(*ref); err != nil {
|
|
return err
|
|
}
|
|
delete(visiting, ref)
|
|
collected[ref] = true
|
|
}
|
|
}
|
|
for object := range unit.objects() {
|
|
switch object.Type {
|
|
case "TextObject":
|
|
value := object.TextObject.Text()
|
|
if !seen[value] {
|
|
seen[value] = true
|
|
texts = append(texts, value)
|
|
}
|
|
case "CompositeObject", "CompositeGraphicUnit":
|
|
if err := collect(object.CompositeGraphicUnit); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := collect(CompositeGraphicUnit{Objects: annotation.Appearance.Objects}); err != nil {
|
|
return nil, err
|
|
}
|
|
return texts, nil
|
|
}
|
|
|
|
// annotationXML 按ST_ID数值读取唯一注解的原文,不以类型、大小或可见性筛选
|
|
// 入参: index 页面索引, id 注解标识
|
|
// 返回: []byte 注解XML, error 错误信息
|
|
func (e *Editor) annotationXML(index int, id string) ([]byte, error) {
|
|
if index < 0 || index >= len(e.pages) {
|
|
return nil, fmt.Errorf("page index %d out of range", index)
|
|
}
|
|
key := editorResourceID(id)
|
|
if key == "" {
|
|
return nil, fmt.Errorf("invalid annotation ID %q", id)
|
|
}
|
|
if e.source == nil || e.source.document.Annotations == "" {
|
|
return nil, fmt.Errorf("page has no annotations")
|
|
}
|
|
reader := e.source.reader
|
|
name := reader.ResPath(e.source.document.Annotations)
|
|
data, err := reader.readFile(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []byte
|
|
pageID := editorResourceID(e.pages[index].ID)
|
|
for _, ref := range root.children {
|
|
if !ref.matchesOFD("Page") || pageID == "" || editorResourceID(ref.attr("PageID")) != pageID || ref.child("FileLoc") == nil {
|
|
continue
|
|
}
|
|
file, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, ref.child("FileLoc"))))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
content, err := reader.readFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
page, err := parseEditorXML(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, node := range page.children {
|
|
if !node.matchesOFD("Annot") || editorResourceID(node.attr("ID")) != key {
|
|
continue
|
|
}
|
|
if result != nil {
|
|
return nil, fmt.Errorf("ambiguous annotation ID %q", id)
|
|
}
|
|
result, err = editorXMLStandalone(content[node.start:node.end], node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
if result == nil {
|
|
return nil, fmt.Errorf("annotation %q not found", id)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// appendAnnotations 在独立页面注解文件末尾追加原文,保留原索引及未知内容
|
|
// 入参: index 页面索引, annotations 注解XML片段, generated 是否为自产内容
|
|
// 返回: error 错误信息
|
|
func (e *Editor) appendAnnotations(index int, annotations []byte, generated bool) error {
|
|
base := e.source
|
|
var err error
|
|
if base == nil {
|
|
base, err = e.importBase()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
reader := base.reader
|
|
name := reader.ResPath(base.document.Annotations)
|
|
var data []byte
|
|
if base.document.Annotations == "" {
|
|
name = e.packageName("Annotations.xml")
|
|
data, err = editorXMLContainer("Annotations", nil, nil)
|
|
} else {
|
|
if file, ok := reader.packageFile(name); ok {
|
|
name = cleanPackagePath(file.Name)
|
|
}
|
|
data, err = reader.readFile(name)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var ref *editorXML
|
|
var existing string
|
|
content, err := editorXMLContainer("PageAnnot", nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pageID := editorResourceID(e.pages[index].ID)
|
|
for _, item := range root.children {
|
|
if item.matchesOFD("Page") && pageID != "" && editorResourceID(item.attr("PageID")) == pageID {
|
|
ref = item
|
|
loc := item.child("FileLoc")
|
|
if loc == nil {
|
|
return fmt.Errorf("annotation page has no FileLoc")
|
|
}
|
|
file, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, loc)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
content, err = reader.readFile(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing = file
|
|
break
|
|
}
|
|
}
|
|
page, err := parseEditorXML(content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
added := bytes.TrimPrefix(annotations, []byte(xml.Header))
|
|
if generated {
|
|
added, err = editorXMLGenerated(added, page.name.Space)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if page.open == page.end {
|
|
content = editorPatchXML(content, []editorXMLPatch{editorXMLContent(content, page, added)})
|
|
} else {
|
|
content = editorPatchXML(content, []editorXMLPatch{{page.close, page.close, added}})
|
|
}
|
|
file := existing
|
|
if file != "" {
|
|
for _, other := range root.children {
|
|
if other == ref || !other.matchesOFD("Page") || other.child("FileLoc") == nil {
|
|
continue
|
|
}
|
|
shared, err := editorPageLocation(reader, nil, name, strings.TrimSpace(editorImportText(data, other.child("FileLoc"))))
|
|
if err == nil && shared == existing {
|
|
file = ""
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if file == "" {
|
|
file = e.packageName(path.Join("Annotations", "Page_"+e.pages[index].ID+".xml"))
|
|
}
|
|
if ref != nil {
|
|
if file != existing {
|
|
loc := ref.child("FileLoc")
|
|
var value bytes.Buffer
|
|
_ = xml.EscapeText(&value, []byte("/"+file))
|
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, loc, value.Bytes())})
|
|
}
|
|
} else {
|
|
entry, err := root.containerXML("Page", ofdAttrs{{Name: xml.Name{Local: "PageID"}, Value: e.pages[index].ID}}, root.textXML("FileLoc", "/"+file))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
entry = bytes.TrimPrefix(entry, []byte(xml.Header))
|
|
if root.open == root.end {
|
|
data = editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, root, entry)})
|
|
} else {
|
|
data = editorPatchXML(data, []editorXMLPatch{{root.close, root.close, entry}})
|
|
}
|
|
}
|
|
parts := map[string][]byte{name: data, file: content}
|
|
if base.document.Annotations == "" {
|
|
docName := reader.documentRoot
|
|
docData, err := reader.readFile(docName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
doc, err := parseEditorXML(docData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
position := doc.close
|
|
for _, child := range doc.children {
|
|
if slices.Contains([]string{"CustomTags", "Attachments", "Extensions"}, child.name.Local) {
|
|
position = child.start
|
|
break
|
|
}
|
|
}
|
|
patch := editorXMLPatch{position, position, doc.textXML("Annotations", "/"+name)}
|
|
if node := doc.child("Annotations"); node != nil {
|
|
var value bytes.Buffer
|
|
_ = xml.EscapeText(&value, []byte("/"+name))
|
|
patch = editorXMLContent(docData, node, value.Bytes())
|
|
}
|
|
parts[docName] = editorPatchXML(docData, []editorXMLPatch{patch})
|
|
}
|
|
if err := e.commitAnnotationParts(base, parts); err != nil {
|
|
return err
|
|
}
|
|
pages := maps.Clone(e.source.annotationPages)
|
|
if pages == nil {
|
|
pages = make(map[string]bool)
|
|
}
|
|
pages[e.pages[index].ID] = true
|
|
e.source.annotationPages = pages
|
|
return nil
|
|
}
|
|
|
|
// annotationFiles 获取仍被引用的注解文件,保留原文件中的未知页引用
|
|
// 返回: []string 文件路径
|
|
func (e *Editor) annotationFiles() []string {
|
|
if len(e.source.reader.annotationFiles) == 0 {
|
|
return nil
|
|
}
|
|
removed := make(map[string]bool)
|
|
for id := range e.source.pages {
|
|
removed[editorResourceID(id)] = true
|
|
}
|
|
for id := range e.source.annotationPages {
|
|
removed[editorResourceID(id)] = true
|
|
}
|
|
for _, page := range e.pages {
|
|
delete(removed, editorResourceID(page.ID))
|
|
}
|
|
delete(removed, "")
|
|
var files []string
|
|
for name, pages := range e.source.reader.annotationFiles {
|
|
for _, page := range pages {
|
|
if !removed[editorResourceID(page)] {
|
|
files = append(files, name)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return files
|
|
}
|
|
|
|
// commitAnnotationParts 提交独立注解包快照,新建文档复用导入包基底
|
|
// 入参: base 原包, parts 修改的文件
|
|
// 返回: error 错误信息
|
|
func (e *Editor) commitAnnotationParts(base *editorSource, parts map[string][]byte) error {
|
|
return e.commitAnnotationPartsContext(context.Background(), base, parts)
|
|
}
|
|
|
|
// commitAnnotationPartsContext 准备并提交独立包快照,取消时不更新编辑状态
|
|
// 入参: ctx 取消上下文, base 原包, parts 修改的文件
|
|
// 返回: error 结构、读取或取消错误
|
|
func (e *Editor) commitAnnotationPartsContext(ctx context.Context, base *editorSource, parts map[string][]byte) error {
|
|
parts, err := base.reader.versionChanges(ctx, parts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files := maps.Clone(base.reader.files)
|
|
if files == nil {
|
|
files = make(map[string][]byte)
|
|
}
|
|
for name, data := range parts {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if file, ok := base.reader.packageFile(name); ok {
|
|
name = cleanPackagePath(file.Name)
|
|
}
|
|
files[name] = data
|
|
}
|
|
reader := &Reader{Zip: base.reader.Zip, files: files, encryption: base.reader.encryption, documentIndex: base.reader.documentIndex, selectedVersion: base.reader.selectedVersion}
|
|
if err := reader.initRootContext(ctx); err != nil {
|
|
return err
|
|
}
|
|
doc, err := reader.docStructureContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = reader.annotationIndexContext(ctx, doc)
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
before, after := e.source, *base
|
|
after.reader, after.document = reader, doc
|
|
resources := e.resources
|
|
if before == nil {
|
|
resources = slices.Clone(resources)
|
|
for i := range resources {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
resource := &resources[i]
|
|
if resource.font != nil {
|
|
resource.font = cloneEditorData(resource.font)
|
|
if resource.name != "" {
|
|
resource.font.FontFile = "/" + resource.name
|
|
}
|
|
}
|
|
if resource.image != nil {
|
|
resource.image = cloneEditorData(resource.image)
|
|
resource.image.MediaFile = "/" + resource.name
|
|
}
|
|
}
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
e.resources = resources
|
|
e.source = &after
|
|
if change := e.recordChange(); change != nil {
|
|
change.undo = func(e *Editor) { e.source = before }
|
|
change.redo = func(e *Editor) { e.source = &after }
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// annotationScope 将注解外观作为内部编辑根容器,保留标准页块结构和坐标约定
|
|
// 入参: page 页面索引, path 注解内部路径
|
|
// 返回: *Reader 预览, *Renderer 度量器, *editorCompositeNode 根, []*editorCompositeNode 成员, error 错误信息
|
|
func (e *Editor) annotationScope(page int, path ObjectPath) (*Reader, *Renderer, *editorCompositeNode, []*editorCompositeNode, error) {
|
|
data, err := e.annotationXML(page, path.Annotation)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
annotation, err := parseEditorXML(data)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
appearance := annotation.child("Appearance")
|
|
if appearance == nil {
|
|
return nil, nil, nil, nil, fmt.Errorf("annotation has no appearance")
|
|
}
|
|
data, err = editorXMLStandalone(data[appearance.start:appearance.end], appearance)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
root, err := newEditorCompositeNode(data)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
root.setStates(e.source.annotationStates[e.pages[page].ID+"/"+editorResourceID(path.Annotation)])
|
|
reader, err := e.reader(nil)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
if _, err = reader.PageContentByIndex(page); err != nil {
|
|
reader.Close()
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
renderer := e.newRenderer(reader)
|
|
root.parent, root.visible = IdentityMatrix, true
|
|
scope := root
|
|
for depth := 0; ; depth++ {
|
|
members, loadErr := e.compositeMembers(scope, reader, renderer, make(map[string]bool))
|
|
if loadErr != nil {
|
|
err = loadErr
|
|
break
|
|
}
|
|
if depth == len(path.Children) {
|
|
for i, member := range members {
|
|
member.index = i
|
|
}
|
|
return reader, renderer, root, members, nil
|
|
}
|
|
i := path.Children[depth]
|
|
if i < 0 || i >= len(members) {
|
|
err = fmt.Errorf("annotation member index %d out of range", i)
|
|
break
|
|
}
|
|
scope = members[i]
|
|
}
|
|
reader.Close()
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
// writeAnnotationScope 只替换外观片段并保存会话排版状态,不改写注解元数据和动作
|
|
// 入参: page 页面索引, id 注解标识, data 外观XML, states 内部编辑状态
|
|
// 返回: error 错误信息
|
|
func (e *Editor) writeAnnotationScope(page int, id string, data []byte, states map[string]editorCompositeState) error {
|
|
revision := e.Revision()
|
|
err := e.editAnnotations(page, []string{id}, func(original []byte, node *editorXML) ([]byte, error) {
|
|
fragment, err := editorXMLStandalone(original[node.start:node.end], node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root, err := parseEditorXML(fragment)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
appearance := root.child("Appearance")
|
|
return editorAnnotationDate(editorPatchXML(fragment, []editorXMLPatch{{appearance.start, appearance.end, bytes.TrimPrefix(data, []byte(xml.Header))}}))
|
|
})
|
|
if err == nil && revision != e.Revision() {
|
|
statesByID := maps.Clone(e.source.annotationStates)
|
|
if statesByID == nil {
|
|
statesByID = make(map[string]map[string]editorCompositeState)
|
|
}
|
|
statesByID[e.pages[page].ID+"/"+editorResourceID(id)] = maps.Clone(states)
|
|
e.source.annotationStates = statesByID
|
|
}
|
|
return err
|
|
}
|