feat: office-view 支持内嵌字体;数学公式;初步支持 textbox;修复 highlight 和加粗渲染不正确问题 (#6459)

* 初步支持 textbox

* 开始实现内嵌字体

* 支持内嵌字体

* 避免 style 导致可能的 xss

* 支持公式渲染

* 修复 highlight 和加粗渲染不正确问题

* 删掉不用的文件

* 修复编译报错
This commit is contained in:
吴多益 2023-03-23 20:53:51 +08:00 committed by GitHub
parent 8314ba4909
commit 3941b21e7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 7801 additions and 3677 deletions

View File

@ -5,3 +5,4 @@ npm/
.git/
.github/
.vscode/
.rollup.cache

View File

@ -18,14 +18,10 @@ order: 23
- 列表
- 注音
- 链接
- 文本框
- 数学公式(依赖 MathML需要比较新的浏览器或者试试 [polyfill](https://github.com/w3c/mathml-polyfills)
不支持的功能:
- 分页符
- 形状
- 艺术字
- 域
- 对象
不支持的功能:分页符、形状、艺术字、域、对象、目录
## 基本用法
@ -34,7 +30,8 @@ order: 23
"type": "office-viewer",
"src": "/examples/static/simple.docx",
"wordOptions": {
"padding": "8px"
"padding": "8px",
"ignoreWidth": false
}
}
```
@ -50,25 +47,37 @@ order: 23
"type": "office-viewer",
"wordOptions": {
"padding": "8px",
"classPrefix": "docx"
"ignoreWidth": false
}
}
```
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | --------- | ------------- | ------------------------------------------ |
| classPrefix | `string` | 'docx-viewer' | 渲染的 class 类前缀 |
| bulletUseFont | `boolean` | true | 列表使用字体渲染,请参考下面的乱码说明 |
| fontMapping | `object` | | 字体映射,是个键值对,用于替换文档中的字体 |
| forceLineHeight | `string` | | 设置段落行高,忽略文档中的设置 |
| padding | `string` | | 设置页面间距,忽略文档中的设置 |
| enableReplaceText | `boolean` | true | 是否开启变量替换功能 |
| 属性名 | 类型 | 默认值 | 说明 |
| ----------------- | --------- | ------------- | ---------------------------------------------------------- |
| classPrefix | `string` | 'docx-viewer' | 渲染的 class 类前缀 |
| ignoreWidth | `boolean` | false | 忽略文档里的宽度设置,用于更好嵌入到页面里,但会减低还原度 |
| padding | `string` | | 设置页面间距,忽略文档中的设置 |
| bulletUseFont | `boolean` | true | 列表使用字体渲染,请参考下面的乱码说明 |
| fontMapping | `object` | | 字体映射,是个键值对,用于替换文档中的字体 |
| forceLineHeight | `string` | | 设置段落行高,忽略文档中的设置 |
| enableReplaceText | `boolean` | true | 是否开启变量替换功能 |
### 关于渲染效果差异
目前的实现难以保证和本地 Word 渲染完全一致,会遇到以下问题:
1. 字体大小不一致
1. 单元格宽度不一致,表格完全依赖浏览器渲染
1. 分页显示,目前的渲染不会分页,而是内容有多长就有多高
1. 分栏显示,这个是因为没有分页导致的,不限制高度没法分栏
如果追求完整效果打印,目前只能使用下载文件的方式用本地 Word 进行打印。
## 列表符号出现乱码问题
默认情况下列表左侧的符号使用字体渲染,这样能做到最接近 Word 渲染效果,但如果用户的系统中没有这些字体就会显示乱码,为了解决这个问题需要手动在 amis 渲染的页面里导入对应的字体,比如
```
```html
<style>
@font-face {
font-family: Wingdings;

View File

@ -6,8 +6,13 @@ docx 渲染器,原理是将 docx 里的 xml 格式转成 html
相对于 Canvas 渲染,这个实现方案比较简单,最终页面也可以很方便复制,但无法保证和原始 docx 文件展现一致,因为有部分功能难以在 HTML 中实现,比如图文环绕效果。
## 不支持的功能
## 已知不支持的功能
- 分页符
- 形状
- 艺术字
- 域
- 对象
- wmf需要使用 https://github.com/SheetJS/js-wmf
## 参考资料

View File

@ -1,6 +1,6 @@
import {createWord} from './EmptyWord';
import {mergeRun} from '../src/util/mergeRun';
import {parseXML, buildXML} from '../src/util/xml';
import {parseXML} from '../src/util/xml';
test('proofErr', async () => {
const xmlDoc = parseXML(

View File

@ -0,0 +1,3 @@
来自 https://github.com/science-periodicals/dedocx
似乎很关注公式及引用相关的

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
text inline

View File

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
<w:body>
<w:p w:rsidR="00A97433" w:rsidRPr="003741F5" w:rsidRDefault="00A97433" w:rsidP="00A97433">
<w:pPr>
<w:pStyle w:val="EndNoteBibliography"/>
</w:pPr>
<w:r w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:t xml:space="preserve">US is about 400,000 and over two million </w:t>
</w:r>
<w:r w:rsidR="007714DF" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:t xml:space="preserve">people </w:t>
</w:r>
<w:r w:rsidR="00F21877" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:t xml:space="preserve">are </w:t>
</w:r>
<w:r w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:instrText xml:space="preserve"> ADDIN EN.REFLIST </w:instrText>
</w:r>
<w:r w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:bookmarkStart w:id="0" w:name="_ENREF_1"/>
<w:r w:rsidRPr="003741F5">
<w:t>1.</w:t>
</w:r>
<w:r w:rsidRPr="003741F5">
<w:tab/>
<w:t>Orton SM, Herrera BM, Yee IM, et al. Sex ratio of multiple sclerosis in Canada: a longitudinal study. Lancet Neurol 2006;5:932-6.</w:t>
</w:r>
<w:bookmarkEnd w:id="0"/>
</w:p>
<w:p w:rsidR="00A97433" w:rsidRPr="003741F5" w:rsidRDefault="00A97433" w:rsidP="00A97433">
<w:pPr>
<w:pStyle w:val="EndNoteBibliography"/>
</w:pPr>
<w:bookmarkStart w:id="1" w:name="_ENREF_2"/>
<w:r w:rsidRPr="003741F5">
<w:t>2.</w:t>
</w:r>
<w:r w:rsidRPr="003741F5">
<w:tab/>
<w:t>Kister I, Chamot E, Salter AR, Cutter GR, Bacon TE, Herbert J. Disability in multiple sclerosis: a reference for patients and clinicians. Neurology 2013;80:1018-24.</w:t>
</w:r>
<w:bookmarkEnd w:id="1"/>
</w:p>
<w:p w:rsidR="00A97433" w:rsidRPr="003741F5" w:rsidRDefault="00A97433" w:rsidP="00A97433">
<w:pPr>
<w:pStyle w:val="EndNoteBibliography"/>
</w:pPr>
<w:bookmarkStart w:id="2" w:name="_ENREF_3"/>
<w:r w:rsidRPr="003741F5">
<w:t>3.</w:t>
</w:r>
<w:r w:rsidRPr="003741F5">
<w:tab/>
<w:t>Mayr WT, Pittock SJ, McClelland RL, Jorgensen NW, Noseworthy JH, Rodriguez M. Incidence and prevalence of multiple sclerosis in Olmsted County, Minnesota, 1985-2000. Neurology 2003;61:1373-7.</w:t>
</w:r>
<w:bookmarkEnd w:id="2"/>
</w:p>
<w:p w:rsidR="00A97433" w:rsidRPr="00E63F36" w:rsidRDefault="00A97433" w:rsidP="00A97433">
<w:pPr>
<w:pStyle w:val="Default"/>
<w:spacing w:line="480" w:lineRule="auto"/>
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
</w:pPr>
<w:r w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="000E1EAF" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:t xml:space="preserve"> expected </w:t>
</w:r>
<w:r w:rsidR="00B22C62" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman"/>
</w:rPr>
<w:t>increase in the number of cases in future</w:t>
</w:r>
</w:p>
</w:body>
</w:document>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
<w:body>
<w:p w:rsidR="00D26E0C" w:rsidRPr="00D26E0C" w:rsidRDefault="00D26E0C" w:rsidP="00D26E0C">
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:instrText>HYPERLINK \l "_ENREF_95" \o "Kappos, 2010 #67"</w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="004C1AB8" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:t xml:space="preserve"> </w:t>
</w:r>
</w:p>
</w:body>
</w:document>

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
<w:body>
<w:p w:rsidR="00D26E0C" w:rsidRPr="00D26E0C" w:rsidRDefault="00D26E0C" w:rsidP="00D26E0C">
<w:proofErr w:type="gramEnd"/>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:instrText>HYPERLINK \l "_ENREF_95" \o "Kappos, 2010 #67"</w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="00C76D57" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:fldChar w:fldCharType="begin">
<w:fldData xml:space="preserve">XXX</w:fldData>
</w:fldChar>
</w:r>
<w:r w:rsidR="003741F5">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:instrText xml:space="preserve"> ADDIN EN.CITE </w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:fldChar w:fldCharType="begin">
<w:fldData xml:space="preserve">XXX</w:fldData>
</w:fldChar>
</w:r>
<w:r w:rsidR="003741F5">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:instrText xml:space="preserve"> ADDIN EN.CITE.DATA </w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
</w:r>
<w:r w:rsidR="00C76D57">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="00C76D57" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
</w:r>
<w:r w:rsidR="00C76D57" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="003741F5" w:rsidRPr="00FA3B8B">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:noProof/>
<w:color w:val="auto"/>
<w:vertAlign w:val="superscript"/>
</w:rPr>
<w:t>95-98</w:t>
</w:r>
<w:r w:rsidR="00C76D57" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="004C1AB8" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:t xml:space="preserve"> </w:t>
</w:r>
</w:p>
</w:body>
</w:document>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
<w:body>
<w:p w:rsidR="00D26E0C" w:rsidRPr="00D26E0C" w:rsidRDefault="00D26E0C" w:rsidP="00D26E0C">
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:instrText>HYPERLINK \l "_ENREF_95" \o "Kappos, 2010 #67"</w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:instrText>HYPERLINK \l "_ENREF_2" \o "Kappos, 2010 #67"</w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="004C1AB8" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:t xml:space="preserve"> </w:t>
</w:r>
</w:p>
</w:body>
</w:document>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 wp14">
<w:body>
<w:p w:rsidR="00D26E0C" w:rsidRPr="00D26E0C" w:rsidRDefault="00D26E0C" w:rsidP="00D26E0C">
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:instrText>HYPERLINK \l "_ENREF_95" \o "Kappos, 2010 #67"</w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="begin"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:instrText>HYPERLINK \l "_ENREF_2" \o "Kappos, 2010 #67"</w:instrText>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="separate"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="00C76D57">
<w:fldChar w:fldCharType="end"/>
</w:r>
<w:r w:rsidR="004C1AB8" w:rsidRPr="00E63F36">
<w:rPr>
<w:rFonts w:ascii="Times New Roman" w:hAnsi="Times New Roman" w:cs="Times New Roman"/>
<w:color w:val="auto"/>
</w:rPr>
<w:t xml:space="preserve"> </w:t>
</w:r>
</w:p>
</w:body>
</w:document>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -11,11 +11,17 @@ const testDir = '__tests__/docx';
const fileLists = {
simple: [
'helloworld.docx',
'image.docx',
'list.docx',
'tableborder.docx',
'tablestyle.docx',
'pinyin.docx'
'pinyin.docx',
'em.docx',
'w.docx',
'textbox.docx',
'embed-font.docx',
'math.docx',
'highlight.docx'
],
docx4j: [
'ArialUnicodeMS.docx',

View File

@ -6,12 +6,12 @@ body {
@font-face {
font-family: Wingdings;
src: url(/examples/static/font/wingding.ttf);
src: url(/examples/static/font/wingding.ttf) format('truetype');
}
@font-face {
font-family: Symbol;
src: url(/examples/static/font/symbol.ttf);
src: url(/examples/static/font/symbol.ttf) format('truetype');
}
/** 参考 bulma 的命名 */

View File

@ -49,6 +49,7 @@
"@types/jest": "^28.1.0",
"amis-formula": "^2.7.2",
"jest": "^29.0.3",
"fast-xml-parser": "4.1.3",
"ts-jest": "^29.0.2",
"ts-loader": "^9.2.3",
"ts-node": "^10.4.0",

View File

@ -63,6 +63,47 @@ export function getAttrBoolean(
return normalizeBoolean(element.getAttribute(attr), defaultValue);
}
/**
*
*
* @param attr
* @param defaultValue
* @returns
*/
export function getAttrNumber(
element: Element,
attr: string,
defaultValue: number = 0
) {
const value = element.getAttribute(attr);
if (value) {
return parseInt(value, 10);
} else {
return defaultValue;
}
}
/**
*
* http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/ST_Percentage.html
* https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_ST_Percentage_topic_ID0EY3XNB.html#topic_ID0EY3XNB
*
* @returns 0-1
*/
export function getAttrPercentage(element: Element, attr: string) {
const value = element.getAttribute(attr);
if (value) {
const num = parseInt(value, 10);
return num / 100000;
}
return 1;
}
/**
* hex
*/
export function getValHex(element: Element) {
return parseInt(getVal(element) || '0', 16);
}

View File

@ -1,3 +1,4 @@
import {FontTable} from './openxml/word/FontTable';
/**
* word
*/
@ -10,7 +11,7 @@ import {parseTheme, Theme} from './openxml/Theme';
import renderDocument from './render/renderDocument';
import {blobToDataURL, downloadBlob} from './util/blob';
import {Numbering} from './openxml/word/numbering/Numbering';
import {appendChild} from './util/dom';
import {appendChild, createElement} from './util/dom';
import {renderStyle} from './render/renderStyle';
import {mergeRun} from './util/mergeRun';
import {WDocument} from './openxml/word/WDocument';
@ -19,6 +20,8 @@ import {updateVariableText} from './render/renderRun';
import ZipPackageParser from './package/ZipPackageParser';
import {buildXML} from './util/xml';
import {Paragraph} from './openxml/word/Paragraph';
import {deobfuscate} from './openxml/word/Font';
import {renderFont} from './render/renderFont';
/**
*
@ -29,9 +32,6 @@ export interface WordRenderOptions {
*/
classPrefix: string;
/** 图片是否使用 data url */
imageDataURL: boolean;
/**
* 使 Windings
*/
@ -86,18 +86,23 @@ export interface WordRenderOptions {
* 使
*/
forceLineHeight?: string;
/**
*
*/
printWaitTime?: number;
}
const defaultRenderOptions: WordRenderOptions = {
imageDataURL: false,
classPrefix: 'docx-viewer',
inWrap: true,
bulletUseFont: true,
ignoreHeight: true,
ignoreWidth: true,
ignoreWidth: false,
minLineHeight: 1.0,
enableVar: false,
debug: false
debug: false,
printWaitTime: 100
};
export default class Word {
@ -138,8 +143,21 @@ export default class Word {
renderOptions: WordRenderOptions;
/**
*
*/
relationships: Record<string, Relationship>;
/**
*
*/
documentRels: Record<string, Relationship>;
/**
*
*/
fontTableRels: Record<string, Relationship>;
/**
* css 使
*/
@ -150,6 +168,11 @@ export default class Word {
*/
styleIdNum: number = 0;
/**
*
*/
fontTable?: FontTable;
/**
*
*/
@ -191,10 +214,12 @@ export default class Word {
// 这个必须在最前面,因为后面很多依赖它来查找文件的
this.initContentType();
// relation 需要排第二
this.initRelation();
this.initTheme();
this.initFontTable();
this.initStyle();
this.initRelation();
this.initNumbering();
this.inited = true;
@ -223,6 +248,20 @@ export default class Word {
}
}
/**
*
*/
initFontTable() {
for (const override of this.conentTypes.overrides) {
if (override.partName.startsWith('/word/fontTable.xml')) {
this.fontTable = FontTable.fromXML(
this,
this.parser.getXML('/word/fontTable.xml')
);
}
}
}
/**
*
*/
@ -232,6 +271,8 @@ export default class Word {
rels = parseRelationships(this.parser.getXML('/_rels/.rels'), 'root');
}
this.relationships = rels;
let documentRels = {};
if (this.parser.fileExists('/word/_rels/document.xml.rels')) {
documentRels = parseRelationships(
@ -239,7 +280,16 @@ export default class Word {
'word'
);
}
this.relationships = {...rels, ...documentRels};
this.documentRels = documentRels;
let fontTableRels = {};
if (this.parser.fileExists('/word/_rels/fontTable.xml.rels')) {
fontTableRels = parseRelationships(
this.parser.getXML('/word/_rels/fontTable.xml.rels'),
'word'
);
}
this.fontTableRels = fontTableRels;
}
/**
@ -263,15 +313,35 @@ export default class Word {
}
/**
* id
*
*/
getRelationship(id?: string) {
if (id) {
if (id && this.relationships) {
return this.relationships[id];
}
return null;
}
/**
*
*/
getDocumentRels(id?: string) {
if (id && this.documentRels) {
return this.documentRels[id];
}
return null;
}
/**
*
*/
getFontTableRels(id?: string) {
if (id && this.fontTableRels) {
return this.fontTableRels[id];
}
return null;
}
/**
*
*/
@ -288,7 +358,7 @@ export default class Word {
/**
*
*/
loadImage(relation: Relationship): Promise<string> | null {
loadImage(relation: Relationship): string | null {
let path = relation.target;
if (relation.part === 'word') {
path = 'word/' + path;
@ -296,13 +366,26 @@ export default class Word {
const data = this.parser.getFileByType(path, 'blob');
if (data) {
if (this.renderOptions.imageDataURL) {
return blobToDataURL(data as Blob);
} else {
return new Promise<string>((resolve, reject) => {
resolve(URL.createObjectURL(data as Blob));
});
}
return URL.createObjectURL(data as Blob);
}
return null;
}
loadFont(rId: string, key: string) {
const relation = this.getFontTableRels(rId);
if (!relation) {
return null;
}
let path = relation.target;
if (relation.part === 'word') {
path = 'word/' + path;
}
const data = this.parser.getFileByType(path, 'uint8array') as Uint8Array;
if (data) {
return URL.createObjectURL(new Blob([deobfuscate(data, key)]));
}
return null;
@ -344,9 +427,9 @@ export default class Word {
/**
*
*/
appendStyle(style: string) {
const styleElement = document.createElement('style');
styleElement.innerHTML = style;
appendStyle(style: string = '') {
const styleElement = createElement('style');
styleElement.textContent = style;
this.rootElement.appendChild(styleElement);
}
@ -377,10 +460,18 @@ export default class Word {
}
/**
* css 便
*
*/
getThemeColor(name: string) {
return `var(--docx-${this.id}-theme-${name}-color)`;
if (this.themes && this.themes.length > 0) {
const theme = this.themes[0];
const color = theme.themeElements?.clrScheme?.colors?.[name];
if (color) {
return color;
}
}
return '';
}
addClass(element: HTMLElement, className: string) {
@ -436,7 +527,7 @@ export default class Word {
iframe.focus();
iframe.contentWindow?.print();
iframe.parentNode?.removeChild(iframe);
}, 100);
}, this.renderOptions.printWaitTime || 100); // 需要等一下图片渲染
window.focus();
}
@ -472,6 +563,7 @@ export default class Word {
}
appendChild(root, renderStyle(this));
appendChild(root, renderFont(this.fontTable));
appendChild(root, documentElement);
}
}

View File

@ -2,14 +2,17 @@
* Style
*/
import {parseTcPr} from '../parse/parseTcPr';
import {getVal} from '../OpenXML';
import Word from '../Word';
import {ST_StyleType, ST_TblStyleOverrideType} from './Types';
import {Paragraph, ParagraphPr} from './word/Paragraph';
import {Run, RunPr} from './word/Run';
import {Table, TablePr} from './word/Table';
import {Tc, TcPr} from './word/table/Tc';
import {TablePr} from './word/Table';
import {TcPr} from './word/table/Tc';
import {Tr, TrPr} from './word/table/Tr';
import {parseTablePr} from '../parse/parseTablePr';
import {parseTrPr} from '../parse/parseTrPr';
export interface CSSStyle {
[key: string]: string;
@ -94,15 +97,15 @@ function parseTblStylePr(word: Word, element: Element) {
break;
case 'w:tblPr':
style.tblPr = Table.parseTablePr(word, child);
style.tblPr = parseTablePr(word, child);
break;
case 'w:tcPr':
style.tcPr = Tc.parseTcPr(word, child);
style.tcPr = parseTcPr(word, child);
break;
case 'w:trPr':
style.trPr = Tr.parseTrPr(word, child);
style.trPr = parseTrPr(word, child);
break;
}
}

View File

@ -2,6 +2,8 @@
* 14.2.7 Theme Part
*/
import {getAttrNumber, getAttrPercentage, getVal} from '../OpenXML';
// http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/clrScheme.html
class ClrScheme {
name?: string;
@ -21,8 +23,26 @@ function parseClrScheme(doc: Element | null): ClrScheme {
const clrName = clr.nodeName.replace('a:', '');
if (clrName === 'sysClr') {
scheme.colors[colorName] = clr.getAttribute('lastClr') || '';
} else if (clrName === 'srgbClr') {
scheme.colors[colorName] = '#' + clr.getAttribute('val') || '';
} else if (clrName === 'scrgbClr') {
// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_scrgbClr_topic_ID0EOOPJB.html
// 没测过
const r = getAttrPercentage(child, 'r') * 256;
const g = getAttrPercentage(child, 'g') * 256;
const b = getAttrPercentage(child, 'b') * 256;
scheme.colors[colorName] = `rgb(${r}, ${g}, ${b})`;
} else if (clrName === 'hslClr') {
// https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_hslClr_topic_ID0EQ5FJB.html
// 没测过
const h = getAttrNumber(child, 'hue') / 60000;
const s = getAttrPercentage(child, 'sat') * 100;
const l = getAttrPercentage(child, 'lum') * 100;
scheme.colors[colorName] = `hsl(${h}, ${s}%, ${l}%)`;
} else if (clrName === 'prstClr') {
scheme.colors[colorName] = getVal(child);
} else {
scheme.colors[colorName] = clr.getAttribute('val') || '';
console.error('unknown clr name', clrName);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
import Word from '../../Word';
export class OMath {
element: Element;
static fromXML(word: Word, element: Element): OMath {
const math = new OMath();
math.element = element;
return math;
}
}

View File

@ -0,0 +1 @@
OMML2MML.XSL 文件来自安装 Windows 版本 Word 后 `C:\Program Files\Microsoft Office\root\Office16` 目录

View File

@ -0,0 +1,11 @@
import {xsl} from './xsl';
/**
* officel xml mathml
*/
export function convertOOXML(element: Element) {
const xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsl);
const fragment = xsltProcessor.transformToFragment(element, document);
return fragment;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
import {parseTable} from '../../parse/parseTable';
import Word from '../../Word';
import {Paragraph} from './Paragraph';
import {Section, SectionChild, SectionPr} from './Section';
import {Table} from './Table';
/**
* body
@ -48,7 +48,7 @@ export class Body {
break;
case 'w:tbl':
const table = Table.fromXML(word, child);
const table = parseTable(word, child);
body.addChild(table);
break;

View File

@ -9,7 +9,7 @@ export class Break {
/**
*
*/
type: ST_BrType = ST_BrType.textWrapping;
type: ST_BrType = 'textWrapping';
clear?: ST_BrClear;
static fromXML(word: Word, element: Element): Break {

View File

@ -0,0 +1,78 @@
/**
*
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/Font%20Embedding.html
*/
import {getVal} from '../../OpenXML';
import Word from '../../Word';
/**
* docxjs 17.8.1
*/
export function deobfuscate(data: Uint8Array, guidKey: string): Uint8Array {
const len = 16;
const trimmed = guidKey.replace(/{|}|-/g, '');
const numbers = new Array(len);
for (let i = 0; i < len; i++)
numbers[len - i - 1] = parseInt(trimmed.substr(i * 2, 2), 16);
for (let i = 0; i < 32; i++) data[i] = data[i] ^ numbers[i % len];
return data;
}
export class Font {
name: string;
family: string;
altName?: string;
// 字体文件地址
url?: string;
static fromXML(word: Word, element: Element): Font {
const font = new Font();
font.name = element.getAttribute('w:name') || '';
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:family':
font.family = getVal(child);
break;
case 'w:altName':
font.altName = getVal(child);
break;
case 'w:panose1':
// 不知道是啥
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/panose1.html
break;
case 'w:charset':
case 'w:sig':
case 'w:pitch':
// 用不上
break;
case 'w:embedRegular':
case 'w:embedBold':
case 'w:embedItalic':
case 'w:embedBoldItalic':
case 'w:embedSystemFonts':
case 'w:embedTrueTypeFonts':
const id = child.getAttribute('r:id') || '';
const fontKey = child.getAttribute('w:fontKey') || '';
const fontURL = word.loadFont(id, fontKey);
if (fontURL) {
font.url = fontURL;
}
break;
default:
console.warn('parse Font: Unknown key', tagName, child);
}
}
return font;
}
}

View File

@ -0,0 +1,20 @@
/**
*
*/
import Word from '../../Word';
import {Font} from './Font';
export class FontTable {
fonts: Font[] = [];
static fromXML(word: Word, doc: Document): FontTable {
const fonts = Array.from(doc.getElementsByTagName('w:font'));
const fontTable = new FontTable();
for (const child of fonts) {
const font = Font.fromXML(word, child);
fontTable.fonts.push(font);
}
return fontTable;
}
}

View File

@ -20,7 +20,7 @@ export class Hyperlink {
const rId = element.getAttribute('r:id');
if (rId) {
const rel = word.getRelationship(rId);
const rel = word.getDocumentRels(rId);
if (rel) {
hyperlink.relation = rel;
}

View File

@ -13,6 +13,7 @@ import {Run, RunPr} from './Run';
import {Tab} from './Tab';
import {SmartTag} from './SmartTag';
import {FldSimple} from './FldSimple';
import {OMath} from '../math/OMath';
/**
* CSS CSS
@ -33,7 +34,8 @@ export type ParagraphChild =
| BookmarkStart
| Hyperlink
| SmartTag
| FldSimple;
| FldSimple
| OMath;
// | SymbolRun
// | PageBreak
// | ColumnBreak
@ -142,6 +144,10 @@ export class Paragraph {
paragraph.fldSimples.push(FldSimple.fromXML(word, child));
break;
case 'm:oMathPara':
paragraph.addChild(OMath.fromXML(word, child));
break;
default:
console.warn('parse Paragraph: Unknown key', tagName, child);
}

View File

@ -1,8 +1,7 @@
import {Relationship} from '../../parse/parseRelationship';
import Word from '../../Word';
export class Pict {
imagedata?: Relationship;
src?: string | null;
static fromXML(word: Word, element: Element): Pict | null {
const pict = new Pict();
@ -11,9 +10,9 @@ export class Pict {
if (imagedataElement) {
const rId = imagedataElement.getAttribute('r:id') || '';
const rel = word.getRelationship(rId);
const rel = word.getDocumentRels(rId);
if (rel) {
pict.imagedata = rel;
pict.src = word.loadImage(rel);
}
}

View File

@ -103,6 +103,13 @@ export class Run {
run.addChild(Sym.parseXML(child));
break;
case 'mc:AlternateContent':
const drawingChild = child.getElementsByTagName('w:drawing').item(0);
if (drawingChild) {
run.addChild(Drawing.fromXML(word, drawingChild));
}
break;
default:
console.warn('parse Run: Unknown key', tagName, child);
}

View File

@ -5,23 +5,10 @@
*/
import {parseSize} from '../../parse/parseSize';
import {
ST_ChapterSep,
ST_DocGrid,
ST_LineNumberRestart,
ST_NumberFormat,
ST_PageBorderDisplay,
ST_PageBorderOffset,
ST_PageBorderZOrder,
ST_PageOrientation,
ST_SectionMark,
ST_TextDirection
} from '../Types';
import {BorderOptions} from './Border';
import {ST_PageOrientation} from '../Types';
import {Hyperlink} from './Hyperlink';
import {Paragraph} from './Paragraph';
import {Table} from './Table';
import {VerticalAlign} from './VerticalAlign';
export type PageSize = {
width: string;

View File

@ -2,35 +2,24 @@
* http://officeopenxml.com/WPtable.php
*/
import {
getAttrBoolean,
getVal,
getValBoolean,
getValHex,
getValNumber
} from '../../OpenXML';
import {parseBorder, parseBorders} from '../../parse/parseBorder';
import {parseColorAttr, parseShdColor} from '../../parse/parseColor';
import {addSize, LengthUsage, parseSize} from '../../parse/parseSize';
import {parseSize} from '../../parse/parseSize';
import Word from '../../Word';
import {CSSStyle} from '../Style';
import {
CT_TblLook,
ST_TblLayoutType,
ST_TblStyleOverrideType,
ST_TblWidth
} from '../Types';
import {
parseCellMargin,
parseInsideBorders,
parseTblCellSpacing,
parseTblWidth,
Tc
} from './table/Tc';
import {Properties} from './properties/Properties';
import {Tr} from './table/Tr';
export type CT_TblLookKey = keyof CT_TblLook;
import {Properties} from './properties/Properties';
import type {Tr} from './table/Tr';
import {parseTablePr} from '../../parse/parseTablePr';
import {Tc} from './table/Tc';
import {parseTr} from '../../parse/parseTr';
export type TblLookKey =
| 'firstRow'
| 'firstRow'
| 'lastRow'
| 'firstColumn'
| 'lastColumn'
| 'noHBand'
| 'noVBand';
export interface TablePr extends Properties {
/**
@ -54,7 +43,7 @@ export interface TablePr extends Properties {
/**
*
*/
tblLook?: Record<CT_TblLookKey, boolean>;
tblLook?: Record<TblLookKey, boolean>;
/**
*
@ -67,244 +56,12 @@ export interface TablePr extends Properties {
colBandSize?: number;
}
/**
* jc 使 float
* http://officeopenxml.com/WPtableAlignment.php
*/
function parseTblJc(element: Element, cssStyle: CSSStyle) {
const val = getVal(element);
switch (val) {
case 'left':
case 'start':
// TODO: 会导致前面的文字掉下去,感觉还是不能支持这个功能
// cssStyle['float'] = 'left';
break;
case 'right':
case 'end':
cssStyle['float'] = 'right';
}
}
/**
*
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblInd_2.html
*/
function parseTblInd(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style['margin-left'] = width;
}
}
function parseTblW(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style['width'] = width;
}
}
/**
* http://officeopenxml.com/WPtableLayout.php
*/
function parseTblLayout(element: Element, style: CSSStyle) {
const type = element.getAttribute('w:type') as ST_TblLayoutType;
if (type === ST_TblLayoutType.fixed) {
style['table-layout'] = 'fixed';
}
}
interface GridCol {
export interface GridCol {
w: string;
}
function parseTblGrid(element: Element) {
const gridCol: GridCol[] = [];
const gridColElements = element.getElementsByTagName('w:gridCol');
for (const gridColElement of gridColElements) {
const w = parseSize(gridColElement, 'w:w');
gridCol.push({w});
}
return gridCol;
}
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/ST_TblStyleOverrideType.html
// val 是旧的格式
function parseTblLook(child: Element) {
const tblLook: Record<CT_TblLookKey, boolean> = {} as Record<
CT_TblLookKey,
boolean
>;
const tblLookVal = getValHex(child);
if (getAttrBoolean(child, 'firstRow', false) || tblLookVal & 0x0020) {
tblLook['firstRow'] = true;
}
if (getAttrBoolean(child, 'lastRow', false) || tblLookVal & 0x0040) {
tblLook['lastRow'] = true;
}
if (getAttrBoolean(child, 'firstColumn', false) || tblLookVal & 0x0080) {
tblLook['firstColumn'] = true;
}
if (getAttrBoolean(child, 'lastColumn', false) || tblLookVal & 0x0100) {
tblLook['lastColumn'] = true;
}
if (getAttrBoolean(child, 'noHBand', false) || tblLookVal & 0x0200) {
tblLook['noHBand'] = true;
} else {
tblLook['noHBand'] = false;
}
if (getAttrBoolean(child, 'noVBand', false) || tblLookVal & 0x0400) {
tblLook['noVBand'] = true;
} else {
tblLook['noVBand'] = false;
}
return tblLook;
}
/**
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblpPr.html
*
*/
function parsetTlpPr(word: Word, child: Element, style: CSSStyle) {
// 如果设置 padding 会导致绝对定位不准确,所以一旦设置就不支持
if (typeof word.renderOptions.padding === 'undefined') {
const tplpX = parseSize(child, 'w:tblpX');
const tplpY = parseSize(child, 'w:tblpY');
style.position = 'absolute';
style.top = tplpY;
style.left = tplpX;
}
// 之前想用 float 来实现,但是会导致文字掉下去
// const topFromText = parseSize(child, 'w:topFromText');
// const bottomFromText = parseSize(child, 'w:bottomFromText');
// const rightFromText = parseSize(child, 'w:rightFromText');
// const leftFromText = parseSize(child, 'w:leftFromText');
// style['float'] = 'left';
// style['margin-bottom'] = addSize(style['margin-bottom'], bottomFromText);
// style['margin-left'] = addSize(style['margin-left'], leftFromText);
// style['margin-right'] = addSize(style['margin-right'], rightFromText);
// style['margin-top'] = addSize(style['margin-top'], topFromText);
}
export class Table {
properties: TablePr = {};
tblGrid: GridCol[] = [];
trs: Tr[] = [];
static parseTablePr(word: Word, element: Element): TablePr {
const properties: TablePr = {};
const tableStyle: CSSStyle = {};
const tcStyle: CSSStyle = {};
properties.tblLook = {} as Record<CT_TblLookKey, boolean>;
properties.cssStyle = tableStyle;
properties.tcCSSStyle = tcStyle;
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tblBorders':
parseBorders(word, child, tableStyle);
properties.insideBorder = parseInsideBorders(word, child);
break;
case 'w:tcBorders':
parseBorders(word, child, tableStyle);
break;
case 'w:tblInd':
parseTblInd(child, tableStyle);
break;
case 'w:jc':
parseTblJc(child, tableStyle);
break;
case 'w:tblCellMar':
case 'w:tcMar':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblCellMar_1.html
parseCellMargin(child, tcStyle);
break;
case 'w:tblStyle':
properties.pStyle = getVal(child);
break;
case 'w:tblW':
parseTblW(child, tableStyle);
break;
case 'w:shd':
// http://officeopenxml.com/WPtableShading.php
tableStyle['background-color'] = parseShdColor(word, child);
break;
case 'w:tblCaption':
properties.tblCaption = getVal(child);
break;
case 'w:tblCellSpacing':
parseTblCellSpacing(child, tableStyle);
break;
case 'w:tblLayout':
parseTblLayout(child, tableStyle);
break;
case 'w:tblLook':
properties.tblLook = parseTblLook(child);
break;
case 'w:tblStyleRowBandSize':
properties.rowBandSize = getValNumber(child);
break;
case 'w:tblStyleColBandSize':
properties.colBandSize = getValNumber(child);
break;
case 'w:tblpPr':
parsetTlpPr(word, child, tableStyle);
break;
default:
console.warn('parseTableProperties unknown tag', tagName, child);
}
}
return properties;
}
static fromXML(word: Word, element: Element): Table {
const table = new Table();
// 用于计算列的跨行,这里记下前面的跨行情况
const rowSpanMap: {[key: string]: Tc} = {};
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tblPr':
table.properties = Table.parseTablePr(word, child);
break;
case 'w:tr':
table.trs.push(Tr.fromXML(word, child, rowSpanMap));
break;
case 'w:tblGrid':
table.tblGrid = parseTblGrid(child);
break;
default:
console.warn('Table.fromXML unknown tag', tagName, child);
}
}
return table;
}
}

View File

@ -3,14 +3,16 @@ import Word from '../../../Word';
export class Blip {
embled?: Relationship;
src?: string | null;
static fromXML(word: Word, element: Element): Blip {
const blip = new Blip();
// 目前值支持 embed 这一种
const embedId = element.getAttribute('r:embed') || '';
const rel = word.getRelationship(embedId);
const rel = word.getDocumentRels(embedId);
if (rel) {
blip.embled = rel;
blip.src = word.loadImage(blip.embled);
}
return blip;

View File

@ -1,13 +1,167 @@
/**
* textbox
*/
import {LengthUsage, convertLength} from './../../../parse/parseSize';
import {CSSStyle} from './../../Style';
import {getAttrBoolean, getValBoolean} from '../../../OpenXML';
import Word from '../../../Word';
import {Pic} from './Pic';
import {parseSize} from '../../../parse/parseSize';
import {ST_RelFromH, ST_RelFromV} from '../../Types';
import {WPS} from '../wps/WPS';
/**
* drawing child anchor
*/
export enum Position {
inline = 'inline',
anchor = 'anchor'
}
/**
* css
* http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/anchor_2.html
*/
export interface Anchor {
simplePos: boolean;
hidden?: boolean;
}
function parseAnchor(element: Element): Anchor {
const simplePos = getAttrBoolean(element, 'simplePos', false);
const hidden = getAttrBoolean(element, 'hidden', false);
return {
simplePos,
hidden
};
}
export class Drawing {
pic: Pic;
// 如果是图片,这里会有值
pic?: Pic;
// 主要用于文本框
wps?: WPS;
// drawing 的位置配置
position: Position = Position.inline;
// 如果是 anchor描述具体配置
anchor?: Anchor;
// 外层容器样式
containerStyle?: CSSStyle;
// 是否是相对容器的垂直高度,如果是的话需要将容器的 p 也设置为 relative
relativeFromParagraph?: boolean;
static fromXML(word: Word, element: Element): Drawing | null {
const drawing = new Drawing();
const pic = element.querySelector('pic');
drawing.pic = Pic.fromXML(word, pic);
const containerStyle: CSSStyle = {};
drawing.containerStyle = containerStyle;
const position = element.firstElementChild;
if (position) {
if (position.tagName === 'wp:anchor') {
drawing.position = Position.anchor;
drawing.anchor = parseAnchor(position);
}
for (const child of position.children) {
const tagName = child.tagName;
switch (tagName) {
case 'wp:simplePos':
// 只有设置了 simplePos 才会生效
// 据说 word 其实不支持这个属性,所以目前实现估计没啥用
if (drawing.anchor?.simplePos) {
containerStyle['position'] = 'absolute';
containerStyle['x'] = parseSize(child, 'x', LengthUsage.Emu);
containerStyle['y'] = parseSize(child, 'y', LengthUsage.Emu);
}
break;
case 'wp:positionH':
const relativeFromH = child.getAttribute(
'relativeFrom'
) as ST_RelFromH;
if (relativeFromH === 'column' || relativeFromH === 'page') {
const positionType = child.firstElementChild;
if (positionType) {
const positionTypeTagName = positionType.tagName;
if (positionTypeTagName === 'wp:posOffset') {
containerStyle['position'] = 'absolute';
containerStyle['left'] = convertLength(
positionType.innerHTML,
LengthUsage.Emu
);
} else {
console.warn('unsupport positionType', positionTypeTagName);
}
}
} else {
console.warn('unsupport relativeFrom', relativeFromH);
}
break;
case 'wp:positionV':
const relativeFromV = child.getAttribute(
'relativeFrom'
) as ST_RelFromV;
if (relativeFromV === 'paragraph' || relativeFromV === 'page') {
if (relativeFromV === 'paragraph') {
drawing.relativeFromParagraph = true;
}
const positionType = child.firstElementChild;
if (positionType) {
const positionTypeTagName = positionType.tagName;
if (positionTypeTagName === 'wp:posOffset') {
containerStyle['position'] = 'absolute';
containerStyle['top'] = convertLength(
positionType.innerHTML,
LengthUsage.Emu
);
} else {
console.warn('unsupport positionType', positionTypeTagName);
}
}
} else {
console.warn('unsupport relativeFrom', relativeFromV);
}
break;
case 'wp:docPr':
// 和展现无关
// http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/docPr.html
break;
case 'a:graphic':
const graphicData = child.firstElementChild;
const graphicDataChild = graphicData?.firstElementChild;
if (graphicDataChild) {
const graphicDataChildTagName = graphicDataChild.tagName;
switch (graphicDataChildTagName) {
case 'pic:pic':
drawing.pic = Pic.fromXML(word, graphicDataChild);
break;
case 'wps:wsp':
drawing.wps = WPS.fromXML(word, graphicDataChild);
break;
default:
console.warn('unknown graphicData child tag', tagName);
}
}
break;
default:
console.warn('unknown tag', tagName);
}
}
}
return drawing;
}
}

View File

@ -0,0 +1,17 @@
/**
*
*/
import {ST_ShapeType} from '../../Types';
import Word from '../../../Word';
import {CSSStyle} from './../../Style';
export class Geom {
type: ST_ShapeType;
static fromXML(word: Word, element: Element, style: CSSStyle): Geom {
const geom = new Geom();
geom.type = element.getAttribute('prst') as ST_ShapeType;
// 后面得改成用 SVG 实现
return geom;
}
}

View File

@ -2,18 +2,131 @@
* http://webapp.docx4java.org/OnlineDemo/ecma376/DrawingML/spPr_2.html
*/
import {ST_PresetLineDashVal, ST_ShapeType} from '../../Types';
import Word from '../../../Word';
import {Transform} from './Transform';
import {CSSStyle} from './../../Style';
import {parseSize, LengthUsage} from '../../../parse/parseSize';
import {Geom} from './Geom';
function prstDashToCSSBorderType(prstDash: ST_PresetLineDashVal) {
let borderType = 'solid';
switch (prstDash) {
case 'dash':
case 'dashDot':
case 'lgDash':
case 'lgDashDot':
case 'lgDashDotDot':
case 'sysDash':
case 'sysDashDot':
case 'sysDashDotDot':
borderType = 'dashed';
break;
case 'dot':
case 'sysDot':
borderType = 'dotted';
break;
}
return borderType;
}
function parseOutline(word: Word, element: Element, style: CSSStyle) {
const borderWidth = parseSize(element, 'w', LengthUsage.Emu);
style['border-width'] = borderWidth;
style['border-style'] = 'solid';
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:solidFill':
// 目前只支持 solidFill
const colorChild = child.firstElementChild;
if (colorChild) {
const colorType = colorChild.tagName;
switch (colorType) {
case 'a:prstClr':
const color = colorChild.getAttribute('val') || '';
style['border-color'] = color;
break;
case 'a:srgbClr':
const rgbColor = colorChild.getAttribute('val') || '';
style['border-color'] = '#' + rgbColor;
case 'a:schemeClr':
const schemeClr = colorChild.getAttribute('val') || '';
if (schemeClr) {
style['border-color'] = word.getThemeColor(schemeClr);
}
default:
console.warn(
'parseOutline: Unknown color type ',
colorType,
colorChild
);
}
}
break;
case 'a:noFill':
style['border'] = 'none';
break;
case 'a:round':
// 瞎写的,规范里也没写是多少
style['border-radius'] = '8%';
break;
case 'a:prstDash':
style['border-style'] = prstDashToCSSBorderType(
child.getAttribute('val') as ST_PresetLineDashVal
);
break;
default:
console.warn('parseOutline: Unknown tag ', tagName, child);
}
}
}
export class ShapePr {
xfrm?: Transform;
// 内置图形
prstGeom?: Geom;
// 主要是边框样式
style?: CSSStyle;
static fromXML(word: Word, element?: Element | null): ShapePr {
const shapePr = new ShapePr();
const xfrm = element?.querySelector('xfrm');
if (xfrm) {
shapePr.xfrm = Transform.fromXML(word, xfrm);
const style = {};
shapePr.style = style;
if (element) {
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'a:xfrm':
shapePr.xfrm = Transform.fromXML(word, child);
break;
case 'a:prstGeom':
shapePr.prstGeom = Geom.fromXML(word, child, style);
break;
case 'a:ln':
// http://officeopenxml.com/drwSp-outline.php
parseOutline(word, child, style);
break;
default:
console.warn('ShapePr: Unknown tag ', tagName, child);
}
}
}
return shapePr;
}
}

View File

@ -13,8 +13,8 @@ export class Lvl {
numFmt: ST_NumberFormat;
lvlText: string = '%1.';
isLgl: boolean = false;
lvlJc: ST_Jc = ST_Jc.start;
suff: ST_LevelSuffix = ST_LevelSuffix.space;
lvlJc: ST_Jc = 'start';
suff: ST_LevelSuffix = 'space';
pPr?: ParagraphPr;
rPr?: RunPr;
@ -43,6 +43,11 @@ export class Lvl {
lvl.lvlJc = getVal(child) as ST_Jc;
break;
case 'w:legacy':
// 老的属性应该不需要支持了
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/legacy.html
break;
case 'w:pPr':
lvl.pPr = Paragraph.parseParagraphPr(word, child);
break;

View File

@ -1,13 +1,7 @@
import {getValBoolean, getValNumber, getVal} from '../../../OpenXML';
import Word from '../../../Word';
import {CSSStyle} from '../../Style';
import {Paragraph} from '../Paragraph';
import {Table} from '../Table';
import {LengthUsage, parseSize} from '../../../parse/parseSize';
import {parseColorAttr, parseShdColor} from '../../../parse/parseColor';
import {parseBorder, parseBorders} from '../../../parse/parseBorder';
import {parseTextDirection} from '../../../parse/parseTextDirection';
import {ST_Merge, ST_TblWidth, ST_VerticalJc} from '../../Types';
import {ST_Merge} from '../../Types';
export interface TcPr {
cssStyle?: CSSStyle;
@ -32,104 +26,6 @@ export interface TcPr {
type TcChild = Paragraph | Table;
// http://officeopenxml.com/WPtableCellProperties-Margins.php
export function parseCellMargin(element: Element, style: CSSStyle) {
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:left':
case 'w:start':
style['padding-left'] = parseSize(child, 'w:w');
break;
case 'w:right':
case 'w:end':
style['padding-right'] = parseSize(child, 'w:w');
break;
case 'w:top':
style['padding-top'] = parseSize(child, 'w:w');
break;
case 'w:bottom':
style['padding-bottom'] = parseSize(child, 'w:w');
break;
}
}
}
function parseVAlign(element: Element, style: CSSStyle) {
const vAlign = getVal(element) as ST_VerticalJc;
switch (vAlign) {
case ST_VerticalJc.bottom:
style['vertical-align'] = 'bottom';
break;
case ST_VerticalJc.center:
style['vertical-align'] = 'middle';
break;
case ST_VerticalJc.top:
style['vertical-align'] = 'top';
break;
}
}
export function parseTblCellSpacing(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style['cell-spacing'] = width;
}
}
/**
* parseBorders insideH insideV
*
*/
export function parseInsideBorders(word: Word, element: Element) {
let H;
const insideH = element.querySelector('insideH');
if (insideH) {
H = parseBorder(word, insideH);
}
let V;
const insideV = element.querySelector('insideV');
if (insideV) {
V = parseBorder(word, insideV);
}
return {
H,
V
};
}
/**
* http://officeopenxml.com/WPtableWidth.php
*/
export function parseTblWidth(element: Element) {
const type = element.getAttribute('w:type') as ST_TblWidth;
if (!type || type === ST_TblWidth.dxa) {
return parseSize(element, 'w:w');
} else if (type === ST_TblWidth.pct) {
return parseSize(element, 'w:w', LengthUsage.Percent);
} else if (type === ST_TblWidth.auto) {
return 'auto';
} else {
console.warn('parseTblWidth: ignore type', type, element);
}
return '';
}
function parseTcW(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style.width = width;
}
}
export class Tc {
properties: TcPr = {};
children: TcChild[] = [];
@ -139,120 +35,4 @@ export class Tc {
this.children.push(child);
}
}
static parseTcPr(word: Word, element: Element) {
const properties: TcPr = {};
const style: CSSStyle = {};
properties.cssStyle = style;
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tcMar':
parseCellMargin(child, style);
break;
case 'w:shd':
style['background-color'] = parseShdColor(word, child);
break;
case 'w:tcW':
parseTcW(child, style);
break;
case 'w:noWrap':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/noWrap.html
const noWrap = getValBoolean(child);
if (noWrap) {
style['white-space'] = 'nowrap';
}
break;
case 'w:vAlign':
parseVAlign(child, style);
break;
case 'w:tcBorders':
parseBorders(word, child, style);
properties.insideBorder = parseInsideBorders(word, child);
break;
case 'w:gridSpan':
properties.gridSpan = getValNumber(child);
break;
case 'w:vMerge':
properties.vMerge = (getVal(child) as ST_Merge) || ST_Merge.continue;
break;
case 'w:textDirection':
parseTextDirection(child, style);
break;
case 'w:cnfStyle':
// 目前是自动计算的,所以不需要这个了
break;
default:
console.warn('parseTcPr: ignore', tagName, child);
}
}
return properties;
}
static fromXML(
word: Word,
element: Element,
currentCol: {index: number},
rowSpanMap: {[key: string]: Tc}
): Tc | null {
const tc = new Tc();
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tcPr':
tc.properties = Tc.parseTcPr(word, child);
break;
case 'w:p':
tc.add(Paragraph.fromXML(word, child));
break;
case 'w:tbl':
tc.add(Table.fromXML(word, child));
break;
}
}
const lastCol = rowSpanMap[currentCol.index];
// 如果是 continue 意味着这个被合并了
if (tc.properties.vMerge) {
if (tc.properties.vMerge === ST_Merge.restart) {
tc.properties.rowSpan = 1;
rowSpanMap[currentCol.index] = tc;
} else if (lastCol) {
if (lastCol.properties && lastCol.properties.rowSpan) {
lastCol.properties.rowSpan = lastCol.properties.rowSpan + 1;
const colSpan = tc.properties.gridSpan || 1;
currentCol.index += colSpan;
return null;
} else {
console.warn(
'Tc.fromXML: continue but not found lastCol',
currentCol.index,
tc,
rowSpanMap
);
}
}
} else {
delete rowSpanMap[currentCol.index];
}
const colSpan = tc.properties.gridSpan || 1;
currentCol.index += colSpan;
return tc;
}
}

View File

@ -1,10 +1,5 @@
import {CSSStyle} from '../../Style';
import {getVal, getValBoolean} from '../../../OpenXML';
import {parseTblCellSpacing, Tc} from './Tc';
import Word from '../../../Word';
import {parseTrHeight} from '../../../parse/parseTrHeight';
import {jcToTextAlign} from '../../../parse/jcToTextAlign';
import {Table} from '../Table';
import {Tc} from './Tc';
export interface TrPr {
cssStyle?: CSSStyle;
@ -18,93 +13,4 @@ export interface TrPr {
export class Tr {
properties: TrPr = {};
tcs: Tc[] = [];
static parseTrPr(word: Word, element: Element): TrPr {
const cssStyle: CSSStyle = {};
const tcStyle: CSSStyle = {};
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:hidden':
if (getValBoolean(child)) {
cssStyle.display = 'none';
}
break;
case 'w:trHeight':
parseTrHeight(child, cssStyle);
break;
case 'w:jc':
cssStyle['text-align'] = jcToTextAlign(getVal(child));
break;
case 'w:cantSplit':
// 目前也不支持分页
break;
case 'w:tblPrEx':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblPrEx_1.html
const tablePr = Table.parseTablePr(word, child);
Object.assign(cssStyle, tablePr.cssStyle);
break;
case 'w:tblCellSpacing':
parseTblCellSpacing(child, tcStyle);
break;
case 'w:cnfStyle':
// 目前是自动计算的,所以不需要这个了
break;
default:
console.warn(`Tr: Unknown tag `, tagName, child);
}
}
return {
cssStyle
};
}
static fromXML(
word: Word,
element: Element,
rowSpanMap: {[key: string]: Tc}
): Tr {
const tr = new Tr();
// 做成对象是为了传递引用来修改
const currentCol = {
index: 0
};
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tc':
const tc = Tc.fromXML(word, child, currentCol, rowSpanMap);
if (tc) {
tr.tcs.push(tc);
}
break;
case 'w:trPr':
tr.properties = Tr.parseTrPr(word, child);
break;
case 'w:tblPrEx':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblPrEx_1.html
const tablePr = Table.parseTablePr(word, child);
Object.assign(tr.properties.cssStyle || {}, tablePr.cssStyle);
break;
default:
console.warn(`Tr: Unknown tag `, tagName, child);
}
}
return tr;
}
}

View File

@ -0,0 +1,5 @@
/**
*
*/
export class WPG {}

View File

@ -0,0 +1,64 @@
import {Paragraph} from './../Paragraph';
import {ShapePr} from './../drawing/ShapeProperties';
/**
* wps wordprocessingShape drawing word shape
* textbox
*/
import Word from '../../../Word';
import {Table} from '../Table';
import {parseTable} from '../../../parse/parseTable';
export type TxbxContentChild = Paragraph | Table;
export class WPS {
spPr?: ShapePr;
txbxContent: TxbxContentChild[];
static fromXML(word: Word, element: Element) {
const wps = new WPS();
wps.txbxContent = [];
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'wps:cNvSpPr':
// 和展现无关
break;
case 'wps:spPr':
wps.spPr = ShapePr.fromXML(word, child);
break;
case 'wps:txbx':
// 文本框内容
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/txbxContent.html
const txbxContent = child.firstElementChild;
if (txbxContent) {
for (const txbxContentChild of txbxContent.children) {
const txbxContentTagName = txbxContentChild.tagName;
switch (txbxContentTagName) {
case 'w:p':
wps.txbxContent.push(
Paragraph.fromXML(word, txbxContentChild)
);
break;
case 'w:tbl':
wps.txbxContent.push(parseTable(word, txbxContentChild));
break;
}
}
} else {
console.warn('unknown wps:txbx', child);
}
break;
default:
console.warn('WPS: Unknown tag ', tagName, child);
}
}
return wps;
}
}

View File

@ -15,7 +15,10 @@ export interface PackageParser {
/**
*
*/
getFileByType(filePath: string, type: 'string' | 'blob'): string | Blob;
getFileByType(
filePath: string,
type: 'string' | 'blob' | 'uint8array'
): string | Blob | Uint8Array;
/**
*

View File

@ -42,7 +42,7 @@ export default class ZipPackageParser implements PackageParser {
/**
*
*/
getFileByType(filePath: string, type: 'string' | 'blob') {
getFileByType(filePath: string, type: 'string' | 'blob' | 'uint8array') {
filePath = filePath.startsWith('/') ? filePath.slice(1) : filePath;
const file = this.zip[filePath];
if (file) {
@ -50,6 +50,8 @@ export default class ZipPackageParser implements PackageParser {
return strFromU8(file);
} else if (type === 'blob') {
return new Blob([file]);
} else if (type === 'uint8array') {
return file;
}
}
throw new Error('file not found');

View File

@ -15,9 +15,9 @@ const DEFAULT_BORDER_COLOR = 'black';
*
*/
export function parseBorder(word: Word, element: Element) {
const type = getVal(element);
const type = getVal(element) as ST_Border;
if (type === ST_Border.nil || type === ST_Border.none) {
if (type === 'nil' || type === 'none') {
return 'none';
}
@ -25,24 +25,24 @@ export function parseBorder(word: Word, element: Element) {
// 这里和 css 不完全一致css 能表现的要少很多,也是导致展现效果难以一致的原因
switch (type) {
case ST_Border.dashed:
case ST_Border.dashDotStroked:
case ST_Border.dashSmallGap:
case 'dashed':
case 'dashDotStroked':
case 'dashSmallGap':
cssType = 'dashed';
break;
case ST_Border.dotDash:
case ST_Border.dotDotDash:
case ST_Border.dotted:
case 'dotDash':
case 'dotDotDash':
case 'dotted':
cssType = 'dotted';
break;
case ST_Border.double:
case ST_Border.doubleWave:
case 'double':
case 'doubleWave':
cssType = 'double';
break;
case ST_Border.inset:
case 'inset':
cssType = 'inset';
break;
case ST_Border.outset:
case 'outset':
cssType = 'outset';
break;
}

View File

@ -0,0 +1,28 @@
import {CSSStyle} from '../openxml/Style';
import {parseSize} from './parseSize';
// http://officeopenxml.com/WPtableCellProperties-Margins.php
export function parseCellMargin(element: Element, style: CSSStyle) {
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:left':
case 'w:start':
style['padding-left'] = parseSize(child, 'w:w');
break;
case 'w:right':
case 'w:end':
style['padding-right'] = parseSize(child, 'w:w');
break;
case 'w:top':
style['padding-top'] = parseSize(child, 'w:w');
break;
case 'w:bottom':
style['padding-bottom'] = parseSize(child, 'w:w');
break;
}
}
}

View File

@ -6,25 +6,27 @@ import {getVal} from '../OpenXML';
import {ST_Shd} from '../openxml/Types';
import Word from '../Word';
const knownColors = [
'black',
'blue',
'cyan',
'darkBlue',
'darkCyan',
'darkGray',
'darkGreen',
'darkMagenta',
'darkRed',
'darkYellow',
'green',
'lightGray',
'magenta',
'none',
'red',
'white',
'yellow'
];
/**
* css
*/
export const cssColors = ['black', 'blue', 'green', 'red', 'white', 'yellow'];
/**
* hex chrome
*/
export const colorNameMap = {
cyan: '#00FFFF',
magenta: '#FF00FF',
darkBlue: '#00008B',
darkCyan: '#008B8B',
darkGray: '#A9A9A9',
darkGreen: '#006400',
darkMagenta: '#800080',
darkRed: '#8B0000',
darkYellow: '#808000',
lightGray: '#D3D3D3',
none: 'transparent'
};
/**
*
@ -44,8 +46,13 @@ export function parseColorAttr(
if (color) {
if (color == 'auto') {
return autoColor;
} else if (typeof color === 'string' && knownColors.includes(color)) {
/**
* css
*/
} else if (cssColors.includes(color)) {
return color;
} else if (color in colorNameMap) {
return colorNameMap[color as keyof typeof colorNameMap];
}
return `#${color}`;
@ -71,51 +78,51 @@ export function parseShdColor(word: Word, element: Element) {
if (color.length === 6) {
switch (val) {
case ST_Shd.clear:
case 'clear':
return `#${color}`;
case ST_Shd.pct10:
case 'pct10':
return colorPercent(color, 0.1);
case ST_Shd.pct12:
case 'pct12':
return colorPercent(color, 0.125);
case ST_Shd.pct15:
case 'pct15':
return colorPercent(color, 0.15);
case ST_Shd.pct20:
case 'pct20':
return colorPercent(color, 0.2);
case ST_Shd.pct25:
case 'pct25':
return colorPercent(color, 0.25);
case ST_Shd.pct30:
case 'pct30':
return colorPercent(color, 0.3);
case ST_Shd.pct35:
case 'pct35':
return colorPercent(color, 0.35);
case ST_Shd.pct37:
case 'pct37':
return colorPercent(color, 0.375);
case ST_Shd.pct40:
case 'pct40':
return colorPercent(color, 0.4);
case ST_Shd.pct45:
case 'pct45':
return colorPercent(color, 0.45);
case ST_Shd.pct5:
case 'pct5':
return colorPercent(color, 0.05);
case ST_Shd.pct50:
case 'pct50':
return colorPercent(color, 0.5);
case ST_Shd.pct55:
case 'pct55':
return colorPercent(color, 0.55);
case ST_Shd.pct60:
case 'pct60':
return colorPercent(color, 0.6);
case ST_Shd.pct65:
case 'pct65':
return colorPercent(color, 0.65);
case ST_Shd.pct70:
case 'pct70':
return colorPercent(color, 0.7);
case ST_Shd.pct75:
case 'pct75':
return colorPercent(color, 0.75);
case ST_Shd.pct80:
case 'pct80':
return colorPercent(color, 0.8);
case ST_Shd.pct85:
case 'pct85':
return colorPercent(color, 0.85);
case ST_Shd.pct87:
case 'pct87':
return colorPercent(color, 0.87);
case ST_Shd.pct90:
case 'pct90':
return colorPercent(color, 0.9);
case ST_Shd.pct95:
case 'pct95':
return colorPercent(color, 0.95);
default:

View File

@ -0,0 +1,25 @@
import Word from '../Word';
import {parseBorder} from './parseBorder';
/**
* parseBorders insideH insideV
*
*/
export function parseInsideBorders(word: Word, element: Element) {
let H;
const insideH = element.querySelector('insideH');
if (insideH) {
H = parseBorder(word, insideH);
}
let V;
const insideV = element.querySelector('insideV');
if (insideV) {
V = parseBorder(word, insideV);
}
return {
H,
V
};
}

View File

@ -1,4 +1,3 @@
import {ST_TextAlignment} from './../openxml/Types';
/**
* rPr pPr docxjs
*/
@ -6,14 +5,14 @@ import {ST_TextAlignment} from './../openxml/Types';
import {LengthUsage} from './parseSize';
import {CSSStyle} from '../openxml/Style';
import Word from '../Word';
import {getVal, getValBoolean} from '../OpenXML';
import {getVal, getValBoolean, getValNumber} from '../OpenXML';
import {parseBorder, parseBorders} from './parseBorder';
import {parseColor, parseColorAttr, parseShdColor} from './parseColor';
import {parseInd} from './parseInd';
import {parseSize} from './parseSize';
import {parseSpacing} from './parseSpacing';
import {parseFont} from './parseFont';
import {ST_VerticalAlignRun} from '../openxml/Types';
import {ST_Em, ST_HighlightColor, ST_TextAlignment} from '../openxml/Types';
import {parseTrHeight} from './parseTrHeight';
import {jcToTextAlign} from './jcToTextAlign';
import {parseTextDirection} from './parseTextDirection';
@ -108,6 +107,35 @@ function parseFrame(element: Element, style: CSSStyle) {
}
}
/**
* em css text-emphasis
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/ST_Em.html
*/
function convertEm(em: ST_Em, style: CSSStyle) {
switch (em) {
case 'dot':
style['text-emphasis'] = 'filled';
// 按规范里描述应该是在文字上面,但在 word 实际显示的时候是在下面
// 怀疑是中文场景下做了特殊处理
style['text-emphasis-position'] = 'under right';
break;
case 'comma':
style['text-emphasis'] = 'filled sesame';
break;
case 'circle':
style['text-emphasis'] = 'open';
break;
case 'underDot':
style['text-emphasis'] = 'filled';
style['text-emphasis-position'] = 'under right';
break;
case 'none':
break;
}
}
const HighLightColor = 'transparent';
/**
@ -163,8 +191,8 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
style['background-color'] = parseColorAttr(
word,
child,
'w:fill',
HighLightColor
'w:val',
'yellow'
);
break;
@ -172,9 +200,9 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/vertAlign.html
// 这个其实和 position 有冲突,但预计这两个同时出现的概率不高
const vertAlign = getVal(child);
if (vertAlign === ST_VerticalAlignRun.superscript) {
if (vertAlign === 'superscript') {
style['vertical-align'] = 'super';
} else if (vertAlign === ST_VerticalAlignRun.subscript) {
} else if (vertAlign === 'subscript') {
style['vertical-align'] = 'sub';
}
break;
@ -328,9 +356,9 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
case 'w:textAlignment':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/textAlignment.html
const alignment = getVal(child) as ST_TextAlignment;
if (alignment === ST_TextAlignment.center) {
if (alignment === 'center') {
style['vertical-align'] = 'middle';
} else if (alignment !== ST_TextAlignment.auto) {
} else if (alignment !== 'auto') {
style['vertical-align'] = alignment;
}
break;
@ -366,6 +394,22 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
// 支持不了
break;
case 'w:em':
convertEm(getVal(child) as ST_Em, style);
break;
case 'w:w':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/w_1.html
// 看起来应该是把文字拉伸
const w = getValNumber(child);
style['transform'] = `scaleX(${w / 100})`;
style['display'] = 'inline-block'; // 需要这样才能生效
break;
case 'w:webHidden':
// 虽然是 web 渲染但希望是按 word 显示,所以先不处理了
break;
default:
console.warn('parsePr Unknown tagName', tagName, child);
}

View File

@ -0,0 +1,45 @@
import {Tc} from '../openxml/word/table/Tc';
import {GridCol, Table} from '../openxml/word/Table';
import {parseTr} from './parseTr';
import {parseTablePr} from './parseTablePr';
import Word from '../Word';
import {parseSize} from './parseSize';
function parseTblGrid(element: Element) {
const gridCol: GridCol[] = [];
const gridColElements = element.getElementsByTagName('w:gridCol');
for (const gridColElement of gridColElements) {
const w = parseSize(gridColElement, 'w:w');
gridCol.push({w});
}
return gridCol;
}
export function parseTable(word: Word, element: Element) {
const table = new Table();
// 用于计算列的跨行,这里记下前面的跨行情况
const rowSpanMap: {[key: string]: Tc} = {};
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tblPr':
table.properties = parseTablePr(word, child);
break;
case 'w:tr':
table.trs.push(parseTr(word, child, rowSpanMap));
break;
case 'w:tblGrid':
table.tblGrid = parseTblGrid(child);
break;
default:
console.warn('Table.fromXML unknown tag', tagName, child);
}
}
return table;
}

View File

@ -0,0 +1,207 @@
import {ST_TblLayoutType} from './../openxml/Types';
import {getAttrBoolean, getVal, getValHex, getValNumber} from '../OpenXML';
import {CSSStyle} from '../openxml/Style';
import {TablePr, TblLookKey} from '../openxml/word/Table';
import Word from '../Word';
import {parseBorders} from './parseBorder';
import {parseInsideBorders} from './parseInsideBorders';
import {parseTblWidth} from './parseTblWidth';
import {parseShdColor} from './parseColor';
import {parseSize} from './parseSize';
import {parseTblCellSpacing} from './parseTcPr';
import {parseCellMargin} from './parseCellMargin';
/**
* jc 使 float
* http://officeopenxml.com/WPtableAlignment.php
*/
function parseTblJc(element: Element, cssStyle: CSSStyle) {
const val = getVal(element);
switch (val) {
case 'left':
case 'start':
// TODO: 会导致前面的文字掉下去,感觉还是不能支持这个功能
// cssStyle['float'] = 'left';
break;
case 'right':
case 'end':
cssStyle['float'] = 'right';
}
}
/**
*
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblInd_2.html
*/
function parseTblInd(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style['margin-left'] = width;
}
}
function parseTblW(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style['width'] = width;
}
}
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/ST_TblStyleOverrideType.html
// val 是旧的格式
function parseTblLook(child: Element) {
const tblLook: Record<TblLookKey, boolean> = {} as Record<
TblLookKey,
boolean
>;
const tblLookVal = getValHex(child);
if (getAttrBoolean(child, 'firstRow', false) || tblLookVal & 0x0020) {
tblLook['firstRow'] = true;
}
if (getAttrBoolean(child, 'lastRow', false) || tblLookVal & 0x0040) {
tblLook['lastRow'] = true;
}
if (getAttrBoolean(child, 'firstColumn', false) || tblLookVal & 0x0080) {
tblLook['firstColumn'] = true;
}
if (getAttrBoolean(child, 'lastColumn', false) || tblLookVal & 0x0100) {
tblLook['lastColumn'] = true;
}
if (getAttrBoolean(child, 'noHBand', false) || tblLookVal & 0x0200) {
tblLook['noHBand'] = true;
} else {
tblLook['noHBand'] = false;
}
if (getAttrBoolean(child, 'noVBand', false) || tblLookVal & 0x0400) {
tblLook['noVBand'] = true;
} else {
tblLook['noVBand'] = false;
}
return tblLook;
}
/**
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblpPr.html
*
*/
function parseTblpPr(word: Word, child: Element, style: CSSStyle) {
// 如果设置 padding 会导致绝对定位不准确,所以一旦设置就不支持
if (typeof word.renderOptions.padding === 'undefined') {
const tplpX = parseSize(child, 'w:tblpX');
const tplpY = parseSize(child, 'w:tblpY');
style.position = 'absolute';
style.top = tplpY;
style.left = tplpX;
}
// 之前想用 float 来实现,但是会导致文字掉下去
// const topFromText = parseSize(child, 'w:topFromText');
// const bottomFromText = parseSize(child, 'w:bottomFromText');
// const rightFromText = parseSize(child, 'w:rightFromText');
// const leftFromText = parseSize(child, 'w:leftFromText');
// style['float'] = 'left';
// style['margin-bottom'] = addSize(style['margin-bottom'], bottomFromText);
// style['margin-left'] = addSize(style['margin-left'], leftFromText);
// style['margin-right'] = addSize(style['margin-right'], rightFromText);
// style['margin-top'] = addSize(style['margin-top'], topFromText);
}
/**
* http://officeopenxml.com/WPtableLayout.php
*/
function parseTblLayout(element: Element, style: CSSStyle) {
const type = element.getAttribute('w:type') as ST_TblLayoutType;
if (type === 'fixed') {
style['table-layout'] = 'fixed';
}
}
export function parseTablePr(word: Word, element: Element): TablePr {
const properties: TablePr = {};
const tableStyle: CSSStyle = {};
const tcStyle: CSSStyle = {};
properties.tblLook = {} as Record<TblLookKey, boolean>;
properties.cssStyle = tableStyle;
properties.tcCSSStyle = tcStyle;
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tblBorders':
parseBorders(word, child, tableStyle);
properties.insideBorder = parseInsideBorders(word, child);
break;
case 'w:tcBorders':
parseBorders(word, child, tableStyle);
break;
case 'w:tblInd':
parseTblInd(child, tableStyle);
break;
case 'w:jc':
parseTblJc(child, tableStyle);
break;
case 'w:tblCellMar':
case 'w:tcMar':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblCellMar_1.html
parseCellMargin(child, tcStyle);
break;
case 'w:tblStyle':
properties.pStyle = getVal(child);
break;
case 'w:tblW':
parseTblW(child, tableStyle);
break;
case 'w:shd':
// http://officeopenxml.com/WPtableShading.php
tableStyle['background-color'] = parseShdColor(word, child);
break;
case 'w:tblCaption':
properties.tblCaption = getVal(child);
break;
case 'w:tblCellSpacing':
parseTblCellSpacing(child, tableStyle);
break;
case 'w:tblLayout':
parseTblLayout(child, tableStyle);
break;
case 'w:tblLook':
properties.tblLook = parseTblLook(child);
break;
case 'w:tblStyleRowBandSize':
properties.rowBandSize = getValNumber(child);
break;
case 'w:tblStyleColBandSize':
properties.colBandSize = getValNumber(child);
break;
case 'w:tblpPr':
parseTblpPr(word, child, tableStyle);
break;
default:
console.warn('parseTableProperties unknown tag', tagName, child);
}
}
return properties;
}

View File

@ -0,0 +1,19 @@
import {ST_TblWidth} from './../openxml/Types';
import {parseSize, LengthUsage} from './parseSize';
/**
* http://officeopenxml.com/WPtableWidth.php
*/
export function parseTblWidth(element: Element) {
const type = element.getAttribute('w:type') as ST_TblWidth;
if (!type || type === 'dxa') {
return parseSize(element, 'w:w');
} else if (type === 'pct') {
return parseSize(element, 'w:w', LengthUsage.Percent);
} else if (type === 'auto') {
return 'auto';
} else {
console.warn('parseTblWidth: ignore type', type, element);
}
return '';
}

View File

@ -0,0 +1,65 @@
/**
*
*/
import {Tc} from '../openxml/word/table/Tc';
import Word from '../Word';
import {parseTcPr} from './parseTcPr';
import {Paragraph} from '../openxml/word/Paragraph';
import {parseTable} from './parseTable';
export function parseTc(
word: Word,
element: Element,
currentCol: {index: number},
rowSpanMap: {[key: string]: Tc}
) {
const tc = new Tc();
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tcPr':
tc.properties = parseTcPr(word, child);
break;
case 'w:p':
tc.add(Paragraph.fromXML(word, child));
break;
case 'w:tbl':
tc.add(parseTable(word, child));
break;
}
}
const lastCol = rowSpanMap[currentCol.index];
// 如果是 continue 意味着这个被合并了
if (tc.properties.vMerge) {
if (tc.properties.vMerge === 'restart') {
tc.properties.rowSpan = 1;
rowSpanMap[currentCol.index] = tc;
} else if (lastCol) {
if (lastCol.properties && lastCol.properties.rowSpan) {
lastCol.properties.rowSpan = lastCol.properties.rowSpan + 1;
const colSpan = tc.properties.gridSpan || 1;
currentCol.index += colSpan;
return null;
} else {
console.warn(
'Tc.fromXML: continue but not found lastCol',
currentCol.index,
tc,
rowSpanMap
);
}
}
} else {
delete rowSpanMap[currentCol.index];
}
const colSpan = tc.properties.gridSpan || 1;
currentCol.index += colSpan;
return tc;
}

View File

@ -0,0 +1,105 @@
import {CSSStyle} from '../openxml/Style';
import {TcPr} from '../openxml/word/table/Tc';
import Word from '../Word';
import {parseCellMargin} from './parseCellMargin';
import {parseShdColor} from './parseColor';
import {getVal, getValBoolean, getValNumber} from '../OpenXML';
import {ST_Merge, ST_TblWidth, ST_VerticalJc} from '../openxml/Types';
import {parseSize, LengthUsage} from './parseSize';
import {parseBorders} from './parseBorder';
import {parseTextDirection} from './parseTextDirection';
import {parseTblWidth} from './parseTblWidth';
import {parseInsideBorders} from './parseInsideBorders';
function parseVAlign(element: Element, style: CSSStyle) {
const vAlign = getVal(element) as ST_VerticalJc;
switch (vAlign) {
case 'bottom':
style['vertical-align'] = 'bottom';
break;
case 'center':
style['vertical-align'] = 'middle';
break;
case 'top':
style['vertical-align'] = 'top';
break;
}
}
export function parseTblCellSpacing(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style['cell-spacing'] = width;
}
}
function parseTcW(element: Element, style: CSSStyle) {
const width = parseTblWidth(element);
if (width) {
style.width = width;
}
}
export function parseTcPr(word: Word, element: Element) {
const properties: TcPr = {};
const style: CSSStyle = {};
properties.cssStyle = style;
for (const child of element.children) {
const tagName = child.tagName;
switch (tagName) {
case 'w:tcMar':
parseCellMargin(child, style);
break;
case 'w:shd':
style['background-color'] = parseShdColor(word, child);
break;
case 'w:tcW':
parseTcW(child, style);
break;
case 'w:noWrap':
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/noWrap.html
const noWrap = getValBoolean(child);
if (noWrap) {
style['white-space'] = 'nowrap';
}
break;
case 'w:vAlign':
parseVAlign(child, style);
break;
case 'w:tcBorders':
parseBorders(word, child, style);
properties.insideBorder = parseInsideBorders(word, child);
break;
case 'w:gridSpan':
properties.gridSpan = getValNumber(child);
break;
case 'w:vMerge':
properties.vMerge = (getVal(child) as ST_Merge) || 'continue';
break;
case 'w:textDirection':
parseTextDirection(child, style);
break;
case 'w:cnfStyle':
// 目前是自动计算的,所以不需要这个了
break;
default:
console.warn('parseTcPr: ignore', tagName, child);
}
}
return properties;
}

Some files were not shown because too many files have changed in this diff Show More