mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
584 lines
21 KiB
Go
584 lines
21 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 (
|
|
"context"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// InvoiceLayout 指定发票信息页的字体资源及毫米尺寸,不代表税务机关的固定票样
|
|
// Font必填;其余零值依次使用4毫米字号、210毫米宽、297毫米高、15毫米页边距
|
|
type InvoiceLayout struct {
|
|
Font string `json:"font"`
|
|
FontSize float64 `json:"fontSize"`
|
|
PageWidth float64 `json:"pageWidth"`
|
|
PageHeight float64 `json:"pageHeight"`
|
|
Margin float64 `json:"margin"`
|
|
}
|
|
|
|
// InvoiceBinding 将一个完整字段绑定至当前文档的文字对象,页码及明细索引从0开始
|
|
// Field使用number、buyer.name、items.name等规范字段名,非明细字段的Item必须为-1
|
|
// 原始事实使用facts.N或items.fields.N,N为原始事实索引,不根据名称合并不同上下文
|
|
type InvoiceBinding struct {
|
|
Field string `json:"field"`
|
|
Item int `json:"item"`
|
|
Page int `json:"page"`
|
|
ObjectID string `json:"objectId"`
|
|
}
|
|
|
|
// invoiceLayoutField 保存票面字段及独立标签
|
|
type invoiceLayoutField struct {
|
|
field, label, value string
|
|
item int
|
|
}
|
|
|
|
// invoiceAppearanceMatches 比较票面Unicode字符序列,仅忽略回车与换行,保留全部空格
|
|
// 不校验显式换行、软折行、位置或可见性,字形索引文本无法确定原文时返回false
|
|
// 入参: text 票面文字对象, value 结构化字段原文
|
|
// 返回: bool 忽略回车与换行后的字符序列是否一致
|
|
func invoiceAppearanceMatches(text TextObject, value string) bool {
|
|
var content strings.Builder
|
|
for _, code := range text.TextCode {
|
|
if code.Index != "" {
|
|
return false
|
|
}
|
|
content.WriteString(string(textCodeRunes(code.Value)))
|
|
}
|
|
actual := strings.ReplaceAll(strings.ReplaceAll(content.String(), "\r", ""), "\n", "")
|
|
expected := strings.ReplaceAll(strings.ReplaceAll(value, "\r", ""), "\n", "")
|
|
return actual == expected
|
|
}
|
|
|
|
// invoiceLayoutFields 按稳定顺序展开公共字段及明细,不推算缺失值
|
|
// 入参: invoice 发票数据
|
|
// 返回: []invoiceLayoutField 字段列表
|
|
func invoiceLayoutFields(invoice Invoice) []invoiceLayoutField {
|
|
fields := []invoiceLayoutField{
|
|
{"kind", "票种", invoice.Kind, -1}, {"standard", "规范", invoice.Standard, -1},
|
|
{"version", "版本", invoice.Version, -1}, {"number", "发票号码", invoice.Number, -1},
|
|
{"code", "发票代码", invoice.Code, -1}, {"date", "开票日期", invoice.Date, -1},
|
|
{"currency", "币种", invoice.Currency, -1}, {"status", "状态", invoice.Status, -1},
|
|
}
|
|
for _, party := range []struct {
|
|
prefix, label string
|
|
value InvoiceParty
|
|
}{{"buyer", "购买方", invoice.Buyer}, {"seller", "销售方", invoice.Seller}} {
|
|
for _, field := range []invoiceLayoutField{{"name", "名称", party.value.Name, -1}, {"taxId", "税号", party.value.TaxID, -1}, {"address", "地址", party.value.Address, -1}, {"phone", "电话", party.value.Phone, -1}, {"bank", "开户行", party.value.Bank, -1}, {"account", "账号", party.value.Account, -1}} {
|
|
field.field, field.label = party.prefix+"."+field.field, party.label+field.label
|
|
fields = append(fields, field)
|
|
}
|
|
}
|
|
for i, item := range invoice.Items {
|
|
for _, field := range []invoiceLayoutField{{"name", "名称", item.Name, i}, {"specification", "规格", item.Specification, i}, {"unit", "单位", item.Unit, i}, {"quantity", "数量", item.Quantity, i}, {"unitPrice", "单价", item.UnitPrice, i}, {"amount", "金额", item.Amount, i}, {"taxRate", "税率", item.TaxRate, i}, {"taxAmount", "税额", item.TaxAmount, i}} {
|
|
field.field, field.label = "items."+field.field, fmt.Sprintf("明细%d %s", i+1, field.label)
|
|
fields = append(fields, field)
|
|
}
|
|
}
|
|
fields = append(fields, invoiceLayoutField{"amount", "不含税金额", invoice.Amount, -1}, invoiceLayoutField{"taxAmount", "税额", invoice.TaxAmount, -1}, invoiceLayoutField{"totalAmount", "价税合计", invoice.TotalAmount, -1}, invoiceLayoutField{"remarks", "备注", invoice.Remarks, -1})
|
|
for i, fact := range invoice.Facts {
|
|
if invoiceLayoutFactVisible(invoice, fact) {
|
|
fields = append(fields, invoiceLayoutField{fmt.Sprintf("facts.%d", i), fact.Name, fact.Value, -1})
|
|
}
|
|
}
|
|
for i, item := range invoice.Items {
|
|
for j, fact := range item.Fields {
|
|
duplicate := false
|
|
for _, existing := range invoice.Facts {
|
|
if reflect.DeepEqual(existing, fact) {
|
|
duplicate = true
|
|
break
|
|
}
|
|
}
|
|
if !duplicate && invoiceLayoutFactVisible(invoice, fact) {
|
|
fields = append(fields, invoiceLayoutField{fmt.Sprintf("items.fields.%d", j), fmt.Sprintf("明细%d %s", i+1, fact.Name), fact.Value, i})
|
|
}
|
|
}
|
|
}
|
|
return fields
|
|
}
|
|
|
|
// invoiceLayoutFactVisible 排除上下文、单位及已明确映射的相同字段值
|
|
// 入参: invoice 发票数据, fact 原始业务事实
|
|
// 返回: bool 是否需要单独显示
|
|
func invoiceLayoutFactVisible(invoice Invoice, fact InvoiceFact) bool {
|
|
if fact.Nil || fact.Value == "" || fact.Namespace == invoiceXBRL && (fact.Name == "context" || fact.Name == "unit") {
|
|
return false
|
|
}
|
|
kind, _ := invoiceTaxonomy(fact.Namespace)
|
|
if kind != "" && kind == invoice.Kind {
|
|
if field := invoiceXMLField(&invoice, kind, fact.Name); field != nil && *field == fact.Value {
|
|
return false
|
|
}
|
|
for _, item := range invoice.Items {
|
|
if field := invoiceItemField(&item, kind, fact.Name); field != nil && *field == fact.Value {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
if fact.Namespace == "" {
|
|
var path strings.Builder
|
|
valid := true
|
|
for _, segment := range strings.Split(strings.TrimPrefix(fact.Source.NodePath, "/"), "/") {
|
|
name, index, ok := strings.Cut(segment, "[")
|
|
if !ok || !strings.HasPrefix(name, "{}") || !strings.HasSuffix(index, "]") {
|
|
valid = false
|
|
break
|
|
}
|
|
path.WriteByte('/')
|
|
path.WriteString(strings.TrimPrefix(name, "{}"))
|
|
}
|
|
if valid {
|
|
if path.String() == "/EInvoice/Header/Version" && fact.Value == invoice.Version {
|
|
return false
|
|
}
|
|
if field, _ := invoicePlainField(&invoice, nil, path.String()); field != nil && *field == fact.Value {
|
|
return false
|
|
}
|
|
for _, item := range invoice.Items {
|
|
if field, _ := invoicePlainField(&invoice, &item, path.String()); field != nil && *field == fact.Value {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// invoiceBindingValue 读取明确字段与明细索引,不接受未知字段或隐式索引
|
|
// 入参: invoice 发票数据, binding 字段绑定
|
|
// 返回: string 字段原文, error 字段或索引错误
|
|
func invoiceBindingValue(invoice Invoice, binding InvoiceBinding) (string, error) {
|
|
if strings.HasPrefix(binding.Field, "facts.") && binding.Item == -1 {
|
|
index, err := strconv.Atoi(strings.TrimPrefix(binding.Field, "facts."))
|
|
if err == nil && index >= 0 && index < len(invoice.Facts) && binding.Field == fmt.Sprintf("facts.%d", index) {
|
|
return invoice.Facts[index].Value, nil
|
|
}
|
|
}
|
|
if strings.HasPrefix(binding.Field, "items.") {
|
|
if binding.Item >= 0 && binding.Item < len(invoice.Items) {
|
|
item := invoice.Items[binding.Item]
|
|
if strings.HasPrefix(binding.Field, "items.fields.") {
|
|
index, err := strconv.Atoi(strings.TrimPrefix(binding.Field, "items.fields."))
|
|
if err == nil && index >= 0 && index < len(item.Fields) && binding.Field == fmt.Sprintf("items.fields.%d", index) {
|
|
return item.Fields[index].Value, nil
|
|
}
|
|
}
|
|
switch binding.Field {
|
|
case "items.name":
|
|
return item.Name, nil
|
|
case "items.specification":
|
|
return item.Specification, nil
|
|
case "items.unit":
|
|
return item.Unit, nil
|
|
case "items.quantity":
|
|
return item.Quantity, nil
|
|
case "items.unitPrice":
|
|
return item.UnitPrice, nil
|
|
case "items.amount":
|
|
return item.Amount, nil
|
|
case "items.taxRate":
|
|
return item.TaxRate, nil
|
|
case "items.taxAmount":
|
|
return item.TaxAmount, nil
|
|
}
|
|
}
|
|
} else if binding.Item == -1 {
|
|
switch binding.Field {
|
|
case "kind":
|
|
return invoice.Kind, nil
|
|
case "standard":
|
|
return invoice.Standard, nil
|
|
case "version":
|
|
return invoice.Version, nil
|
|
case "number":
|
|
return invoice.Number, nil
|
|
case "code":
|
|
return invoice.Code, nil
|
|
case "date":
|
|
return invoice.Date, nil
|
|
case "currency":
|
|
return invoice.Currency, nil
|
|
case "status":
|
|
return invoice.Status, nil
|
|
case "amount":
|
|
return invoice.Amount, nil
|
|
case "taxAmount":
|
|
return invoice.TaxAmount, nil
|
|
case "totalAmount":
|
|
return invoice.TotalAmount, nil
|
|
case "remarks":
|
|
return invoice.Remarks, nil
|
|
}
|
|
prefix, name, ok := strings.Cut(binding.Field, ".")
|
|
if ok && (prefix == "buyer" || prefix == "seller") {
|
|
party := invoice.Buyer
|
|
if prefix == "seller" {
|
|
party = invoice.Seller
|
|
}
|
|
switch name {
|
|
case "name":
|
|
return party.Name, nil
|
|
case "taxId":
|
|
return party.TaxID, nil
|
|
case "address":
|
|
return party.Address, nil
|
|
case "phone":
|
|
return party.Phone, nil
|
|
case "bank":
|
|
return party.Bank, nil
|
|
case "account":
|
|
return party.Account, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("invalid invoice binding field %q item %d", binding.Field, binding.Item)
|
|
}
|
|
|
|
// AppendInvoicePages 追加可搜索的发票信息页,字段内折行、字段间分页,不修改已有页面
|
|
// 每字段对应一个完整文字对象,单字段超过页面容量时失败;绑定持久化由调用方处理
|
|
// 入参: ctx 取消上下文, invoice 发票数据, options 字体与页面尺寸
|
|
// 返回: []InvoiceBinding 新增字段绑定, error 排版或字体错误
|
|
func (e *Editor) AppendInvoicePages(ctx context.Context, invoice Invoice, options InvoiceLayout) ([]InvoiceBinding, error) {
|
|
if e == nil || ctx == nil {
|
|
return nil, fmt.Errorf("editor and context are required")
|
|
}
|
|
if options.Font == "" {
|
|
return nil, fmt.Errorf("invoice font resource is required")
|
|
}
|
|
if options.FontSize == 0 {
|
|
options.FontSize = 4
|
|
}
|
|
if options.PageWidth == 0 {
|
|
options.PageWidth = 210
|
|
}
|
|
if options.PageHeight == 0 {
|
|
options.PageHeight = 297
|
|
}
|
|
if options.Margin == 0 {
|
|
options.Margin = 15
|
|
}
|
|
for _, value := range []float64{options.FontSize, options.PageWidth, options.PageHeight, options.Margin} {
|
|
if !finite(value) || value <= 0 {
|
|
return nil, fmt.Errorf("invoice layout dimensions must be positive and finite")
|
|
}
|
|
}
|
|
width, height := options.PageWidth-2*options.Margin, options.PageHeight-2*options.Margin
|
|
if width <= 0 || height <= 0 {
|
|
return nil, fmt.Errorf("invoice margins leave no page area")
|
|
}
|
|
var bindings []InvoiceBinding
|
|
err := e.Transaction(func(editor *Editor) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
metrics, err := editor.editorFont(options.Font)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
page, y := -1, options.Margin
|
|
for _, field := range invoiceLayoutFields(invoice) {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if field.value == "" {
|
|
continue
|
|
}
|
|
labelWidth := math.Min(width*.4, options.FontSize*8)
|
|
gap := options.FontSize
|
|
label := TextObject{Font: options.Font, Size: options.FontSize, Boundary: fmt.Sprintf("0 0 %s %s", ofdNumber(labelWidth), ofdNumber(height))}
|
|
value := TextObject{Font: options.Font, Size: options.FontSize, Boundary: fmt.Sprintf("0 0 %s %s", ofdNumber(width-labelWidth-gap), ofdNumber(height))}
|
|
labelHeight, err := invoiceLayoutText(&label, field.label, metrics)
|
|
if err != nil {
|
|
return fmt.Errorf("invoice label %s: %w", field.field, err)
|
|
}
|
|
valueHeight, err := invoiceLayoutText(&value, field.value, metrics)
|
|
if err != nil {
|
|
return fmt.Errorf("invoice field %s: %w", field.field, err)
|
|
}
|
|
rowHeight := math.Max(labelHeight, valueHeight)
|
|
if page < 0 || y+rowHeight > options.PageHeight-options.Margin {
|
|
page, err = editor.AddPage(options.PageWidth, options.PageHeight)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
y = options.Margin
|
|
}
|
|
label.Boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(options.Margin), ofdNumber(y), ofdNumber(labelWidth), ofdNumber(rowHeight))
|
|
value.Boundary = fmt.Sprintf("%s %s %s %s", ofdNumber(options.Margin+labelWidth+gap), ofdNumber(y), ofdNumber(width-labelWidth-gap), ofdNumber(rowHeight))
|
|
if _, err := editor.AddObject(page, GraphicObject{Type: "TextObject", TextObject: label}); err != nil {
|
|
return err
|
|
}
|
|
id, err := editor.AddObject(page, GraphicObject{Type: "TextObject", TextObject: value})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bindings = append(bindings, InvoiceBinding{Field: field.field, Item: field.item, Page: page, ObjectID: id})
|
|
y += rowHeight + options.FontSize*.5
|
|
}
|
|
if len(bindings) == 0 {
|
|
return fmt.Errorf("invoice has no display fields")
|
|
}
|
|
return ctx.Err()
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bindings, nil
|
|
}
|
|
|
|
// SyncInvoicePages 原子更新显式绑定的普通横向文字,不新增、移动或缩小对象
|
|
// 不接受重复绑定、空文本、缺失对象及超出原框的内容;可嵌入上层事务与结构化数据一起提交
|
|
// 入参: ctx 取消上下文, invoice 发票数据, bindings 当前文档字段绑定
|
|
// 返回: error 绑定、字体或容量错误
|
|
func (e *Editor) SyncInvoicePages(ctx context.Context, invoice Invoice, bindings []InvoiceBinding) error {
|
|
if e == nil || ctx == nil {
|
|
return fmt.Errorf("editor and context are required")
|
|
}
|
|
return e.Transaction(func(editor *Editor) error {
|
|
objects, fields := make(map[string]bool), make(map[string]bool)
|
|
for _, binding := range bindings {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
objectKey := fmt.Sprintf("%d/%s", binding.Page, editorResourceID(binding.ObjectID))
|
|
fieldKey := fmt.Sprintf("%d/%s", binding.Item, binding.Field)
|
|
if binding.ObjectID == "" || objects[objectKey] || fields[fieldKey] {
|
|
return fmt.Errorf("ambiguous invoice binding %q", binding.Field)
|
|
}
|
|
objects[objectKey], fields[fieldKey] = true, true
|
|
value, err := invoiceBindingValue(invoice, binding)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
page, err := editor.page(binding.Page)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
matches := 0
|
|
for _, layer := range page.Content.Layer {
|
|
for _, candidate := range layer.Objects {
|
|
if sameResourceID(editorObjectID(candidate), binding.ObjectID) {
|
|
matches++
|
|
}
|
|
}
|
|
}
|
|
if matches != 1 {
|
|
return fmt.Errorf("invoice binding %q requires one text object", binding.Field)
|
|
}
|
|
object, err := editor.Object(binding.Page, binding.ObjectID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if object.Type != "TextObject" {
|
|
return fmt.Errorf("invoice binding %q is not text", binding.Field)
|
|
}
|
|
text := &object.TextObject
|
|
if text.CTM != "" || text.Clips != nil || text.Italic || text.Weight > 400 || text.VScale != 0 || text.Decoration != "" || text.DrawParam != "" || text.Stroke != nil && *text.Stroke || text.HScale != 0 && text.HScale != 1 {
|
|
return fmt.Errorf("invoice binding %q requires plain untransformed text", binding.Field)
|
|
}
|
|
metrics, err := editor.editorFont(text.Font)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := invoiceLayoutText(text, value, metrics); err != nil {
|
|
return fmt.Errorf("invoice field %s: %w", binding.Field, err)
|
|
}
|
|
if err := editor.UpdateObject(binding.Page, binding.ObjectID, object); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
})
|
|
}
|
|
|
|
// invoiceLayoutText 按实际字形及步进检查折行容量,并将负侧承移入原框
|
|
// 入参: text 普通横向文字, value 字段原文, metrics 字体度量
|
|
// 返回: float64 内容高度, error 缺字或越界错误
|
|
func invoiceLayoutText(text *TextObject, value string, metrics FontMetrics) (float64, error) {
|
|
box, err := text.TextFrame()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if value == "" {
|
|
return 0, fmt.Errorf("empty invoice field requires removing its binding and text object")
|
|
}
|
|
if strings.Trim(value, "\r\n") == "" {
|
|
return 0, fmt.Errorf("invoice text contains no display characters")
|
|
}
|
|
if err := LayoutText(text, value, TextLayout{Wrap: true}, metrics); err != nil {
|
|
return 0, err
|
|
}
|
|
outlines, ok := metrics.(FontOutlines)
|
|
if !ok {
|
|
return 0, fmt.Errorf("invoice layout requires glyph outlines")
|
|
}
|
|
unit := text.Size / float64(metrics.UnitsPerEm())
|
|
_, descender, _ := metrics.VerticalMetrics()
|
|
left, top, right, bottom := 0.0, 0.0, 0.0, 0.0
|
|
for _, code := range text.TextCode {
|
|
x, err := strconv.ParseFloat(code.X, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
y, err := strconv.ParseFloat(code.Y, 64)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
bottom = math.Max(bottom, y+float64(descender)*unit)
|
|
for _, char := range textCodeRunes(code.Value) {
|
|
glyph := metrics.GlyphIndex(char)
|
|
var bounds Box
|
|
if provider, ok := metrics.(FontGlyphBounds); ok {
|
|
bounds, err = provider.GlyphBounds(glyph, text.Size)
|
|
} else {
|
|
var outline GeometryPath
|
|
outline, err = outlines.GlyphOutline(glyph, text.Size)
|
|
if err == nil {
|
|
bounds, err = outline.Bounds()
|
|
}
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
left, top = math.Min(left, x+bounds.X), math.Min(top, y+bounds.Y)
|
|
right, bottom = math.Max(right, x+bounds.X+bounds.W), math.Max(bottom, y+bounds.Y+bounds.H)
|
|
x += float64(metrics.GlyphAdvance(glyph)) * unit
|
|
right = math.Max(right, x)
|
|
}
|
|
}
|
|
if right-left > box.W+1e-6 || bottom-top > box.H+1e-6 {
|
|
return 0, fmt.Errorf("invoice text exceeds binding frame")
|
|
}
|
|
for i := range text.TextCode {
|
|
x, _ := strconv.ParseFloat(text.TextCode[i].X, 64)
|
|
y, _ := strconv.ParseFloat(text.TextCode[i].Y, 64)
|
|
text.TextCode[i].X, text.TextCode[i].Y = ofdNumber(x-left), ofdNumber(y-top)
|
|
}
|
|
return math.Ceil((bottom-top)*1e6) / 1e6, nil
|
|
}
|
|
|
|
// invoiceCheckReflowReferences 检查当前文档快照中指向待重排页的标准导航及已识别标引
|
|
// 排除本次发票自身的标引,不推测未知扩展中的引用,也不读取其他文档或历史入口
|
|
// 入参: ctx 取消上下文, e 当前编辑器, ownedPageIDs 待替换页面标识, ownStructureIndex 自有标引索引,-1表示无
|
|
// 返回: error 引用冲突、结构读取或取消错误
|
|
func invoiceCheckReflowReferences(ctx context.Context, e *Editor, ownedPageIDs []string, ownStructureIndex int) error {
|
|
if ctx == nil || e == nil {
|
|
return fmt.Errorf("editor and context are required")
|
|
}
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
owned := make(map[string]bool, len(ownedPageIDs))
|
|
for _, id := range ownedPageIDs {
|
|
key := editorResourceID(id)
|
|
if key == "" {
|
|
return fmt.Errorf("invalid generated invoice page identifier")
|
|
}
|
|
owned[key] = true
|
|
}
|
|
reader, err := e.ReaderContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer reader.Close()
|
|
structures, err := reader.StructureContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ownStructureIndex < -1 || ownStructureIndex >= len(structures) {
|
|
return fmt.Errorf("invoice structure index out of range")
|
|
}
|
|
var checkStructure func(*StructureNode) error
|
|
checkStructure = func(node *StructureNode) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if node == nil {
|
|
return nil
|
|
}
|
|
for _, reference := range node.References {
|
|
if owned[editorResourceID(reference.PageID)] {
|
|
return fmt.Errorf("generated invoice page is referenced by another structure")
|
|
}
|
|
}
|
|
for i := range node.Children {
|
|
if err := checkStructure(&node.Children[i]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for index, structure := range structures {
|
|
if index == ownStructureIndex {
|
|
continue
|
|
}
|
|
if structure.Error != "" {
|
|
return fmt.Errorf("cannot inspect invoice reflow references: %s", structure.Error)
|
|
}
|
|
if err := checkStructure(structure.Root); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, files, err := reader.versionDependencies(ctx, reader.documentRoot, nil, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var walk func(*editorXML) error
|
|
walk = func(node *editorXML) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
if !node.matchesOFD(node.name.Local) {
|
|
return nil
|
|
}
|
|
if node.matchesOFD("Data") && node.parent != nil && node.parent.matchesOFD("Extension") {
|
|
return nil
|
|
}
|
|
if node.matchesOFD("Dest") && node.parent != nil && (node.parent.matchesOFD("Goto") || node.parent.matchesOFD("Bookmark") || node.parent.matchesOFD("OutlineElem")) && owned[editorResourceID(node.attr("PageID"))] {
|
|
return fmt.Errorf("generated invoice page is referenced by navigation")
|
|
}
|
|
if node.matchesOFD("Extension") && owned[editorResourceID(node.attr("RefId"))] {
|
|
return fmt.Errorf("generated invoice page is referenced by an extension")
|
|
}
|
|
for _, child := range node.children {
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
for _, file := range slices.Sorted(maps.Keys(files)) {
|
|
data, err := reader.readFileViewContext(ctx, file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := parseEditorXMLContext(ctx, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := walk(root); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ctx.Err()
|
|
}
|