mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
chore: office viewer 支持中英文空行;优化 tblpPr 的支持 (#6433)
This commit is contained in:
parent
7eb20c0a75
commit
3f1e39c560
@ -385,6 +385,7 @@ export function enableDebug() {
|
|||||||
|
|
||||||
interface DebugWrapperProps {
|
interface DebugWrapperProps {
|
||||||
renderer: any;
|
renderer: any;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DebugWrapper extends Component<DebugWrapperProps> {
|
export class DebugWrapper extends Component<DebugWrapperProps> {
|
||||||
|
@ -6,6 +6,10 @@ docx 渲染器,原理是将 docx 里的 xml 格式转成 html
|
|||||||
|
|
||||||
相对于 Canvas 渲染,这个实现方案比较简单,最终页面也可以很方便复制,但无法保证和原始 docx 文件展现一致,因为有部分功能难以在 HTML 中实现,比如图文环绕效果。
|
相对于 Canvas 渲染,这个实现方案比较简单,最终页面也可以很方便复制,但无法保证和原始 docx 文件展现一致,因为有部分功能难以在 HTML 中实现,比如图文环绕效果。
|
||||||
|
|
||||||
|
## 还不支持的功能
|
||||||
|
|
||||||
|
- wmf,需要使用 https://github.com/SheetJS/js-wmf
|
||||||
|
|
||||||
## 参考资料
|
## 参考资料
|
||||||
|
|
||||||
- [官方规范](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/)
|
- [官方规范](https://www.ecma-international.org/publications-and-standards/standards/ecma-376/)
|
||||||
|
BIN
packages/office-viewer/__tests__/docx/simple/text.docx
Normal file
BIN
packages/office-viewer/__tests__/docx/simple/text.docx
Normal file
Binary file not shown.
9
packages/office-viewer/__tests__/util/autoSpace.test.ts
Normal file
9
packages/office-viewer/__tests__/util/autoSpace.test.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import {cjkspace} from '../../src/util/autoSpace';
|
||||||
|
|
||||||
|
test('autoSpace', async () => {
|
||||||
|
expect(cjkspace('a中'.split(''))).toBe('a 中');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('autoSpace 2', async () => {
|
||||||
|
expect(cjkspace('abc中def,测试'.split(''))).toBe('abc 中 def,测试');
|
||||||
|
});
|
@ -52,6 +52,9 @@ export class Body {
|
|||||||
body.addChild(table);
|
body.addChild(table);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'w:bookmarkEnd':
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('Body.fromXML Unknown key', tagName, child);
|
console.warn('Body.fromXML Unknown key', tagName, child);
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,11 @@ export interface ParagraphPr extends Properties {
|
|||||||
numPr?: NumberPr;
|
numPr?: NumberPr;
|
||||||
runPr?: RunPr;
|
runPr?: RunPr;
|
||||||
tabs?: Tab[];
|
tabs?: Tab[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 其实是区分 autoSpaceDN 和 autoSpaceDE 的,但这里简化了
|
||||||
|
*/
|
||||||
|
autoSpace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ParagraphChild =
|
export type ParagraphChild =
|
||||||
@ -45,6 +50,12 @@ export type ParagraphChild =
|
|||||||
// | CommentRangeEnd
|
// | CommentRangeEnd
|
||||||
// | CommentReference;
|
// | CommentReference;
|
||||||
|
|
||||||
|
function parseAutoSpace(element: Element): boolean {
|
||||||
|
const autoSpaceDE = element.getElementsByTagName('w:autoSpaceDE').item(0);
|
||||||
|
const autoSpaceDN = element.getElementsByTagName('w:autoSpaceDN').item(0);
|
||||||
|
return !!autoSpaceDE || !!autoSpaceDN;
|
||||||
|
}
|
||||||
|
|
||||||
export class Paragraph {
|
export class Paragraph {
|
||||||
// 主要是为了方便调试用的
|
// 主要是为了方便调试用的
|
||||||
paraId?: string;
|
paraId?: string;
|
||||||
@ -78,7 +89,9 @@ export class Paragraph {
|
|||||||
tabs.push(Tab.fromXML(word, tabElement));
|
tabs.push(Tab.fromXML(word, tabElement));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {cssStyle, pStyle, numPr, tabs};
|
const autoSpace = parseAutoSpace(element);
|
||||||
|
|
||||||
|
return {cssStyle, pStyle, numPr, tabs, autoSpace};
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromXML(word: Word, element: Element): Paragraph {
|
static fromXML(word: Word, element: Element): Paragraph {
|
||||||
|
@ -76,7 +76,8 @@ function parseTblJc(element: Element, cssStyle: CSSStyle) {
|
|||||||
switch (val) {
|
switch (val) {
|
||||||
case 'left':
|
case 'left':
|
||||||
case 'start':
|
case 'start':
|
||||||
cssStyle['float'] = 'left';
|
// TODO: 会导致前面的文字掉下去,感觉还是不能支持这个功能
|
||||||
|
// cssStyle['float'] = 'left';
|
||||||
break;
|
break;
|
||||||
case 'right':
|
case 'right':
|
||||||
case 'end':
|
case 'end':
|
||||||
@ -165,16 +166,26 @@ function parseTblLook(child: Element) {
|
|||||||
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblpPr.html
|
* http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/tblpPr.html
|
||||||
* 只支持部分
|
* 只支持部分
|
||||||
*/
|
*/
|
||||||
function parsetTlpPr(child: Element, style: CSSStyle) {
|
function parsetTlpPr(word: Word, child: Element, style: CSSStyle) {
|
||||||
const topFromText = parseSize(child, 'w:topFromText');
|
// 如果设置 padding 会导致绝对定位不准确,所以一旦设置就不支持
|
||||||
const bottomFromText = parseSize(child, 'w:bottomFromText');
|
if (typeof word.renderOptions.padding === 'undefined') {
|
||||||
const rightFromText = parseSize(child, 'w:rightFromText');
|
const tplpX = parseSize(child, 'w:tblpX');
|
||||||
const leftFromText = parseSize(child, 'w:leftFromText');
|
const tplpY = parseSize(child, 'w:tblpY');
|
||||||
style['float'] = 'left';
|
style.position = 'absolute';
|
||||||
style['margin-bottom'] = addSize(style['margin-bottom'], bottomFromText);
|
style.top = tplpY;
|
||||||
style['margin-left'] = addSize(style['margin-left'], leftFromText);
|
style.left = tplpX;
|
||||||
style['margin-right'] = addSize(style['margin-right'], rightFromText);
|
}
|
||||||
style['margin-top'] = addSize(style['margin-top'], topFromText);
|
|
||||||
|
// 之前想用 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 {
|
export class Table {
|
||||||
@ -257,7 +268,7 @@ export class Table {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'w:tblpPr':
|
case 'w:tblpPr':
|
||||||
parsetTlpPr(child, tableStyle);
|
parsetTlpPr(word, child, tableStyle);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -343,6 +343,29 @@ export function parsePr(word: Word, element: Element, type: 'r' | 'p' = 'p') {
|
|||||||
// 目前是自动计算的,所以不需要这个了
|
// 目前是自动计算的,所以不需要这个了
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'w:bidi':
|
||||||
|
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/bidi_1.html
|
||||||
|
// TODO: 还不清楚和 w:textDirection 是什么关系
|
||||||
|
if (getValBoolean(child, true)) {
|
||||||
|
console.warn('w:bidi is not supported.');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'w:autoSpaceDE':
|
||||||
|
case 'w:autoSpaceDN':
|
||||||
|
// 这个在其它地方实现了
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'w:kinsoku':
|
||||||
|
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/kinsoku.html
|
||||||
|
// 控制不了所以忽略了
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'w:overflowPunct':
|
||||||
|
// http://webapp.docx4java.org/OnlineDemo/ecma376/WordML/overflowPunct.html
|
||||||
|
// 支持不了
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn('parsePr Unknown tagName', tagName, child);
|
console.warn('parsePr Unknown tagName', tagName, child);
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,16 @@ import {appendChild, createElement} from '../util/dom';
|
|||||||
import Word from '../Word';
|
import Word from '../Word';
|
||||||
import {Run} from '../openxml/word/Run';
|
import {Run} from '../openxml/word/Run';
|
||||||
import renderRun from './renderRun';
|
import renderRun from './renderRun';
|
||||||
|
import type {Paragraph} from '../openxml/word/Paragraph';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 渲染链接
|
* 渲染链接
|
||||||
*/
|
*/
|
||||||
export function renderHyperLink(word: Word, hyperlink: Hyperlink): HTMLElement {
|
export function renderHyperLink(
|
||||||
|
word: Word,
|
||||||
|
hyperlink: Hyperlink,
|
||||||
|
paragraph?: Paragraph
|
||||||
|
): HTMLElement {
|
||||||
const a = createElement('a') as HTMLAnchorElement;
|
const a = createElement('a') as HTMLAnchorElement;
|
||||||
|
|
||||||
if (hyperlink.relation) {
|
if (hyperlink.relation) {
|
||||||
@ -24,7 +29,7 @@ export function renderHyperLink(word: Word, hyperlink: Hyperlink): HTMLElement {
|
|||||||
|
|
||||||
for (const child of hyperlink.children) {
|
for (const child of hyperlink.children) {
|
||||||
if (child instanceof Run) {
|
if (child instanceof Run) {
|
||||||
const span = renderRun(word, child);
|
const span = renderRun(word, child, paragraph);
|
||||||
appendChild(a, span);
|
appendChild(a, span);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,11 @@ export function renderNumbering(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!numbering) {
|
||||||
|
console.warn('renderNumbering: numbering is empty');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const num = numbering.nums[numId];
|
const num = numbering.nums[numId];
|
||||||
|
|
||||||
if (!num) {
|
if (!num) {
|
||||||
|
@ -45,11 +45,11 @@ export default function renderParagraph(
|
|||||||
|
|
||||||
for (const child of paragraph.children) {
|
for (const child of paragraph.children) {
|
||||||
if (child instanceof Run) {
|
if (child instanceof Run) {
|
||||||
appendChild(p, renderRun(word, child));
|
appendChild(p, renderRun(word, child, paragraph));
|
||||||
} else if (child instanceof BookmarkStart) {
|
} else if (child instanceof BookmarkStart) {
|
||||||
appendChild(p, renderBookmarkStart(word, child));
|
appendChild(p, renderBookmarkStart(word, child));
|
||||||
} else if (child instanceof Hyperlink) {
|
} else if (child instanceof Hyperlink) {
|
||||||
const hyperlink = renderHyperLink(word, child);
|
const hyperlink = renderHyperLink(word, child, paragraph);
|
||||||
appendChild(p, hyperlink);
|
appendChild(p, hyperlink);
|
||||||
} else if (child instanceof SmartTag) {
|
} else if (child instanceof SmartTag) {
|
||||||
renderInlineText(word, child, p);
|
renderInlineText(word, child, p);
|
||||||
|
@ -20,16 +20,27 @@ import {InstrText} from '../openxml/word/InstrText';
|
|||||||
import {renderInstrText} from './renderInstrText';
|
import {renderInstrText} from './renderInstrText';
|
||||||
import {Sym} from '../openxml/word/Sym';
|
import {Sym} from '../openxml/word/Sym';
|
||||||
import {renderSym} from './renderSym';
|
import {renderSym} from './renderSym';
|
||||||
|
import {cjkspace} from '../util/autoSpace';
|
||||||
|
import type {Paragraph} from './../openxml/word/Paragraph';
|
||||||
|
|
||||||
const VARIABLE_CLASS_NAME = 'variable';
|
const VARIABLE_CLASS_NAME = 'variable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 对文本进行替换
|
* 对文本进行替换
|
||||||
*/
|
*/
|
||||||
function renderText(span: HTMLElement, word: Word, text: string) {
|
function renderText(
|
||||||
|
span: HTMLElement,
|
||||||
|
word: Word,
|
||||||
|
text: string,
|
||||||
|
paragraph?: Paragraph
|
||||||
|
) {
|
||||||
// 简单过滤一下提升性能
|
// 简单过滤一下提升性能
|
||||||
if (text.indexOf('{{') === -1) {
|
if (text.indexOf('{{') === -1) {
|
||||||
span.textContent = text;
|
if (paragraph?.properties?.autoSpace) {
|
||||||
|
span.textContent = cjkspace(text.split(''));
|
||||||
|
} else {
|
||||||
|
span.textContent = text;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
span.dataset.originText = text;
|
span.dataset.originText = text;
|
||||||
// 加个标识,后续可以通过它来查找哪些变量需要替换,这样就不用重新渲染整个文档了
|
// 加个标识,后续可以通过它来查找哪些变量需要替换,这样就不用重新渲染整个文档了
|
||||||
@ -53,7 +64,7 @@ export function updateVariableText(word: Word) {
|
|||||||
/**
|
/**
|
||||||
* 渲染 run 节点
|
* 渲染 run 节点
|
||||||
*/
|
*/
|
||||||
export default function renderRun(word: Word, run: Run) {
|
export default function renderRun(word: Word, run: Run, paragraph?: Paragraph) {
|
||||||
const span = createElement('span');
|
const span = createElement('span');
|
||||||
|
|
||||||
word.addClass(span, 'r');
|
word.addClass(span, 'r');
|
||||||
@ -62,12 +73,12 @@ export default function renderRun(word: Word, run: Run) {
|
|||||||
|
|
||||||
if (run.children.length === 1 && run.children[0] instanceof Text) {
|
if (run.children.length === 1 && run.children[0] instanceof Text) {
|
||||||
const text = run.children[0] as Text;
|
const text = run.children[0] as Text;
|
||||||
renderText(span, word, text.text);
|
renderText(span, word, text.text, paragraph);
|
||||||
} else {
|
} else {
|
||||||
for (const child of run.children) {
|
for (const child of run.children) {
|
||||||
if (child instanceof Text) {
|
if (child instanceof Text) {
|
||||||
let newSpan = createElement('span');
|
let newSpan = createElement('span');
|
||||||
renderText(span, word, child.text);
|
renderText(span, word, child.text, paragraph);
|
||||||
appendChild(span, newSpan);
|
appendChild(span, newSpan);
|
||||||
} else if (child instanceof Break) {
|
} else if (child instanceof Break) {
|
||||||
const br = renderBr(child);
|
const br = renderBr(child);
|
||||||
|
@ -8,6 +8,8 @@ import Word from '../Word';
|
|||||||
export function renderSection(word: Word, section: Section) {
|
export function renderSection(word: Word, section: Section) {
|
||||||
const sectionEl = createElement('section') as HTMLElement;
|
const sectionEl = createElement('section') as HTMLElement;
|
||||||
|
|
||||||
|
// 用于后续绝对定位
|
||||||
|
sectionEl.style.position = 'relative';
|
||||||
const props = section.properties;
|
const props = section.properties;
|
||||||
const pageSize = props.pageSize;
|
const pageSize = props.pageSize;
|
||||||
if (pageSize) {
|
if (pageSize) {
|
||||||
|
41
packages/office-viewer/src/util/autoSpace.ts
Normal file
41
packages/office-viewer/src/util/autoSpace.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 中英文间自动加空格,基于下面代码改的,去掉了 lodash 依赖
|
||||||
|
* https://gist.github.com/wyl8899/e0f31068681023480e20c34f6b19a275
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Partial implementation from https://zhuanlan.zhihu.com/p/33612593 */
|
||||||
|
|
||||||
|
/* 标点 */
|
||||||
|
const punctuationRegex = /\p{Punctuation}/u;
|
||||||
|
/* 空格 */
|
||||||
|
const spaceRegex = /\p{Separator}/u;
|
||||||
|
/* CJK 字符,中日韩 */
|
||||||
|
const cjkRegex =
|
||||||
|
/\p{Script=Han}|\p{Script=Katakana}|\p{Script=Hiragana}|\p{Script=Hangul}/u;
|
||||||
|
|
||||||
|
const shouldSpace = (a: string, b: string): boolean => {
|
||||||
|
if (cjkRegex.test(a)) {
|
||||||
|
return !(
|
||||||
|
punctuationRegex.test(b) ||
|
||||||
|
spaceRegex.test(b) ||
|
||||||
|
cjkRegex.test(b)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return cjkRegex.test(b) && !punctuationRegex.test(a) && !spaceRegex.test(a);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const join = (
|
||||||
|
parts: string[],
|
||||||
|
sepFunc: (a: string, b: string) => string
|
||||||
|
): string => {
|
||||||
|
return parts.reduce((r, p, i) => {
|
||||||
|
const sep = i !== 0 ? sepFunc(p, parts[i - 1]) : '';
|
||||||
|
return r + sep + p;
|
||||||
|
}, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cjkspace = (strings: string[]): string => {
|
||||||
|
const filtered = strings.filter(c => c !== undefined && c !== '') as string[];
|
||||||
|
return join(filtered, (a, b) => (shouldSpace(a, b) ? ' ' : ''));
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user