tijiao
This commit is contained in:
@@ -19,8 +19,8 @@ const service = axios.create({
|
|||||||
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
||||||
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
||||||
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
||||||
baseURL: '/api', //本地
|
// baseURL: '/api', //本地
|
||||||
// baseURL: '/', //正式
|
baseURL: '/', //正式
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Vue from 'vue';
|
|||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import mammoth from "mammoth";
|
import mammoth from "mammoth";
|
||||||
|
import { importWordDocumentWithMath as parseWordDocumentWithMath, parseHtmlToLatex as convertHtmlToLatex } from '@/utils/wordMathImport';
|
||||||
import api from '../../api/index.js';
|
import api from '../../api/index.js';
|
||||||
import Common from '@/components/common/common'
|
import Common from '@/components/common/common'
|
||||||
import Tiff from 'tiff.js';
|
import Tiff from 'tiff.js';
|
||||||
@@ -935,6 +936,14 @@ str = str.replace(regex, function (match, content, offset, fullString) {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
importWordDocumentWithMath(file) {
|
||||||
|
return parseWordDocumentWithMath(file);
|
||||||
|
},
|
||||||
|
|
||||||
|
parseHtmlToLatex(html) {
|
||||||
|
return convertHtmlToLatex(html);
|
||||||
|
},
|
||||||
|
|
||||||
cleanAndParseWordContent(content) {
|
cleanAndParseWordContent(content) {
|
||||||
// 1️⃣ 解析成 <p> 段落数组
|
// 1️⃣ 解析成 <p> 段落数组
|
||||||
let tempDiv = document.createElement('div');
|
let tempDiv = document.createElement('div');
|
||||||
@@ -2205,6 +2214,68 @@ str = str.replace(regex, function (match, content, offset, fullString) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ed.ui.registry.addButton('importWordMath', {
|
||||||
|
text: vueInstance.$t ? vueInstance.$t('commonTable.importWordMath') : 'Word',
|
||||||
|
icon: 'upload',
|
||||||
|
tooltip: vueInstance.$t ? vueInstance.$t('commonTable.importWordMathTip') : 'Import Word with formulas',
|
||||||
|
onAction: function () {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = '.docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
const file = input.files && input.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
const loading = vueInstance.$loading
|
||||||
|
? vueInstance.$loading({
|
||||||
|
lock: true,
|
||||||
|
text: vueInstance.$t ? vueInstance.$t('commonTable.importWordMathLoading') : 'Importing Word...',
|
||||||
|
spinner: 'el-icon-loading',
|
||||||
|
background: 'rgba(0, 0, 0, 0.45)'
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
parseWordDocumentWithMath(file)
|
||||||
|
.then((html) => {
|
||||||
|
if (!html) {
|
||||||
|
throw new Error('empty content');
|
||||||
|
}
|
||||||
|
ed.insertContent(html);
|
||||||
|
const body = ed.getBody();
|
||||||
|
body.querySelectorAll('wmath').forEach((el) => {
|
||||||
|
let latex = (el.getAttribute('data-latex') || '').trim();
|
||||||
|
if (!latex) return;
|
||||||
|
// 已有 $$ 或 $ 包裹则跳过
|
||||||
|
if (/^\$\$[\s\S]+\$\$$/.test(latex) || /^\$[\s\S]+\$$/.test(latex)) return;
|
||||||
|
latex = `$$${latex}$$`;
|
||||||
|
el.setAttribute('data-latex', latex);
|
||||||
|
el.innerHTML = latex;
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
window.renderMathJax(ed.id);
|
||||||
|
}, 10);
|
||||||
|
if (vueInstance.$message) {
|
||||||
|
vueInstance.$message.success(
|
||||||
|
vueInstance.$t ? vueInstance.$t('commonTable.importWordMathSuccess') : 'Word imported'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('importWordMath failed', err);
|
||||||
|
if (vueInstance.$message) {
|
||||||
|
vueInstance.$message.error(
|
||||||
|
vueInstance.$t ? vueInstance.$t('commonTable.importWordMathFail') : 'Word import failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (loading && typeof loading.close === 'function') {
|
||||||
|
loading.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1078,6 +1078,11 @@ const en = {
|
|||||||
selectComment: 'Please select the text to add annotations to!',
|
selectComment: 'Please select the text to add annotations to!',
|
||||||
selectLinkText: 'Please select the target text content before associating it with a figure or table.',
|
selectLinkText: 'Please select the target text content before associating it with a figure or table.',
|
||||||
selectWord: 'Please select only a single word!',
|
selectWord: 'Please select only a single word!',
|
||||||
|
importWordMath: 'Word',
|
||||||
|
importWordMathTip: 'Upload Word and parse math formulas',
|
||||||
|
importWordMathLoading: 'Importing Word...',
|
||||||
|
importWordMathSuccess: 'Word imported. Formulas converted to LaTeX.',
|
||||||
|
importWordMathFail: 'Word import failed. Please check the file format.',
|
||||||
selectOne: 'Please select only a single paragraph!',
|
selectOne: 'Please select only a single paragraph!',
|
||||||
alreadyCommented: 'There are already annotations in the text, please select again!',
|
alreadyCommented: 'There are already annotations in the text, please select again!',
|
||||||
Multicolumn: 'Multicolumn',
|
Multicolumn: 'Multicolumn',
|
||||||
|
|||||||
@@ -1064,6 +1064,11 @@ const zh = {
|
|||||||
selectComment: '请选择要添加批注的文本',
|
selectComment: '请选择要添加批注的文本',
|
||||||
selectLinkText: '执行图表关联前,请先选定目标文本内容',
|
selectLinkText: '执行图表关联前,请先选定目标文本内容',
|
||||||
selectWord:'请只选中单个单词!',
|
selectWord:'请只选中单个单词!',
|
||||||
|
importWordMath: 'Word',
|
||||||
|
importWordMathTip: '上传 Word 并解析数学公式',
|
||||||
|
importWordMathLoading: '正在导入 Word...',
|
||||||
|
importWordMathSuccess: 'Word 导入成功,公式已转为 LaTeX',
|
||||||
|
importWordMathFail: 'Word 导入失败,请检查文件格式',
|
||||||
selectOne:'请只勾选单个段落!',
|
selectOne:'请只勾选单个段落!',
|
||||||
alreadyCommented:'文本中已有批注内容请重新选择',
|
alreadyCommented:'文本中已有批注内容请重新选择',
|
||||||
Multicolumn:'多列',
|
Multicolumn:'多列',
|
||||||
|
|||||||
@@ -129,14 +129,17 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value: {
|
value: {
|
||||||
|
|
||||||
handler(val) {
|
handler(val) {
|
||||||
if (!this.hasChange && this.hasInit) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
window.tinymce.get(this.tinymceId).setContent(val);
|
if (!this.hasChange && this.hasInit) {
|
||||||
});
|
this.$nextTick(() => {
|
||||||
}
|
window.tinymce.get(this.tinymceId).setContent(val);
|
||||||
},
|
});
|
||||||
immediate: true
|
}
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -484,10 +487,7 @@ export default {
|
|||||||
|
|
||||||
if (!this.isPlainLatexText(normalized)) return '';
|
if (!this.isPlainLatexText(normalized)) return '';
|
||||||
|
|
||||||
const lines = normalized
|
const lines = normalized.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
||||||
.split(/\n/)
|
|
||||||
.map((l) => l.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
if (lines.length > 1 && lines.every((l) => this.isPlainLatexText(l))) {
|
if (lines.length > 1 && lines.every((l) => this.isPlainLatexText(l))) {
|
||||||
return lines.map((l) => this.createWmathHtml(l, this.resolveWmathWrapMode(l))).join('');
|
return lines.map((l) => this.createWmathHtml(l, this.resolveWmathWrapMode(l))).join('');
|
||||||
}
|
}
|
||||||
@@ -531,86 +531,68 @@ export default {
|
|||||||
return new Blob([u8arr], { type: mime });
|
return new Blob([u8arr], { type: mime });
|
||||||
},
|
},
|
||||||
formatHtml(val) {
|
formatHtml(val) {
|
||||||
const rawValue = val || ''; // 处理 null
|
const rawValue = val || ''; // 处理 null
|
||||||
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
||||||
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
||||||
const removeBr = /<br\s*\/?>/gi; // 移除所有 br 标签
|
const removeBr = /<br\s*\/?>/gi; // 移除所有 br 标签
|
||||||
|
|
||||||
if (rawValue.includes('wordTableHtml')) {
|
if (rawValue.includes('wordTableHtml')) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(rawValue, 'text/html');
|
const doc = parser.parseFromString(rawValue, 'text/html');
|
||||||
const cells = doc.querySelectorAll('td, th');
|
const cells = doc.querySelectorAll('td, th');
|
||||||
|
|
||||||
cells.forEach((cell) => {
|
cells.forEach((cell) => {
|
||||||
cell.innerHTML = cell.innerHTML
|
cell.innerHTML = cell.innerHTML
|
||||||
.replace(cleanEmptyTags, '')
|
.replace(cleanEmptyTags, '')
|
||||||
.replace(removeBr, '') // 针对你“不想要br”的需求
|
.replace(removeBr, '') // 针对你“不想要br”的需求
|
||||||
.replace(replaceSpaces, ' ');
|
.replace(replaceSpaces, ' ');
|
||||||
});
|
});
|
||||||
return doc.body.innerHTML;
|
return doc.body.innerHTML;
|
||||||
} else {
|
} else {
|
||||||
return rawValue.replace(cleanEmptyTags, '').replace(removeBr, '').replace(replaceSpaces, ' ');
|
return rawValue
|
||||||
}
|
.replace(cleanEmptyTags, '')
|
||||||
},
|
.replace(removeBr, '')
|
||||||
getSafeContent(val) {
|
.replace(replaceSpaces, ' ');
|
||||||
const rawValue = val || '';
|
}
|
||||||
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
},
|
||||||
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
getSafeContent(val) {
|
||||||
|
const rawValue = val || '';
|
||||||
|
const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g;
|
||||||
|
const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g;
|
||||||
|
|
||||||
const escapeIllegalLT = (str) => {
|
|
||||||
return str.replace(
|
|
||||||
/<(?!(\/?(p|div|span|table|tr|td|th|b|i|strong|em|ul|ol|li|br|img|myh3|myfigure|mytable|wmath)))/gi,
|
|
||||||
'<'
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let processedHtml = '';
|
const escapeIllegalLT = (str) => {
|
||||||
|
return str.replace(/<(?!(\/?(p|div|span|table|tr|td|th|b|i|strong|em|ul|ol|li|br|img|myh3|myfigure|mytable|wmath)))/gi, '<');
|
||||||
|
};
|
||||||
|
|
||||||
if (rawValue.includes('wordTableHtml')) {
|
let processedHtml = '';
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(rawValue, 'text/html');
|
|
||||||
const cells = doc.querySelectorAll('td, th');
|
|
||||||
|
|
||||||
cells.forEach((cell) => {
|
if (rawValue.includes('wordTableHtml')) {
|
||||||
let cellText = cell.innerHTML;
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(rawValue, 'text/html');
|
||||||
|
const cells = doc.querySelectorAll('td, th');
|
||||||
|
|
||||||
cell.innerHTML = cellText.replace(cleanEmptyTags, '').replace(replaceSpaces, ' ');
|
cells.forEach((cell) => {
|
||||||
});
|
|
||||||
processedHtml = doc.body.innerHTML;
|
|
||||||
} else {
|
|
||||||
processedHtml = rawValue.replace(cleanEmptyTags, '').replace(replaceSpaces, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
return processedHtml;
|
let cellText = cell.innerHTML;
|
||||||
},
|
|
||||||
parseWordLinearTextToStandardLatex(text) {
|
|
||||||
let result = text;
|
|
||||||
|
|
||||||
// 🌟 动态特征一:修复分式与包裹大括号(如把 13(...) 转换为 \frac{1}{3}\left( ... \right))
|
cell.innerHTML = cellText
|
||||||
// 通过捕捉连续的两位数字和紧跟的括号,不管它是 12 还是 13 还是 25,动态拆分为分子分母
|
.replace(cleanEmptyTags, '')
|
||||||
result = result.replace(/^(\d)(\d)\s*\(([\s\S]+)\)$/g, '\\frac{$1}{$2}\\left( $3 \\right)');
|
.replace(replaceSpaces, ' ');
|
||||||
|
});
|
||||||
|
processedHtml = doc.body.innerHTML;
|
||||||
|
} else {
|
||||||
|
|
||||||
// 🌟 动态特征二:学术公式多变量复合下标高精度自动恢复(如 MIi 变成 \widehat{\text{MI}}_i)
|
processedHtml = rawValue
|
||||||
// 抽象规律:大写字母组成的复合单词(代表一个统计变量),后面紧跟着一个用于做索引循环的小写字母(如 i, j, k, n)
|
.replace(cleanEmptyTags, '')
|
||||||
// 我们用正则边界 \b 精准识别这种大小写交替的数学边界,动态完成 \text 包裹和 _ 下标追加
|
.replace(replaceSpaces, ' ');
|
||||||
result = result.replace(/\b([A-Z]{2,})([ijkmn])\b/g, '\\widehat{\\text{$1 exterior_flag}}_$2');
|
}
|
||||||
result = result.replace(/ exterior_flag/g, ''); // 清理临时标记
|
|
||||||
|
|
||||||
// 🌟 动态特征三:单字母变量的帽子与下标高精度自动恢复(如 Fi 变成 \widehat{\text{F}}_i)
|
return processedHtml;
|
||||||
// 抽象规律:单个大写字母后面紧跟单个小写字母索引。同时通过排除机制 (?!SGMS) 确保左边的复合变量不被错误套上帽子
|
},
|
||||||
result = result.replace(/\b(?!SGMS)([A-Z])([ijkmn\d])\b/g, '\\widehat{\\text{$1 single_flag}}_$2');
|
|
||||||
result = result.replace(/ single_flag/g, '');
|
|
||||||
|
|
||||||
// 🌟 动态特征四:左侧纯主变量的普通下标处理(如 SGMSi 变成 \text{SGMS}_i,不需要加帽子)
|
|
||||||
// 匹配任何在等号左侧或独立区域的、不需要戴帽子的复合纯文本下标变量
|
|
||||||
result = result.replace(/\b([A-Z]{3,})([ijkmn\d])\b/g, '\\text{$1}_$2');
|
|
||||||
|
|
||||||
// 🌟 动态特征五:基础数学连字符规范化
|
|
||||||
result = result.replace(/\s*\*\s*/g, ' \\cdot ');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
initTinymce() {
|
initTinymce() {
|
||||||
|
|
||||||
var _this = this;
|
var _this = this;
|
||||||
window.tinymce.init({
|
window.tinymce.init({
|
||||||
..._this.tinymceOtherInit,
|
..._this.tinymceOtherInit,
|
||||||
@@ -783,56 +765,6 @@ export default {
|
|||||||
|
|
||||||
ed.on('paste', async (event) => {
|
ed.on('paste', async (event) => {
|
||||||
const rtf = event.clipboardData.getData('text/rtf');
|
const rtf = event.clipboardData.getData('text/rtf');
|
||||||
console.log('🚀 ~ setup ~ rtf:', rtf);
|
|
||||||
|
|
||||||
let plainText = event.clipboardData.getData('text/plain') || '';
|
|
||||||
|
|
||||||
// ========================================================
|
|
||||||
// 1. 【通用公式内核判定】—— 绝不绑定任何具体公式的字母
|
|
||||||
// ========================================================
|
|
||||||
// 只要 RTF 中包含微软 Office 原生公式对象标记(objdata / mmath / object),
|
|
||||||
// 并且纯文本里有数学等号,说明当前用户复制的 100% 是一个公式对象,而不是普通插图!
|
|
||||||
const isWordFormulaObject =
|
|
||||||
rtf.includes('\\rtf') &&
|
|
||||||
(rtf.includes('objdata') || rtf.includes('\\mmath') || rtf.includes('\\object')) &&
|
|
||||||
plainText.includes('=');
|
|
||||||
|
|
||||||
if (isWordFormulaObject) {
|
|
||||||
// 【第一步:绝对截胡断流】
|
|
||||||
// 强行扼杀浏览器的默认粘贴行为,并阻止事件冒泡。
|
|
||||||
// 这会使下方原本属于普通插图的图片转换、接口上传、绿色进度条等代码直接被彻底绕过!
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
let finalLatex = '';
|
|
||||||
|
|
||||||
// 如果用户复制时,纯文本里已经带有标准 LaTeX 控制符了(比如 \frac, \hat 等)
|
|
||||||
if (/\\frac|\\hat|\\sqrt|\\alpha/i.test(plainText)) {
|
|
||||||
finalLatex = plainText.trim();
|
|
||||||
}
|
|
||||||
// 【核心动态翻译引擎】如果拿到的只是被 Word 阉割后的线性纯文本,
|
|
||||||
// 我们基于通用的“数学结构特征”进行全自动高保真翻译,不写死任何具体的变量名!
|
|
||||||
else if (plainText.trim()) {
|
|
||||||
finalLatex = parseWordLinearTextToStandardLatex(plainText.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================================
|
|
||||||
// 【高保真 LaTeX 公式注入】
|
|
||||||
// ========================================================
|
|
||||||
if (finalLatex) {
|
|
||||||
// 清洗掉首尾可能重复的 $ 符号
|
|
||||||
finalLatex = finalLatex.replace(/^(\$\$?)|(\$\$?)$/g, '').trim();
|
|
||||||
|
|
||||||
// 包装成标准的块级公式
|
|
||||||
const latexContainer = `$$${finalLatex}$$`;
|
|
||||||
|
|
||||||
ed.insertContent(latexContainer);
|
|
||||||
console.log('【通用架构级公式转换成功】:', latexContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
return; // 【绝杀】直接中断整个 paste 函数,下面你原有的图片代码直接气化,绝不触发上传!
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rtf && rtf.includes('\\pict')) {
|
if (rtf && rtf.includes('\\pict')) {
|
||||||
const extracted = extractHexImagesFromRTF(rtf);
|
const extracted = extractHexImagesFromRTF(rtf);
|
||||||
_this.totalUploadImages = extracted.length; // 设置总数
|
_this.totalUploadImages = extracted.length; // 设置总数
|
||||||
@@ -898,36 +830,40 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
ed.on('init', function () {
|
ed.on('init', function () {
|
||||||
_this.editorInstance = ed;
|
_this.editorInstance = ed;
|
||||||
_this.hasInit = true;
|
_this.hasInit = true;
|
||||||
_this.$commonJS.inTinymceButtonClass();
|
_this.$commonJS.inTinymceButtonClass();
|
||||||
if (_this.isAutomaticUpdate) {
|
if (_this.isAutomaticUpdate) {
|
||||||
_this.$emit('updateChange', _this.value);
|
_this.$emit('updateChange', _this.value);
|
||||||
}
|
|
||||||
_this.content = _this.getSafeContent(_this.value);
|
|
||||||
|
|
||||||
_this.handleSetContent(_this.content || '');
|
|
||||||
|
|
||||||
// 3. 监听内容变化
|
}
|
||||||
ed.on('NodeChange Change KeyUp SetContent', () => {
|
_this.content = _this.getSafeContent(_this.value);
|
||||||
_this.hasChange = true;
|
|
||||||
_this.$emit('input', ed.getContent({ format: 'raw' }));
|
|
||||||
});
|
|
||||||
|
|
||||||
// 4. 监听 DOM 变化
|
_this.handleSetContent(_this.content || '');
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const currentContent = ed.getContent({ format: 'raw' });
|
|
||||||
if (_this.isAutomaticUpdate) {
|
|
||||||
_this.$emit('updateChange', currentContent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(ed.getBody(), {
|
// 3. 监听内容变化
|
||||||
childList: true,
|
ed.on('NodeChange Change KeyUp SetContent', () => {
|
||||||
subtree: true,
|
_this.hasChange = true;
|
||||||
characterData: true
|
_this.$emit('input', ed.getContent({ format: 'raw' }));
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// 4. 监听 DOM 变化
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
const currentContent = ed.getContent({ format: 'raw' });
|
||||||
|
if (_this.isAutomaticUpdate) {
|
||||||
|
_this.$emit('updateChange', currentContent);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(ed.getBody(), {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 定义自定义按钮
|
// 定义自定义按钮
|
||||||
ed.ui.registry.addButton('customButtonExportWord', {
|
ed.ui.registry.addButton('customButtonExportWord', {
|
||||||
@@ -985,6 +921,7 @@ export default {
|
|||||||
|
|
||||||
if (tempDiv.querySelector('table')) {
|
if (tempDiv.querySelector('table')) {
|
||||||
if (_this.type == 'table') {
|
if (_this.type == 'table') {
|
||||||
|
|
||||||
_this.$commonJS.parseTableToArray(content, (tableList) => {
|
_this.$commonJS.parseTableToArray(content, (tableList) => {
|
||||||
var contentHtml = `
|
var contentHtml = `
|
||||||
<div class="thumbnailTableBox wordTableHtml table_Box table_Box3333" style="">
|
<div class="thumbnailTableBox wordTableHtml table_Box table_Box3333" style="">
|
||||||
|
|||||||
286
src/utils/wordMathImport.js
Normal file
286
src/utils/wordMathImport.js
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import JSZip from 'jszip';
|
||||||
|
import mammoth from 'mammoth';
|
||||||
|
|
||||||
|
const MATH_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/math';
|
||||||
|
const WORD_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
||||||
|
|
||||||
|
function localName(node) {
|
||||||
|
if (!node || !node.tagName) return '';
|
||||||
|
return (node.localName || node.tagName.split(':').pop() || '').toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeLatexForAttr(latex) {
|
||||||
|
return String(latex || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWmathHtml(latex, wrap) {
|
||||||
|
const raw = String(latex || '').trim();
|
||||||
|
if (!raw) return '';
|
||||||
|
const uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
const stored = `${raw}`;
|
||||||
|
|
||||||
|
const safe = escapeLatexForAttr(stored);
|
||||||
|
const mode =
|
||||||
|
wrap === 'inline' || wrap === 'block'
|
||||||
|
? wrap
|
||||||
|
: /\\begin\{|\\frac|\\sum|\\int|\\prod|\\lim|\\sqrt|\\displaystyle/.test(raw) || raw.length > 80
|
||||||
|
? 'block'
|
||||||
|
: 'inline';
|
||||||
|
return `<wmath contenteditable="false" data-id="${uid}" data-latex="${safe}" data-wrap="${mode}">1</wmath>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstByLocal(parent, name) {
|
||||||
|
if (!parent) return null;
|
||||||
|
return Array.from(parent.children || []).find((child) => localName(child) === name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function allByLocal(parent, name) {
|
||||||
|
if (!parent) return [];
|
||||||
|
return Array.from(parent.children || []).filter((child) => localName(child) === name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkOmml(node) {
|
||||||
|
if (!node) return '';
|
||||||
|
const name = localName(node);
|
||||||
|
|
||||||
|
if (name === 't') {
|
||||||
|
return (node.textContent || '').replace(/\u00a0/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'r') {
|
||||||
|
return allByLocal(node, 't')
|
||||||
|
.map((t) => (t.textContent || '').replace(/\u00a0/g, ' '))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'f') {
|
||||||
|
const num = firstByLocal(node, 'num');
|
||||||
|
const den = firstByLocal(node, 'den');
|
||||||
|
return `\\frac{${walkOmmlChildren(num)}}{${walkOmmlChildren(den)}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'rad') {
|
||||||
|
const deg = firstByLocal(node, 'deg');
|
||||||
|
const base = firstByLocal(node, 'e') || node;
|
||||||
|
const inner = walkOmmlChildren(base);
|
||||||
|
const degree = deg ? walkOmmlChildren(deg) : '';
|
||||||
|
return degree ? `\\sqrt[${degree}]{${inner}}` : `\\sqrt{${inner}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'ssup') {
|
||||||
|
return `${walkOmmlChildren(firstByLocal(node, 'e'))}^{${walkOmmlChildren(firstByLocal(node, 'sup'))}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'ssub') {
|
||||||
|
return `${walkOmmlChildren(firstByLocal(node, 'e'))}_{${walkOmmlChildren(firstByLocal(node, 'sub'))}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'ssubsup') {
|
||||||
|
const base = walkOmmlChildren(firstByLocal(node, 'e'));
|
||||||
|
const sub = walkOmmlChildren(firstByLocal(node, 'sub'));
|
||||||
|
const sup = walkOmmlChildren(firstByLocal(node, 'sup'));
|
||||||
|
return `${base}_{${sub}}^{${sup}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sSub') {
|
||||||
|
return `${walkOmmlChildren(firstByLocal(node, 'e'))}_{${walkOmmlChildren(firstByLocal(node, 'sub'))}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sSup') {
|
||||||
|
return `${walkOmmlChildren(firstByLocal(node, 'e'))}^{${walkOmmlChildren(firstByLocal(node, 'sup'))}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'd') {
|
||||||
|
const begChr = node.getAttribute('m:begChr') || node.getAttribute('begChr') || '(';
|
||||||
|
const endChr = node.getAttribute('m:endChr') || node.getAttribute('endChr') || ')';
|
||||||
|
const inner = allByLocal(node, 'e')
|
||||||
|
.map((item) => walkOmmlChildren(item))
|
||||||
|
.join('');
|
||||||
|
if (begChr === '(' && endChr === ')') return `\\left(${inner}\\right)`;
|
||||||
|
if (begChr === '[' && endChr === ']') return `\\left[${inner}\\right]`;
|
||||||
|
if (begChr === '{' && endChr === '}') return `\\left\\{${inner}\\right\\}`;
|
||||||
|
return `${begChr}${inner}${endChr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'nary') {
|
||||||
|
const chr = node.getAttribute('m:chr') || node.getAttribute('chr') || '\\int';
|
||||||
|
const sub = walkOmmlChildren(firstByLocal(node, 'sub'));
|
||||||
|
const sup = walkOmmlChildren(firstByLocal(node, 'sup'));
|
||||||
|
const body = walkOmmlChildren(firstByLocal(node, 'e'));
|
||||||
|
let op = chr;
|
||||||
|
if (chr === '∫') op = '\\int';
|
||||||
|
if (chr === '∑') op = '\\sum';
|
||||||
|
if (chr === '∏') op = '\\prod';
|
||||||
|
let result = op;
|
||||||
|
if (sub) result += `_{${sub}}`;
|
||||||
|
if (sup) result += `^{${sup}}`;
|
||||||
|
result += `{${body}}`;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'eqarr' || name === 'm') {
|
||||||
|
return walkOmmlChildren(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'e') {
|
||||||
|
return walkOmmlChildren(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'omath' || name === 'omathpara') {
|
||||||
|
return walkOmmlChildren(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
return walkOmmlChildren(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function walkOmmlChildren(node) {
|
||||||
|
if (!node) return '';
|
||||||
|
return Array.from(node.children || [])
|
||||||
|
.map((child) => walkOmml(child))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ommlNodeToLatex(node) {
|
||||||
|
if (!node) return '';
|
||||||
|
return walkOmml(node).replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunBold(run) {
|
||||||
|
const rPr = firstByLocal(run, 'rPr');
|
||||||
|
if (!rPr) return false;
|
||||||
|
return !!firstByLocal(rPr, 'b');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRunItalic(run) {
|
||||||
|
const rPr = firstByLocal(run, 'rPr');
|
||||||
|
if (!rPr) return false;
|
||||||
|
return !!firstByLocal(rPr, 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRunVertAlign(run) {
|
||||||
|
const rPr = firstByLocal(run, 'rPr');
|
||||||
|
const vert = rPr ? firstByLocal(rPr, 'vertAlign') : null;
|
||||||
|
if (!vert) return '';
|
||||||
|
const val = vert.getAttribute('w:val') || vert.getAttribute('val') || '';
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runToHtml(run) {
|
||||||
|
let text = allByLocal(run, 't')
|
||||||
|
.map((t) => (t.textContent || '').replace(/\u00a0/g, ' '))
|
||||||
|
.join('');
|
||||||
|
if (!text) return '';
|
||||||
|
const vert = getRunVertAlign(run);
|
||||||
|
if (vert === 'superscript') text = `<sup>${text}</sup>`;
|
||||||
|
else if (vert === 'subscript') text = `<sub>${text}</sub>`;
|
||||||
|
if (isRunBold(run)) text = `<b>${text}</b>`;
|
||||||
|
if (isRunItalic(run)) text = `<i>${text}</i>`;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function paragraphToHtml(p) {
|
||||||
|
let html = '';
|
||||||
|
Array.from(p.children || []).forEach((child) => {
|
||||||
|
const name = localName(child);
|
||||||
|
if (name === 'r') {
|
||||||
|
const omathInRun = Array.from(child.children || []).find((item) => localName(item) === 'omath');
|
||||||
|
if (omathInRun) {
|
||||||
|
const latex = ommlNodeToLatex(omathInRun);
|
||||||
|
if (latex) html += createWmathHtml(latex);
|
||||||
|
} else {
|
||||||
|
html += runToHtml(child);
|
||||||
|
}
|
||||||
|
} else if (name === 'omath' || name === 'omathpara') {
|
||||||
|
const latex = ommlNodeToLatex(name === 'omathpara' ? firstByLocal(child, 'omath') || child : child);
|
||||||
|
if (latex) html += createWmathHtml(latex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return html.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableToHtml(table) {
|
||||||
|
let html = "<table border='1' style='border-collapse: collapse; width: 100%;'>";
|
||||||
|
allByLocal(table, 'tr').forEach((row) => {
|
||||||
|
html += '<tr>';
|
||||||
|
allByLocal(row, 'tc').forEach((cell) => {
|
||||||
|
let cellHtml = '';
|
||||||
|
allByLocal(cell, 'p').forEach((p) => {
|
||||||
|
cellHtml += paragraphToHtml(p);
|
||||||
|
});
|
||||||
|
html += `<td>${cellHtml || ' '}</td>`;
|
||||||
|
});
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
html += '</table>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function documentXmlToHtml(documentXml) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const xmlDoc = parser.parseFromString(documentXml, 'application/xml');
|
||||||
|
const body =
|
||||||
|
xmlDoc.getElementsByTagNameNS(WORD_NS, 'body')[0] ||
|
||||||
|
Array.from(xmlDoc.getElementsByTagName('*')).find((el) => localName(el) === 'body');
|
||||||
|
if (!body) return '';
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
Array.from(body.children || []).forEach((child) => {
|
||||||
|
const name = localName(child);
|
||||||
|
if (name === 'p') {
|
||||||
|
const inner = paragraphToHtml(child);
|
||||||
|
html += inner ? `<p>${inner}</p>` : '<p><br></p>';
|
||||||
|
} else if (name === 'tbl') {
|
||||||
|
html += tableToHtml(child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将 HTML 中的 $$...$$ / $...$ 转为 wmath 标签 */
|
||||||
|
export function parseHtmlToLatex(html) {
|
||||||
|
if (!html) return '';
|
||||||
|
let result = String(html);
|
||||||
|
|
||||||
|
result = result.replace(/\$\$([\s\S]+?)\$\$/g, (match, blockFormula) => {
|
||||||
|
const formula = String(blockFormula || '').trim();
|
||||||
|
return formula ? createWmathHtml(formula, 'block') : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
result = result.replace(/(^|[^\\])\$([^\$\n]+?)\$/g, (match, prefix, inlineFormula) => {
|
||||||
|
const formula = String(inlineFormula || '').trim();
|
||||||
|
return formula ? `${prefix}${createWmathHtml(formula, 'inline')}` : match;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function importWordDocumentWithMath(file) {
|
||||||
|
if (!file) {
|
||||||
|
throw new Error('No file selected');
|
||||||
|
}
|
||||||
|
const arrayBuffer = await readFileAsArrayBuffer(file);
|
||||||
|
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||||
|
const documentFile = zip.file('word/document.xml');
|
||||||
|
if (!documentFile) {
|
||||||
|
throw new Error('Invalid Word file: missing document.xml');
|
||||||
|
}
|
||||||
|
const documentXml = await documentFile.async('string');
|
||||||
|
let html = documentXmlToHtml(documentXml);
|
||||||
|
if (!html || !html.replace(/<[^>]+>/g, '').trim()) {
|
||||||
|
const result = await mammoth.convertToHtml({ arrayBuffer });
|
||||||
|
html = result.value || '';
|
||||||
|
}
|
||||||
|
return parseHtmlToLatex(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFileAsArrayBuffer(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => resolve(e.target.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user