/**
* DOCX MathML to OMML 增强转换器模块
* 支持更多复杂的 MathML 元素,包括矩阵、可拉伸运算符等
* 版本: 2.0.0
*
* 新增支持:
* - mtable/mtr/mtd (矩阵和表格)
* - mspace (空格)
* - mstyle (样式,部分支持)
* - menclose (包围符号)
* - mo stretchy (可拉伸运算符)
*/
(function(window) {
'use strict';
/**
* 增强的 MathML 到 OMML 转换器类
*/
class MathMlToOmmlConverterEnhanced {
/**
* 转换 MathML 元素为 OMML
* @param {Element} mathEl - MathML math 元素
* @returns {string} OMML XML 字符串
*/
convert(mathEl) {
if (!mathEl) return '';
try {
const inner = this.convertChildren(mathEl.childNodes);
if (!inner || !inner.trim()) return '';
// 验证生成的 OMML 不包含非法字符
const sanitized = this.sanitizeOmml(inner);
if (!sanitized) return '';
return `${sanitized}`;
} catch (error) {
console.warn('[MathML→OMML Enhanced] Conversion failed:', error);
return '';
}
}
/**
* 清理 OMML 内容,移除非法字符
* @param {string} omml - OMML 字符串
* @returns {string} 清理后的 OMML
*/
sanitizeOmml(omml) {
if (!omml) return '';
// 移除控制字符,但保留换行符和制表符
return String(omml).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F]/g, '');
}
/**
* 转换子节点列表
* @param {NodeList} nodeList - 子节点列表
* @returns {string} 转换后的 OMML
*/
convertChildren(nodeList) {
let result = '';
try {
Array.from(nodeList || []).forEach(node => {
const converted = this.convertNode(node);
if (converted) result += converted;
});
} catch (error) {
console.warn('[MathML→OMML Enhanced] Error converting children:', error);
}
return result;
}
/**
* 转换单个节点
* @param {Node} node - DOM 节点
* @returns {string} 转换后的 OMML
*/
convertNode(node) {
if (!node) return '';
try {
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
if (!text || !text.trim()) return '';
return this.createTextRun(text.trim());
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return '';
}
const tag = node.tagName.toLowerCase();
const childNodes = node.childNodes;
switch (tag) {
// 基础容器
case 'math':
case 'mrow':
case 'semantics':
return this.convertChildren(childNodes);
case 'annotation':
return '';
// 基础文本
case 'mi':
case 'mn':
case 'mtext':
return this.createTextRun(node.textContent || '');
// 运算符 (增强处理)
case 'mo':
return this.convertOperator(node);
// 上下标
case 'msup':
return this.convertSup(node, childNodes);
case 'msub':
return this.convertSub(node, childNodes);
case 'msubsup':
return this.convertSubSup(node, childNodes);
// 分数
case 'mfrac':
return this.convertFrac(node, childNodes);
// 根式
case 'msqrt':
return this.convertSqrt(childNodes);
case 'mroot':
return this.convertRoot(childNodes);
// 括号
case 'mfenced':
return this.convertFenced(node, childNodes);
// 上下装饰
case 'mover':
return this.convertOver(node, childNodes);
case 'munder':
return this.convertUnder(node, childNodes);
case 'munderover':
return this.convertUnderOver(node, childNodes);
// 矩阵和表格 (新增)
case 'mtable':
return this.convertTable(node, childNodes);
// 空格 (新增)
case 'mspace':
return this.convertSpace(node);
// 样式 (新增)
case 'mstyle':
return this.convertStyle(node, childNodes);
// 包围符号 (新增)
case 'menclose':
return this.convertEnclose(node, childNodes);
// 填充 (新增)
case 'mpadded':
return this.convertPadded(node, childNodes);
// 多行脚本 (新增)
case 'mmultiscripts':
return this.convertMultiscripts(node, childNodes);
default:
console.warn(`[MathML→OMML Enhanced] Unsupported tag: ${tag}`);
return this.convertChildren(childNodes);
}
} catch (error) {
console.warn('[MathML→OMML Enhanced] Error converting node:', error);
return '';
}
}
/**
* 转换运算符 (增强支持 stretchy 属性)
* @param {Element} node - mo 元素
* @returns {string} OMML
*/
convertOperator(node) {
const text = node.textContent || '';
const stretchy = node.getAttribute('stretchy');
const largeop = node.getAttribute('largeop');
// 如果是大型运算符(如求和、积分),使用特殊格式
if (largeop === 'true' || this.isLargeOperator(text)) {
return this.createNaryOperator(text);
}
// 普通运算符
return this.createTextRun(text);
}
/**
* 判断是否为大型运算符
* @param {string} text - 运算符文本
* @returns {boolean}
*/
isLargeOperator(text) {
const largeOps = ['∑', '∫', '∬', '∭', '∮', '∯', '∰', '∱', '∏', '∐', '⋃', '⋂', '⋁', '⋀'];
return largeOps.includes(text.trim());
}
/**
* 创建 N-ary 运算符 (求和、积分等)
* @param {string} operator - 运算符文本
* @returns {string} OMML
*/
createNaryOperator(operator) {
// OMML 的 nary 结构用于大型运算符
const charMap = {
'∑': '2211',
'∫': '222B',
'∬': '222C',
'∭': '222D',
'∮': '222E',
'∯': '222F',
'∰': '2230',
'∱': '2231',
'∏': '220F',
'∐': '2210'
};
const charCode = charMap[operator] || operator.charCodeAt(0).toString(16).toUpperCase();
return ``;
}
/**
* 转换上标
*/
convertSup(node, childNodes) {
if (childNodes.length < 2) return this.convertChildren(childNodes);
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}${this.wrapWith('m:sup', this.convertNode(childNodes[1]))}`;
}
/**
* 转换下标
*/
convertSub(node, childNodes) {
if (childNodes.length < 2) return this.convertChildren(childNodes);
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}${this.wrapWith('m:sub', this.convertNode(childNodes[1]))}`;
}
/**
* 转换上下标
*/
convertSubSup(node, childNodes) {
if (childNodes.length < 3) return this.convertChildren(childNodes);
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}${this.wrapWith('m:sub', this.convertNode(childNodes[1]))}${this.wrapWith('m:sup', this.convertNode(childNodes[2]))}`;
}
/**
* 转换分数
*/
convertFrac(node, childNodes) {
if (childNodes.length < 2) return this.convertChildren(childNodes);
// 检查是否有 linethickness 属性(用于控制分数线)
const lineThickness = node.getAttribute('linethickness');
const noLine = lineThickness === '0' || lineThickness === '0pt';
if (noLine) {
// 无分数线,使用 stack
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}${this.convertNode(childNodes[1])}`;
}
return `${this.wrapWith('m:num', this.convertNode(childNodes[0]))}${this.wrapWith('m:den', this.convertNode(childNodes[1]))}`;
}
/**
* 转换平方根
*/
convertSqrt(childNodes) {
return `${this.wrapWith('m:e', this.convertChildren(childNodes))}`;
}
/**
* 转换根式
*/
convertRoot(childNodes) {
if (childNodes.length < 2) return this.convertChildren(childNodes);
return `${this.wrapWith('m:deg', this.convertNode(childNodes[1]))}${this.wrapWith('m:e', this.convertNode(childNodes[0]))}`;
}
/**
* 转换括号
*/
convertFenced(node, childNodes) {
const open = node.getAttribute('open') || '(';
const close = node.getAttribute('close') || ')';
return `${this.createTextRun(open)}${this.convertChildren(childNodes)}${this.createTextRun(close)}`;
}
/**
* 转换上装饰
*/
convertOver(node, childNodes) {
if (childNodes.length < 2) return this.convertChildren(childNodes);
const accent = node.getAttribute('accent');
if (accent === 'true') {
// 重音符号
const accentChar = childNodes[1].textContent || '';
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}`;
}
// 普通上标
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}${this.wrapWith('m:lim', this.convertNode(childNodes[1]))}`;
}
/**
* 转换下装饰
*/
convertUnder(node, childNodes) {
if (childNodes.length < 2) return this.convertChildren(childNodes);
const accentunder = node.getAttribute('accentunder');
if (accentunder === 'true') {
// 下重音
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}`;
}
// 普通下标
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}${this.wrapWith('m:lim', this.convertNode(childNodes[1]))}`;
}
/**
* 转换上下装饰
*/
convertUnderOver(node, childNodes) {
if (childNodes.length < 3) return this.convertChildren(childNodes);
return `${this.wrapWith('m:e', this.convertNode(childNodes[0]))}`;
}
/**
* 转换矩阵 (新增 - 最重要的功能!)
* @param {Element} node - mtable 元素
* @param {NodeList} childNodes - 子节点
* @returns {string} OMML
*/
convertTable(node, childNodes) {
const rows = Array.from(childNodes).filter(child =>
child.nodeType === Node.ELEMENT_NODE && child.tagName.toLowerCase() === 'mtr'
);
if (rows.length === 0) {
return this.convertChildren(childNodes);
}
// 检查是否有括号(通过 columnalign 或其他属性判断)
const columnalign = node.getAttribute('columnalign') || 'center';
// 构建矩阵
let omml = '';
rows.forEach(row => {
const cells = Array.from(row.childNodes).filter(child =>
child.nodeType === Node.ELEMENT_NODE && child.tagName.toLowerCase() === 'mtd'
);
omml += '';
cells.forEach(cell => {
omml += `${this.convertChildren(cell.childNodes)}`;
});
omml += '';
});
omml += '';
// 添加矩阵括号(可选)
const frame = node.getAttribute('frame');
if (frame) {
// 用 borderBox 包裹
return `${omml}`;
}
return omml;
}
/**
* 转换空格 (新增)
* @param {Element} node - mspace 元素
* @returns {string} OMML
*/
convertSpace(node) {
const width = node.getAttribute('width') || '1em';
// OMML 中使用空格字符
return this.createTextRun(' ');
}
/**
* 转换样式 (新增)
* @param {Element} node - mstyle 元素
* @param {NodeList} childNodes - 子节点
* @returns {string} OMML
*/
convertStyle(node, childNodes) {
// OMML 中样式通常通过属性控制,这里简单处理
// 可以根据 mathvariant, mathsize 等属性调整
return this.convertChildren(childNodes);
}
/**
* 转换包围符号 (新增)
* @param {Element} node - menclose 元素
* @param {NodeList} childNodes - 子节点
* @returns {string} OMML
*/
convertEnclose(node, childNodes) {
const notation = node.getAttribute('notation') || 'longdiv';
switch (notation) {
case 'box':
case 'roundedbox':
return `${this.wrapWith('m:e', this.convertChildren(childNodes))}`;
case 'circle':
return `${this.wrapWith('m:e', this.convertChildren(childNodes))}`;
case 'top':
case 'bottom':
case 'left':
case 'right':
return `${this.wrapWith('m:e', this.convertChildren(childNodes))}`;
default:
return this.convertChildren(childNodes);
}
}
/**
* 转换填充 (新增)
* @param {Element} node - mpadded 元素
* @param {NodeList} childNodes - 子节点
* @returns {string} OMML
*/
convertPadded(node, childNodes) {
// OMML 不直接支持 padding,简单忽略
return this.convertChildren(childNodes);
}
/**
* 转换多行脚本 (新增)
* @param {Element} node - mmultiscripts 元素
* @param {NodeList} childNodes - 子节点
* @returns {string} OMML
*/
convertMultiscripts(node, childNodes) {
// 简化处理:转换为连续的上下标
return this.convertChildren(childNodes);
}
/**
* 用标签包裹内容
* @param {string} tag - 标签名
* @param {string} content - 内容
* @returns {string} 包裹后的 XML
*/
wrapWith(tag, content) {
if (!content || !content.trim()) {
// 空内容时返回空格占位,防止生成空标签
return `<${tag}>${this.createTextRun(' ')}${tag}>`;
}
return `<${tag}>${content}${tag}>`;
}
/**
* 创建文本运行
* @param {string} text - 文本内容
* @returns {string} OMML 文本运行 XML
*/
createTextRun(text) {
const normalized = text ? text.replace(/\s+/g, ' ').trim() : '';
if (!normalized) {
return ' ';
}
// 使用全局的 escapeXml 函数
const escapeXml = window.PBXDocxXmlUtils?.escapeXml || function(str) {
if (!str) return '';
let result = String(str);
result = result.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F]/g, '');
return result.replace(/[&<>"']/g, function(ch) {
switch (ch) {
case '&': return '&';
case '<': return '<';
case '>': return '>';
case '"': return '"';
case "'": return ''';
default: return ch;
}
});
};
return `${escapeXml(normalized)}`;
}
}
// 导出到全局
window.PBXMathMlToOmmlConverterEnhanced = MathMlToOmmlConverterEnhanced;
console.log('%c[DOCX Math] ✨ 增强 MathML → OMML 转换器已加载', 'color: #3b82f6; font-weight: bold');
console.log('新增支持: 矩阵 | 可拉伸运算符 | 空格 | 包围符号');
})(window);