- 修复模板中字体没有继承问题

- 修复多种样式没有在模板继承中添加到问题
This commit is contained in:
zero
2025-06-04 11:49:11 +08:00
parent 544714e481
commit 067e7d7bfa
4 changed files with 465 additions and 38 deletions

View File

@@ -109,10 +109,11 @@ type ParagraphProperties struct {
// Spacing 间距设置
type Spacing struct {
XMLName xml.Name `xml:"w:spacing"`
Before string `xml:"w:before,attr,omitempty"`
After string `xml:"w:after,attr,omitempty"`
Line string `xml:"w:line,attr,omitempty"`
XMLName xml.Name `xml:"w:spacing"`
Before string `xml:"w:before,attr,omitempty"`
After string `xml:"w:after,attr,omitempty"`
Line string `xml:"w:line,attr,omitempty"`
LineRule string `xml:"w:lineRule,attr,omitempty"`
}
// Justification 对齐方式
@@ -135,9 +136,15 @@ type Run struct {
type RunProperties struct {
XMLName xml.Name `xml:"w:rPr"`
Bold *Bold `xml:"w:b,omitempty"`
BoldCs *BoldCs `xml:"w:bCs,omitempty"`
Italic *Italic `xml:"w:i,omitempty"`
ItalicCs *ItalicCs `xml:"w:iCs,omitempty"`
Underline *Underline `xml:"w:u,omitempty"`
Strike *Strike `xml:"w:strike,omitempty"`
FontSize *FontSize `xml:"w:sz,omitempty"`
FontSizeCs *FontSizeCs `xml:"w:szCs,omitempty"`
Color *Color `xml:"w:color,omitempty"`
Highlight *Highlight `xml:"w:highlight,omitempty"`
FontFamily *FontFamily `xml:"w:rFonts,omitempty"`
}
@@ -146,23 +153,56 @@ type Bold struct {
XMLName xml.Name `xml:"w:b"`
}
// BoldCs 复杂脚本粗体
type BoldCs struct {
XMLName xml.Name `xml:"w:bCs"`
}
// Italic 斜体
type Italic struct {
XMLName xml.Name `xml:"w:i"`
}
// ItalicCs 复杂脚本斜体
type ItalicCs struct {
XMLName xml.Name `xml:"w:iCs"`
}
// Underline 下划线
type Underline struct {
XMLName xml.Name `xml:"w:u"`
Val string `xml:"w:val,attr,omitempty"`
}
// Strike 删除线
type Strike struct {
XMLName xml.Name `xml:"w:strike"`
}
// FontSize 字体大小
type FontSize struct {
XMLName xml.Name `xml:"w:sz"`
Val string `xml:"w:val,attr"`
}
// FontSizeCs 复杂脚本字体大小
type FontSizeCs struct {
XMLName xml.Name `xml:"w:szCs"`
Val string `xml:"w:val,attr"`
}
// Color 颜色
type Color struct {
XMLName xml.Name `xml:"w:color"`
Val string `xml:"w:val,attr"`
}
// Highlight 背景色
type Highlight struct {
XMLName xml.Name `xml:"w:highlight"`
Val string `xml:"w:val,attr"`
}
// Text 文本内容
type Text struct {
XMLName xml.Name `xml:"w:t"`
@@ -380,6 +420,13 @@ func Open(filename string) (*Document, error) {
return nil, WrapErrorWithContext("parse_document", err, filename)
}
// 解析样式文件
if err := doc.parseStyles(); err != nil {
Debugf("解析样式失败,使用默认样式: %v", err)
// 如果样式解析失败,重新初始化为默认样式
doc.styleManager = style.NewStyleManager()
}
Infof("成功打开文档: %s", filename)
return doc, nil
}
@@ -1197,6 +1244,7 @@ func (d *Document) parseParagraphProperties(decoder *xml.Decoder, paragraph *Par
spacing.Before = getAttributeValue(t.Attr, "before")
spacing.After = getAttributeValue(t.Attr, "after")
spacing.Line = getAttributeValue(t.Attr, "line")
spacing.LineRule = getAttributeValue(t.Attr, "lineRule")
paragraph.Properties.Spacing = spacing
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
@@ -1295,11 +1343,32 @@ func (d *Document) parseRunProperties(decoder *xml.Decoder, run *Run) error {
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "bCs":
run.Properties.BoldCs = &BoldCs{}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "i":
run.Properties.Italic = &Italic{}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "iCs":
run.Properties.ItalicCs = &ItalicCs{}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "u":
val := getAttributeValue(t.Attr, "val")
run.Properties.Underline = &Underline{Val: val}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "strike":
run.Properties.Strike = &Strike{}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "sz":
val := getAttributeValue(t.Attr, "val")
if val != "" {
@@ -1308,6 +1377,14 @@ func (d *Document) parseRunProperties(decoder *xml.Decoder, run *Run) error {
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "szCs":
val := getAttributeValue(t.Attr, "val")
if val != "" {
run.Properties.FontSizeCs = &FontSizeCs{Val: val}
}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "color":
val := getAttributeValue(t.Attr, "val")
if val != "" {
@@ -1316,6 +1393,14 @@ func (d *Document) parseRunProperties(decoder *xml.Decoder, run *Run) error {
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "highlight":
val := getAttributeValue(t.Attr, "val")
if val != "" {
run.Properties.Highlight = &Highlight{Val: val}
}
if err := d.skipElement(decoder, t.Name.Local); err != nil {
return err
}
case "rFonts":
ascii := getAttributeValue(t.Attr, "ascii")
hAnsi := getAttributeValue(t.Attr, "hAnsi")
@@ -1793,6 +1878,25 @@ func (d *Document) serializeStyles() error {
return nil
}
// parseStyles 解析样式文件
func (d *Document) parseStyles() error {
Debugf("开始解析样式文件")
// 查找样式文件
stylesData, ok := d.parts["word/styles.xml"]
if !ok {
return WrapError("parse_styles", fmt.Errorf("样式文件不存在"))
}
// 使用样式管理器解析样式
if err := d.styleManager.LoadStylesFromDocument(stylesData); err != nil {
return WrapError("parse_styles", err)
}
Debugf("样式解析完成")
return nil
}
// ToBytes 将文档转换为字节数组
func (d *Document) ToBytes() ([]byte, error) {
buf := new(bytes.Buffer)

View File

@@ -707,9 +707,10 @@ func (te *TemplateEngine) cloneParagraphProperties(source *ParagraphProperties)
// 复制间距
if source.Spacing != nil {
props.Spacing = &Spacing{
Before: source.Spacing.Before,
After: source.Spacing.After,
Line: source.Spacing.Line,
Before: source.Spacing.Before,
After: source.Spacing.After,
Line: source.Spacing.Line,
LineRule: source.Spacing.LineRule,
}
}
@@ -785,11 +786,33 @@ func (te *TemplateEngine) cloneRunProperties(source *RunProperties) *RunProperti
props.Bold = &Bold{}
}
// 复制复杂脚本粗体
if source.BoldCs != nil {
props.BoldCs = &BoldCs{}
}
// 复制斜体
if source.Italic != nil {
props.Italic = &Italic{}
}
// 复制复杂脚本斜体
if source.ItalicCs != nil {
props.ItalicCs = &ItalicCs{}
}
// 复制下划线
if source.Underline != nil {
props.Underline = &Underline{
Val: source.Underline.Val,
}
}
// 复制删除线
if source.Strike != nil {
props.Strike = &Strike{}
}
// 复制字体大小
if source.FontSize != nil {
props.FontSize = &FontSize{
@@ -797,6 +820,13 @@ func (te *TemplateEngine) cloneRunProperties(source *RunProperties) *RunProperti
}
}
// 复制复杂脚本字体大小
if source.FontSizeCs != nil {
props.FontSizeCs = &FontSizeCs{
Val: source.FontSizeCs.Val,
}
}
// 复制颜色
if source.Color != nil {
props.Color = &Color{
@@ -804,6 +834,13 @@ func (te *TemplateEngine) cloneRunProperties(source *RunProperties) *RunProperti
}
}
// 复制背景色
if source.Highlight != nil {
props.Highlight = &Highlight{
Val: source.Highlight.Val,
}
}
// 完整复制字体族属性,包括所有字体设置
if source.FontFamily != nil {
props.FontFamily = &FontFamily{
@@ -1347,6 +1384,12 @@ func (te *TemplateEngine) RenderTemplateToDocument(templateName string, data *Te
// replaceVariablesInDocument 在文档结构中直接替换变量
func (te *TemplateEngine) replaceVariablesInDocument(doc *Document, data *TemplateData) error {
// 首先处理文档级别的循环(跨段落)
err := te.processDocumentLevelLoops(doc, data)
if err != nil {
return err
}
for _, element := range doc.Body.Elements {
switch elem := element.(type) {
case *Paragraph:
@@ -1368,6 +1411,117 @@ func (te *TemplateEngine) replaceVariablesInDocument(doc *Document, data *Templa
return nil
}
// processDocumentLevelLoops 处理文档级别的循环(跨段落)
func (te *TemplateEngine) processDocumentLevelLoops(doc *Document, data *TemplateData) error {
elements := doc.Body.Elements
newElements := make([]interface{}, 0)
i := 0
for i < len(elements) {
element := elements[i]
// 检查当前元素是否包含循环开始标记
if para, ok := element.(*Paragraph); ok {
// 获取段落的完整文本
fullText := ""
for _, run := range para.Runs {
fullText += run.Text.Content
}
// 检查是否包含循环开始标记
eachPattern := regexp.MustCompile(`\{\{#each\s+(\w+)\}\}`)
matches := eachPattern.FindStringSubmatch(fullText)
if len(matches) > 1 {
listVarName := matches[1]
// 找到循环结束位置
loopEndIndex := -1
templateElements := make([]interface{}, 0)
// 收集循环模板元素(从当前位置到结束标记)
for j := i; j < len(elements); j++ {
templateElements = append(templateElements, elements[j])
if nextPara, ok := elements[j].(*Paragraph); ok {
nextText := ""
for _, run := range nextPara.Runs {
nextText += run.Text.Content
}
if strings.Contains(nextText, "{{/each}}") {
loopEndIndex = j
break
}
}
}
if loopEndIndex >= 0 {
// 处理循环
if listData, exists := data.Lists[listVarName]; exists {
// 为每个数据项生成元素
for _, item := range listData {
if itemMap, ok := item.(map[string]interface{}); ok {
// 复制模板元素并替换变量
for _, templateElement := range templateElements {
if templatePara, ok := templateElement.(*Paragraph); ok {
newPara := te.cloneParagraph(templatePara)
// 处理段落文本
fullText := ""
for _, run := range newPara.Runs {
fullText += run.Text.Content
}
// 移除循环标记
content := fullText
content = regexp.MustCompile(`\{\{#each\s+\w+\}\}`).ReplaceAllString(content, "")
content = regexp.MustCompile(`\{\{/each\}\}`).ReplaceAllString(content, "")
// 替换变量
for key, value := range itemMap {
placeholder := fmt.Sprintf("{{%s}}", key)
content = strings.ReplaceAll(content, placeholder, te.interfaceToString(value))
}
// 如果内容不为空,创建新段落
if strings.TrimSpace(content) != "" {
newPara.Runs = []Run{{
Text: Text{Content: content},
Properties: &RunProperties{
FontFamily: &FontFamily{
ASCII: "仿宋",
HAnsi: "仿宋",
EastAsia: "仿宋",
},
Bold: &Bold{},
},
}}
newElements = append(newElements, newPara)
}
}
}
}
}
}
// 跳过循环模板元素
i = loopEndIndex + 1
continue
}
}
}
// 不是循环元素,直接添加
newElements = append(newElements, element)
i++
}
// 更新文档元素
doc.Body.Elements = newElements
return nil
}
// replaceVariablesInParagraph 在段落中替换变量(改进版本,更好地保持样式)
func (te *TemplateEngine) replaceVariablesInParagraph(para *Paragraph, data *TemplateData) error {
// 首先识别所有变量占位符的位置
@@ -1401,17 +1555,86 @@ func (te *TemplateEngine) replaceVariablesInParagraph(para *Paragraph, data *Tem
return nil
}
// 先处理循环语句(包括非表格循环)
processedText, hasLoopChanges := te.processNonTableLoops(fullText, data)
if hasLoopChanges {
// 重新构建段落
para.Runs = []Run{{
Text: Text{Content: processedText},
Properties: &RunProperties{
FontFamily: &FontFamily{
ASCII: "仿宋",
HAnsi: "仿宋",
EastAsia: "仿宋",
},
Bold: &Bold{},
},
}}
fullText = processedText
}
// 使用新的逐个变量替换方法
newRuns, hasChanges := te.replaceVariablesSequentially(runInfos, fullText, data)
newRuns, hasVarChanges := te.replaceVariablesSequentially(runInfos, fullText, data)
// 如果有变化更新段落的Run
if hasChanges {
if hasVarChanges || hasLoopChanges {
para.Runs = newRuns
}
return nil
}
// processNonTableLoops 处理非表格循环
func (te *TemplateEngine) processNonTableLoops(content string, data *TemplateData) (string, bool) {
eachPattern := regexp.MustCompile(`(?s)\{\{#each\s+(\w+)\}\}(.*?)\{\{/each\}\}`)
matches := eachPattern.FindAllStringSubmatchIndex(content, -1)
if len(matches) == 0 {
return content, false
}
var result strings.Builder
lastEnd := 0
hasChanges := false
for _, match := range matches {
// 找到变量名和块内容
fullMatch := content[match[0]:match[1]]
submatch := eachPattern.FindStringSubmatch(fullMatch)
if len(submatch) >= 3 {
listVar := submatch[1]
blockContent := submatch[2]
// 添加循环前的内容
result.WriteString(content[lastEnd:match[0]])
// 处理循环
if listData, exists := data.Lists[listVar]; exists {
for _, item := range listData {
if itemMap, ok := item.(map[string]interface{}); ok {
loopContent := blockContent
for key, value := range itemMap {
placeholder := fmt.Sprintf("{{%s}}", key)
loopContent = strings.ReplaceAll(loopContent, placeholder, te.interfaceToString(value))
}
result.WriteString(loopContent)
}
}
}
lastEnd = match[1]
hasChanges = true
}
}
// 添加剩余内容
if lastEnd < len(content) {
result.WriteString(content[lastEnd:])
}
return result.String(), hasChanges
}
// replaceVariablesSequentially 逐个替换变量,保持样式
func (te *TemplateEngine) replaceVariablesSequentially(originalRunInfos []struct {
startIndex int
@@ -1676,21 +1899,22 @@ func (te *TemplateEngine) renderTableTemplate(table *Table, data *TemplateData)
for i, row := range table.Rows {
found := false
// 重构每个单元格的文本解决跨Run的变量问题
for _, cell := range row.Cells {
for _, para := range cell.Paragraphs {
// 合并所有Run的文本
fullText := ""
for _, run := range para.Runs {
if run.Text.Content != "" {
eachPattern := regexp.MustCompile(`\{\{#each\s+(\w+)\}\}`)
matches := eachPattern.FindStringSubmatch(run.Text.Content)
if len(matches) > 1 {
templateRowIndex = i
listVarName = matches[1]
found = true
break
}
}
fullText += run.Text.Content
}
if found {
// 检查合并后的文本中是否包含循环语法
eachPattern := regexp.MustCompile(`\{\{#each\s+(\w+)\}\}`)
matches := eachPattern.FindStringSubmatch(fullText)
if len(matches) > 1 {
templateRowIndex = i
listVarName = matches[1]
found = true
break
}
}
@@ -1730,24 +1954,46 @@ func (te *TemplateEngine) renderTableTemplate(table *Table, data *TemplateData)
if itemMap, ok := item.(map[string]interface{}); ok {
for i := range newRow.Cells {
for j := range newRow.Cells[i].Paragraphs {
for k := range newRow.Cells[i].Paragraphs[j].Runs {
if newRow.Cells[i].Paragraphs[j].Runs[k].Text.Content != "" {
// 移除模板语法标记
content := newRow.Cells[i].Paragraphs[j].Runs[k].Text.Content
content = regexp.MustCompile(`\{\{#each\s+\w+\}\}`).ReplaceAllString(content, "")
content = regexp.MustCompile(`\{\{/each\}\}`).ReplaceAllString(content, "")
// 合并所有Run的文本
fullText := ""
originalRuns := newRow.Cells[i].Paragraphs[j].Runs
for _, run := range originalRuns {
fullText += run.Text.Content
}
// 替换变量
for key, value := range itemMap {
placeholder := fmt.Sprintf("{{%s}}", key)
content = strings.ReplaceAll(content, placeholder, te.interfaceToString(value))
}
// 移除模板语法标记
content := fullText
content = regexp.MustCompile(`\{\{#each\s+\w+\}\}`).ReplaceAllString(content, "")
content = regexp.MustCompile(`\{\{/each\}\}`).ReplaceAllString(content, "")
// 处理条件语句
content = te.renderLoopConditionals(content, itemMap)
// 替换变量
for key, value := range itemMap {
placeholder := fmt.Sprintf("{{%s}}", key)
content = strings.ReplaceAll(content, placeholder, te.interfaceToString(value))
}
newRow.Cells[i].Paragraphs[j].Runs[k].Text.Content = content
}
// 处理条件语句
content = te.renderLoopConditionals(content, itemMap)
// 重建Run结构保持第一个Run的样式
if len(originalRuns) > 0 {
firstRun := originalRuns[0]
newRun := te.cloneRun(&firstRun)
newRun.Text.Content = content
newRow.Cells[i].Paragraphs[j].Runs = []Run{newRun}
} else {
// 如果没有原始Run创建新的
newRow.Cells[i].Paragraphs[j].Runs = []Run{{
Text: Text{Content: content},
Properties: &RunProperties{
FontFamily: &FontFamily{
ASCII: "仿宋",
HAnsi: "仿宋",
EastAsia: "仿宋",
},
Bold: &Bold{},
},
}}
}
}
}

View File

@@ -228,7 +228,7 @@ func (sm *StyleManager) initializePredefinedStyles() {
sm.addSpecialStyles()
}
// addNormalStyle 添加普通文本样式
// addNormalStyle 添加Normal样式
func (sm *StyleManager) addNormalStyle() {
normalStyle := &Style{
Type: string(StyleTypeParagraph),
@@ -246,7 +246,7 @@ func (sm *StyleManager) addNormalStyle() {
},
RunPr: &RunProperties{
FontSize: &FontSize{
Val: "22", // 11磅字体Word中以半磅为单位
Val: "21", // 五号字体10.5磅,Word中以半磅为单位
},
FontFamily: &FontFamily{
ASCII: "Calibri",
@@ -1335,3 +1335,80 @@ func convertRunPropertiesToMap(props *RunProperties) map[string]interface{} {
return result
}
// ParseStylesFromXML 从XML数据解析样式
func (sm *StyleManager) ParseStylesFromXML(xmlData []byte) error {
type stylesXML struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/wordprocessingml/2006/main styles"`
Styles []Style `xml:"http://schemas.openxmlformats.org/wordprocessingml/2006/main style"`
}
var styles stylesXML
if err := xml.Unmarshal(xmlData, &styles); err != nil {
return fmt.Errorf("解析样式XML失败: %v", err)
}
// 清空现有样式(除非我们想要合并)
sm.styles = make(map[string]*Style)
// 添加解析的样式
for i := range styles.Styles {
sm.AddStyle(&styles.Styles[i])
}
return nil
}
// MergeStylesFromXML 从XML数据合并样式保留现有样式只添加新的
func (sm *StyleManager) MergeStylesFromXML(xmlData []byte) error {
type stylesXML struct {
XMLName xml.Name `xml:"http://schemas.openxmlformats.org/wordprocessingml/2006/main styles"`
Styles []Style `xml:"http://schemas.openxmlformats.org/wordprocessingml/2006/main style"`
}
var styles stylesXML
if err := xml.Unmarshal(xmlData, &styles); err != nil {
return fmt.Errorf("解析样式XML失败: %v", err)
}
// 只添加不存在的样式,保留现有样式
for i := range styles.Styles {
if !sm.StyleExists(styles.Styles[i].StyleID) {
sm.AddStyle(&styles.Styles[i])
}
}
return nil
}
// LoadStylesFromDocument 从现有文档加载样式,优先保留原有样式设置
func (sm *StyleManager) LoadStylesFromDocument(xmlData []byte) error {
if len(xmlData) == 0 {
// 如果没有样式数据,使用默认样式
sm.initializePredefinedStyles()
return nil
}
// 先解析现有样式
if err := sm.ParseStylesFromXML(xmlData); err != nil {
// 如果解析失败,使用默认样式
sm.initializePredefinedStyles()
return fmt.Errorf("解析现有样式失败,使用默认样式: %v", err)
}
// 确保基本样式存在,如果不存在则添加
if !sm.StyleExists("Normal") {
sm.addNormalStyle()
}
// 确保基本标题样式存在
headingStyles := []string{"Heading1", "Heading2", "Heading3", "Heading4", "Heading5", "Heading6", "Heading7", "Heading8", "Heading9"}
for _, styleID := range headingStyles {
if !sm.StyleExists(styleID) {
sm.addHeadingStyles()
break
}
}
return nil
}

Submodule wordZero.wiki updated: 3b91af7d88...6dc76fa50b