Files
xh-medical-sign-web/clinical-web/src/utils/rich-text.ts
T

125 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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
}