Files
ofdgo/ofdgo_editor_outline.go
T
2026-10-10 16:47:47 +08:00

505 lines
14 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"
"encoding/xml"
"fmt"
"slices"
"strconv"
"strings"
)
// Outlines 获取当前目录层级,目标页码从1开始,无目标时为0
// 返回: []OutlineInfo 目录信息, error 错误信息
func (e *Editor) Outlines() ([]OutlineInfo, error) {
data, err := e.outlineXML()
if err != nil {
return nil, err
}
var doc Document
if err := xml.Unmarshal(data, &doc.Outlines); err != nil {
return nil, err
}
if e.source != nil {
doc.Bookmarks = e.source.document.Bookmarks
}
for _, page := range e.pages {
doc.Pages.Page = append(doc.Pages.Page, Page{ID: page.ID})
}
return doc.OutlineInfos(), nil
}
// AddOutline 在指定父目录末尾添加节点,空路径表示根目录
// 入参: parent 从0开始的层级索引, title 标题, page 目标页面索引,-1表示无目标
// 返回: []int 新节点路径, error 错误信息
func (e *Editor) AddOutline(parent []int, title string, page int) ([]int, error) {
if err := e.validateOutline(title, page); err != nil {
return nil, err
}
data, err := e.outlineXML()
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
node, err := editorOutlineNode(root, parent)
if err != nil {
return nil, err
}
attrs := ofdAttrs{}
attrs.add("Title", title)
item, err := editorXMLContainer("OutlineElem", attrs, e.outlineAction(page))
if err != nil {
return nil, err
}
item, err = editorXMLGenerated(item, node.name.Space)
if err != nil {
return nil, err
}
var content []byte
if node.open != node.end {
content = bytes.Clone(data[node.open:node.close])
}
content = append(content, item...)
result := append(slices.Clone(parent), len(editorOutlineChildren(node)))
err = e.setOutlineXML(editorPatchXML(data, []editorXMLPatch{editorXMLContent(data, node, content)}))
return result, err
}
// UpdateOutline 修改目录标题和目标,目标未改变时保留原跳转坐标及动作
// 入参: path 从0开始的层级索引, title 标题, page 目标页面索引,-1表示无目标
// 返回: error 错误信息
func (e *Editor) UpdateOutline(path []int, title string, page int) error {
if err := e.validateOutline(title, page); err != nil {
return err
}
data, err := e.outlineXML()
if err != nil {
return err
}
root, err := parseEditorXML(data)
if err != nil {
return err
}
node, err := editorOutlineNode(root, path)
if err != nil {
return err
}
if len(path) == 0 {
return fmt.Errorf("outline path must not be empty")
}
infos, err := e.Outlines()
if err != nil {
return err
}
var info OutlineInfo
for _, index := range path {
info = infos[index]
infos = info.Children
}
var patches []editorXMLPatch
if info.Page != page+1 {
actions := node.child("Actions")
if actions != nil {
var removals []editorXMLPatch
for _, action := range actions.children {
if !action.matchesOFD("Action") {
continue
}
if action.child("Goto") != nil {
removals = append(removals, editorXMLPatch{action.start - actions.open, action.end - actions.open, nil})
}
}
var content []byte
if actions.open != actions.end {
content = editorPatchXML(data[actions.open:actions.close], removals)
}
added, err := editorXMLGenerated(e.outlineAction(page), actions.name.Space)
if err != nil {
return err
}
if len(added) > 0 {
addRoot, err := parseEditorXML(added)
if err != nil {
return err
}
action := addRoot.children[0]
standalone, err := editorXMLStandalone(added[action.start:action.end], action)
if err != nil {
return err
}
content = append(content, standalone...)
}
patches = append(patches, editorXMLContent(data, actions, content))
} else if page >= 0 {
content, err := editorXMLGenerated(e.outlineAction(page), node.name.Space)
if err != nil {
return err
}
if node.open != node.end {
content = append(content, data[node.open:node.close]...)
}
patches = append(patches, editorXMLContent(data, node, content))
}
}
updated := editorPatchXML(data, patches)
root, err = parseEditorXML(updated)
if err != nil {
return err
}
node, err = editorOutlineNode(root, path)
if err != nil {
return err
}
value, err := editorXMLAttribute(updated, node, "Title", title)
if err != nil {
return err
}
return e.setOutlineXML(editorPatchXML(updated, []editorXMLPatch{{node.start, node.end, value}}))
}
// SetOutlineActions 替换目录节点的动作序列,空列表清除动作,不改变子目录
// 入参: path 从0开始的层级索引, actions 按激活顺序执行的动作
// 返回: error 路径、动作或提交错误
func (e *Editor) SetOutlineActions(path []int, actions []Action) error {
if len(path) == 0 {
return fmt.Errorf("outline path must not be empty")
}
if err := validateActions(actions); err != nil {
return err
}
data, err := e.outlineXML()
if err != nil {
return err
}
root, err := parseEditorXML(data)
if err != nil {
return err
}
node, err := editorOutlineNode(root, path)
if err != nil {
return err
}
encoded, err := editorActionsXML(actions)
if err != nil {
return err
}
context := node
if existing := node.child("Actions"); existing != nil {
context = existing
}
encoded, err = editorXMLGenerated(encoded, context.name.Space)
if err != nil {
return err
}
var patch editorXMLPatch
if context != node {
patch = editorXMLPatch{context.start, context.end, encoded}
} else {
if node.open != node.end {
encoded = append(encoded, data[node.open:node.close]...)
}
patch = editorXMLContent(data, node, encoded)
}
return e.setOutlineXML(editorPatchXML(data, []editorXMLPatch{patch}))
}
// DeleteOutline 删除目录节点及其子节点
// 入参: path 从0开始的层级索引
// 返回: error 错误信息
func (e *Editor) DeleteOutline(path []int) error {
if len(path) == 0 {
return fmt.Errorf("outline path must not be empty")
}
data, err := e.outlineXML()
if err != nil {
return err
}
root, err := parseEditorXML(data)
if err != nil {
return err
}
node, err := editorOutlineNode(root, path)
if err != nil {
return err
}
return e.setOutlineXML(editorPatchXML(data, []editorXMLPatch{{node.start, node.end, nil}}))
}
// MoveOutline 移动目录及其子树,保留原始动作和扩展字段,一次撤销恢复
// 入参: path 节点路径, parent 移动前的目标父路径,空路径表示根, index 移除源节点后在目标父节点中的插入索引
// 返回: []int 移动后的路径, error 错误信息
func (e *Editor) MoveOutline(path, parent []int, index int) ([]int, error) {
if len(path) == 0 || len(parent) >= len(path) && slices.Equal(path, parent[:len(path)]) {
return nil, fmt.Errorf("cannot move an outline into itself")
}
data, err := e.outlineXML()
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
node, err := editorOutlineNode(root, path)
if err != nil {
return nil, err
}
target, err := editorOutlineNode(root, parent)
if err != nil {
return nil, err
}
children := editorOutlineChildren(target)
same := slices.Equal(path[:len(path)-1], parent)
if same {
children = slices.Delete(children, path[len(path)-1], path[len(path)-1]+1)
}
if index < 0 || index > len(children) {
return nil, fmt.Errorf("outline insertion index %d out of range", index)
}
result := slices.Clone(parent)
if len(result) >= len(path) && slices.Equal(path[:len(path)-1], result[:len(path)-1]) && result[len(path)-1] > path[len(path)-1] {
result[len(path)-1]--
}
result = append(result, index)
if same && path[len(path)-1] == index {
return result, nil
}
value, err := editorXMLStandalone(data[node.start:node.end], node)
if err != nil {
return nil, err
}
patches := []editorXMLPatch{{node.start, node.end, nil}}
if target.open == target.end {
patches = append(patches, editorXMLContent(data, target, value))
} else {
at := target.close
if index < len(children) {
at = children[index].start
}
patches = append(patches, editorXMLPatch{at, at, value})
}
return result, e.setOutlineXML(editorPatchXML(data, patches))
}
// validateOutline 校验目录标题和目标页面
// 入参: title 标题, page 页面索引
// 返回: error 错误信息
func (e *Editor) validateOutline(title string, page int) error {
if strings.TrimSpace(title) == "" {
return fmt.Errorf("outline title must not be empty")
}
if page < -1 || page >= len(e.pages) {
return fmt.Errorf("page index %d out of range", page)
}
return nil
}
// outlineXML 获取保留命名空间的目录原文
// 返回: []byte XML内容, error 错误信息
func (e *Editor) outlineXML() ([]byte, error) {
if e.outlines != nil {
return e.outlines, nil
}
if e.source != nil {
reader := e.source.reader
data, err := reader.readFile(reader.ResPath(reader.OFD.DocBody[reader.documentIndex].DocRoot))
if err != nil {
return nil, err
}
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
if node := root.child("Outlines"); node != nil {
return editorXMLStandalone(data[node.start:node.end], node)
}
return root.containerXML("Outlines", nil, nil)
}
return editorXMLContainer("Outlines", nil, nil)
}
// outlineAction 编码页面适配跳转,负数表示无目标
// 入参: page 页面索引
// 返回: []byte 动作集合
func (e *Editor) outlineAction(page int) []byte {
if page < 0 {
return nil
}
data, _ := encodeOFDXML(func(x *ofdXML) {
x.root("Actions", nil)
x.start("Action", ofdAttrs{{Name: xml.Name{Local: "Event"}, Value: "CLICK"}})
x.start("Goto", nil)
x.start("Dest", ofdAttrs{{Name: xml.Name{Local: "Type"}, Value: "Fit"}, {Name: xml.Name{Local: "PageID"}, Value: e.pages[page].ID}})
x.end("Dest")
x.end("Goto")
x.end("Action")
x.end("Actions")
})
return bytes.TrimPrefix(data, []byte(xml.Header))
}
// editorOutlineChildren 获取标准目录子节点
// 入参: node 父节点
// 返回: []*editorXML 子节点
func editorOutlineChildren(node *editorXML) []*editorXML {
var children []*editorXML
for _, child := range node.children {
if child.matchesOFD("OutlineElem") {
children = append(children, child)
}
}
return children
}
// editorOutlineNode 按层级路径定位目录
// 入参: root 目录根节点, path 层级索引
// 返回: *editorXML 节点, error 错误信息
func editorOutlineNode(root *editorXML, path []int) (*editorXML, error) {
for _, index := range path {
children := editorOutlineChildren(root)
if index < 0 || index >= len(children) {
return nil, fmt.Errorf("outline index %d out of range", index)
}
root = children[index]
}
return root, nil
}
// setOutlineXML 更新目录及可选叶节点计数,保存一次撤销记录
// 入参: data 新目录原文
// 返回: error 错误信息
func (e *Editor) setOutlineXML(data []byte) error {
root, err := parseEditorXML(data)
if err != nil {
return err
}
var patches []editorXMLPatch
var leaves func(*editorXML) int
leaves = func(node *editorXML) int {
children := editorOutlineChildren(node)
if len(children) == 0 {
return 1
}
count := 0
for _, child := range children {
count += leaves(child)
}
return count
}
var walk func(*editorXML) error
walk = func(node *editorXML) error {
if node.attr("Count") != "" {
count := 0
for _, child := range editorOutlineChildren(node) {
count += leaves(child)
}
value, err := editorXMLAttribute(data, node, "Count", strconv.Itoa(count))
if err != nil {
return err
}
updated, err := parseEditorXML(value)
if err != nil {
return err
}
if node.open != node.end {
patches = append(patches, editorXMLPatch{node.start, node.open, value[:updated.open]})
} else {
patches = append(patches, editorXMLPatch{node.start, node.end, value})
}
}
for _, child := range editorOutlineChildren(node) {
if err := walk(child); err != nil {
return err
}
}
return nil
}
if err := walk(root); err != nil {
return err
}
data = editorPatchXML(data, patches)
current, err := e.outlineXML()
if err != nil {
return err
}
if bytes.Equal(current, data) {
return nil
}
before := e.outlines
e.outlines = data
if change := e.recordChange(); change != nil {
change.undo = func(e *Editor) { e.outlines = before }
change.redo = func(e *Editor) { e.outlines = data }
}
return nil
}
// withOutlines 将修改后的目录写入文档,清除指向已删除页面的新增跳转
// 入参: data 文档原文
// 返回: []byte 更新文档, error 错误信息
func (e *Editor) withOutlines(data []byte) ([]byte, error) {
if e.outlines == nil {
return data, nil
}
outline, err := parseEditorXML(e.outlines)
if err != nil {
return nil, err
}
valid := make(map[string]int, len(e.pages))
for i, page := range e.pages {
valid[page.ID] = i
}
var removals []editorXMLPatch
var walk func(*editorXML)
walk = func(node *editorXML) {
if actions := node.child("Actions"); actions != nil {
for _, action := range actions.children {
if !action.matchesOFD("Action") {
continue
}
if target := action.child("Goto"); target != nil {
if dest := target.child("Dest"); dest != nil {
if _, ok := resourceValue(valid, dest.attr("PageID")); !ok {
removals = append(removals, editorXMLPatch{action.start, action.end, nil})
}
}
}
}
}
for _, child := range editorOutlineChildren(node) {
walk(child)
}
}
walk(outline)
value := editorPatchXML(e.outlines, removals)
root, err := parseEditorXML(data)
if err != nil {
return nil, err
}
if node := root.child("Outlines"); node != nil {
return editorPatchXML(data, []editorXMLPatch{{node.start, node.end, value}}), nil
}
pages := root.child("Pages")
if pages == nil {
return nil, fmt.Errorf("document has no Pages")
}
return editorPatchXML(data, []editorXMLPatch{{pages.end, pages.end, value}}), nil
}