mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
1 parent
d3a713f504
commit
528162a276
16 files changed
+3998
-254
No files matched your search
@@ -0,0 +1,583 @@
|
||||
// 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()
|
||||
}
|
||||
Reference in new issue
Block a user