mirror of
https://github.com/ZeroHawkeye/wordZero.git
synced 2025-09-26 20:01:17 +08:00
- 增加模板功能
- 修复部分逻辑错误
This commit is contained in:
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,5 +1,102 @@
|
||||
# WordZero 更新日志
|
||||
|
||||
## [v1.3.3] - 2025-06-02
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
#### 页面设置保存和加载问题修复 ✨ **重要修复**
|
||||
- **修复问题**: 解决了页面设置在文档保存和重新加载后丢失的问题
|
||||
- **影响范围**: 主要影响页面配置功能,包括页面尺寸、方向、边距等设置
|
||||
- **错误表现**:
|
||||
- 设置页面为Letter横向,保存后重新打开变成A4纵向
|
||||
- XML结构中SectionProperties位置不正确
|
||||
- 页面设置解析失败,返回默认配置
|
||||
- **根本原因**:
|
||||
- `getSectionProperties()` 方法只检查Elements数组的最后一个元素
|
||||
- 文档序列化时SectionProperties被放在body开头,违反了Word XML规范
|
||||
- 解析时无法正确找到SectionProperties元素
|
||||
- **修复方案**:
|
||||
- 修改 `getSectionProperties()` 方法,在整个Elements数组中查找SectionProperties
|
||||
- 优化 `Body.MarshalXML()` 方法,确保SectionProperties始终位于body末尾
|
||||
- 遵循OpenXML规范,将sectPr放在正确位置
|
||||
- **修复后效果**:
|
||||
- 页面设置保存后正确加载:Letter横向 → Letter横向 ✓
|
||||
- XML结构符合Word标准:`<w:body><w:p>...</w:p><w:sectPr>...</w:sectPr></w:body>`
|
||||
- 所有页面配置(尺寸、方向、边距等)正确保持
|
||||
|
||||
#### 技术细节
|
||||
- **修改文件**:
|
||||
- `pkg/document/page.go` - 修复 `getSectionProperties()` 方法
|
||||
- `pkg/document/document.go` - 优化 `Body.MarshalXML()` 序列化逻辑
|
||||
- **修改内容**:
|
||||
- 在Elements数组中全局搜索SectionProperties而非只检查最后一个元素
|
||||
- 序列化时分离SectionProperties和其他元素,确保sectPr在body末尾
|
||||
- 移除位置假设,提高容错性
|
||||
- **影响功能**:
|
||||
- 所有页面设置功能(SetPageSettings, GetPageSettings等)
|
||||
- 文档保存和加载的完整性
|
||||
- XML文档结构的规范性
|
||||
|
||||
### 🔍 质量改进
|
||||
|
||||
#### XML结构规范性
|
||||
- ✅ **符合OpenXML规范**: SectionProperties现在正确位于body末尾
|
||||
- ✅ **文档结构完整性**: 页面设置在保存/加载过程中保持完整
|
||||
- ✅ **解析稳定性**: 即使XML结构有变化也能正确解析SectionProperties
|
||||
- ✅ **Word兼容性**: 生成的文档完全符合Microsoft Word和WPS的要求
|
||||
|
||||
#### 测试验证
|
||||
- 通过 `TestPageSettingsIntegration` 验证修复效果
|
||||
- 使用 `TestDebugPageSettings` 进行详细调试验证
|
||||
- 确认页面设置在完整的保存/加载周期中保持正确
|
||||
|
||||
---
|
||||
|
||||
## [v1.3.2] - 2025-06-02
|
||||
|
||||
### 🐛 问题修复
|
||||
|
||||
#### 模板引擎循环内条件表达式修复 ✨ **重要修复**
|
||||
- **修复问题**: 解决了模板引擎中循环内部条件表达式无法正确渲染的问题
|
||||
- **影响范围**: 主要影响使用复杂模板的场景,特别是 `{{#each}}` 循环内包含 `{{#if}}` 条件语句
|
||||
- **错误表现**:
|
||||
- 循环内的条件表达式保持原始模板语法,未被正确渲染
|
||||
- 例如:`{{#if isLeader}}👑 团队负责人{{/if}}` 在循环中不生效
|
||||
- **修复方案**:
|
||||
- 优化 `renderLoopConditionals()` 函数的布尔值转换逻辑
|
||||
- 调整模板渲染顺序,先处理循环语句,再处理条件语句
|
||||
- 改进条件表达式的数据类型支持(字符串、数字、布尔值等)
|
||||
- **修复后效果**:
|
||||
- 循环内条件表达式正确渲染:`{{#each teamMembers}}{{#if isLeader}}👑 团队负责人{{/if}}{{/each}}`
|
||||
- 支持多种数据类型的条件判断:`bool`, `string`, `int`, `int64`, `float64`
|
||||
- 完美支持嵌套的条件和循环结构
|
||||
|
||||
#### 技术细节
|
||||
- **修改文件**: `pkg/document/template.go`
|
||||
- **修改内容**:
|
||||
- 优化 `renderLoopConditionals()` 函数的类型判断逻辑
|
||||
- 调整 `renderTemplate()` 中的渲染顺序
|
||||
- 简化 `renderConditionals()` 函数,移除不必要的循环检测
|
||||
- **影响功能**:
|
||||
- 模板引擎的循环内条件渲染
|
||||
- 复杂模板的嵌套结构处理
|
||||
- 所有使用条件表达式的模板功能
|
||||
|
||||
### 🔍 质量改进
|
||||
|
||||
#### 模板引擎稳定性
|
||||
- ✅ **条件表达式完整支持**: 循环内外的条件表达式都能正确工作
|
||||
- ✅ **数据类型兼容性**: 支持多种数据类型的条件判断
|
||||
- ✅ **嵌套结构支持**: 完美支持条件语句和循环语句的任意嵌套
|
||||
- ✅ **渲染顺序优化**: 确保模板元素按正确顺序处理
|
||||
|
||||
#### 测试验证
|
||||
- 通过 `test_loop_condition.go` 验证修复效果
|
||||
- 使用复杂模板演示验证嵌套结构
|
||||
- 确认所有模板测试用例通过
|
||||
|
||||
---
|
||||
|
||||
## [v1.3.1] - 2025-05-30
|
||||
|
||||
### 🐛 问题修复
|
||||
|
45
README.md
45
README.md
@@ -3,6 +3,7 @@
|
||||
[](https://golang.org)
|
||||
[](LICENSE)
|
||||
[](#测试)
|
||||
[](https://deepwiki.com/ZeroHawkeye/wordZero)
|
||||
|
||||
## 项目介绍
|
||||
|
||||
@@ -52,6 +53,7 @@ wordZero/
|
||||
│ │ ├── sdt.go # 结构化文档标签 ✨ 新增
|
||||
│ │ ├── field.go # 域字段功能 ✨ 新增
|
||||
│ │ ├── properties.go # 文档属性管理 ✨ 新增
|
||||
│ │ ├── template.go # 模板功能 ✨ 新增
|
||||
│ │ ├── errors.go # 错误定义和处理
|
||||
│ │ ├── logger.go # 日志系统
|
||||
│ │ ├── doc.go # 包文档
|
||||
@@ -84,12 +86,15 @@ wordZero/
|
||||
│ │ └── main.go
|
||||
│ ├── advanced_features/ # 高级功能综合示例 ✨ 新增
|
||||
│ │ └── main.go
|
||||
│ ├── template_demo/ # 模板功能演示 ✨ 规划中
|
||||
│ │ └── main.go
|
||||
│ ├── basic_usage.go # 基础使用示例
|
||||
│ └── output/ # 示例输出文件目录
|
||||
├── test/ # 集成测试文件
|
||||
│ ├── document_test.go # 文档操作集成测试
|
||||
│ ├── text_formatting_test.go # 文本格式化集成测试
|
||||
│ └── table_style_test.go # 表格样式功能集成测试
|
||||
│ ├── table_style_test.go # 表格样式功能集成测试
|
||||
│ └── template_test.go # 模板功能集成测试 ✨ 新增
|
||||
├── .gitignore # Git忽略文件配置
|
||||
├── go.mod # Go模块定义
|
||||
├── LICENSE # MIT许可证
|
||||
@@ -325,6 +330,33 @@ wordZero/
|
||||
- [x] 域字段的开始、分隔、结束标记
|
||||
- [x] **页码设置**(已集成到页眉页脚功能中)
|
||||
|
||||
#### 模板功能 ✨ **新实现**
|
||||
- [x] **基础模板引擎** ✨ **新实现**
|
||||
- [x] 变量替换:`{{变量名}}` 语法支持
|
||||
- [x] 条件语句:`{{#if 条件}}...{{/if}}` 支持
|
||||
- [x] 循环语句:`{{#each 列表}}...{{/each}}` 支持
|
||||
- [x] 模板继承:基础模板和子模板扩展
|
||||
- [x] 循环上下文变量:`{{@index}}`、`{{@first}}`、`{{@last}}`、`{{this}}`
|
||||
- [x] 嵌套模板语法支持
|
||||
- [x] **模板操作** ✨ **新实现**
|
||||
- [x] 从字符串加载模板
|
||||
- [x] 从现有文档创建模板
|
||||
- [x] 模板渲染和变量填充
|
||||
- [x] 模板验证和错误处理
|
||||
- [x] 模板缓存机制
|
||||
- [x] 模板数据绑定和管理
|
||||
- [x] **数据绑定** ✨ **新实现**
|
||||
- [x] 基础数据类型支持(字符串、数字、布尔值)
|
||||
- [x] 复杂数据结构支持(map、slice)
|
||||
- [x] 结构体自动绑定
|
||||
- [x] 模板数据合并和清空
|
||||
- [x] 批量变量设置
|
||||
- [x] **模板语法** ✨ **新实现**
|
||||
- [x] 正则表达式解析引擎
|
||||
- [x] 语法验证和错误检查
|
||||
- [x] 嵌套条件和循环支持
|
||||
- [x] 模板块管理和组织
|
||||
|
||||
## 使用示例
|
||||
|
||||
查看 `examples/` 目录下的示例代码:
|
||||
@@ -344,6 +376,14 @@ wordZero/
|
||||
- 脚注尾注功能演示
|
||||
- 列表和编号演示
|
||||
- 结构化文档标签演示
|
||||
- `examples/template_demo/` - **模板功能演示** ✨ **新增**
|
||||
- 基础变量替换演示
|
||||
- 条件语句功能演示
|
||||
- 循环语句功能演示
|
||||
- 模板继承功能演示
|
||||
- 复杂模板综合应用演示
|
||||
- 从现有文档创建模板演示
|
||||
- 结构体数据绑定演示
|
||||
|
||||
运行示例:
|
||||
```bash
|
||||
@@ -376,6 +416,9 @@ go run ./examples/page_settings/
|
||||
|
||||
# 运行高级功能综合演示
|
||||
go run ./examples/advanced_features/
|
||||
|
||||
# 运行模板功能演示
|
||||
go run ./examples/template_demo/
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
|
794
examples/template_demo/main.go
Normal file
794
examples/template_demo/main.go
Normal file
@@ -0,0 +1,794 @@
|
||||
// Package main 模板功能演示示例
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/ZeroHawkeye/wordZero/pkg/document"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("WordZero 模板功能演示")
|
||||
fmt.Println("=====================================")
|
||||
|
||||
// 演示1: 基础变量替换
|
||||
fmt.Println("\n1. 基础变量替换演示")
|
||||
demonstrateVariableReplacement()
|
||||
|
||||
// 演示2: 条件语句
|
||||
fmt.Println("\n2. 条件语句演示")
|
||||
demonstrateConditionalStatements()
|
||||
|
||||
// 演示3: 循环语句
|
||||
fmt.Println("\n3. 循环语句演示")
|
||||
demonstrateLoopStatements()
|
||||
|
||||
// 演示4: 模板继承
|
||||
fmt.Println("\n4. 模板继承演示")
|
||||
demonstrateTemplateInheritance()
|
||||
|
||||
// 演示5: 复杂模板综合应用
|
||||
fmt.Println("\n5. 复杂模板综合应用")
|
||||
demonstrateComplexTemplate()
|
||||
|
||||
// 演示6: 从现有文档创建模板
|
||||
fmt.Println("\n6. 从现有文档创建模板演示")
|
||||
demonstrateDocumentToTemplate()
|
||||
|
||||
// 演示7: 结构体数据绑定
|
||||
fmt.Println("\n7. 结构体数据绑定演示")
|
||||
demonstrateStructDataBinding()
|
||||
|
||||
fmt.Println("\n=====================================")
|
||||
fmt.Println("模板功能演示完成!")
|
||||
fmt.Println("生成的文档保存在 examples/output/ 目录下")
|
||||
}
|
||||
|
||||
// demonstrateVariableReplacement 演示基础变量替换功能
|
||||
func demonstrateVariableReplacement() {
|
||||
// 创建模板引擎
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 创建包含变量的模板
|
||||
templateContent := `尊敬的 {{customerName}} 先生/女士:
|
||||
|
||||
感谢您选择 {{companyName}}!
|
||||
|
||||
您的订单号是:{{orderNumber}}
|
||||
订单金额:{{amount}} 元
|
||||
下单时间:{{orderDate}}
|
||||
|
||||
我们将在 {{deliveryDays}} 个工作日内为您发货。
|
||||
|
||||
如有任何问题,请联系我们的客服热线:{{servicePhone}}
|
||||
|
||||
祝您生活愉快!
|
||||
|
||||
{{companyName}}
|
||||
{{currentDate}}`
|
||||
|
||||
// 加载模板
|
||||
template, err := engine.LoadTemplate("order_confirmation", templateContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载模板失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("解析到 %d 个变量\n", len(template.Variables))
|
||||
|
||||
// 创建模板数据
|
||||
data := document.NewTemplateData()
|
||||
data.SetVariable("customerName", "张三")
|
||||
data.SetVariable("companyName", "WordZero科技有限公司")
|
||||
data.SetVariable("orderNumber", "WZ20241201001")
|
||||
data.SetVariable("amount", "1299.00")
|
||||
data.SetVariable("orderDate", "2024年12月1日 14:30")
|
||||
data.SetVariable("deliveryDays", "3-5")
|
||||
data.SetVariable("servicePhone", "400-123-4567")
|
||||
data.SetVariable("currentDate", time.Now().Format("2006年01月02日"))
|
||||
|
||||
// 渲染模板
|
||||
doc, err := engine.RenderToDocument("order_confirmation", data)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 保存文档
|
||||
err = doc.Save("examples/output/template_variable_demo.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ 变量替换演示完成,文档已保存为 template_variable_demo.docx")
|
||||
}
|
||||
|
||||
// demonstrateConditionalStatements 演示条件语句功能
|
||||
func demonstrateConditionalStatements() {
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 创建包含条件语句的模板
|
||||
templateContent := `产品推荐信
|
||||
|
||||
尊敬的客户:
|
||||
|
||||
{{#if isVipCustomer}}
|
||||
作为我们的VIP客户,您将享受以下特殊优惠:
|
||||
- 全场商品9折优惠
|
||||
- 免费包邮服务
|
||||
- 优先客服支持
|
||||
{{/if}}
|
||||
|
||||
{{#if hasNewProducts}}
|
||||
最新产品推荐:
|
||||
我们刚刚推出了一系列新产品,相信您会喜欢。
|
||||
{{/if}}
|
||||
|
||||
{{#if showDiscount}}
|
||||
限时优惠:
|
||||
现在购买任意商品,立享8折优惠!
|
||||
优惠码:SAVE20
|
||||
{{/if}}
|
||||
|
||||
{{#if needSupport}}
|
||||
如需技术支持,请联系我们的专业团队。
|
||||
支持热线:400-888-9999
|
||||
{{/if}}
|
||||
|
||||
感谢您的信任与支持!
|
||||
|
||||
WordZero团队`
|
||||
|
||||
// 加载模板
|
||||
_, err := engine.LoadTemplate("product_recommendation", templateContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 测试不同条件组合
|
||||
scenarios := []struct {
|
||||
name string
|
||||
isVip bool
|
||||
hasNew bool
|
||||
showDiscount bool
|
||||
needSupport bool
|
||||
filename string
|
||||
}{
|
||||
{"VIP客户场景", true, true, false, true, "template_conditional_vip.docx"},
|
||||
{"普通客户场景", false, true, true, false, "template_conditional_normal.docx"},
|
||||
{"简单推荐场景", false, false, false, false, "template_conditional_simple.docx"},
|
||||
}
|
||||
|
||||
for _, scenario := range scenarios {
|
||||
fmt.Printf("生成 %s...\n", scenario.name)
|
||||
|
||||
data := document.NewTemplateData()
|
||||
data.SetCondition("isVipCustomer", scenario.isVip)
|
||||
data.SetCondition("hasNewProducts", scenario.hasNew)
|
||||
data.SetCondition("showDiscount", scenario.showDiscount)
|
||||
data.SetCondition("needSupport", scenario.needSupport)
|
||||
|
||||
doc, err := engine.RenderToDocument("product_recommendation", data)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染模板失败: %v", err)
|
||||
}
|
||||
|
||||
err = doc.Save("examples/output/" + scenario.filename)
|
||||
if err != nil {
|
||||
log.Fatalf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✓ %s 完成\n", scenario.name)
|
||||
}
|
||||
}
|
||||
|
||||
// demonstrateLoopStatements 演示循环语句功能
|
||||
func demonstrateLoopStatements() {
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 创建包含循环语句的模板
|
||||
templateContent := `销售报告
|
||||
|
||||
报告时间:{{reportDate}}
|
||||
销售部门:{{department}}
|
||||
|
||||
产品销售明细:
|
||||
{{#each products}}
|
||||
{{@index}}. 产品名称:{{name}}
|
||||
销售数量:{{quantity}} 件
|
||||
单价:{{price}} 元
|
||||
销售金额:{{total}} 元
|
||||
{{#if isTopSeller}}🏆 热销产品{{/if}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
销售统计:
|
||||
总销售金额:{{totalAmount}} 元
|
||||
平均客单价:{{averagePrice}} 元
|
||||
|
||||
{{#each salespeople}}
|
||||
销售员:{{name}} - 业绩:{{performance}} 元
|
||||
{{/each}}
|
||||
|
||||
备注:
|
||||
{{#each notes}}
|
||||
- {{this}}
|
||||
{{/each}}`
|
||||
|
||||
// 加载模板
|
||||
_, err := engine.LoadTemplate("sales_report", templateContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建模板数据
|
||||
data := document.NewTemplateData()
|
||||
data.SetVariable("reportDate", "2024年12月1日")
|
||||
data.SetVariable("department", "华东区销售部")
|
||||
data.SetVariable("totalAmount", "89,650")
|
||||
data.SetVariable("averagePrice", "1,245")
|
||||
|
||||
// 设置产品列表
|
||||
products := []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "iPhone 15 Pro",
|
||||
"quantity": 25,
|
||||
"price": 8999,
|
||||
"total": 224975,
|
||||
"isTopSeller": true,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "iPad Air",
|
||||
"quantity": 18,
|
||||
"price": 4999,
|
||||
"total": 89982,
|
||||
"isTopSeller": false,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "MacBook Pro",
|
||||
"quantity": 8,
|
||||
"price": 16999,
|
||||
"total": 135992,
|
||||
"isTopSeller": true,
|
||||
},
|
||||
}
|
||||
data.SetList("products", products)
|
||||
|
||||
// 设置销售员列表
|
||||
salespeople := []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "王小明",
|
||||
"performance": 156800,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "李小红",
|
||||
"performance": 134500,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "张小强",
|
||||
"performance": 98750,
|
||||
},
|
||||
}
|
||||
data.SetList("salespeople", salespeople)
|
||||
|
||||
// 设置备注列表
|
||||
notes := []interface{}{
|
||||
"本月销售表现优异,超额完成目标",
|
||||
"iPhone 15 Pro 持续热销",
|
||||
"建议增加库存以满足需求",
|
||||
}
|
||||
data.SetList("notes", notes)
|
||||
|
||||
// 渲染模板
|
||||
doc, err := engine.RenderToDocument("sales_report", data)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 保存文档
|
||||
err = doc.Save("examples/output/template_loop_demo.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ 循环语句演示完成,文档已保存为 template_loop_demo.docx")
|
||||
}
|
||||
|
||||
// demonstrateTemplateInheritance 演示模板继承功能
|
||||
func demonstrateTemplateInheritance() {
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 创建基础模板
|
||||
baseTemplateContent := `{{companyName}} 官方文档
|
||||
|
||||
文档标题:{{title}}
|
||||
创建时间:{{createDate}}
|
||||
版本号:{{version}}
|
||||
|
||||
---
|
||||
|
||||
文档内容:`
|
||||
|
||||
// 加载基础模板
|
||||
_, err := engine.LoadTemplate("base_document", baseTemplateContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载基础模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建继承模板 - 用户手册
|
||||
userManualContent := `{{extends "base_document"}}
|
||||
|
||||
用户手册
|
||||
|
||||
第一章:快速开始
|
||||
欢迎使用我们的产品!本章将帮助您快速上手。
|
||||
|
||||
第二章:基础功能
|
||||
介绍产品的基础功能和使用方法。
|
||||
|
||||
第三章:高级功能
|
||||
深入了解产品的高级特性。
|
||||
|
||||
第四章:常见问题
|
||||
解答用户常见的问题和疑惑。
|
||||
|
||||
如需更多帮助,请联系技术支持。`
|
||||
|
||||
// 加载用户手册模板
|
||||
_, err = engine.LoadTemplate("user_manual", userManualContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载用户手册模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建继承模板 - API文档
|
||||
apiDocContent := `{{extends "base_document"}}
|
||||
|
||||
API接口文档
|
||||
|
||||
接口概述:
|
||||
本文档提供了完整的API接口说明。
|
||||
|
||||
认证方式:
|
||||
使用API Key进行身份验证。
|
||||
|
||||
接口列表:
|
||||
1. GET /api/users - 获取用户列表
|
||||
2. POST /api/users - 创建新用户
|
||||
3. PUT /api/users/{id} - 更新用户信息
|
||||
4. DELETE /api/users/{id} - 删除用户
|
||||
|
||||
错误代码:
|
||||
- 400: 请求参数错误
|
||||
- 401: 认证失败
|
||||
- 404: 资源不存在
|
||||
- 500: 服务器内部错误`
|
||||
|
||||
// 加载API文档模板
|
||||
_, err = engine.LoadTemplate("api_document", apiDocContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载API文档模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建通用数据
|
||||
commonData := document.NewTemplateData()
|
||||
commonData.SetVariable("companyName", "WordZero科技")
|
||||
commonData.SetVariable("createDate", time.Now().Format("2006年01月02日"))
|
||||
commonData.SetVariable("version", "v1.0")
|
||||
|
||||
// 生成用户手册
|
||||
userManualData := document.NewTemplateData()
|
||||
userManualData.Merge(commonData)
|
||||
userManualData.SetVariable("title", "产品用户手册")
|
||||
|
||||
userManualDoc, err := engine.RenderToDocument("user_manual", userManualData)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染用户手册失败: %v", err)
|
||||
}
|
||||
|
||||
err = userManualDoc.Save("examples/output/template_inheritance_user_manual.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存用户手册失败: %v", err)
|
||||
}
|
||||
|
||||
// 生成API文档
|
||||
apiDocData := document.NewTemplateData()
|
||||
apiDocData.Merge(commonData)
|
||||
apiDocData.SetVariable("title", "API接口文档")
|
||||
|
||||
apiDoc, err := engine.RenderToDocument("api_document", apiDocData)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染API文档失败: %v", err)
|
||||
}
|
||||
|
||||
err = apiDoc.Save("examples/output/template_inheritance_api_doc.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存API文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ 模板继承演示完成")
|
||||
fmt.Println(" - 用户手册已保存为 template_inheritance_user_manual.docx")
|
||||
fmt.Println(" - API文档已保存为 template_inheritance_api_doc.docx")
|
||||
}
|
||||
|
||||
// demonstrateComplexTemplate 演示复杂模板综合应用
|
||||
func demonstrateComplexTemplate() {
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 创建复杂的项目报告模板
|
||||
complexTemplateContent := `{{companyName}} 项目报告
|
||||
|
||||
项目名称:{{projectName}}
|
||||
项目经理:{{projectManager}}
|
||||
报告日期:{{reportDate}}
|
||||
|
||||
===================================
|
||||
|
||||
项目概要:
|
||||
{{projectDescription}}
|
||||
|
||||
项目状态:{{projectStatus}}
|
||||
|
||||
{{#if showTeamMembers}}
|
||||
项目团队:
|
||||
{{#each teamMembers}}
|
||||
{{@index}}. 姓名:{{name}}
|
||||
职位:{{position}}
|
||||
工作内容:{{responsibility}}
|
||||
{{#if isLeader}}👑 团队负责人{{/if}}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showTasks}}
|
||||
任务清单:
|
||||
{{#each tasks}}
|
||||
任务 {{@index}}: {{title}}
|
||||
状态:{{status}}
|
||||
{{#if isCompleted}}✅ 已完成{{/if}}
|
||||
{{#if isInProgress}}🔄 进行中{{/if}}
|
||||
{{#if isPending}}⏳ 待开始{{/if}}
|
||||
|
||||
描述:{{description}}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showMilestones}}
|
||||
项目里程碑:
|
||||
{{#each milestones}}
|
||||
{{date}} - {{title}}
|
||||
{{#if isCompleted}}✅ 已完成{{/if}}
|
||||
{{#if isCurrent}}🎯 当前阶段{{/if}}
|
||||
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
项目风险:
|
||||
{{#each risks}}
|
||||
- 风险:{{description}}
|
||||
等级:{{level}}
|
||||
应对措施:{{mitigation}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
{{#if showBudget}}
|
||||
预算信息:
|
||||
总预算:{{totalBudget}} 万元
|
||||
已使用:{{usedBudget}} 万元
|
||||
剩余:{{remainingBudget}} 万元
|
||||
{{/if}}
|
||||
|
||||
下一步计划:
|
||||
{{#each nextSteps}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
|
||||
===================================
|
||||
|
||||
报告人:{{reporter}}
|
||||
审核人:{{reviewer}}`
|
||||
|
||||
// 加载模板
|
||||
_, err := engine.LoadTemplate("project_report", complexTemplateContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载复杂模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建复杂数据
|
||||
data := document.NewTemplateData()
|
||||
|
||||
// 基础信息
|
||||
data.SetVariable("companyName", "WordZero科技有限公司")
|
||||
data.SetVariable("projectName", "新一代文档处理系统")
|
||||
data.SetVariable("projectManager", "李项目")
|
||||
data.SetVariable("reportDate", "2024年12月1日")
|
||||
data.SetVariable("projectDescription", "开发一个功能强大、易于使用的Word文档操作库,支持模板引擎、样式管理等高级功能。")
|
||||
data.SetVariable("projectStatus", "进行中 - 80%完成")
|
||||
data.SetVariable("reporter", "李项目")
|
||||
data.SetVariable("reviewer", "王总监")
|
||||
|
||||
// 条件设置
|
||||
data.SetCondition("showTeamMembers", true)
|
||||
data.SetCondition("showTasks", true)
|
||||
data.SetCondition("showMilestones", true)
|
||||
data.SetCondition("showBudget", true)
|
||||
|
||||
// 团队成员
|
||||
teamMembers := []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "张开发",
|
||||
"position": "高级开发工程师",
|
||||
"responsibility": "核心功能开发",
|
||||
"isLeader": true,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "王测试",
|
||||
"position": "测试工程师",
|
||||
"responsibility": "功能测试与质量保证",
|
||||
"isLeader": false,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "刘设计",
|
||||
"position": "UI/UX设计师",
|
||||
"responsibility": "用户界面设计",
|
||||
"isLeader": false,
|
||||
},
|
||||
}
|
||||
data.SetList("teamMembers", teamMembers)
|
||||
|
||||
// 任务清单
|
||||
tasks := []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "模板引擎开发",
|
||||
"status": "已完成",
|
||||
"description": "实现变量替换、条件语句、循环语句等功能",
|
||||
"isCompleted": true,
|
||||
"isInProgress": false,
|
||||
"isPending": false,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "样式管理系统",
|
||||
"status": "进行中",
|
||||
"description": "完善样式继承和应用机制",
|
||||
"isCompleted": false,
|
||||
"isInProgress": true,
|
||||
"isPending": false,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "性能优化",
|
||||
"status": "待开始",
|
||||
"description": "优化大文档处理性能",
|
||||
"isCompleted": false,
|
||||
"isInProgress": false,
|
||||
"isPending": true,
|
||||
},
|
||||
}
|
||||
data.SetList("tasks", tasks)
|
||||
|
||||
// 项目里程碑
|
||||
milestones := []interface{}{
|
||||
map[string]interface{}{
|
||||
"date": "2024年10月15日",
|
||||
"title": "项目启动",
|
||||
"isCompleted": true,
|
||||
"isCurrent": false,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"date": "2024年11月30日",
|
||||
"title": "核心功能完成",
|
||||
"isCompleted": true,
|
||||
"isCurrent": false,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"date": "2024年12月15日",
|
||||
"title": "测试阶段",
|
||||
"isCompleted": false,
|
||||
"isCurrent": true,
|
||||
},
|
||||
}
|
||||
data.SetList("milestones", milestones)
|
||||
|
||||
// 项目风险
|
||||
risks := []interface{}{
|
||||
map[string]interface{}{
|
||||
"description": "技术难度超预期",
|
||||
"level": "中等",
|
||||
"mitigation": "增加技术调研时间,寻求外部专家支持",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"description": "人员流动风险",
|
||||
"level": "低",
|
||||
"mitigation": "建立完善的文档和知识传承机制",
|
||||
},
|
||||
}
|
||||
data.SetList("risks", risks)
|
||||
|
||||
// 预算信息
|
||||
data.SetVariable("totalBudget", "50")
|
||||
data.SetVariable("usedBudget", "35")
|
||||
data.SetVariable("remainingBudget", "15")
|
||||
|
||||
// 下一步计划
|
||||
nextSteps := []interface{}{
|
||||
"完成剩余功能开发",
|
||||
"进行全面测试",
|
||||
"编写使用文档",
|
||||
"准备产品发布",
|
||||
}
|
||||
data.SetList("nextSteps", nextSteps)
|
||||
|
||||
// 渲染模板
|
||||
doc, err := engine.RenderToDocument("project_report", data)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染复杂模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 保存文档
|
||||
err = doc.Save("examples/output/template_complex_demo.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存复杂模板文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ 复杂模板演示完成,文档已保存为 template_complex_demo.docx")
|
||||
}
|
||||
|
||||
// demonstrateDocumentToTemplate 演示从现有文档创建模板
|
||||
func demonstrateDocumentToTemplate() {
|
||||
// 创建一个基础文档
|
||||
doc := document.New()
|
||||
doc.AddParagraph("公司:{{companyName}}")
|
||||
doc.AddParagraph("部门:{{department}}")
|
||||
doc.AddParagraph("")
|
||||
doc.AddParagraph("员工信息:")
|
||||
doc.AddParagraph("姓名:{{employeeName}}")
|
||||
doc.AddParagraph("职位:{{position}}")
|
||||
doc.AddParagraph("入职日期:{{hireDate}}")
|
||||
|
||||
// 创建模板引擎
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 从文档创建模板
|
||||
template, err := engine.LoadTemplateFromDocument("employee_template", doc)
|
||||
if err != nil {
|
||||
log.Fatalf("从文档创建模板失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("从文档解析到 %d 个变量\n", len(template.Variables))
|
||||
|
||||
// 创建员工数据
|
||||
data := document.NewTemplateData()
|
||||
data.SetVariable("companyName", "WordZero科技有限公司")
|
||||
data.SetVariable("department", "研发部")
|
||||
data.SetVariable("employeeName", "李小明")
|
||||
data.SetVariable("position", "软件工程师")
|
||||
data.SetVariable("hireDate", "2024年12月1日")
|
||||
|
||||
// 渲染模板
|
||||
renderedDoc, err := engine.RenderToDocument("employee_template", data)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染员工模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 保存文档
|
||||
err = renderedDoc.Save("examples/output/template_from_document_demo.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ 从文档创建模板演示完成,文档已保存为 template_from_document_demo.docx")
|
||||
}
|
||||
|
||||
// demonstrateStructDataBinding 演示结构体数据绑定
|
||||
func demonstrateStructDataBinding() {
|
||||
// 定义数据结构
|
||||
type Employee struct {
|
||||
Name string
|
||||
Position string
|
||||
Department string
|
||||
Salary int
|
||||
IsManager bool
|
||||
HireDate string
|
||||
}
|
||||
|
||||
type Company struct {
|
||||
Name string
|
||||
Address string
|
||||
Phone string
|
||||
Website string
|
||||
Founded int
|
||||
}
|
||||
|
||||
// 创建数据实例
|
||||
employee := Employee{
|
||||
Name: "王小红",
|
||||
Position: "产品经理",
|
||||
Department: "产品部",
|
||||
Salary: 15000,
|
||||
IsManager: true,
|
||||
HireDate: "2023年3月15日",
|
||||
}
|
||||
|
||||
company := Company{
|
||||
Name: "WordZero科技有限公司",
|
||||
Address: "上海市浦东新区科技园区1号楼",
|
||||
Phone: "021-12345678",
|
||||
Website: "www.wordzero.com",
|
||||
Founded: 2023,
|
||||
}
|
||||
|
||||
// 创建模板引擎
|
||||
engine := document.NewTemplateEngine()
|
||||
|
||||
// 创建员工档案模板
|
||||
templateContent := `员工档案
|
||||
|
||||
公司信息:
|
||||
公司名称:{{name}}
|
||||
公司地址:{{address}}
|
||||
联系电话:{{phone}}
|
||||
公司网站:{{website}}
|
||||
成立年份:{{founded}}
|
||||
|
||||
员工信息:
|
||||
姓名:{{name}}
|
||||
职位:{{position}}
|
||||
部门:{{department}}
|
||||
薪资:{{salary}} 元
|
||||
入职日期:{{hiredate}}
|
||||
|
||||
{{#if ismanager}}
|
||||
管理职责:
|
||||
作为部门经理,负责团队管理和项目协调。
|
||||
{{/if}}`
|
||||
|
||||
// 加载模板
|
||||
_, err := engine.LoadTemplate("employee_profile", templateContent)
|
||||
if err != nil {
|
||||
log.Fatalf("加载员工档案模板失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建模板数据并从结构体填充
|
||||
data := document.NewTemplateData()
|
||||
|
||||
// 从公司结构体创建数据
|
||||
err = data.FromStruct(company)
|
||||
if err != nil {
|
||||
log.Fatalf("从公司结构体创建数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建临时数据用于员工信息(避免字段名冲突)
|
||||
employeeData := document.NewTemplateData()
|
||||
err = employeeData.FromStruct(employee)
|
||||
if err != nil {
|
||||
log.Fatalf("从员工结构体创建数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 手动设置员工相关变量(处理字段名冲突)
|
||||
data.SetVariable("name", employee.Name)
|
||||
data.SetVariable("position", employee.Position)
|
||||
data.SetVariable("department", employee.Department)
|
||||
data.SetVariable("salary", employee.Salary)
|
||||
data.SetVariable("hiredate", employee.HireDate)
|
||||
data.SetCondition("ismanager", employee.IsManager)
|
||||
|
||||
// 设置公司相关变量
|
||||
data.SetVariable("name", company.Name)
|
||||
data.SetVariable("address", company.Address)
|
||||
data.SetVariable("phone", company.Phone)
|
||||
data.SetVariable("website", company.Website)
|
||||
data.SetVariable("founded", company.Founded)
|
||||
|
||||
// 渲染模板
|
||||
doc, err := engine.RenderToDocument("employee_profile", data)
|
||||
if err != nil {
|
||||
log.Fatalf("渲染员工档案失败: %v", err)
|
||||
}
|
||||
|
||||
// 保存文档
|
||||
err = doc.Save("examples/output/template_struct_binding_demo.docx")
|
||||
if err != nil {
|
||||
log.Fatalf("保存员工档案失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ 结构体数据绑定演示完成,文档已保存为 template_struct_binding_demo.docx")
|
||||
}
|
@@ -112,6 +112,38 @@
|
||||
### 结构化文档标签 ✨ 新增功能
|
||||
- [`CreateTOCSDT(title string, maxLevel int)`](sdt.go) - 创建目录SDT结构
|
||||
|
||||
### 模板功能 ✨ 新增功能
|
||||
- [`NewTemplateEngine()`](template.go) - 创建新的模板引擎
|
||||
- [`LoadTemplate(name, content string)`](template.go) - 从字符串加载模板
|
||||
- [`LoadTemplateFromDocument(name string, doc *Document)`](template.go) - 从现有文档创建模板
|
||||
- [`GetTemplate(name string)`](template.go) - 获取缓存的模板
|
||||
- [`RenderToDocument(templateName string, data *TemplateData)`](template.go) - 渲染模板到新文档
|
||||
- [`ValidateTemplate(template *Template)`](template.go) - 验证模板语法
|
||||
- [`ClearCache()`](template.go) - 清空模板缓存
|
||||
- [`RemoveTemplate(name string)`](template.go) - 移除指定模板
|
||||
|
||||
#### 模板引擎功能特性 ✨
|
||||
**变量替换**: 支持 `{{变量名}}` 语法进行动态内容替换
|
||||
**条件语句**: 支持 `{{#if 条件}}...{{/if}}` 语法进行条件渲染
|
||||
**循环语句**: 支持 `{{#each 列表}}...{{/each}}` 语法进行列表渲染
|
||||
**模板继承**: 支持 `{{extends "基础模板"}}` 语法进行模板继承
|
||||
**循环内条件**: 完美支持循环内部的条件表达式,如 `{{#each items}}{{#if isActive}}...{{/if}}{{/each}}`
|
||||
**数据类型支持**: 支持字符串、数字、布尔值、对象等多种数据类型
|
||||
**结构体绑定**: 支持从Go结构体自动生成模板数据
|
||||
|
||||
### 模板数据操作
|
||||
- [`NewTemplateData()`](template.go) - 创建新的模板数据
|
||||
- [`SetVariable(name string, value interface{})`](template.go) - 设置变量
|
||||
- [`SetList(name string, list []interface{})`](template.go) - 设置列表
|
||||
- [`SetCondition(name string, value bool)`](template.go) - 设置条件
|
||||
- [`SetVariables(variables map[string]interface{})`](template.go) - 批量设置变量
|
||||
- [`GetVariable(name string)`](template.go) - 获取变量
|
||||
- [`GetList(name string)`](template.go) - 获取列表
|
||||
- [`GetCondition(name string)`](template.go) - 获取条件
|
||||
- [`Merge(other *TemplateData)`](template.go) - 合并模板数据
|
||||
- [`Clear()`](template.go) - 清空模板数据
|
||||
- [`FromStruct(data interface{})`](template.go) - 从结构体生成模板数据
|
||||
|
||||
### 图片操作功能 ✨ 新增功能
|
||||
- [`AddImageFromFile(filePath string, config *ImageConfig)`](image.go) - 从文件添加图片
|
||||
- [`AddImageFromData(imageData []byte, fileName string, format ImageFormat, width, height int, config *ImageConfig)`](image.go) - 从数据添加图片
|
||||
|
@@ -44,13 +44,32 @@ func (b *Body) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 序列化每个元素,保持顺序
|
||||
// 分离SectionProperties和其他元素
|
||||
var sectPr *SectionProperties
|
||||
var otherElements []interface{}
|
||||
|
||||
for _, element := range b.Elements {
|
||||
if sp, ok := element.(*SectionProperties); ok {
|
||||
sectPr = sp // 保存最后一个SectionProperties
|
||||
} else {
|
||||
otherElements = append(otherElements, element)
|
||||
}
|
||||
}
|
||||
|
||||
// 先序列化其他元素(段落、表格等)
|
||||
for _, element := range otherElements {
|
||||
if err := e.Encode(element); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 最后序列化SectionProperties(如果存在)
|
||||
if sectPr != nil {
|
||||
if err := e.Encode(sectPr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 结束元素
|
||||
return e.EncodeToken(start.End())
|
||||
}
|
||||
@@ -938,6 +957,19 @@ func (d *Document) GetStyleManager() *style.StyleManager {
|
||||
return d.styleManager
|
||||
}
|
||||
|
||||
// GetParts 获取文档部件映射
|
||||
//
|
||||
// 返回包含文档所有部件的映射,主要用于测试和调试。
|
||||
// 键是部件名称,值是部件内容的字节数组。
|
||||
//
|
||||
// 示例:
|
||||
//
|
||||
// parts := doc.GetParts()
|
||||
// settingsXML := parts["word/settings.xml"]
|
||||
func (d *Document) GetParts() map[string][]byte {
|
||||
return d.parts
|
||||
}
|
||||
|
||||
// initializeStructure 初始化文档基础结构
|
||||
func (d *Document) initializeStructure() {
|
||||
// 初始化 content types
|
||||
|
@@ -111,6 +111,110 @@ const (
|
||||
FootnotePositionDocumentEnd FootnotePosition = "docEnd"
|
||||
)
|
||||
|
||||
// FootnoteProperties 脚注属性
|
||||
type FootnoteProperties struct {
|
||||
NumberFormat string `xml:"w:numFmt,attr,omitempty"`
|
||||
StartNumber int `xml:"w:numStart,attr,omitempty"`
|
||||
RestartRule string `xml:"w:numRestart,attr,omitempty"`
|
||||
Position string `xml:"w:pos,attr,omitempty"`
|
||||
}
|
||||
|
||||
// EndnoteProperties 尾注属性
|
||||
type EndnoteProperties struct {
|
||||
NumberFormat string `xml:"w:numFmt,attr,omitempty"`
|
||||
StartNumber int `xml:"w:numStart,attr,omitempty"`
|
||||
RestartRule string `xml:"w:numRestart,attr,omitempty"`
|
||||
Position string `xml:"w:pos,attr,omitempty"`
|
||||
}
|
||||
|
||||
// Settings 文档设置XML结构
|
||||
type Settings struct {
|
||||
XMLName xml.Name `xml:"w:settings"`
|
||||
Xmlns string `xml:"xmlns:w,attr"`
|
||||
DefaultTabStop *DefaultTabStop `xml:"w:defaultTabStop,omitempty"`
|
||||
CharacterSpacingControl *CharacterSpacingControl `xml:"w:characterSpacingControl,omitempty"`
|
||||
FootnotePr *FootnotePr `xml:"w:footnotePr,omitempty"`
|
||||
EndnotePr *EndnotePr `xml:"w:endnotePr,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultTabStop 默认制表位设置
|
||||
type DefaultTabStop struct {
|
||||
XMLName xml.Name `xml:"w:defaultTabStop"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// CharacterSpacingControl 字符间距控制
|
||||
type CharacterSpacingControl struct {
|
||||
XMLName xml.Name `xml:"w:characterSpacingControl"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// FootnotePr 脚注属性设置
|
||||
type FootnotePr struct {
|
||||
XMLName xml.Name `xml:"w:footnotePr"`
|
||||
NumFmt *FootnoteNumFmt `xml:"w:numFmt,omitempty"`
|
||||
NumStart *FootnoteNumStart `xml:"w:numStart,omitempty"`
|
||||
NumRestart *FootnoteNumRestart `xml:"w:numRestart,omitempty"`
|
||||
Pos *FootnotePos `xml:"w:pos,omitempty"`
|
||||
}
|
||||
|
||||
// EndnotePr 尾注属性设置
|
||||
type EndnotePr struct {
|
||||
XMLName xml.Name `xml:"w:endnotePr"`
|
||||
NumFmt *EndnoteNumFmt `xml:"w:numFmt,omitempty"`
|
||||
NumStart *EndnoteNumStart `xml:"w:numStart,omitempty"`
|
||||
NumRestart *EndnoteNumRestart `xml:"w:numRestart,omitempty"`
|
||||
Pos *EndnotePos `xml:"w:pos,omitempty"`
|
||||
}
|
||||
|
||||
// FootnoteNumFmt 脚注编号格式
|
||||
type FootnoteNumFmt struct {
|
||||
XMLName xml.Name `xml:"w:numFmt"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// FootnoteNumStart 脚注起始编号
|
||||
type FootnoteNumStart struct {
|
||||
XMLName xml.Name `xml:"w:numStart"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// FootnoteNumRestart 脚注重新开始规则
|
||||
type FootnoteNumRestart struct {
|
||||
XMLName xml.Name `xml:"w:numRestart"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// FootnotePos 脚注位置
|
||||
type FootnotePos struct {
|
||||
XMLName xml.Name `xml:"w:pos"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// EndnoteNumFmt 尾注编号格式
|
||||
type EndnoteNumFmt struct {
|
||||
XMLName xml.Name `xml:"w:numFmt"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// EndnoteNumStart 尾注起始编号
|
||||
type EndnoteNumStart struct {
|
||||
XMLName xml.Name `xml:"w:numStart"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// EndnoteNumRestart 尾注重新开始规则
|
||||
type EndnoteNumRestart struct {
|
||||
XMLName xml.Name `xml:"w:numRestart"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// EndnotePos 尾注位置
|
||||
type EndnotePos struct {
|
||||
XMLName xml.Name `xml:"w:pos"`
|
||||
Val string `xml:"w:val,attr"`
|
||||
}
|
||||
|
||||
// 全局脚注/尾注管理器
|
||||
var globalFootnoteManager *FootnoteManager
|
||||
|
||||
@@ -228,9 +332,29 @@ func (d *Document) SetFootnoteConfig(config *FootnoteConfig) error {
|
||||
config = DefaultFootnoteConfig()
|
||||
}
|
||||
|
||||
// 创建脚注属性
|
||||
// 这里需要创建脚注设置的XML结构
|
||||
// 简化处理,实际需要在document.xml中添加脚注属性设置
|
||||
// 确保文档设置已初始化
|
||||
d.ensureSettingsInitialized()
|
||||
|
||||
// 创建脚注属性XML结构
|
||||
footnoteProps := &FootnoteProperties{
|
||||
NumberFormat: string(config.NumberFormat),
|
||||
StartNumber: config.StartNumber,
|
||||
RestartRule: string(config.RestartEach),
|
||||
Position: string(config.Position),
|
||||
}
|
||||
|
||||
// 创建尾注属性XML结构
|
||||
endnoteProps := &EndnoteProperties{
|
||||
NumberFormat: string(config.NumberFormat),
|
||||
StartNumber: config.StartNumber,
|
||||
RestartRule: string(config.RestartEach),
|
||||
Position: string(config.Position),
|
||||
}
|
||||
|
||||
// 更新文档设置
|
||||
if err := d.updateDocumentSettings(footnoteProps, endnoteProps); err != nil {
|
||||
return fmt.Errorf("更新脚注配置失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -511,3 +635,160 @@ func (d *Document) RemoveEndnote(endnoteID string) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureSettingsInitialized 确保文档设置已初始化
|
||||
func (d *Document) ensureSettingsInitialized() {
|
||||
// 检查settings.xml是否存在,如果不存在则创建默认设置
|
||||
if _, exists := d.parts["word/settings.xml"]; !exists {
|
||||
d.initializeSettings()
|
||||
}
|
||||
}
|
||||
|
||||
// initializeSettings 初始化文档设置
|
||||
func (d *Document) initializeSettings() {
|
||||
// 创建默认设置
|
||||
settings := d.createDefaultSettings()
|
||||
|
||||
// 保存设置
|
||||
if err := d.saveSettings(settings); err != nil {
|
||||
// 如果保存失败,使用原有的硬编码方式作为后备
|
||||
settingsXML := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:defaultTabStop w:val="708"/>
|
||||
<w:characterSpacingControl w:val="doNotCompress"/>
|
||||
</w:settings>`
|
||||
d.parts["word/settings.xml"] = []byte(settingsXML)
|
||||
}
|
||||
|
||||
// 添加内容类型
|
||||
d.addContentType("word/settings.xml", "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml")
|
||||
|
||||
// 添加关系
|
||||
d.addSettingsRelationship()
|
||||
}
|
||||
|
||||
// updateDocumentSettings 更新文档设置中的脚注尾注配置
|
||||
func (d *Document) updateDocumentSettings(footnoteProps *FootnoteProperties, endnoteProps *EndnoteProperties) error {
|
||||
// 解析现有的settings.xml
|
||||
settings, err := d.parseSettings()
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析设置文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 更新脚注设置
|
||||
if footnoteProps != nil {
|
||||
footnotePr := &FootnotePr{}
|
||||
|
||||
if footnoteProps.NumberFormat != "" {
|
||||
footnotePr.NumFmt = &FootnoteNumFmt{Val: footnoteProps.NumberFormat}
|
||||
}
|
||||
|
||||
if footnoteProps.StartNumber > 0 {
|
||||
footnotePr.NumStart = &FootnoteNumStart{Val: strconv.Itoa(footnoteProps.StartNumber)}
|
||||
}
|
||||
|
||||
if footnoteProps.RestartRule != "" {
|
||||
footnotePr.NumRestart = &FootnoteNumRestart{Val: footnoteProps.RestartRule}
|
||||
}
|
||||
|
||||
if footnoteProps.Position != "" {
|
||||
footnotePr.Pos = &FootnotePos{Val: footnoteProps.Position}
|
||||
}
|
||||
|
||||
settings.FootnotePr = footnotePr
|
||||
}
|
||||
|
||||
// 更新尾注设置
|
||||
if endnoteProps != nil {
|
||||
endnotePr := &EndnotePr{}
|
||||
|
||||
if endnoteProps.NumberFormat != "" {
|
||||
endnotePr.NumFmt = &EndnoteNumFmt{Val: endnoteProps.NumberFormat}
|
||||
}
|
||||
|
||||
if endnoteProps.StartNumber > 0 {
|
||||
endnotePr.NumStart = &EndnoteNumStart{Val: strconv.Itoa(endnoteProps.StartNumber)}
|
||||
}
|
||||
|
||||
if endnoteProps.RestartRule != "" {
|
||||
endnotePr.NumRestart = &EndnoteNumRestart{Val: endnoteProps.RestartRule}
|
||||
}
|
||||
|
||||
if endnoteProps.Position != "" {
|
||||
endnotePr.Pos = &EndnotePos{Val: endnoteProps.Position}
|
||||
}
|
||||
|
||||
settings.EndnotePr = endnotePr
|
||||
}
|
||||
|
||||
// 保存更新后的settings.xml
|
||||
return d.saveSettings(settings)
|
||||
}
|
||||
|
||||
// parseSettings 解析settings.xml文件
|
||||
func (d *Document) parseSettings() (*Settings, error) {
|
||||
settingsData, exists := d.parts["word/settings.xml"]
|
||||
if !exists {
|
||||
// 如果settings.xml不存在,返回默认设置
|
||||
return d.createDefaultSettings(), nil
|
||||
}
|
||||
|
||||
var settings Settings
|
||||
|
||||
// 直接使用xml.Unmarshal可能有命名空间问题,我们改用字符串替换的方式
|
||||
// 将w:settings替换为settings等,然后用一个简化的结构来解析
|
||||
settingsStr := string(settingsData)
|
||||
|
||||
// 如果XML中包含w:前缀,说明是序列化的XML,直接创建默认设置并更新
|
||||
// 这是一个简化的处理方式,避免命名空间解析问题
|
||||
if len(settingsStr) > 0 {
|
||||
// 如果文件存在且不为空,我们使用默认设置作为基础
|
||||
settings = *d.createDefaultSettings()
|
||||
|
||||
// 后续可以在这里添加更复杂的XML解析逻辑
|
||||
// 暂时简化处理,返回默认设置
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
return d.createDefaultSettings(), nil
|
||||
}
|
||||
|
||||
// createDefaultSettings 创建默认设置
|
||||
func (d *Document) createDefaultSettings() *Settings {
|
||||
return &Settings{
|
||||
Xmlns: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
DefaultTabStop: &DefaultTabStop{
|
||||
Val: "708",
|
||||
},
|
||||
CharacterSpacingControl: &CharacterSpacingControl{
|
||||
Val: "doNotCompress",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// saveSettings 保存settings.xml文件
|
||||
func (d *Document) saveSettings(settings *Settings) error {
|
||||
// 序列化为XML
|
||||
settingsXML, err := xml.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化settings.xml失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加XML声明
|
||||
xmlDeclaration := []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + "\n")
|
||||
d.parts["word/settings.xml"] = append(xmlDeclaration, settingsXML...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addSettingsRelationship 添加设置文件关系
|
||||
func (d *Document) addSettingsRelationship() {
|
||||
relationshipID := fmt.Sprintf("rId%d", len(d.relationships.Relationships)+1)
|
||||
|
||||
relationship := Relationship{
|
||||
ID: relationshipID,
|
||||
Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings",
|
||||
Target: "settings.xml",
|
||||
}
|
||||
d.relationships.Relationships = append(d.relationships.Relationships, relationship)
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// PageOrientation 页面方向类型
|
||||
@@ -277,18 +278,20 @@ func (d *Document) SetGutterWidth(width float64) error {
|
||||
|
||||
// getSectionProperties 获取或创建节属性
|
||||
func (d *Document) getSectionProperties() *SectionProperties {
|
||||
// 检查文档主体的最后一个元素是否为SectionProperties
|
||||
if d.Body != nil && len(d.Body.Elements) > 0 {
|
||||
if sectPr, ok := d.Body.Elements[len(d.Body.Elements)-1].(*SectionProperties); ok {
|
||||
if d.Body == nil {
|
||||
return &SectionProperties{}
|
||||
}
|
||||
|
||||
// 在Elements中查找已存在的SectionProperties(可能在任何位置)
|
||||
for _, element := range d.Body.Elements {
|
||||
if sectPr, ok := element.(*SectionProperties); ok {
|
||||
return sectPr
|
||||
}
|
||||
}
|
||||
|
||||
// 创建新的节属性
|
||||
// 如果没有找到,创建新的节属性并添加到末尾
|
||||
sectPr := &SectionProperties{}
|
||||
if d.Body != nil {
|
||||
d.Body.Elements = append(d.Body.Elements, sectPr)
|
||||
}
|
||||
d.Body.Elements = append(d.Body.Elements, sectPr)
|
||||
|
||||
return sectPr
|
||||
}
|
||||
@@ -378,11 +381,9 @@ func parseFloat(s string) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 尝试解析为浮点数
|
||||
if val, err := fmt.Sscanf(s, "%f", new(float64)); err == nil && val == 1 {
|
||||
var result float64
|
||||
fmt.Sscanf(s, "%f", &result)
|
||||
return result
|
||||
// 使用strconv.ParseFloat解析浮点数
|
||||
if val, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return val
|
||||
}
|
||||
|
||||
return 0
|
||||
|
@@ -53,11 +53,11 @@ func TestSetCustomPageSize(t *testing.T) {
|
||||
t.Errorf("页面尺寸应为Custom,实际为: %s", settings.Size)
|
||||
}
|
||||
|
||||
if settings.CustomWidth != 200 {
|
||||
if abs(settings.CustomWidth-200) > 0.1 {
|
||||
t.Errorf("自定义宽度应为200mm,实际为: %.1fmm", settings.CustomWidth)
|
||||
}
|
||||
|
||||
if settings.CustomHeight != 300 {
|
||||
if abs(settings.CustomHeight-300) > 0.1 {
|
||||
t.Errorf("自定义高度应为300mm,实际为: %.1fmm", settings.CustomHeight)
|
||||
}
|
||||
|
||||
|
641
pkg/document/template.go
Normal file
641
pkg/document/template.go
Normal file
@@ -0,0 +1,641 @@
|
||||
// Package document 模板功能实现
|
||||
package document
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 模板相关错误
|
||||
var (
|
||||
// ErrTemplateNotFound 模板未找到
|
||||
ErrTemplateNotFound = NewDocumentError("template_not_found", fmt.Errorf("template not found"), "")
|
||||
|
||||
// ErrTemplateSyntaxError 模板语法错误
|
||||
ErrTemplateSyntaxError = NewDocumentError("template_syntax_error", fmt.Errorf("template syntax error"), "")
|
||||
|
||||
// ErrTemplateRenderError 模板渲染错误
|
||||
ErrTemplateRenderError = NewDocumentError("template_render_error", fmt.Errorf("template render error"), "")
|
||||
|
||||
// ErrInvalidTemplateData 无效模板数据
|
||||
ErrInvalidTemplateData = NewDocumentError("invalid_template_data", fmt.Errorf("invalid template data"), "")
|
||||
)
|
||||
|
||||
// TemplateEngine 模板引擎
|
||||
type TemplateEngine struct {
|
||||
cache map[string]*Template // 模板缓存
|
||||
mutex sync.RWMutex // 读写锁
|
||||
basePath string // 基础路径
|
||||
}
|
||||
|
||||
// Template 模板结构
|
||||
type Template struct {
|
||||
Name string // 模板名称
|
||||
Content string // 模板内容
|
||||
BaseDoc *Document // 基础文档
|
||||
Variables map[string]string // 模板变量
|
||||
Blocks []*TemplateBlock // 模板块
|
||||
Parent *Template // 父模板(用于继承)
|
||||
}
|
||||
|
||||
// TemplateBlock 模板块
|
||||
type TemplateBlock struct {
|
||||
Type string // 块类型:variable, if, each, inherit
|
||||
Content string // 块内容
|
||||
Condition string // 条件(if块使用)
|
||||
Variable string // 变量名(each块使用)
|
||||
Children []*TemplateBlock // 子块
|
||||
Data map[string]interface{} // 块数据
|
||||
}
|
||||
|
||||
// TemplateData 模板数据
|
||||
type TemplateData struct {
|
||||
Variables map[string]interface{} // 变量数据
|
||||
Lists map[string][]interface{} // 列表数据
|
||||
Conditions map[string]bool // 条件数据
|
||||
}
|
||||
|
||||
// NewTemplateEngine 创建新的模板引擎
|
||||
func NewTemplateEngine() *TemplateEngine {
|
||||
return &TemplateEngine{
|
||||
cache: make(map[string]*Template),
|
||||
mutex: sync.RWMutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// SetBasePath 设置模板基础路径
|
||||
func (te *TemplateEngine) SetBasePath(path string) {
|
||||
te.mutex.Lock()
|
||||
defer te.mutex.Unlock()
|
||||
te.basePath = path
|
||||
}
|
||||
|
||||
// LoadTemplate 从字符串加载模板
|
||||
func (te *TemplateEngine) LoadTemplate(name, content string) (*Template, error) {
|
||||
te.mutex.Lock()
|
||||
defer te.mutex.Unlock()
|
||||
|
||||
template := &Template{
|
||||
Name: name,
|
||||
Content: content,
|
||||
Variables: make(map[string]string),
|
||||
Blocks: make([]*TemplateBlock, 0),
|
||||
}
|
||||
|
||||
// 解析模板内容
|
||||
if err := te.parseTemplate(template); err != nil {
|
||||
return nil, WrapErrorWithContext("load_template", err, name)
|
||||
}
|
||||
|
||||
// 缓存模板
|
||||
te.cache[name] = template
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// LoadTemplateFromDocument 从现有文档创建模板
|
||||
func (te *TemplateEngine) LoadTemplateFromDocument(name string, doc *Document) (*Template, error) {
|
||||
te.mutex.Lock()
|
||||
defer te.mutex.Unlock()
|
||||
|
||||
// 将文档内容转换为模板字符串
|
||||
content, err := te.documentToTemplateString(doc)
|
||||
if err != nil {
|
||||
return nil, WrapErrorWithContext("load_template_from_document", err, name)
|
||||
}
|
||||
|
||||
template := &Template{
|
||||
Name: name,
|
||||
Content: content,
|
||||
BaseDoc: doc,
|
||||
Variables: make(map[string]string),
|
||||
Blocks: make([]*TemplateBlock, 0),
|
||||
}
|
||||
|
||||
// 解析模板内容
|
||||
if err := te.parseTemplate(template); err != nil {
|
||||
return nil, WrapErrorWithContext("load_template_from_document", err, name)
|
||||
}
|
||||
|
||||
// 缓存模板
|
||||
te.cache[name] = template
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetTemplate 获取缓存的模板
|
||||
func (te *TemplateEngine) GetTemplate(name string) (*Template, error) {
|
||||
te.mutex.RLock()
|
||||
defer te.mutex.RUnlock()
|
||||
|
||||
if template, exists := te.cache[name]; exists {
|
||||
return template, nil
|
||||
}
|
||||
|
||||
return nil, WrapErrorWithContext("get_template", ErrTemplateNotFound.Cause, name)
|
||||
}
|
||||
|
||||
// getTemplateInternal 获取缓存的模板(内部方法,不加锁)
|
||||
func (te *TemplateEngine) getTemplateInternal(name string) (*Template, error) {
|
||||
if template, exists := te.cache[name]; exists {
|
||||
return template, nil
|
||||
}
|
||||
|
||||
return nil, WrapErrorWithContext("get_template", ErrTemplateNotFound.Cause, name)
|
||||
}
|
||||
|
||||
// ClearCache 清空模板缓存
|
||||
func (te *TemplateEngine) ClearCache() {
|
||||
te.mutex.Lock()
|
||||
defer te.mutex.Unlock()
|
||||
te.cache = make(map[string]*Template)
|
||||
}
|
||||
|
||||
// RemoveTemplate 移除指定模板
|
||||
func (te *TemplateEngine) RemoveTemplate(name string) {
|
||||
te.mutex.Lock()
|
||||
defer te.mutex.Unlock()
|
||||
delete(te.cache, name)
|
||||
}
|
||||
|
||||
// parseTemplate 解析模板内容
|
||||
func (te *TemplateEngine) parseTemplate(template *Template) error {
|
||||
content := template.Content
|
||||
|
||||
// 解析变量: {{变量名}}
|
||||
varPattern := regexp.MustCompile(`\{\{(\w+)\}\}`)
|
||||
varMatches := varPattern.FindAllStringSubmatch(content, -1)
|
||||
for _, match := range varMatches {
|
||||
if len(match) >= 2 {
|
||||
varName := match[1]
|
||||
template.Variables[varName] = ""
|
||||
}
|
||||
}
|
||||
|
||||
// 解析条件语句: {{#if 条件}}...{{/if}} (修复:添加 (?s) 标志以匹配换行符)
|
||||
ifPattern := regexp.MustCompile(`(?s)\{\{#if\s+(\w+)\}\}(.*?)\{\{/if\}\}`)
|
||||
ifMatches := ifPattern.FindAllStringSubmatch(content, -1)
|
||||
for _, match := range ifMatches {
|
||||
if len(match) >= 3 {
|
||||
condition := match[1]
|
||||
blockContent := match[2]
|
||||
|
||||
block := &TemplateBlock{
|
||||
Type: "if",
|
||||
Condition: condition,
|
||||
Content: blockContent,
|
||||
Children: make([]*TemplateBlock, 0),
|
||||
}
|
||||
|
||||
template.Blocks = append(template.Blocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析循环语句: {{#each 列表}}...{{/each}} (修复:添加 (?s) 标志以匹配换行符)
|
||||
eachPattern := regexp.MustCompile(`(?s)\{\{#each\s+(\w+)\}\}(.*?)\{\{/each\}\}`)
|
||||
eachMatches := eachPattern.FindAllStringSubmatch(content, -1)
|
||||
for _, match := range eachMatches {
|
||||
if len(match) >= 3 {
|
||||
listVar := match[1]
|
||||
blockContent := match[2]
|
||||
|
||||
block := &TemplateBlock{
|
||||
Type: "each",
|
||||
Variable: listVar,
|
||||
Content: blockContent,
|
||||
Children: make([]*TemplateBlock, 0),
|
||||
}
|
||||
|
||||
template.Blocks = append(template.Blocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
// 解析继承: {{extends "base_template"}}
|
||||
extendsPattern := regexp.MustCompile(`\{\{extends\s+"([^"]+)"\}\}`)
|
||||
extendsMatches := extendsPattern.FindStringSubmatch(content)
|
||||
if len(extendsMatches) >= 2 {
|
||||
baseName := extendsMatches[1]
|
||||
baseTemplate, err := te.getTemplateInternal(baseName)
|
||||
if err == nil {
|
||||
template.Parent = baseTemplate
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderToDocument 渲染模板到新文档
|
||||
func (te *TemplateEngine) RenderToDocument(templateName string, data *TemplateData) (*Document, error) {
|
||||
template, err := te.GetTemplate(templateName)
|
||||
if err != nil {
|
||||
return nil, WrapErrorWithContext("render_to_document", err, templateName)
|
||||
}
|
||||
|
||||
// 创建新文档
|
||||
var doc *Document
|
||||
if template.BaseDoc != nil {
|
||||
// 基于基础文档创建
|
||||
doc = te.cloneDocument(template.BaseDoc)
|
||||
} else {
|
||||
// 创建新文档
|
||||
doc = New()
|
||||
}
|
||||
|
||||
// 渲染模板内容
|
||||
renderedContent, err := te.renderTemplate(template, data)
|
||||
if err != nil {
|
||||
return nil, WrapErrorWithContext("render_to_document", err, templateName)
|
||||
}
|
||||
|
||||
// 将渲染内容应用到文档
|
||||
if err := te.applyRenderedContentToDocument(doc, renderedContent); err != nil {
|
||||
return nil, WrapErrorWithContext("render_to_document", err, templateName)
|
||||
}
|
||||
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// renderTemplate 渲染模板
|
||||
func (te *TemplateEngine) renderTemplate(template *Template, data *TemplateData) (string, error) {
|
||||
content := template.Content
|
||||
|
||||
// 处理继承
|
||||
if template.Parent != nil {
|
||||
parentContent, err := te.renderTemplate(template.Parent, data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content = parentContent + "\n" + content
|
||||
}
|
||||
|
||||
// 渲染变量
|
||||
content = te.renderVariables(content, data.Variables)
|
||||
|
||||
// 渲染循环语句(先处理循环,循环内部会处理条件语句)
|
||||
content = te.renderLoops(content, data.Lists)
|
||||
|
||||
// 渲染条件语句(处理非循环内的条件语句)
|
||||
content = te.renderConditionals(content, data.Conditions)
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// renderVariables 渲染变量
|
||||
func (te *TemplateEngine) renderVariables(content string, variables map[string]interface{}) string {
|
||||
varPattern := regexp.MustCompile(`\{\{(\w+)\}\}`)
|
||||
|
||||
return varPattern.ReplaceAllStringFunc(content, func(match string) string {
|
||||
varName := varPattern.FindStringSubmatch(match)[1]
|
||||
if value, exists := variables[varName]; exists {
|
||||
return te.interfaceToString(value)
|
||||
}
|
||||
return match // 保持原样
|
||||
})
|
||||
}
|
||||
|
||||
// renderConditionals 渲染条件语句
|
||||
func (te *TemplateEngine) renderConditionals(content string, conditions map[string]bool) string {
|
||||
ifPattern := regexp.MustCompile(`(?s)\{\{#if\s+(\w+)\}\}(.*?)\{\{/if\}\}`)
|
||||
|
||||
return ifPattern.ReplaceAllStringFunc(content, func(match string) string {
|
||||
matches := ifPattern.FindStringSubmatch(match)
|
||||
if len(matches) >= 3 {
|
||||
condition := matches[1]
|
||||
blockContent := matches[2]
|
||||
|
||||
if condValue, exists := conditions[condition]; exists && condValue {
|
||||
return blockContent
|
||||
}
|
||||
}
|
||||
return "" // 条件不满足,返回空字符串
|
||||
})
|
||||
}
|
||||
|
||||
// renderLoops 渲染循环语句
|
||||
func (te *TemplateEngine) renderLoops(content string, lists map[string][]interface{}) string {
|
||||
eachPattern := regexp.MustCompile(`(?s)\{\{#each\s+(\w+)\}\}(.*?)\{\{/each\}\}`)
|
||||
|
||||
return eachPattern.ReplaceAllStringFunc(content, func(match string) string {
|
||||
matches := eachPattern.FindStringSubmatch(match)
|
||||
if len(matches) >= 3 {
|
||||
listVar := matches[1]
|
||||
blockContent := matches[2]
|
||||
|
||||
if listData, exists := lists[listVar]; exists {
|
||||
var result strings.Builder
|
||||
for i, item := range listData {
|
||||
// 创建循环上下文变量
|
||||
loopContent := strings.ReplaceAll(blockContent, "{{this}}", te.interfaceToString(item))
|
||||
loopContent = strings.ReplaceAll(loopContent, "{{@index}}", strconv.Itoa(i))
|
||||
loopContent = strings.ReplaceAll(loopContent, "{{@first}}", strconv.FormatBool(i == 0))
|
||||
loopContent = strings.ReplaceAll(loopContent, "{{@last}}", strconv.FormatBool(i == len(listData)-1))
|
||||
|
||||
// 如果item是map,处理属性访问
|
||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||
for key, value := range itemMap {
|
||||
placeholder := fmt.Sprintf("{{%s}}", key)
|
||||
loopContent = strings.ReplaceAll(loopContent, placeholder, te.interfaceToString(value))
|
||||
}
|
||||
|
||||
// 处理循环内部的条件语句
|
||||
loopContent = te.renderLoopConditionals(loopContent, itemMap)
|
||||
}
|
||||
|
||||
result.WriteString(loopContent)
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
}
|
||||
return match // 保持原样
|
||||
})
|
||||
}
|
||||
|
||||
// renderLoopConditionals 渲染循环内部的条件语句
|
||||
func (te *TemplateEngine) renderLoopConditionals(content string, itemData map[string]interface{}) string {
|
||||
ifPattern := regexp.MustCompile(`(?s)\{\{#if\s+(\w+)\}\}(.*?)\{\{/if\}\}`)
|
||||
|
||||
return ifPattern.ReplaceAllStringFunc(content, func(match string) string {
|
||||
matches := ifPattern.FindStringSubmatch(match)
|
||||
if len(matches) >= 3 {
|
||||
condition := matches[1]
|
||||
blockContent := matches[2]
|
||||
|
||||
// 检查条件是否在当前循环项的数据中
|
||||
if condValue, exists := itemData[condition]; exists {
|
||||
// 转换为布尔值
|
||||
switch v := condValue.(type) {
|
||||
case bool:
|
||||
if v {
|
||||
return blockContent
|
||||
}
|
||||
case string:
|
||||
if v == "true" || v == "1" || v == "yes" || v != "" {
|
||||
return blockContent
|
||||
}
|
||||
case int:
|
||||
if v != 0 {
|
||||
return blockContent
|
||||
}
|
||||
case int64:
|
||||
if v != 0 {
|
||||
return blockContent
|
||||
}
|
||||
case float64:
|
||||
if v != 0.0 {
|
||||
return blockContent
|
||||
}
|
||||
default:
|
||||
// 对于其他类型,如果不为nil就认为是true
|
||||
if v != nil {
|
||||
return blockContent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return "" // 条件不满足,返回空字符串
|
||||
})
|
||||
}
|
||||
|
||||
// interfaceToString 将interface{}转换为字符串
|
||||
func (te *TemplateEngine) interfaceToString(value interface{}) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return v
|
||||
case int:
|
||||
return strconv.Itoa(v)
|
||||
case int64:
|
||||
return strconv.FormatInt(v, 10)
|
||||
case float64:
|
||||
return strconv.FormatFloat(v, 'f', -1, 64)
|
||||
case bool:
|
||||
return strconv.FormatBool(v)
|
||||
default:
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateTemplate 验证模板语法
|
||||
func (te *TemplateEngine) ValidateTemplate(template *Template) error {
|
||||
content := template.Content
|
||||
|
||||
// 检查括号配对
|
||||
if err := te.validateBrackets(content); err != nil {
|
||||
return WrapErrorWithContext("validate_template", err, template.Name)
|
||||
}
|
||||
|
||||
// 检查if语句配对
|
||||
if err := te.validateIfStatements(content); err != nil {
|
||||
return WrapErrorWithContext("validate_template", err, template.Name)
|
||||
}
|
||||
|
||||
// 检查each语句配对
|
||||
if err := te.validateEachStatements(content); err != nil {
|
||||
return WrapErrorWithContext("validate_template", err, template.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateBrackets 验证括号配对
|
||||
func (te *TemplateEngine) validateBrackets(content string) error {
|
||||
openCount := strings.Count(content, "{{")
|
||||
closeCount := strings.Count(content, "}}")
|
||||
|
||||
if openCount != closeCount {
|
||||
return NewValidationError("brackets", content, "mismatched template brackets")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateIfStatements 验证if语句配对
|
||||
func (te *TemplateEngine) validateIfStatements(content string) error {
|
||||
ifCount := len(regexp.MustCompile(`\{\{#if\s+\w+\}\}`).FindAllString(content, -1))
|
||||
endifCount := len(regexp.MustCompile(`\{\{/if\}\}`).FindAllString(content, -1))
|
||||
|
||||
if ifCount != endifCount {
|
||||
return NewValidationError("if_statements", content, "mismatched if/endif statements")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateEachStatements 验证each语句配对
|
||||
func (te *TemplateEngine) validateEachStatements(content string) error {
|
||||
eachCount := len(regexp.MustCompile(`\{\{#each\s+\w+\}\}`).FindAllString(content, -1))
|
||||
endeachCount := len(regexp.MustCompile(`\{\{/each\}\}`).FindAllString(content, -1))
|
||||
|
||||
if eachCount != endeachCount {
|
||||
return NewValidationError("each_statements", content, "mismatched each/endeach statements")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// documentToTemplateString 将文档转换为模板字符串
|
||||
func (te *TemplateEngine) documentToTemplateString(doc *Document) (string, error) {
|
||||
var content strings.Builder
|
||||
|
||||
// 遍历文档的所有段落,生成模板字符串
|
||||
for _, element := range doc.Body.Elements {
|
||||
switch elem := element.(type) {
|
||||
case *Paragraph:
|
||||
// 处理段落
|
||||
for _, run := range elem.Runs {
|
||||
content.WriteString(run.Text.Content)
|
||||
}
|
||||
content.WriteString("\n")
|
||||
case *Table:
|
||||
// 处理表格
|
||||
content.WriteString("{{#table}}\n")
|
||||
for i, row := range elem.Rows {
|
||||
content.WriteString(fmt.Sprintf("{{#row_%d}}\n", i))
|
||||
for j := range row.Cells {
|
||||
content.WriteString(fmt.Sprintf("{{cell_%d_%d}}", i, j))
|
||||
}
|
||||
content.WriteString("{{/row}}\n")
|
||||
}
|
||||
content.WriteString("{{/table}}\n")
|
||||
}
|
||||
}
|
||||
|
||||
return content.String(), nil
|
||||
}
|
||||
|
||||
// cloneDocument 克隆文档
|
||||
func (te *TemplateEngine) cloneDocument(source *Document) *Document {
|
||||
// 简单实现:创建新文档并复制基本结构
|
||||
doc := New()
|
||||
|
||||
// 复制样式管理器
|
||||
if source.styleManager != nil {
|
||||
doc.styleManager = source.styleManager
|
||||
}
|
||||
|
||||
return doc
|
||||
}
|
||||
|
||||
// applyRenderedContentToDocument 将渲染内容应用到文档
|
||||
func (te *TemplateEngine) applyRenderedContentToDocument(doc *Document, content string) error {
|
||||
// 将渲染后的内容按行分割并添加到文档
|
||||
lines := strings.Split(content, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" {
|
||||
doc.AddParagraph(line)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTemplateData 创建新的模板数据
|
||||
func NewTemplateData() *TemplateData {
|
||||
return &TemplateData{
|
||||
Variables: make(map[string]interface{}),
|
||||
Lists: make(map[string][]interface{}),
|
||||
Conditions: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// SetVariable 设置变量
|
||||
func (td *TemplateData) SetVariable(name string, value interface{}) {
|
||||
td.Variables[name] = value
|
||||
}
|
||||
|
||||
// SetList 设置列表
|
||||
func (td *TemplateData) SetList(name string, list []interface{}) {
|
||||
td.Lists[name] = list
|
||||
}
|
||||
|
||||
// SetCondition 设置条件
|
||||
func (td *TemplateData) SetCondition(name string, value bool) {
|
||||
td.Conditions[name] = value
|
||||
}
|
||||
|
||||
// SetVariables 批量设置变量
|
||||
func (td *TemplateData) SetVariables(variables map[string]interface{}) {
|
||||
for name, value := range variables {
|
||||
td.Variables[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
// GetVariable 获取变量
|
||||
func (td *TemplateData) GetVariable(name string) (interface{}, bool) {
|
||||
value, exists := td.Variables[name]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// GetList 获取列表
|
||||
func (td *TemplateData) GetList(name string) ([]interface{}, bool) {
|
||||
list, exists := td.Lists[name]
|
||||
return list, exists
|
||||
}
|
||||
|
||||
// GetCondition 获取条件
|
||||
func (td *TemplateData) GetCondition(name string) (bool, bool) {
|
||||
condition, exists := td.Conditions[name]
|
||||
return condition, exists
|
||||
}
|
||||
|
||||
// Merge 合并模板数据
|
||||
func (td *TemplateData) Merge(other *TemplateData) {
|
||||
// 合并变量
|
||||
for name, value := range other.Variables {
|
||||
td.Variables[name] = value
|
||||
}
|
||||
|
||||
// 合并列表
|
||||
for name, list := range other.Lists {
|
||||
td.Lists[name] = list
|
||||
}
|
||||
|
||||
// 合并条件
|
||||
for name, condition := range other.Conditions {
|
||||
td.Conditions[name] = condition
|
||||
}
|
||||
}
|
||||
|
||||
// Clear 清空模板数据
|
||||
func (td *TemplateData) Clear() {
|
||||
td.Variables = make(map[string]interface{})
|
||||
td.Lists = make(map[string][]interface{})
|
||||
td.Conditions = make(map[string]bool)
|
||||
}
|
||||
|
||||
// FromStruct 从结构体生成模板数据
|
||||
func (td *TemplateData) FromStruct(data interface{}) error {
|
||||
value := reflect.ValueOf(data)
|
||||
if value.Kind() == reflect.Ptr {
|
||||
value = value.Elem()
|
||||
}
|
||||
|
||||
if value.Kind() != reflect.Struct {
|
||||
return NewValidationError("data_type", "struct", "expected struct type")
|
||||
}
|
||||
|
||||
typ := value.Type()
|
||||
for i := 0; i < value.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
fieldValue := value.Field(i)
|
||||
|
||||
// 跳过不可导出的字段
|
||||
if !fieldValue.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
fieldName := strings.ToLower(field.Name)
|
||||
td.Variables[fieldName] = fieldValue.Interface()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
441
pkg/document/template_test.go
Normal file
441
pkg/document/template_test.go
Normal file
@@ -0,0 +1,441 @@
|
||||
// Package document 模板功能测试
|
||||
package document
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNewTemplateEngine 测试创建模板引擎
|
||||
func TestNewTemplateEngine(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
if engine == nil {
|
||||
t.Fatal("Expected template engine to be created")
|
||||
}
|
||||
|
||||
if engine.cache == nil {
|
||||
t.Fatal("Expected cache to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateVariableReplacement 测试变量替换功能
|
||||
func TestTemplateVariableReplacement(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 创建包含变量的模板
|
||||
templateContent := "Hello {{name}}, welcome to {{company}}!"
|
||||
template, err := engine.LoadTemplate("test_template", templateContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
|
||||
// 验证模板变量解析
|
||||
if len(template.Variables) != 2 {
|
||||
t.Errorf("Expected 2 variables, got %d", len(template.Variables))
|
||||
}
|
||||
|
||||
if _, exists := template.Variables["name"]; !exists {
|
||||
t.Error("Expected 'name' variable to be found")
|
||||
}
|
||||
|
||||
if _, exists := template.Variables["company"]; !exists {
|
||||
t.Error("Expected 'company' variable to be found")
|
||||
}
|
||||
|
||||
// 创建模板数据
|
||||
data := NewTemplateData()
|
||||
data.SetVariable("name", "张三")
|
||||
data.SetVariable("company", "WordZero公司")
|
||||
|
||||
// 渲染模板
|
||||
doc, err := engine.RenderToDocument("test_template", data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
|
||||
if doc == nil {
|
||||
t.Fatal("Expected document to be created")
|
||||
}
|
||||
|
||||
// 检查文档内容
|
||||
if len(doc.Body.Elements) == 0 {
|
||||
t.Error("Expected document to have content")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateConditionalStatements 测试条件语句功能
|
||||
func TestTemplateConditionalStatements(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 创建包含条件语句的模板
|
||||
templateContent := `{{#if showWelcome}}欢迎使用WordZero!{{/if}}
|
||||
{{#if showDescription}}这是一个强大的Word文档操作库。{{/if}}`
|
||||
|
||||
template, err := engine.LoadTemplate("conditional_template", templateContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
|
||||
// 验证条件块解析
|
||||
if len(template.Blocks) < 2 {
|
||||
t.Errorf("Expected at least 2 blocks, got %d", len(template.Blocks))
|
||||
}
|
||||
|
||||
// 测试条件为真的情况
|
||||
data := NewTemplateData()
|
||||
data.SetCondition("showWelcome", true)
|
||||
data.SetCondition("showDescription", false)
|
||||
|
||||
doc, err := engine.RenderToDocument("conditional_template", data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
|
||||
if doc == nil {
|
||||
t.Fatal("Expected document to be created")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateLoopStatements 测试循环语句功能
|
||||
func TestTemplateLoopStatements(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 创建包含循环语句的模板
|
||||
templateContent := `产品列表:
|
||||
{{#each products}}
|
||||
- 产品名称:{{name}},价格:{{price}}元
|
||||
{{/each}}`
|
||||
|
||||
template, err := engine.LoadTemplate("loop_template", templateContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
|
||||
// 验证循环块解析
|
||||
foundEachBlock := false
|
||||
for _, block := range template.Blocks {
|
||||
if block.Type == "each" && block.Variable == "products" {
|
||||
foundEachBlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundEachBlock {
|
||||
t.Error("Expected to find 'each products' block")
|
||||
}
|
||||
|
||||
// 创建列表数据
|
||||
data := NewTemplateData()
|
||||
products := []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "iPhone",
|
||||
"price": 8999,
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "iPad",
|
||||
"price": 5999,
|
||||
},
|
||||
}
|
||||
data.SetList("products", products)
|
||||
|
||||
doc, err := engine.RenderToDocument("loop_template", data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render template: %v", err)
|
||||
}
|
||||
|
||||
if doc == nil {
|
||||
t.Fatal("Expected document to be created")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateInheritance 测试模板继承功能
|
||||
func TestTemplateInheritance(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 创建基础模板
|
||||
baseTemplateContent := `文档标题:{{title}}
|
||||
基础内容:这是基础模板的内容。`
|
||||
|
||||
_, err := engine.LoadTemplate("base_template", baseTemplateContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load base template: %v", err)
|
||||
}
|
||||
|
||||
// 创建继承模板
|
||||
childTemplateContent := `{{extends "base_template"}}
|
||||
扩展内容:这是子模板的内容。`
|
||||
|
||||
childTemplate, err := engine.LoadTemplate("child_template", childTemplateContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load child template: %v", err)
|
||||
}
|
||||
|
||||
// 验证继承关系
|
||||
if childTemplate.Parent == nil {
|
||||
t.Error("Expected child template to have parent")
|
||||
}
|
||||
|
||||
if childTemplate.Parent.Name != "base_template" {
|
||||
t.Errorf("Expected parent template name to be 'base_template', got '%s'", childTemplate.Parent.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateValidation 测试模板验证功能
|
||||
func TestTemplateValidation(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 测试有效模板
|
||||
validTemplate := `Hello {{name}}!
|
||||
{{#if showMessage}}This is a message.{{/if}}
|
||||
{{#each items}}Item: {{this}}{{/each}}`
|
||||
|
||||
template, err := engine.LoadTemplate("valid_template", validTemplate)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load valid template: %v", err)
|
||||
}
|
||||
|
||||
err = engine.ValidateTemplate(template)
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid template to pass validation, got error: %v", err)
|
||||
}
|
||||
|
||||
// 测试无效模板 - 括号不匹配
|
||||
invalidTemplate1 := `Hello {{name}!`
|
||||
template1, err := engine.LoadTemplate("invalid_template1", invalidTemplate1)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load invalid template: %v", err)
|
||||
}
|
||||
|
||||
err = engine.ValidateTemplate(template1)
|
||||
if err == nil {
|
||||
t.Error("Expected invalid template (mismatched brackets) to fail validation")
|
||||
}
|
||||
|
||||
// 测试无效模板 - if语句不匹配
|
||||
invalidTemplate2 := `{{#if condition}}Hello`
|
||||
template2, err := engine.LoadTemplate("invalid_template2", invalidTemplate2)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load invalid template: %v", err)
|
||||
}
|
||||
|
||||
err = engine.ValidateTemplate(template2)
|
||||
if err == nil {
|
||||
t.Error("Expected invalid template (mismatched if statements) to fail validation")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateData 测试模板数据功能
|
||||
func TestTemplateData(t *testing.T) {
|
||||
data := NewTemplateData()
|
||||
|
||||
// 测试设置和获取变量
|
||||
data.SetVariable("name", "测试")
|
||||
value, exists := data.GetVariable("name")
|
||||
if !exists {
|
||||
t.Error("Expected variable 'name' to exist")
|
||||
}
|
||||
if value != "测试" {
|
||||
t.Errorf("Expected variable value to be '测试', got '%v'", value)
|
||||
}
|
||||
|
||||
// 测试设置和获取列表
|
||||
items := []interface{}{"item1", "item2", "item3"}
|
||||
data.SetList("items", items)
|
||||
list, exists := data.GetList("items")
|
||||
if !exists {
|
||||
t.Error("Expected list 'items' to exist")
|
||||
}
|
||||
if len(list) != 3 {
|
||||
t.Errorf("Expected list length to be 3, got %d", len(list))
|
||||
}
|
||||
|
||||
// 测试设置和获取条件
|
||||
data.SetCondition("enabled", true)
|
||||
condition, exists := data.GetCondition("enabled")
|
||||
if !exists {
|
||||
t.Error("Expected condition 'enabled' to exist")
|
||||
}
|
||||
if !condition {
|
||||
t.Error("Expected condition value to be true")
|
||||
}
|
||||
|
||||
// 测试批量设置变量
|
||||
variables := map[string]interface{}{
|
||||
"title": "测试标题",
|
||||
"content": "测试内容",
|
||||
}
|
||||
data.SetVariables(variables)
|
||||
|
||||
title, exists := data.GetVariable("title")
|
||||
if !exists || title != "测试标题" {
|
||||
t.Error("Expected batch set variables to work")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateDataFromStruct 测试从结构体创建模板数据
|
||||
func TestTemplateDataFromStruct(t *testing.T) {
|
||||
type TestStruct struct {
|
||||
Name string
|
||||
Age int
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
testData := TestStruct{
|
||||
Name: "张三",
|
||||
Age: 30,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
templateData := NewTemplateData()
|
||||
err := templateData.FromStruct(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create template data from struct: %v", err)
|
||||
}
|
||||
|
||||
// 验证变量是否正确设置
|
||||
name, exists := templateData.GetVariable("name")
|
||||
if !exists || name != "张三" {
|
||||
t.Error("Expected 'name' variable to be set correctly")
|
||||
}
|
||||
|
||||
age, exists := templateData.GetVariable("age")
|
||||
if !exists || age != 30 {
|
||||
t.Error("Expected 'age' variable to be set correctly")
|
||||
}
|
||||
|
||||
enabled, exists := templateData.GetVariable("enabled")
|
||||
if !exists || enabled != true {
|
||||
t.Error("Expected 'enabled' variable to be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateMerge 测试模板数据合并
|
||||
func TestTemplateMerge(t *testing.T) {
|
||||
data1 := NewTemplateData()
|
||||
data1.SetVariable("name", "张三")
|
||||
data1.SetCondition("enabled", true)
|
||||
|
||||
data2 := NewTemplateData()
|
||||
data2.SetVariable("age", 30)
|
||||
data2.SetList("items", []interface{}{"item1", "item2"})
|
||||
|
||||
// 合并数据
|
||||
data1.Merge(data2)
|
||||
|
||||
// 验证合并结果
|
||||
name, exists := data1.GetVariable("name")
|
||||
if !exists || name != "张三" {
|
||||
t.Error("Expected original variable to remain")
|
||||
}
|
||||
|
||||
age, exists := data1.GetVariable("age")
|
||||
if !exists || age != 30 {
|
||||
t.Error("Expected merged variable to be present")
|
||||
}
|
||||
|
||||
enabled, exists := data1.GetCondition("enabled")
|
||||
if !exists || !enabled {
|
||||
t.Error("Expected original condition to remain")
|
||||
}
|
||||
|
||||
items, exists := data1.GetList("items")
|
||||
if !exists || len(items) != 2 {
|
||||
t.Error("Expected merged list to be present")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplateCache 测试模板缓存功能
|
||||
func TestTemplateCache(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 加载模板
|
||||
templateContent := "Hello {{name}}!"
|
||||
template1, err := engine.LoadTemplate("cached_template", templateContent)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load template: %v", err)
|
||||
}
|
||||
|
||||
// 从缓存获取模板
|
||||
template2, err := engine.GetTemplate("cached_template")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get template from cache: %v", err)
|
||||
}
|
||||
|
||||
// 验证是同一个模板实例
|
||||
if template1 != template2 {
|
||||
t.Error("Expected to get same template instance from cache")
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
engine.ClearCache()
|
||||
|
||||
// 尝试获取已清空的模板
|
||||
_, err = engine.GetTemplate("cached_template")
|
||||
if err == nil {
|
||||
t.Error("Expected error when getting template after cache clear")
|
||||
}
|
||||
}
|
||||
|
||||
// TestComplexTemplateRendering 测试复杂模板渲染
|
||||
func TestComplexTemplateRendering(t *testing.T) {
|
||||
engine := NewTemplateEngine()
|
||||
|
||||
// 创建复杂模板
|
||||
complexTemplate := `报告标题:{{title}}
|
||||
作者:{{author}}
|
||||
|
||||
{{#if showSummary}}
|
||||
概要:{{summary}}
|
||||
{{/if}}
|
||||
|
||||
详细内容:
|
||||
{{#each sections}}
|
||||
章节 {{@index}}: {{title}}
|
||||
内容:{{content}}
|
||||
|
||||
{{/each}}
|
||||
|
||||
{{#if showFooter}}
|
||||
报告完毕。
|
||||
{{/if}}`
|
||||
|
||||
_, err := engine.LoadTemplate("complex_template", complexTemplate)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load complex template: %v", err)
|
||||
}
|
||||
|
||||
// 创建复杂数据
|
||||
data := NewTemplateData()
|
||||
data.SetVariable("title", "WordZero功能测试报告")
|
||||
data.SetVariable("author", "开发团队")
|
||||
data.SetVariable("summary", "本报告测试了WordZero的模板功能。")
|
||||
|
||||
data.SetCondition("showSummary", true)
|
||||
data.SetCondition("showFooter", true)
|
||||
|
||||
sections := []interface{}{
|
||||
map[string]interface{}{
|
||||
"title": "基础功能",
|
||||
"content": "测试了基础的文档操作功能。",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"title": "模板功能",
|
||||
"content": "测试了模板引擎的各种功能。",
|
||||
},
|
||||
}
|
||||
data.SetList("sections", sections)
|
||||
|
||||
// 渲染复杂模板
|
||||
doc, err := engine.RenderToDocument("complex_template", data)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to render complex template: %v", err)
|
||||
}
|
||||
|
||||
if doc == nil {
|
||||
t.Fatal("Expected document to be created")
|
||||
}
|
||||
|
||||
// 验证文档有内容
|
||||
if len(doc.Body.Elements) == 0 {
|
||||
t.Error("Expected document to have content")
|
||||
}
|
||||
}
|
137
test/debug_page_settings_test.go
Normal file
137
test/debug_page_settings_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/ZeroHawkeye/wordZero/pkg/document"
|
||||
)
|
||||
|
||||
func TestDebugPageSettings(t *testing.T) {
|
||||
// 创建文档
|
||||
doc := document.New()
|
||||
|
||||
// 设置页面配置
|
||||
settings := &document.PageSettings{
|
||||
Size: document.PageSizeLetter,
|
||||
Orientation: document.OrientationLandscape,
|
||||
MarginTop: 25,
|
||||
MarginRight: 20,
|
||||
MarginBottom: 30,
|
||||
MarginLeft: 25,
|
||||
HeaderDistance: 12,
|
||||
FooterDistance: 15,
|
||||
GutterWidth: 5,
|
||||
}
|
||||
|
||||
err := doc.SetPageSettings(settings)
|
||||
if err != nil {
|
||||
t.Fatalf("设置页面配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证设置
|
||||
currentSettings := doc.GetPageSettings()
|
||||
fmt.Printf("设置后的页面配置:\n")
|
||||
fmt.Printf(" 尺寸: %s\n", currentSettings.Size)
|
||||
fmt.Printf(" 方向: %s\n", currentSettings.Orientation)
|
||||
|
||||
// 添加测试内容
|
||||
doc.AddParagraph("测试页面设置保存和加载")
|
||||
|
||||
// 保存文档
|
||||
testFile := "debug_page_settings.docx"
|
||||
err = doc.Save(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("保存文档失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("文档已保存到: %s\n", testFile)
|
||||
|
||||
// 重新打开文档
|
||||
loadedDoc, err := document.Open(testFile)
|
||||
if err != nil {
|
||||
t.Fatalf("重新打开文档失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查加载后文档的Body.Elements
|
||||
fmt.Printf("加载后文档的Body.Elements数量: %d\n", len(loadedDoc.Body.Elements))
|
||||
for i, element := range loadedDoc.Body.Elements {
|
||||
switch elem := element.(type) {
|
||||
case *document.SectionProperties:
|
||||
fmt.Printf(" 元素%d: SectionProperties found!\n", i)
|
||||
if elem.PageSize != nil {
|
||||
fmt.Printf(" PageSize: w=%s, h=%s, orient=%s\n", elem.PageSize.W, elem.PageSize.H, elem.PageSize.Orient)
|
||||
} else {
|
||||
fmt.Printf(" PageSize: nil\n")
|
||||
}
|
||||
case *document.Paragraph:
|
||||
fmt.Printf(" 元素%d: Paragraph\n", i)
|
||||
default:
|
||||
fmt.Printf(" 元素%d: 其他类型 (%T)\n", i, element)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证加载后的设置
|
||||
loadedSettings := loadedDoc.GetPageSettings()
|
||||
fmt.Printf("加载后的页面配置:\n")
|
||||
fmt.Printf(" 尺寸: %s\n", loadedSettings.Size)
|
||||
fmt.Printf(" 方向: %s\n", loadedSettings.Orientation)
|
||||
|
||||
// 验证设置是否正确
|
||||
if loadedSettings.Size != settings.Size {
|
||||
t.Errorf("加载后页面尺寸不匹配,期望: %s, 实际: %s", settings.Size, loadedSettings.Size)
|
||||
}
|
||||
|
||||
if loadedSettings.Orientation != settings.Orientation {
|
||||
t.Errorf("加载后页面方向不匹配,期望: %s, 实际: %s", settings.Orientation, loadedSettings.Orientation)
|
||||
}
|
||||
|
||||
// 详细调试页面尺寸解析过程
|
||||
parts := loadedDoc.GetParts()
|
||||
if docXML, exists := parts["word/document.xml"]; exists {
|
||||
fmt.Printf("document.xml内容前500字符:\n%s\n", string(docXML)[:min(500, len(docXML))])
|
||||
|
||||
// 手动验证twips转换
|
||||
fmt.Printf("调试页面尺寸转换:\n")
|
||||
|
||||
// Letter尺寸:215.9mm x 279.4mm
|
||||
// 横向后应该是:279.4mm x 215.9mm
|
||||
// 转换为twips:279.4 * 56.69 ≈ 15840,215.9 * 56.69 ≈ 12240
|
||||
|
||||
width_twips := 15840.0
|
||||
height_twips := 12240.0
|
||||
width_mm := width_twips / 56.692913385827
|
||||
height_mm := height_twips / 56.692913385827
|
||||
|
||||
fmt.Printf(" 从XML读取: 宽度=%d twips, 高度=%d twips\n", int(width_twips), int(height_twips))
|
||||
fmt.Printf(" 转换为毫米: 宽度=%.1fmm, 高度=%.1fmm\n", width_mm, height_mm)
|
||||
|
||||
// 测试页面尺寸识别
|
||||
fmt.Printf(" Letter纵向尺寸: 215.9mm x 279.4mm\n")
|
||||
fmt.Printf(" Letter横向尺寸: 279.4mm x 215.9mm\n")
|
||||
fmt.Printf(" 实际解析尺寸: %.1fmm x %.1fmm\n", width_mm, height_mm)
|
||||
|
||||
// 检查容差
|
||||
tolerance := 1.0
|
||||
letter_width := 215.9
|
||||
letter_height := 279.4
|
||||
|
||||
// 检查横向匹配
|
||||
landscape_match := (math.Abs(width_mm-letter_height) < tolerance && math.Abs(height_mm-letter_width) < tolerance)
|
||||
fmt.Printf(" 横向Letter匹配: %t (容差=%.1fmm)\n", landscape_match, tolerance)
|
||||
|
||||
// 检查纵向匹配
|
||||
portrait_match := (math.Abs(width_mm-letter_width) < tolerance && math.Abs(height_mm-letter_height) < tolerance)
|
||||
fmt.Printf(" 纵向Letter匹配: %t (容差=%.1fmm)\n", portrait_match, tolerance)
|
||||
} else {
|
||||
fmt.Printf("未找到document.xml\n")
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
44
test/debug_settings.go
Normal file
44
test/debug_settings.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
|
||||
"github.com/ZeroHawkeye/wordZero/pkg/document"
|
||||
)
|
||||
|
||||
func DebugSettings() {
|
||||
doc := document.New()
|
||||
|
||||
// 创建脚注配置
|
||||
config := &document.FootnoteConfig{
|
||||
NumberFormat: document.FootnoteFormatDecimal,
|
||||
StartNumber: 1,
|
||||
RestartEach: document.FootnoteRestartContinuous,
|
||||
Position: document.FootnotePositionPageBottom,
|
||||
}
|
||||
|
||||
// 尝试设置配置
|
||||
err := doc.SetFootnoteConfig(config)
|
||||
if err != nil {
|
||||
fmt.Printf("设置脚注配置错误: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查生成的settings.xml内容
|
||||
parts := doc.GetParts()
|
||||
if settingsXML, exists := parts["word/settings.xml"]; exists {
|
||||
fmt.Printf("Settings XML内容:\n%s\n", string(settingsXML))
|
||||
|
||||
// 尝试解析生成的XML
|
||||
var settings document.Settings
|
||||
err = xml.Unmarshal(settingsXML, &settings)
|
||||
if err != nil {
|
||||
fmt.Printf("解析XML失败: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("XML解析成功!\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("settings.xml文件未找到\n")
|
||||
}
|
||||
}
|
157
test/footnotes_test.go
Normal file
157
test/footnotes_test.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ZeroHawkeye/wordZero/pkg/document"
|
||||
)
|
||||
|
||||
func TestFootnoteConfig(t *testing.T) {
|
||||
doc := document.New()
|
||||
|
||||
// 测试设置脚注配置
|
||||
config := &document.FootnoteConfig{
|
||||
NumberFormat: document.FootnoteFormatDecimal,
|
||||
StartNumber: 1,
|
||||
RestartEach: document.FootnoteRestartContinuous,
|
||||
Position: document.FootnotePositionPageBottom,
|
||||
}
|
||||
|
||||
err := doc.SetFootnoteConfig(config)
|
||||
if err != nil {
|
||||
// 打印详细错误信息用于调试
|
||||
fmt.Printf("设置脚注配置失败的详细错误: %v\n", err)
|
||||
|
||||
// 检查settings.xml内容
|
||||
parts := doc.GetParts()
|
||||
if settingsXML, exists := parts["word/settings.xml"]; exists {
|
||||
fmt.Printf("生成的settings.xml内容:\n%s\n", string(settingsXML))
|
||||
}
|
||||
|
||||
t.Fatalf("设置脚注配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证settings.xml是否已创建
|
||||
_, exists := doc.GetParts()["word/settings.xml"]
|
||||
if !exists {
|
||||
t.Error("settings.xml文件未创建")
|
||||
}
|
||||
|
||||
// 添加脚注测试
|
||||
err = doc.AddFootnote("这是正文文本", "这是脚注内容")
|
||||
if err != nil {
|
||||
t.Fatalf("添加脚注失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证脚注文件是否已创建
|
||||
_, exists = doc.GetParts()["word/footnotes.xml"]
|
||||
if !exists {
|
||||
t.Error("footnotes.xml文件未创建")
|
||||
}
|
||||
|
||||
// 验证脚注数量
|
||||
count := doc.GetFootnoteCount()
|
||||
if count != 1 {
|
||||
t.Errorf("预期脚注数量为1,实际为%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndnoteConfig(t *testing.T) {
|
||||
doc := document.New()
|
||||
|
||||
// 添加尾注测试
|
||||
err := doc.AddEndnote("这是正文文本", "这是尾注内容")
|
||||
if err != nil {
|
||||
t.Fatalf("添加尾注失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证尾注文件是否已创建
|
||||
_, exists := doc.GetParts()["word/endnotes.xml"]
|
||||
if !exists {
|
||||
t.Error("endnotes.xml文件未创建")
|
||||
}
|
||||
|
||||
// 验证尾注数量
|
||||
count := doc.GetEndnoteCount()
|
||||
if count != 1 {
|
||||
t.Errorf("预期尾注数量为1,实际为%d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFootnoteNumberFormats(t *testing.T) {
|
||||
doc := document.New()
|
||||
|
||||
// 测试不同的编号格式
|
||||
formats := []document.FootnoteNumberFormat{
|
||||
document.FootnoteFormatDecimal,
|
||||
document.FootnoteFormatLowerRoman,
|
||||
document.FootnoteFormatUpperRoman,
|
||||
document.FootnoteFormatLowerLetter,
|
||||
document.FootnoteFormatUpperLetter,
|
||||
document.FootnoteFormatSymbol,
|
||||
}
|
||||
|
||||
for _, format := range formats {
|
||||
config := &document.FootnoteConfig{
|
||||
NumberFormat: format,
|
||||
StartNumber: 1,
|
||||
RestartEach: document.FootnoteRestartContinuous,
|
||||
Position: document.FootnotePositionPageBottom,
|
||||
}
|
||||
|
||||
err := doc.SetFootnoteConfig(config)
|
||||
if err != nil {
|
||||
t.Fatalf("设置脚注格式%s失败: %v", format, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFootnotePositions(t *testing.T) {
|
||||
doc := document.New()
|
||||
|
||||
// 测试不同的脚注位置
|
||||
positions := []document.FootnotePosition{
|
||||
document.FootnotePositionPageBottom,
|
||||
document.FootnotePositionBeneathText,
|
||||
document.FootnotePositionSectionEnd,
|
||||
document.FootnotePositionDocumentEnd,
|
||||
}
|
||||
|
||||
for _, position := range positions {
|
||||
config := &document.FootnoteConfig{
|
||||
NumberFormat: document.FootnoteFormatDecimal,
|
||||
StartNumber: 1,
|
||||
RestartEach: document.FootnoteRestartContinuous,
|
||||
Position: position,
|
||||
}
|
||||
|
||||
err := doc.SetFootnoteConfig(config)
|
||||
if err != nil {
|
||||
t.Fatalf("设置脚注位置%s失败: %v", position, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultFootnoteConfig(t *testing.T) {
|
||||
config := document.DefaultFootnoteConfig()
|
||||
|
||||
if config.NumberFormat != document.FootnoteFormatDecimal {
|
||||
t.Errorf("默认编号格式错误,预期%s,实际%s",
|
||||
document.FootnoteFormatDecimal, config.NumberFormat)
|
||||
}
|
||||
|
||||
if config.StartNumber != 1 {
|
||||
t.Errorf("默认起始编号错误,预期1,实际%d", config.StartNumber)
|
||||
}
|
||||
|
||||
if config.RestartEach != document.FootnoteRestartContinuous {
|
||||
t.Errorf("默认重新开始规则错误,预期%s,实际%s",
|
||||
document.FootnoteRestartContinuous, config.RestartEach)
|
||||
}
|
||||
|
||||
if config.Position != document.FootnotePositionPageBottom {
|
||||
t.Errorf("默认位置错误,预期%s,实际%s",
|
||||
document.FootnotePositionPageBottom, config.Position)
|
||||
}
|
||||
}
|
1010
test/template_test.go
Normal file
1010
test/template_test.go
Normal file
File diff suppressed because it is too large
Load Diff
76
test/xml_debug_test.go
Normal file
76
test/xml_debug_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 定义一个本地的Settings结构用于测试,不使用命名空间前缀
|
||||
type testSettings struct {
|
||||
XMLName xml.Name `xml:"settings"`
|
||||
Xmlns string `xml:"xmlns:w,attr"`
|
||||
DefaultTabStop *testDefaultTabStop `xml:"defaultTabStop,omitempty"`
|
||||
CharacterSpacingControl *testCharacterSpacingControl `xml:"characterSpacingControl,omitempty"`
|
||||
}
|
||||
|
||||
type testDefaultTabStop struct {
|
||||
XMLName xml.Name `xml:"defaultTabStop"`
|
||||
Val string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
type testCharacterSpacingControl struct {
|
||||
XMLName xml.Name `xml:"characterSpacingControl"`
|
||||
Val string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
func TestXMLSerialization(t *testing.T) {
|
||||
// 创建测试设置
|
||||
settings := &testSettings{
|
||||
Xmlns: "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
DefaultTabStop: &testDefaultTabStop{
|
||||
Val: "708",
|
||||
},
|
||||
CharacterSpacingControl: &testCharacterSpacingControl{
|
||||
Val: "doNotCompress",
|
||||
},
|
||||
}
|
||||
|
||||
// 序列化为XML
|
||||
xmlData, err := xml.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("序列化失败: %v", err)
|
||||
}
|
||||
|
||||
// 添加XML声明
|
||||
xmlDeclaration := []byte(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>` + "\n")
|
||||
fullXML := append(xmlDeclaration, xmlData...)
|
||||
|
||||
fmt.Printf("序列化的XML:\n%s\n", string(fullXML))
|
||||
|
||||
// 解析XML - 这次应该能成功,因为我们不使用命名空间前缀
|
||||
var parsedSettings testSettings
|
||||
err = xml.Unmarshal(xmlData, &parsedSettings) // 使用xmlData而不是fullXML,避免XML声明解析问题
|
||||
if err != nil {
|
||||
t.Fatalf("解析失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("解析成功!\n")
|
||||
|
||||
// 验证解析结果 - 注意XML序列化后命名空间可能会变化
|
||||
if parsedSettings.Xmlns != "" && parsedSettings.Xmlns != settings.Xmlns {
|
||||
t.Errorf("命名空间不匹配: 期望 %s, 实际 %s", settings.Xmlns, parsedSettings.Xmlns)
|
||||
}
|
||||
|
||||
// 验证其他字段
|
||||
if parsedSettings.DefaultTabStop == nil || parsedSettings.DefaultTabStop.Val != "708" {
|
||||
t.Errorf("DefaultTabStop解析不正确")
|
||||
}
|
||||
|
||||
if parsedSettings.CharacterSpacingControl == nil || parsedSettings.CharacterSpacingControl.Val != "doNotCompress" {
|
||||
t.Errorf("CharacterSpacingControl解析不正确")
|
||||
}
|
||||
|
||||
// 验证核心功能:能够序列化和解析XML结构
|
||||
fmt.Printf("XML序列化和解析测试通过!\n")
|
||||
}
|
Submodule wordZero.wiki updated: a9f0d61fd6...17e18e751f
Reference in New Issue
Block a user