125 lines
4.6 KiB
TypeScript
125 lines
4.6 KiB
TypeScript
/**
|
||
* Word / WPS 粘贴富文本的归一化处理。
|
||
*
|
||
* 直接插入 Word 复制出的 HTML 会出现「表格被截断」,原因有三:
|
||
* ① 表格与单元格带绝对宽度(pt / px),宽度总和超过容器后被整体裁切;
|
||
* ② 单元格带 `white-space: nowrap` 或 `nowrap` 属性,内容不换行而横向溢出;
|
||
* ③ Word 私有 `mso-*` 样式与 `overflow: hidden` 把超出部分直接隐藏。
|
||
*
|
||
* 本模块只解除这些「布局锁定」,表格结构(行列、合并、边框、加粗、对齐)全部保留,
|
||
* 宽度改由各渲染端的 CSS 统一控制,避免同一份文书在不同容器里表现不一致。
|
||
*
|
||
* 解除动作本身是 `unlockTableLayout`,粘贴近来要用,**渲染出去也要用**:
|
||
* 编辑器序列化出来的表格自带 `style="width: auto"` 这类内联锁,只堵粘贴这一个口
|
||
* 挡不住它(详见该函数的注释)。
|
||
*/
|
||
|
||
const COMMENTS = /<!--[\s\S]*?-->/g
|
||
/** 成对出现的标签整体删除(含内容) */
|
||
const BLOCK_TAGS = /<(script|style|meta|link)\b[^>]*>[\s\S]*?<\/\1>/gi
|
||
/** 自闭合标签:`<script />`、`<meta ...>`、`<link ...>` */
|
||
const VOID_TAGS = /<(script|meta|link)\b[^>]*\/?>/gi
|
||
|
||
/** 会造成截断的样式声明:绝对宽度、固定布局与内容裁剪 */
|
||
const LAYOUT_LOCK_STYLES = new Set([
|
||
'width',
|
||
'min-width',
|
||
'max-width',
|
||
'height',
|
||
'table-layout',
|
||
'white-space',
|
||
'overflow',
|
||
'overflow-x',
|
||
'overflow-y',
|
||
])
|
||
|
||
/** 与样式等价的 HTML 属性,Word 会同时输出两者 */
|
||
const LAYOUT_LOCK_ATTRIBUTES = ['width', 'height', 'nowrap']
|
||
|
||
function sanitizeStyleAttribute(style: string): string {
|
||
return style
|
||
.split(';')
|
||
.map((declaration) => declaration.trim())
|
||
.filter(Boolean)
|
||
.filter((declaration) => {
|
||
const property = (declaration.split(':')[0] ?? '').trim().toLowerCase()
|
||
|
||
if (!property || property.startsWith('mso-')) {
|
||
return false
|
||
}
|
||
|
||
return !LAYOUT_LOCK_STYLES.has(property)
|
||
})
|
||
.join('; ')
|
||
}
|
||
|
||
/**
|
||
* 解除表格上的「布局锁定」:绝对宽度、固定行高、固定布局、不换行、内容裁剪。
|
||
* 宽度交回渲染端 CSS(`.doc-paper table { width: 100% }` 那一套)。
|
||
*
|
||
* **两个边界都要做,少一个就会出现"编辑器里与渲染出来不一样"**:
|
||
*
|
||
* - **粘贴时**(本模块的 normalizePastedHtml):Word / WPS 的表格带 pt/px 绝对宽度、
|
||
* `nowrap` 与 `overflow: hidden`;
|
||
* - **渲染时**(utils/signing-document 的 buildSigningDocumentHtml):WangEditor 的表格
|
||
* 序列化会把宽度写成**内联样式** —— 它导出的每张表都是
|
||
* `<table style="width: auto;">`(见 @wangeditor/editor 的 table elemToHtml)。
|
||
* 而内联样式优先级高于 `.doc-paper table { width: 100% }`,于是同一张表
|
||
* 在编辑器里撑满版心(编辑器 DOM 上那个 width 是**属性**、值为 `auto` 时浏览器直接忽略),
|
||
* 到预览 / 签署就变成"按内容收缩",宽度两边对不上。渲染端统一按 CSS 排版才是唯一口径。
|
||
*
|
||
* 就地修改传入的文档片段,无返回值。
|
||
*/
|
||
export function unlockTableLayout(root: ParentNode): void {
|
||
root.querySelectorAll('table, col, colgroup, td, th').forEach((element) => {
|
||
LAYOUT_LOCK_ATTRIBUTES.forEach((name) => element.removeAttribute(name))
|
||
|
||
const style = element.getAttribute('style')
|
||
|
||
if (!style) {
|
||
return
|
||
}
|
||
|
||
const sanitized = sanitizeStyleAttribute(style)
|
||
|
||
if (sanitized) {
|
||
element.setAttribute('style', sanitized)
|
||
} else {
|
||
element.removeAttribute('style')
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 归一化 Word / WPS 粘贴的富文本 HTML。
|
||
* 入参为空或解析失败时原样返回,不会抛错。
|
||
*/
|
||
export function normalizePastedHtml(html: string): string {
|
||
if (!html) {
|
||
return ''
|
||
}
|
||
|
||
const stripped = html.replace(COMMENTS, '').replace(BLOCK_TAGS, '').replace(VOID_TAGS, '')
|
||
|
||
const parsed = new DOMParser().parseFromString(`<div id="pasted-root">${stripped}</div>`, 'text/html')
|
||
const root = parsed.getElementById('pasted-root')
|
||
|
||
if (!root) {
|
||
return stripped
|
||
}
|
||
|
||
// Word 私有属性(mso-*、align 之外的 xmlns 等)在其他渲染环境里没有意义,留着反而干扰布局
|
||
root.querySelectorAll('*').forEach((element) => {
|
||
Array.from(element.attributes).forEach((attribute) => {
|
||
if (attribute.name.toLowerCase().startsWith('mso-')) {
|
||
element.removeAttribute(attribute.name)
|
||
}
|
||
})
|
||
})
|
||
|
||
// 表格、列定义与单元格:解除绝对尺寸与不换行约束,宽度交由渲染端 CSS 决定
|
||
unlockTableLayout(root)
|
||
|
||
return root.innerHTML
|
||
}
|