fix(engine): 修复 DOCX 公式因 undefined 虚根在 Word 中丢失的缺陷
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -125,5 +125,21 @@ Test Files 1 passed (1)
|
||||
|
||||
---
|
||||
|
||||
## 八、 测试结论与后续建议 (Conclusion & Next Steps)
|
||||
- **测试结论**:本次针对用户反馈的“Mermaid 图未正确显示”、“公式未正确显示”、“排版不美观”三大问题及实测中的“Canvas Tainted 安全异常”进行了彻底的工程重构与加固。引入 KaTeX + mml2omml 打造了真正的微软 Word 原生数学公式生成体系;通过浏览器 Canvas 防污染超采样实现了 Mermaid 矢量图的高清图像光栅化导出;重构了卷首公文签署、医疗标准三线表与字体行距防孤行系统,使导出的 DOCX 达到出版级专业公文标准,达到上线交付标准。
|
||||
## 八、 突发缺陷排除记录 (Word 公式消失与 <undefined> 标签根除)
|
||||
- **缺陷现象**:导出的 DOCX 中展示公式与行内公式均未渲染显示,在 Word 中直接消失不见。
|
||||
- **根因分析 (Root Cause)**:
|
||||
1. **docx 虚根导致的 `<undefined>` 破坏性标签**:`docx` 库的 `ImportedXmlComponent.fromXmlString(omml)` 在反序列化单一根节点(`<m:oMath>`)时,生成了一个顶层包装组件,其属性为 `rootKey: undefined`,真实的 `m:oMath` 节点被放在了 `comp.root[0]` 中;
|
||||
2. **OpenXML 破坏性序列化**:当 `Packer.toBuffer()` 编译时,直接将带有 `rootKey: undefined` 的顶层组件序列化为了 `<undefined><m:oMath>...</m:oMath></undefined>`;
|
||||
3. **Word Schema 严格校验与丢弃**:Microsoft Word、WPS Office 等软件在加载 `word/document.xml` 时,执行 OpenXML Schema 严格校验,识别到 `<undefined>` 属于非法标签,直接静默剔除整个标签及其内部的所有数学公式;
|
||||
4. **Body 根级裸露问题**:在 OpenXML 规范中,`w:body` 的直接子节点只允许 `<w:p>`、`<w:tbl>` 等,展示公式未包裹在独立的段落 `<w:p>` 中,导致规范违规。
|
||||
- **修复方案与实测验证**:
|
||||
1. **精准解包真实 OMML 节点**:重构 `latexToOmml`,在由 `fromXmlString` 生成组件后,自动解包并提取其真实根节点 `const actualNode = imported.root?.[0] || imported`,其 `rootKey` 严格为 `'m:oMath'`,彻底杜绝任何 `<undefined>` 标签的生成;
|
||||
2. **规范段落包裹 (`<w:p>`)**:在 `case 'math':` 中,将 `ommlComponent` 严格包裹于设置了居中对齐与段前段后间距的 `new Paragraph({ alignment: AlignmentType.CENTER, children: [ommlComponent] })` 中;
|
||||
3. **集成自动化 XML 结构断言防线**:在 `renderers.test.ts` 中引入 `JSZip` 解压编译出的 DOCX 文件,针对底层 `word/document.xml` 进行严格断言:
|
||||
- 断言 `!docXml.includes('<undefined>')`(100% 杜绝破坏性标签);
|
||||
- 断言 `docXml.includes('<m:oMath')`(100% 确保原生微软数学矢量节点存在)。
|
||||
|
||||
---
|
||||
|
||||
## 九、 测试结论与后续建议 (Conclusion & Next Steps)
|
||||
- **测试结论**:本次针对用户反馈的“Mermaid 图未正确显示”、“公式未正确显示/直接消失”、“排版不美观”三大问题及实测中的“Canvas Tainted 安全异常”、“OpenXML <undefined> 破坏性标签”进行了彻底的工程重构与深度加固。引入 KaTeX + mml2omml 打造了真正的微软 Word 原生数学公式生成体系,并彻底消除了 `<undefined>` 标签破坏;通过浏览器 Canvas 防污染超采样实现了 Mermaid 矢量图的高清图像光栅化导出;重构了卷首公文签署、医疗标准三线表与字体行距防孤行系统,使导出的 DOCX 达到出版级专业公文标准,全量测试 100% 通过。
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Trash2,
|
||||
Shield,
|
||||
Layers,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface DeckDrawerProps {
|
||||
@@ -56,7 +57,29 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderBadge = (state: DocLifecycleState, isClaimed: boolean) => {
|
||||
const renderBadge = (state: DocLifecycleState, isClaimed: boolean, isTemplateOnly?: boolean) => {
|
||||
if (isTemplateOnly || state === 'template') {
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
color: '#0284c7',
|
||||
background: '#e0f2fe',
|
||||
border: '1px solid #bae6fd',
|
||||
padding: '2px 8px',
|
||||
borderRadius: 'var(--r-pill)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '3px',
|
||||
}}
|
||||
title="该文档为项目标准预置模板副本,点击可全景查阅大纲与章节规范"
|
||||
>
|
||||
<FileText size={11} />
|
||||
<span>📄 初始模板</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
switch (state) {
|
||||
case 'approved':
|
||||
return (
|
||||
@@ -388,7 +411,7 @@ export const DeckDrawer: React.FC<DeckDrawerProps> = ({
|
||||
<span style={{ fontSize: '12.5px', fontWeight: 600, color: 'var(--text-strong)' }}>
|
||||
{doc.title}
|
||||
</span>
|
||||
{renderBadge(doc.lifecycleState, doc.isClaimed)}
|
||||
{renderBadge(doc.lifecycleState, doc.isClaimed, doc.isTemplateOnly)}
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: '11px', color: 'var(--text-muted)', display: 'flex', gap: '12px' }}>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { DeliverableItem } from '../../types/workbench';
|
||||
|
||||
test('isTemplateOnly 交付物应正确识别为初始模板展示态', () => {
|
||||
const item: DeliverableItem = {
|
||||
docId: 'doc-p101-intake',
|
||||
projectId: 101,
|
||||
taskKey: 'intake',
|
||||
title: '用户初始需求收集表',
|
||||
stage: 'V0.1',
|
||||
category: 'workflow',
|
||||
creatorId: 1,
|
||||
creatorName: '张工',
|
||||
isFixedWorkflow: true,
|
||||
isTemplateOnly: true,
|
||||
lifecycleState: 'template',
|
||||
isClaimed: false,
|
||||
author: '张工',
|
||||
reviewer: '李主管',
|
||||
approver: '王总监',
|
||||
updatedAt: '刚刚',
|
||||
};
|
||||
|
||||
assert.strictEqual(item.isTemplateOnly, true);
|
||||
const badgeLabel = item.isTemplateOnly ? '📄 初始模板' : item.lifecycleState;
|
||||
assert.strictEqual(badgeLabel, '📄 初始模板');
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from './docxRenderer';
|
||||
import { buildXlsxWorkbook, exportDocumentToXlsx } from './xlsxRenderer';
|
||||
import { Packer, ImportedXmlComponent } from 'docx';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const SAMPLE_MD = `# 4K超高清荧光内窥镜系统立项与可行性报告
|
||||
|
||||
@@ -59,11 +60,13 @@ test('DOCX 渲染器行内文本与公式解析测试 (parseInlineContent)', ()
|
||||
|
||||
test('DOCX 渲染器 LaTeX 原生 OMML 转换引擎测试 (latexToOmml)', () => {
|
||||
const latex = 'SNR = 10 \\cdot \\log_{10}\\left(\\frac{P_{signal}}{P_{noise}}\\right)';
|
||||
const displayComp = latexToOmml(latex, true);
|
||||
assert.ok(displayComp, '展示级公式应当成功转换为 m:oMathPara 组件');
|
||||
const comp = latexToOmml(latex);
|
||||
assert.ok(comp, '公式应当成功转换为 m:oMath 组件');
|
||||
assert.equal((comp as any).rootKey, 'm:oMath', 'rootKey 必须为 m:oMath,绝不能为 undefined');
|
||||
|
||||
const inlineComp = latexToOmml('\\sigma_{ambient}', false);
|
||||
const inlineComp = latexToOmml('\\sigma_{ambient}');
|
||||
assert.ok(inlineComp, '行内公式应当成功转换为 m:oMath 组件');
|
||||
assert.equal((inlineComp as any).rootKey, 'm:oMath');
|
||||
});
|
||||
|
||||
test('DOCX 渲染器 Mermaid 语法净化与容错测试 (sanitizeMermaidCode)', () => {
|
||||
@@ -100,6 +103,12 @@ test('DOCX 渲染器全量编译测试 (buildDocxModel & exportDocumentToDocx)',
|
||||
const buffer = await Packer.toBuffer(doc);
|
||||
assert.ok(buffer.length > 5000, `生成的 DOCX Buffer 应当大于 5KB,实际为 ${buffer.length} 字节`);
|
||||
|
||||
// 关键回归防线:解压验证 word/document.xml 中杜绝 <undefined> 且包含合法的 <m:oMath>
|
||||
const zip = await JSZip.loadAsync(buffer);
|
||||
const docXml = await zip.file('word/document.xml')!.async('text');
|
||||
assert.ok(!docXml.includes('<undefined>'), 'Word document.xml 绝对不得包含 <undefined> 破坏性标签');
|
||||
assert.ok(docXml.includes('<m:oMath'), 'Word document.xml 必须包含合法的微软 Office Math (<m:oMath>) 节点');
|
||||
|
||||
// 验证 exportDocumentToDocx 输出 Blob 正常
|
||||
const blob = await exportDocumentToDocx(ast);
|
||||
assert.equal(blob.type, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
||||
|
||||
Reference in New Issue
Block a user