This commit is contained in:
2026-06-04 17:38:10 +08:00
parent def97d62ef
commit 77703a855a
23 changed files with 1610 additions and 172 deletions

View File

@@ -32,17 +32,21 @@ function stripLatexDelimiters(latex) {
.trim();
}
/** 块/行内统一用 $$ 渲染,保证公式大小一致;排版由 data-wrap + CSS 控制 */
function buildLatexContentForRender(latex) {
/** 按 data-wrap 选择行内 $ 或块级 $$ 分隔符 */
function buildLatexContentForRender(latex, wrapMode) {
const raw = stripLatexDelimiters(latex);
if (!raw) return '';
if (wrapMode === 'inline') {
return '$' + raw + '$';
}
return '$$' + raw + '$$';
}
function prepareWmathElement(element) {
const latexContent = element.getAttribute('data-latex');
if (!latexContent) return;
element.innerHTML = buildLatexContentForRender(latexContent);
const wrap = element.getAttribute('data-wrap') || 'block';
element.innerHTML = buildLatexContentForRender(latexContent, wrap);
}
// **定义全局渲染方法**

View File

@@ -2,14 +2,14 @@
//记得切换
//正式
// const mediaUrl = '/public/';
// const baseUrl = '/';
const mediaUrl = '/public/';
const baseUrl = '/';
//正式环境
const mediaUrl = 'https://submission.tmrjournals.com/public/';
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
const baseUrl = '/api'
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
// const baseUrl = '/api'
//测试环境

View File

@@ -1087,11 +1087,11 @@ const en = {
importWordMathNoNew: 'All formulas already exist. No new rows added.',
importWordMathParsed: 'Word parsed: {parsed} formula(s) found, {added} added.',
importWordMathParsedNone: 'Word parsed: {parsed} formula(s) found; all already exist.',
mathFormula: 'Latex',
mathFormula: 'LaTeX-compliant',
mathFormulaMore: 'more formulas',
latexDataCopy: 'Copy',
latexDataCopied: 'Copied',
latexDataClickToCopy: 'Click a formula to copy',
latexDataClickToCopy: 'Click a row number or formula to copy',
latexDataAdd: 'Add',
latexDataEdit: 'Edit',
latexDataDelete: 'Delete',
@@ -1108,6 +1108,23 @@ const en = {
latexMathEditorTooltip: 'Insert or edit a LaTeX formula',
latexMathConfirm: 'OK',
latexDataCancel: 'Cancel',
latexDataRowIssueTip: 'This formula may have recognition or rendering issues; click Edit to review',
latexDataIssueSummary: '{count} formula(s) failed to render (rows {indexes}). Row numbers are in red. This notice hides once all are fixed and saved.',
latexDataIssueInlinePrefix: '{count} failed to render:',
latexDataIssueLocate: 'Locate issues ({count})',
latexDataWordDownloadHtml: 'Download source HTML',
latexDataWordPreviewTitle: 'Word formula source report',
latexDataWordPreviewHint:
'Badge number = formula list row. No badge = not in the list. Top links are failed rows only.',
latexDataWordReportIssuePrefix: 'Fix row:',
latexDataWordPreviewLegendOk: 'Green · OK',
latexDataWordPreviewLegendErr: 'Red · needs fix',
latexDataWordPreviewLegendDup: 'Orange · duplicate',
latexDataWordReportDupPrefix: 'Duplicate rows (same LaTeX):',
latexDataWordReportDupGroup: 'Same formula',
latexDataWordReportDupTag: 'Dup #{n}',
latexDataWordReportDupCount: '×{n}',
latexDataWordReportDownloaded: 'Source HTML downloaded. Open it in your browser.',
latexDataOk: 'Save',
editMathFormulaSuccess: 'Math formulas updated successfully.',
selectOne: 'Please select only a single paragraph',
@@ -1591,6 +1608,7 @@ const en = {
latestInvitationLabel: 'Latest invitation: ',
affiliation: 'Affiliation: ',
field: 'Field: ',
fieldAi: 'AI Field: ',
detail: 'Detail',
select: 'Select',
reviewing: 'Reviewing',
@@ -1605,6 +1623,7 @@ const en = {
dialogAffiliation: 'Affiliation :',
dialogResearchAreas: 'Research areas :',
dialogField: 'Field :',
dialogFieldAi: 'AI Field :',
dialogIntroduction: 'Introduction :',
cancel: 'Cancel'
},

View File

@@ -1073,7 +1073,7 @@ const zh = {
importWordMathNoNew: '公式已存在,未新增重复项',
importWordMathParsed: 'Word 解析完成:识别 {parsed} 个公式,新增 {added} 个',
importWordMathParsedNone: 'Word 解析完成:识别 {parsed} 个公式,均已存在,未新增',
mathFormula: 'Latex',
mathFormula: 'LaTeX-compliant',
mathFormulaMore: '条公式',
latexDataCopy: '复制',
latexDataCopied: '已复制',
@@ -1095,6 +1095,22 @@ const zh = {
latexMathConfirm: '确定',
latexDataCancel: '取消',
latexDataOk: '保存',
latexDataRowIssueTip: '该公式可能识别异常或未正确渲染,建议点「修改」检查',
latexDataIssueSummary: '共 {count} 处未解析成功(序号 {indexes}),对应序号已标红;全部修正保存后本提示将自动消失',
latexDataIssueInlinePrefix: '共 {count} 处未解析:',
latexDataIssueLocate: '定位未解析({count}',
latexDataWordDownloadHtml: '下载源稿 HTML',
latexDataWordPreviewTitle: 'Word 公式源稿报告',
latexDataWordPreviewHint: '角标数字=公式列表行号;无角标表示该处公式不在列表中。顶部仅列出需修改的行号。',
latexDataWordReportIssuePrefix: '需修改:',
latexDataWordPreviewLegendOk: '绿色 · 通过',
latexDataWordPreviewLegendErr: '红色 · 需修改',
latexDataWordPreviewLegendDup: '橙色 · 重复公式',
latexDataWordReportDupPrefix: '重复序号(相同 LaTeX点击跳转',
latexDataWordReportDupGroup: '相同公式序号',
latexDataWordReportDupTag: '重复#{n}',
latexDataWordReportDupCount: '×{n}',
latexDataWordReportDownloaded: '已生成并下载源稿 HTML请用浏览器打开查看',
editMathFormulaSuccess: '数字公式更新成功',
selectOne:'请只勾选单个段落!',
alreadyCommented:'文本中已有批注内容请重新选择',
@@ -1572,6 +1588,7 @@ const zh = {
latestInvitationLabel: '最新邀请:',
affiliation: '单位:',
field: '领域:',
fieldAi: 'AI 领域:',
detail: '详情',
select: '选择',
reviewing: '审稿中',
@@ -1586,6 +1603,7 @@ const zh = {
dialogAffiliation: '单位:',
dialogResearchAreas: '研究领域:',
dialogField: '领域:',
dialogFieldAi: 'AI 领域:',
dialogIntroduction: '简介:',
cancel: '取消'
},

View File

@@ -415,6 +415,7 @@
type="content"
@openLatexEditor="openLatexEditor"
v-if="addContentVisible"
:enable-import-word-math="true"
ref="addContent"
style="margin-left: -115px"
></common-content>
@@ -684,6 +685,12 @@ export default {
handleSaveContent() {
this.$refs.commonContent.getTinymceContent('content');
},
clearWordSelectionUI() {
const word = this.$refs.commonWord;
if (word && typeof word.clearParagraphSelection === 'function') {
word.clearParagraphSelection();
}
},
handleSaveAddContent() {
this.$refs.addContent.getTinymceContent('addcontent');
},
@@ -800,6 +807,7 @@ export default {
if (res.code == 0) {
loading.close();
this.editVisible = false;
this.clearWordSelectionUI();
this.refreshCurrentContent('content', am_id, res.data);
this.getCommentList();
} else {

View File

@@ -409,6 +409,7 @@
@getContent="getContent"
@openLatexEditor="openLatexEditor"
v-if="addContentVisible"
:enable-import-word-math="true"
ref="addContent"
style="margin-left: -115px"
></common-content>
@@ -687,6 +688,12 @@ export default {
handleSaveContent() {
this.$refs.commonContent.getTinymceContent('content');
},
clearWordSelectionUI() {
const word = this.$refs.commonWord;
if (word && typeof word.clearParagraphSelection === 'function') {
word.clearParagraphSelection();
}
},
handleSaveAddContent() {
this.$refs.addContent.getTinymceContent('addcontent');
},
@@ -752,6 +759,7 @@ export default {
.then(async (res) => {
if (res.code == 0) {
this.editVisible = false;
this.clearWordSelectionUI();
this.getDate();
this.getCommentList();
}

View File

@@ -1102,6 +1102,10 @@ import ProgressBar from '@/components/page/components/article/progress.vue';
import { updateStepStatus, markStepAsSaved } from '@/components/page/components/article/checkStepCompletion.js';
import deepEqual from '@/components/page/components/article/deepEqual.js';
import { set } from 'vue';
import {
buildArticleAddTableListFromWordFile,
buildArticleAddTableListItems
} from '@/utils/mathFormulaModule';
export default {
components: {
JournalSelector,
@@ -2750,21 +2754,30 @@ export default {
// event.percentage 就是当前进度(百分比,浮点数)
this.uploadPercentage = Math.round(event.percent); // 取整数
},
submitArticleTableList(list) {
if (!list || !list.length) return Promise.resolve();
return this.$api
.post('api/Article/addArticleTable', {
article_id: this.stagingID,
list
})
.then(() => {
this.isShowCommonWord = true;
setTimeout(() => {
this.isShowProgress = false;
}, 500);
});
},
addWordTablesList(tables) {
var data = {
article_id: this.stagingID,
list: tables.map((e) => ({
table: JSON.stringify([...e]),
type: 0,
html_data: ''
}))
};
this.$api.post('api/Article/addArticleTable', data).then((res) => {
this.isShowCommonWord = true;
setTimeout(() => {
this.isShowProgress = false;
}, 500);
this.submitArticleTableList(buildArticleAddTableListItems(tables, null));
},
processManuscriptWordTables(file) {
if (!file) return;
const extractFn = this.$commonJS.extractWordTablesToArrays.bind(this.$commonJS);
buildArticleAddTableListFromWordFile(file, extractFn).then((list) => {
if (list.length) {
this.submitArticleTableList(list);
}
});
},
upLoadWordTables() {},
@@ -2839,17 +2852,9 @@ export default {
window.history.replaceState({}, document.title, url.toString());
if (this.form.article_id) {
if (File) {
if (File && File.raw) {
loading.close();
var that = this;
const reader = new FileReader();
reader.onload = function (e) {
that.$commonJS.extractWordTablesToArrays(File.raw, function (wordTables) {
that.addWordTablesList(wordTables);
loading.close();
},that.form.article_id);
};
reader.readAsArrayBuffer(File.raw);
this.processManuscriptWordTables(File.raw);
}
await this.onStaging(5);
await this.TempoAuthor();

View File

@@ -525,6 +525,8 @@
</template>
<script>
import { buildArticleAddTableListFromWordFile } from '@/utils/mathFormulaModule';
export default {
data() {
return {
@@ -569,7 +571,8 @@
manuscirptFileList: [],
supplementaryFileList:[],
fileL_supplementary:[],
wordTables:[],
wordTablePayloadList: [],
wordTableParsePromise: null,
};
},
created: function() {
@@ -894,26 +897,17 @@ if(this.comentDeploy.length){
},
// 上传文件
addWordTablesList(tables) {
console.log('tables at line 687:', tables)
var data = {
addWordTablesList() {
const list = this.wordTablePayloadList || [];
if (!list.length) return Promise.resolve();
return this.$api.post('api/Article/addArticleTable', {
article_id: this.fileMesForm.articleId,
list: tables.map(e=>( {
table: JSON.stringify([...e]),
type: 0,
html_data: ''
})),
};
this.$api.post('api/Article/addArticleTable', data).then((res) => {
list
});
},
async clearFileManuscript() {
var that=this
var that = this;
await this.$api
.post('api/Article/reloadArticleImages', {
article_id: this.fileMesForm.articleId
@@ -924,7 +918,7 @@ if(this.comentDeploy.length){
.post('api/Article/reloadArticleTable', {
article_id: this.fileMesForm.articleId
})
.then((res) => {that. addWordTablesList(that.wordTables);});
.then((res) => that.addWordTablesList());
},
async onSubmit() {
@@ -943,6 +937,9 @@ if(this.comentDeploy.length){
}
this.loading = true;
if (this.wordTableParsePromise) {
await this.wordTableParsePromise;
}
await this.$api
.post('api/Article/RepairBack', this.fileMesForm)
.then(async (res) => {
@@ -1025,21 +1022,26 @@ if(this.comentDeploy.length){
// this.fileMesForm.picturesAndTables.push('picturesAndTables/' + url);
// }
},
parseManuscriptWordTables(file) {
if (!file) return Promise.resolve([]);
if (this.wordTableParsePromise) return this.wordTableParsePromise;
const extractFn = this.$commonJS.extractWordTablesToArrays.bind(this.$commonJS);
this.wordTableParsePromise = buildArticleAddTableListFromWordFile(file, extractFn)
.then((list) => {
this.wordTablePayloadList = list || [];
return this.wordTablePayloadList;
})
.finally(() => {
this.wordTableParsePromise = null;
});
return this.wordTableParsePromise;
},
upSuccess_manuscirpt(res, File) {
if (res.code == 0) {
this.fileMesForm.manuscirpt = 'manuscirpt/' + res.upurl;
if (File) {
var that = this;
const reader = new FileReader();
reader.onload = function (e) {
that.$commonJS.extractWordTablesToArrays(File.raw, function (wordTables) {
console.log('tablesHtml at line 61:', wordTables);
that.wordTables=wordTables
});
};
reader.readAsArrayBuffer(File.raw);
}
if (File && File.raw) {
this.parseManuscriptWordTables(File.raw);
}
} else {
this.$message.error('service error' + res.msg);
}
@@ -1065,6 +1067,8 @@ that.wordTables=wordTables
},
removefilemanuscirpt(file, fileList) {
this.fileMesForm.manuscirpt = '';
this.wordTablePayloadList = [];
this.wordTableParsePromise = null;
},
removefileresponse(file, fileList) {
this.fileMesForm.responseFile = '';

View File

@@ -57,6 +57,7 @@
<template slot-scope="scope">
<p class="tab_tie_col" v-if="scope.row.company != ''"><span>{{ $t('reviewerAdd.affiliation') }}</span>{{ scope.row.company }}</p>
<p class="tab_tie_col" v-if="scope.row.field != ''"><span>{{ $t('reviewerAdd.field') }}</span>{{ scope.row.field }}</p>
<p class="tab_tie_col" v-if="scope.row.field_ai"><span>{{ $t('reviewerAdd.fieldAi') }}</span>{{ scope.row.field_ai }}</p>
<!-- <p class="tab_tie_col" v-if="scope.row.majorstr != ''"><span>Major: </span>{{ scope.row.majorstr }}</p> -->
</template>
</el-table-column>

View File

@@ -117,6 +117,7 @@
<template slot-scope="scope">
<p class="tab_tie_col" v-if="scope.row.company != ''"><span>{{ $t('reviewerAdd.affiliation') }}</span>{{ scope.row.company }}</p>
<p class="tab_tie_col" v-if="scope.row.field != ''"><span>{{ $t('reviewerAdd.field') }}</span>{{ scope.row.field }}</p>
<p class="tab_tie_col" v-if="scope.row.field_ai"><span>{{ $t('reviewerAdd.fieldAi') }}</span>{{ scope.row.field_ai }}</p>
<!-- <p class="tab_tie_col" v-if="scope.row.majorstr != ''"><span>Major: </span>{{ scope.row.majorstr }}</p> -->
</template>
</el-table-column>
@@ -213,6 +214,9 @@
<el-form-item :label="$t('reviewerAdd.dialogField')">
<span>{{ mesOpen.field }}</span>
</el-form-item>
<el-form-item v-if="mesOpen.field_ai" :label="$t('reviewerAdd.dialogFieldAi')">
<span>{{ mesOpen.field_ai }}</span>
</el-form-item>
<el-form-item :label="$t('reviewerAdd.dialogIntroduction')">
<span>{{ mesOpen.introduction }}</span>
</el-form-item>
@@ -386,7 +390,10 @@ export default {
uid: row.user_id
})
.then((res) => {
this.mesOpen = res.data;
this.mesOpen = {
...res.data,
field_ai: res.data.field_ai || row.field_ai
};
})
.catch((err) => {
console.log(err);

View File

@@ -271,6 +271,12 @@ export default {
handleSaveContent() {
this.$refs.commonContent.getTinymceContent('content');
},
clearWordSelectionUI() {
const word = this.$refs.commonWord;
if (word && typeof word.clearParagraphSelection === 'function') {
word.clearParagraphSelection();
}
},
async getContent(type, content) {
if (type == 'content') {
@@ -365,6 +371,7 @@ export default {
.then(async (res) => {
if (res.code == 0) {
this.editVisible = false;
this.clearWordSelectionUI();
this.getDate();
this.getCommentList();
}

View File

@@ -75,7 +75,7 @@
<script>
import { TableUtils } from '@/common/js/TableUtils';
import { mediaUrl } from '@/common/js/commonJS.js';
import { isMathFormulaTableTitle, parseFormulaRowsFromTableData } from '@/utils/mathFormulaModule';
import { isMathFormulaTableRecord, parseFormulaRowsFromTableData } from '@/utils/mathFormulaModule';
export default {
name: 'TablePreviewer',
@@ -90,7 +90,7 @@ export default {
},
computed: {
isLatexDataPreview() {
return this.processedItem && isMathFormulaTableTitle(this.processedItem.title);
return isMathFormulaTableRecord(this.processedItem);
},
latexFormulaItems() {
if (!this.isLatexDataPreview || !this.processedItem) return [];
@@ -111,7 +111,7 @@ export default {
this.loading = true;
setTimeout(() => {
try {
if (isMathFormulaTableTitle(item.title)) {
if (isMathFormulaTableRecord(item)) {
this.processedItem = Object.freeze({ ...item });
} else {
const processed = this.processTableData(item.table);

View File

@@ -16,9 +16,35 @@
{{ $t('commonTable.latexDataDeleteAll') }}
</el-button>
</div>
<span v-if="displayFormulaItems.length || isAddingNew" class="toolbar-hint">
{{ $t('commonTable.latexDataClickToCopy') }}
</span>
<div v-if="problemIssueCount > 0" class="toolbar-issue-inline">
<i class="el-icon-warning-outline"></i>
<span class="toolbar-issue-prefix">{{
$t('commonTable.latexDataIssueInlinePrefix', { count: problemIssueCount })
}}</span>
<span class="toolbar-issue-indexes">
<template v-for="(rowIndex, idx) in problemIssueIndexesDisplay">
<button
:key="'issue-' + rowIndex"
type="button"
class="toolbar-issue-index-link"
@click.stop="locateFormulaIssue(rowIndex)"
>{{ rowIndex }}</button><span
v-if="idx < problemIssueIndexesDisplay.length - 1"
class="toolbar-issue-index-sep"
></span>
</template>
<span v-if="problemIssueIndexesOverflow" class="toolbar-issue-more"></span>
</span>
</div>
<el-button
v-if="wordPreviewHtml"
size="mini"
type="text"
class="word-report-download-btn"
@click="downloadWordImportReport"
>
{{ $t('commonTable.latexDataWordDownloadHtml') }}
</el-button>
<el-tooltip :content="$t('commonTable.importWordMathTip')" placement="top">
<button
type="button"
@@ -75,8 +101,7 @@
<button
type="button"
class="action-link action-cancel"
@mousedown.prevent
@click.stop="cancelInlineEdit"
@mousedown.prevent.stop="onCancelButtonPointer"
>
{{ $t('commonTable.latexDataCancel') }}
</button>
@@ -85,10 +110,18 @@
<div
v-for="item in displayFormulaItems"
:key="item.index"
:data-formula-index="item.index"
class="latex-formula-row"
:class="{ 'latex-formula-row--editing': editingIndex === item.index }"
:class="{
'latex-formula-row--editing': editingIndex === item.index,
'latex-formula-row--highlight': highlightFormulaIndex === item.index
}"
>
<span class="row-index">{{ item.index }}</span>
<span
class="row-index"
:class="{ 'row-index--warn': isRowIndexWarn(item) }"
:title="isRowIndexWarn(item) ? $t('commonTable.latexDataRowIssueTip') : ''"
>{{ item.index }}</span>
<div v-if="editingIndex === item.index" class="row-edit">
<div class="tinymce-inline-math-panel latex-data-inline-math latex-data-inline-math--compact">
<div class="tinymce-inline-math-field-wrap latex-inline-math-field-wrap"></div>
@@ -118,8 +151,7 @@
<button
type="button"
class="action-link action-cancel"
@mousedown.prevent
@click.stop="cancelInlineEdit"
@mousedown.prevent.stop="onCancelButtonPointer"
>
{{ $t('commonTable.latexDataCancel') }}
</button>
@@ -149,6 +181,12 @@
<i :class="emptyHint === 'noFormula' ? 'el-icon-warning-outline' : 'el-icon-document'"></i>
<span>{{ emptyMessage }}</span>
</div>
<div
v-if="displayFormulaItems.length || isAddingNew"
class="latex-panel-footer"
>
<span class="latex-panel-footer-hint">{{ $t('commonTable.latexDataClickToCopy') }}</span>
</div>
</div>
</div>
</template>
@@ -169,6 +207,9 @@ import {
normalizeLatexTableApiResponse,
getLatexDataTableFromList
} from '@/utils/mathFormulaModule';
import { normalizeWmathCellHtml, isFormulaDisplayedAsRawSource } from '@/utils/wordMathImport';
import { buildWordImportReportHtml, downloadHtmlFile } from '@/utils/wordImportReport';
import { buildDuplicateFormulaIndexMap } from '@/utils/mathFormulaModule';
import { MathfieldElement } from 'mathlive';
import 'mathlive/dist/mathlive-static.css';
import 'mathlive/dist/mathlive-fonts.css';
@@ -232,6 +273,12 @@ export default {
mathFieldRef: null,
optimisticRows: null,
editOutsideEnabled: false,
/** 取消编辑中:阻止 blur/click 竞态触发保存接口 */
cancelEditInProgress: false,
/** MathJax 渲染失败后标红序号index -> true */
renderIssueIndexes: {},
highlightFormulaIndex: null,
_highlightTimer: null,
wordImportLoading: false,
activeAmtId: null,
panelTip: {
@@ -239,7 +286,9 @@ export default {
type: 'info',
text: ''
},
_panelTipTimer: null
_panelTipTimer: null,
wordPreviewHtml: '',
wordImportFileName: 'word-formulas'
};
},
computed: {
@@ -287,9 +336,33 @@ export default {
? tableItem.table
: parseTableDataFromApiField(tableItem);
return parseFormulaRowsFromTableData(tableData);
},
problemIssueIndexes() {
return (this.displayFormulaItems || [])
.filter((item) => this.isRowIndexWarn(item))
.map((item) => item.index);
},
problemIssueCount() {
return this.problemIssueIndexes.length;
},
problemIssueIndexesDisplay() {
const max = 12;
const list = this.problemIssueIndexes;
if (list.length <= max) return list;
return list.slice(0, max);
},
problemIssueIndexesOverflow() {
return this.problemIssueIndexes.length > 12;
}
},
watch: {
problemIssueCount(val) {
if (!val) {
this._issueLocatePtr = 0;
this.clearHighlightTimer();
this.highlightFormulaIndex = null;
}
},
formulaItems: {
handler(items) {
if (this.optimisticRows !== null && !this.optimisticRows.length) {
@@ -349,6 +422,7 @@ export default {
this.destroyMathField();
this.unbindEditOutsideListener();
this.clearPanelTipTimer();
this.clearHighlightTimer();
},
methods: {
isMathliveUiTarget(target) {
@@ -375,9 +449,17 @@ export default {
this.editOutsideEnabled = false;
this._onEditOutside = (event) => {
if (!this.editOutsideEnabled || !this.isEditingActive) return;
if (this.isInsideActiveEditArea(event.target)) return;
if (this.isMathliveUiTarget(event.target)) return;
this.cancelInlineEdit();
const target = event.target;
if (target && target.closest && target.closest('.action-cancel')) {
this.onCancelButtonPointer();
return;
}
if (target && target.closest && target.closest('.action-save')) {
return;
}
if (this.isInsideActiveEditArea(target)) return;
if (this.isMathliveUiTarget(target)) return;
this.onCancelButtonPointer();
};
document.addEventListener('mousedown', this._onEditOutside, true);
this._editOutsideTimer = setTimeout(() => {
@@ -403,7 +485,7 @@ export default {
}
},
dismissEditing() {
this.cancelInlineEdit();
this.onCancelButtonPointer();
},
/** 列表内滚动仅退出编辑,不关闭弹窗 */
handleListScroll() {
@@ -413,7 +495,8 @@ export default {
},
buildItemsFromRows(rows) {
return (rows || []).map((row, index) => {
const html = row[0] && row[0].text ? row[0].text : '';
const rawHtml = row[0] && row[0].text ? row[0].text : '';
const html = normalizeWmathCellHtml(rawHtml);
return {
index: index + 1,
html,
@@ -421,12 +504,125 @@ export default {
};
});
},
isRowIndexWarn(item) {
if (!item) return false;
return !!this.renderIssueIndexes[item.index];
},
clearHighlightTimer() {
if (this._highlightTimer) {
clearTimeout(this._highlightTimer);
this._highlightTimer = null;
}
},
flashFormulaRow(index) {
this.clearHighlightTimer();
this.highlightFormulaIndex = index;
this._highlightTimer = setTimeout(() => {
this.highlightFormulaIndex = null;
this._highlightTimer = null;
}, 2200);
},
locateFormulaIssue(index) {
this.scrollToFormulaRowInList(index);
},
buildWmathIdToListIndexMap() {
const map = {};
const list = this.$refs.formulaList;
if (!list) return map;
list.querySelectorAll('.latex-formula-row:not(.latex-formula-row--new)').forEach((rowEl) => {
const index = parseInt(rowEl.getAttribute('data-formula-index'), 10);
const w = rowEl.querySelector('wmath');
const id = w && w.getAttribute('data-id');
if (id && index) map[id] = index;
});
return map;
},
downloadWordImportReport() {
if (!this.wordPreviewHtml) return;
this.scanFormulaRenderIssues();
const indexByWmathId = this.buildWmathIdToListIndexMap();
const listDup = buildDuplicateFormulaIndexMap(this.currentFormulaRows || []);
const html = buildWordImportReportHtml({
previewBodyHtml: this.wordPreviewHtml,
issueIndexes: this.renderIssueIndexes,
indexByWmathId,
listDuplicateOf: listDup.duplicateOf || {},
listDuplicateCountByFirst: listDup.countByFirst || {},
title: this.$t('commonTable.latexDataWordPreviewTitle'),
hint: this.$t('commonTable.latexDataWordPreviewHint'),
legendOk: this.$t('commonTable.latexDataWordPreviewLegendOk'),
legendErr: this.$t('commonTable.latexDataWordPreviewLegendErr'),
issueSummaryPrefix: this.$t('commonTable.latexDataWordReportIssuePrefix'),
dupTagRepeat: this.$t('commonTable.latexDataWordReportDupTag'),
dupTagCount: this.$t('commonTable.latexDataWordReportDupCount')
});
const base = this.wordImportFileName || 'word-formulas';
downloadHtmlFile(html, `${base}-report.html`);
},
storeWordReportSource(previewHtml, file) {
this.wordPreviewHtml = previewHtml || '';
const name = file && file.name ? String(file.name) : '';
this.wordImportFileName = name ? name.replace(/\.docx$/i, '') : 'word-formulas';
},
scheduleWordReportDownload() {
if (!this.wordPreviewHtml) return;
this.$nextTick(() => {
if (window.renderMathJax) {
window.renderMathJax();
}
setTimeout(() => {
this.scanFormulaRenderIssues();
this.downloadWordImportReport();
this.$message.success(this.$t('commonTable.latexDataWordReportDownloaded'));
}, 700);
});
},
scrollToFormulaRowInList(index) {
const list = this.$refs.formulaList;
if (!list || !index) return;
const rowEl = list.querySelector(`[data-formula-index="${index}"]`);
if (!rowEl) return;
if (this.isEditingActive) {
this.onCancelButtonPointer();
}
this.$nextTick(() => {
rowEl.scrollIntoView({ block: 'center', behavior: 'smooth' });
this.flashFormulaRow(index);
});
},
scrollToFormulaRow(index) {
this.locateFormulaIssue(index);
},
/** 与顶部「failed to render」、列表行号标红同一规则 */
scanFormulaRenderIssues() {
const issues = {};
const list = this.$refs.formulaList;
const rowEls = list
? list.querySelectorAll('.latex-formula-row:not(.latex-formula-row--new)')
: this.$el
? this.$el.querySelectorAll('.latex-formula-row:not(.latex-formula-row--new)')
: [];
rowEls.forEach((rowEl) => {
const index = parseInt(rowEl.getAttribute('data-formula-index'), 10) || 0;
if (!index) return;
const wmath = rowEl.querySelector('wmath');
if (isFormulaDisplayedAsRawSource(wmath)) {
issues[index] = true;
}
});
this.renderIssueIndexes = { ...issues };
},
refreshFormulaIssueState() {
this.scanFormulaRenderIssues();
},
renderMath() {
this.$nextTick(() => {
this.$nextTick(() => {
if (window.renderMathJax) {
window.renderMathJax();
}
this.$nextTick(() => this.refreshFormulaIssueState());
setTimeout(() => this.refreshFormulaIssueState(), 600);
});
});
},
@@ -506,6 +702,7 @@ export default {
if (result.empty) {
this.emptyHint = 'noFormula';
this.wordPreviewHtml = '';
this.setPanelTip(
'warning',
this.$t('commonTable.importWordMathNoFormula'),
@@ -513,12 +710,19 @@ export default {
);
return;
}
if (result.previewHtml) {
this.storeWordReportSource(result.previewHtml, file);
}
if (result.allDuplicate) {
this.setPanelTip(
'info',
this.$t('commonTable.importWordMathParsedNone', { parsed }),
6000
);
if (result.previewHtml) {
this.renderMath();
this.scheduleWordReportDownload();
}
return;
}
this.emptyHint = '';
@@ -534,6 +738,9 @@ export default {
const list = this.$refs.formulaList;
if (list) list.scrollTop = 0;
});
if (result.previewHtml) {
this.scheduleWordReportDownload();
}
} else if (result.res) {
this.setPanelTip(
'error',
@@ -647,6 +854,7 @@ export default {
return this.mathField ? String(this.mathField.value || '').trim() : '';
},
saveNewInline() {
if (this.cancelEditInProgress) return;
if (!this.isAddingNew) return;
const latex = this.getMathFieldLatex();
@@ -709,12 +917,21 @@ export default {
});
});
},
onCancelButtonPointer() {
if (this.cancelEditInProgress) return;
this.cancelEditInProgress = true;
this.cancelInlineEdit();
this.$nextTick(() => {
this.cancelEditInProgress = false;
});
},
cancelInlineEdit() {
this.destroyMathField();
this.editingIndex = null;
this.isAddingNew = false;
},
saveInlineEdit(item) {
if (this.cancelEditInProgress) return;
if (this.editingIndex !== item.index) return;
const latex = this.getMathFieldLatex();
@@ -892,6 +1109,60 @@ export default {
text-overflow: ellipsis;
}
.toolbar-issue-inline {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 2px 4px;
padding: 0 8px;
font-size: 12px;
line-height: 1.35;
color: #f56c6c;
}
.toolbar-issue-inline > i {
flex-shrink: 0;
font-size: 14px;
}
.toolbar-issue-prefix {
white-space: nowrap;
}
.toolbar-issue-indexes {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.toolbar-issue-index-link {
padding: 0;
margin: 0;
border: none;
background: transparent;
color: #f56c6c;
font-size: 12px;
font-weight: 700;
line-height: 1.35;
cursor: pointer;
text-decoration: underline;
text-underline-offset: 2px;
}
.toolbar-issue-index-link:hover {
color: #f78989;
}
.toolbar-issue-index-sep,
.toolbar-issue-more {
color: #f56c6c;
font-weight: 600;
}
.word-upload-btn {
display: inline-flex;
align-items: center;
@@ -984,6 +1255,20 @@ export default {
min-height: 0;
}
.latex-panel-footer {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding: 6px 8px 2px;
}
.latex-panel-footer-hint {
font-size: 12px;
line-height: 1.4;
color: #79bbff;
}
.latex-formula-list {
flex: 1;
overflow-y: auto;
@@ -1016,6 +1301,11 @@ export default {
align-items: center;
}
.latex-formula-row--highlight {
background: #fff1f0 !important;
box-shadow: inset 0 0 0 1px #fabcbc;
}
.row-index {
flex-shrink: 0;
width: 32px;
@@ -1031,11 +1321,22 @@ export default {
visibility: hidden;
}
.row-index--warn {
color: #f56c6c;
font-weight: 700;
}
.latex-data-panel--popover .row-index {
font-size: 13px;
color: #606266;
}
.latex-data-panel--popover .row-index.row-index--warn,
.latex-data-panel .row-index.row-index--warn {
color: #f56c6c !important;
font-weight: 700;
}
.row-edit {
flex: 1;
min-width: 0;
@@ -1229,6 +1530,12 @@ export default {
.latex-empty--warn i {
color: #e6a23c;
}
.word-report-download-btn {
padding: 0 6px;
margin-right: 4px;
font-size: 12px;
}
</style>
<style>

View File

@@ -16,7 +16,7 @@
:value="value"
:typesettingType="typesettingType"
class="paste-area text-container"
:toolbar="!isAutomaticUpdate?['bold italic |customBlue removeBlue|LateX Latex2| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']:['bold italic |customBlue removeBlue|Latex2| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']"
:toolbar="editorToolbar"
style="
/* white-space: pre-line; */
line-height: 12px;
@@ -36,13 +36,44 @@
<script>
import Tinymce from '@/components/page/components/Tinymce';
export default {
props: ['value','isAutomaticUpdate','height','id'],
props: {
value: {},
isAutomaticUpdate: {
type: Boolean,
default: false
},
height: {},
id: {},
/** 仅「批量添加内容」弹窗开启Word 导入公式importWordMath */
enableImportWordMath: {
type: Boolean,
default: false
}
},
components: {
Tinymce
},
watch: {
lineStyle() {}
},
computed: {
editorToolbar() {
const groups = ['bold italic', 'customBlue removeBlue'];
if (!this.isAutomaticUpdate) {
groups.push('LateX');
}
// if (this.enableImportWordMath) {
// groups.push('importWordMath');
// }
groups.push(
'myuppercase myuppercasea Line MoreSymbols',
'subscript superscript',
'clearButton',
'searchreplace'
);
return [groups.join(' | ')];
}
},
data() {
return {
tableData: [],

View File

@@ -486,12 +486,7 @@ export default {
end_container_on_empty_block: true,
content_css: 'default ',
setup(ed) {
// 禁止输入内容,但工具栏按钮仍然有效
ed.on('keydown', function (e) {
if (ed.mode.get() === 'readonly') {
e.preventDefault(); // 阻止任何输入
}
});
_this.$commonJS.bindReadonlyEditorKeydown(ed);
ed.on('keydown', function (e) {
// 检查是否按下回车键
if (e.keyCode === 13) {

View File

@@ -37,7 +37,7 @@
<ul class="operateBox">
<div class="base-border base-padding-all" :style="{ '--br': '1px', '--p': '0 20px' }">
<ul class="HTitleBox" style="border: none">
<li class="latex-toolbar-item">
<li v-if="showLatexToolbarButton && isEditComment && !isPreview" class="latex-toolbar-item">
<el-popover
v-model="latexPopoverVisible"
placement="bottom-start"
@@ -61,6 +61,7 @@
class="latex-toolbar-trigger"
@click.stop="toggleLatexPopover"
>
<i class="el-icon-s-data latex-toolbar-icon"></i>
{{ $t('commonTable.mathFormula') }}
</span>
</el-popover>
@@ -229,14 +230,6 @@
>
<div :class="currentId == item.am_id ? 'glowing-border' : ''" style="position: relative">
<div
class="base-bg base-pos paragraph-select-overlay"
:style="{ '--p-r': '0px', '--p-t': '-40px' }"
v-if="currentId == item.am_id"
style="z-index: 100"
@click.stop="onParagraphSelectOverlayClick(item)"
></div>
<div
:ref="'row-' + item.am_id"
:key="item.am_id"
@@ -991,7 +984,13 @@
</div>
<!-- 操作按钮 -->
<div v-if="isMenuVisible || bubbleVisible" class="wps-two-layer-bar selection-bubble no-select" :style="bubbleStyle">
<div
v-if="isMenuVisible || bubbleVisible"
class="wps-two-layer-bar selection-bubble no-select"
:style="bubbleStyle"
@mousedown.stop.prevent="handleBubblePointerDown"
@click.stop
>
<div class="bar-row" v-if="currentData">
<div v-if="currentData.type == 0" class="h-group">
<span :class="['h-item', { active: currentData.is_h1 == 1 }]" @click="changeTitle(currentData.is_h1 ? 0 : 1)">H1</span>
@@ -1247,6 +1246,8 @@ export default {
hasInit: false,
selectedIds: [],
isMouseSelecting: false,
_bubblePointerActive: false,
_bubblePointerTimer: null,
_selectionSyncToCheckboxesTimer: null,
_onDocumentSelectionChange: null,
_onDocumentMouseUp: null,
@@ -1255,6 +1256,7 @@ export default {
displayList: [],
currentTypeText: '',
tinymceId: this.id || 'vue-tinymce-' + +new Date(),
showLatexToolbarButton: false,
latexPopoverVisible: false,
_latexPopoverScrollHandler: null,
_latexPopoverOutsideHandler: null
@@ -1299,6 +1301,11 @@ export default {
this.unbindLatexPopoverScrollListener();
this.unbindLatexPopoverOutsideClick();
}
},
isEditComment(val) {
if (!val) {
this.closeLatexPopover();
}
}
},
computed: {
@@ -1416,6 +1423,19 @@ export default {
if (this.mutObs) this.mutObs.disconnect();
this.unbindLatexPopoverScrollListener();
this.unbindLatexPopoverOutsideClick();
if (this._bubblePointerTimer) {
clearTimeout(this._bubblePointerTimer);
this._bubblePointerTimer = null;
}
if (this._onDocumentSelectionChange) {
document.removeEventListener('selectionchange', this._onDocumentSelectionChange);
}
if (this._onDocumentMouseUp) {
document.removeEventListener('mouseup', this._onDocumentMouseUp);
}
if (this.$refs.scroll && this._onManuscriptMouseDown) {
this.$refs.scroll.removeEventListener('mousedown', this._onManuscriptMouseDown);
}
// 页面销毁前清理定时器和事件监听器
},
@@ -1459,7 +1479,7 @@ export default {
this.isMouseSelecting = false;
},
handleDocumentSelectionChange() {
if (this.isPreview) return;
if (this.isPreview || this._bubblePointerActive) return;
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) return;
@@ -1487,7 +1507,7 @@ export default {
this.currentId = allIds[0];
this.currentData = rootItem;
}
} else {
} else if (!this._bubblePointerActive && !this.isMenuVisible) {
this.currentTag = '';
this.currentTagData = {};
this.currentSelection = {
@@ -1654,15 +1674,13 @@ export default {
this.$forceUpdate();
},
handleGlobalClick(e) {
if(this.isPreview)return;
// 如果点在气泡内,不关
if (e.target.closest('.bubble-container')) return;
if (this.isPreview) return;
if (e.target.closest('.selection-bubble, .wps-two-layer-bar, .bubble-container')) return;
const selection = window.getSelection();
const hasSelection = selection && selection.toString().trim() !== '';
const isClickOnParagraph = e.target.closest('.pMain');
// 只有当:既没有选区,也没有点在段落上(点击了背景空白),才真正关闭
if (!hasSelection && !isClickOnParagraph) {
this.bubbleVisible = false;
this.isMenuVisible = false;
@@ -1670,9 +1688,16 @@ export default {
this.currentData = {};
this.currentId = '';
this.clearActiveGlow(); // 清除高亮
this.clearActiveGlow();
}
},
handleBubblePointerDown() {
this._bubblePointerActive = true;
clearTimeout(this._bubblePointerTimer);
this._bubblePointerTimer = setTimeout(() => {
this._bubblePointerActive = false;
}, 200);
},
// 辅助方法:清除发光边框
clearActiveGlow() {
@@ -2117,6 +2142,25 @@ export default {
this.isMenuVisible = false;
},
/** 保存/取消编辑后关闭段落高亮与浮动工具栏 */
clearParagraphSelection() {
this.closeMenu();
this.currentId = '';
this.currentData = {};
this.currentTag = '';
this.currentTagData = null;
this.currentSelection = {
label: '',
mainId: '',
index: 0,
content: {}
};
this.clearActiveGlow();
const selection = window.getSelection();
if (selection) {
selection.removeAllRanges();
}
},
handleAIProofreading() {
if (this.currentId) {
this.$api
@@ -2175,7 +2219,14 @@ export default {
this.onDelete();
break;
}
this.closeMenu();
if (!this.shouldKeepMenuOpenAfterAction(action)) {
this.closeMenu();
}
},
/** 上移/下移后保持工具栏,便于连续操作 */
shouldKeepMenuOpenAfterAction(action) {
return action === 'up' || action === 'down';
},
changeSort(type) {
@@ -3023,26 +3074,24 @@ export default {
}
});
},
onParagraphSelectOverlayClick(item) {
if (this.isPreview) return;
if (item && item.am_id) {
this.$set(this, 'currentId', item.am_id);
}
},
async handleGeneralClick(event, item, index) {
// 1. 如果有原有的 span 点击逻辑,先执行(或根据条件判断)
if (typeof this.onProofSpanClick === 'function') {
this.onProofSpanClick(event);
}
const wmathEl = event.target && event.target.closest && event.target.closest('wmath');
if (wmathEl && !this.isPreview) {
const wmathId = wmathEl.getAttribute('data-id');
await this.initializeEditor(event, item.am_id, 'text', item, index);
this.$emit('onEdit', item.am_id, wmathId ? { wmathId } : undefined);
const selection = window.getSelection();
if (selection && !selection.isCollapsed && selection.toString().trim()) {
return;
}
const wmathEl = event.target && event.target.closest && event.target.closest('wmath');
// if (wmathEl && !this.isPreview) {
// const wmathId = wmathEl.getAttribute('data-id');
// await this.initializeEditor(event, item.am_id, 'text', item, index);
// this.$emit('onEdit', item.am_id, wmathId ? { wmathId } : undefined);
// return;
// }
await this.initializeEditor(event, item.am_id, 'text', item, index);
},
initializeEditor: throttle(async function (event, id, type, data, index) {
@@ -4203,13 +4252,13 @@ export default {
cursor: pointer;
}
.HTitleBox li.latex-toolbar-item {
padding: 2px 10px;
padding: 0 10px;
font-size: 12px;
color: #fff;
background-color: rgb(43, 129, 239);
color: #333;
background: transparent;
border: none;
border-radius: 4px;
height: 26px;
border-radius: 0;
height: auto;
}
.operateBox {
width: auto;
@@ -4681,12 +4730,11 @@ font-weight: bold !important;
display: inline-flex;
align-items: center;
list-style: none;
padding: 2px 10px;
padding: 0 10px;
margin-left: 10px;
border: none;
border-radius: 4px;
background-color: rgb(43, 129, 239);
height: 26px;
background: transparent;
height: auto;
}
.latex-toolbar-trigger {
@@ -4695,14 +4743,21 @@ font-weight: bold !important;
cursor: pointer;
font-size: 12px;
font-weight: bold;
color: #fff;
color: #333;
line-height: 1.4;
white-space: nowrap;
transition: color 0.15s ease;
}
.latex-toolbar-trigger:hover {
opacity: 0.9;
color: #fff;
color: #1654f7;
}
.latex-toolbar-icon {
margin-right: 4px;
font-size: 13px;
color: inherit;
transition: color 0.15s ease;
}
</style>
<style>

View File

@@ -263,6 +263,7 @@
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
import DynamicTable from './DynamicTable.vue';
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
export default {
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
data() {
@@ -473,8 +474,9 @@ export default {
article_id: this.articleId
})
.then(async (res) => {
let parsedList = [];
if (this.urlList) {
this.tables =
parsedList =
res.data.list && res.data.list.length > 0
? res.data.list.map((e) => {
return {
@@ -485,7 +487,7 @@ export default {
})
: [];
} else {
this.tables =
parsedList =
res.data.list && res.data.list.length > 0
? res.data.list.map((e) => {
return {
@@ -496,6 +498,7 @@ export default {
})
: [];
}
this.tables = parsedList.filter((item) => !isMathFormulaTableRecord(item));
await this.filterData('table');

View File

@@ -474,7 +474,7 @@
<script>
import DynamicTable from './DynamicTable.vue';
import { mediaUrl } from '@/common/js/commonJS.js';
import { isMathFormulaTableTitle, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
import { isMathFormulaTableRecord, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
export default {
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
@@ -677,7 +677,7 @@ export default {
}
},
isMathFormulaTable(item) {
return item && isMathFormulaTableTitle(item.title);
return isMathFormulaTableRecord(item);
},
splitTablesAndMathFormulas(list) {
const tables = [];

View File

@@ -192,7 +192,7 @@ If you're still having trouble with Chrome, you could use other browsers to comp
<div class="file_sty" v-for="(item,i) in article_response" style="margin-top: 15px;width: 50%;float: left">
<img src="../../assets/img/icon_0.png" alt="" class="icon_img" />
<a :href="mediaUrl + item.file_url" target="_blank" class="txt_pdf"
>Manuscript
>Rebuttal Letter
<span style="margin-left: 10px; color: #888; font-size: 13px">{{ item.artr_ctime }}</span>
<i class="el-icon-download" style="margin-left: 20px; color: #66b1ff; font-weight: bold"></i>

View File

@@ -12,6 +12,8 @@
</div>
<div class="container" style="padding-top: 0px;" v-if="showSendEmail">
<!-- 阻止回车在收件人/主题等输入框触发表单隐式提交误点发送 -->
<form class="mail-compose-form" @submit.prevent @keydown.enter="preventComposeEnterSubmit">
<div class="mail_shuru" style="position: relative; display: flex; align-items: center;">
<span class="mail_tit">{{ $t('mailboxSend.to') }}</span>
@@ -58,7 +60,7 @@
</div>
<div class="mail_shuru">
<span class="mail_tit">{{ $t('mailboxSend.subject') }}</span>
<el-input v-model="queryMail.sendtitle" class="mail_inp" ></el-input>
<el-input v-model="queryMail.sendtitle" class="mail_inp" @keydown.enter.native.prevent></el-input>
</div>
<!-- <div class="mail_shuru" style="position: relative; display: flex; align-items: center;">
<span class="mail_tit">{{ $t('mailboxSend.cc') }}</span>
@@ -122,6 +124,7 @@
<div class="action-buttons">
<el-button
type="primary"
native-type="button"
icon="el-icon-s-promotion"
:loading="sendLoading"
:disabled="sendLoading"
@@ -133,6 +136,7 @@
<!-- <el-button size="medium" :loading="saveDraftLoading" :disabled="saveDraftLoading" @click="handleSaveDraft">{{ $t('mailboxSend.saveDraft') }}</el-button> -->
</div>
</div>
</form>
</div>
<!-- 选择通讯录 -->
@@ -285,6 +289,18 @@ import TemplateSelectorDialog from '@/components/page/components/email/TemplateS
document.removeEventListener('click', this.handleOutsideAutocompleteClick, true);
},
methods: {
/** 非编辑器区域按回车不触发发送TinyMCE / 源码区仍正常换行 */
preventComposeEnterSubmit(e) {
if (!e || e.key !== 'Enter' || e.isComposing) return;
const target = e.target;
if (!target || typeof target.closest !== 'function') return;
if (target.closest('.tox-tinymce') || target.closest('.mail-editor-wrap')) return;
if (target.tagName === 'TEXTAREA' || target.isContentEditable) return;
if (target.closest('.el-autocomplete-suggestion')) return;
const ac = this.$refs.autocompleteTo;
if (ac && ac.suggestionVisible) return;
e.preventDefault();
},
closeAllDialogs() {
// 点击“去新增模板”后:关闭模板选择弹窗以及可能打开的其它弹窗
this.Librarybox = false;

View File

@@ -1,10 +1,14 @@
import {
importWordDocumentWithMath,
extractWmathTableDataFromHtml,
buildWordImportPreviewHtml,
MATH_FORMULA_TABLE_TITLE,
LEGACY_MATH_FORMULA_TABLE_TITLE,
isMathFormulaTableTitle,
buildWmathCellHtml
buildWmathCellHtml,
normalizeWmathCellHtml,
repairImportedLatex,
decodeLatexFromAttr
} from '@/utils/wordMathImport';
export {
@@ -86,7 +90,7 @@ export function buildClearedLatexTableResponse(data, existingTable) {
return normalizeLatexTableApiResponse(data, existingTable);
}
function normalizeLatexKey(latex) {
export function normalizeLatexKey(latex) {
return String(latex || '')
.trim()
.replace(/^\$\$?/, '')
@@ -94,6 +98,31 @@ function normalizeLatexKey(latex) {
.replace(/\s+/g, ' ');
}
/** 相同 LaTeX 的列表行duplicateOf[行号] = 首次出现行号groups 为重复组 */
export function buildDuplicateFormulaIndexMap(rows) {
const keyToIndexes = {};
(rows || []).forEach((row, i) => {
const key = getLatexKeyFromCellText(row[0] && row[0].text);
if (!key) return;
if (!keyToIndexes[key]) keyToIndexes[key] = [];
keyToIndexes[key].push(i + 1);
});
const duplicateOf = {};
const groups = [];
const countByFirst = {};
Object.keys(keyToIndexes).forEach((key) => {
const sorted = [...keyToIndexes[key]].sort((a, b) => a - b);
if (sorted.length < 2) return;
groups.push(sorted);
const first = sorted[0];
countByFirst[first] = sorted.length;
sorted.forEach((idx) => {
if (idx !== first) duplicateOf[idx] = first;
});
});
return { duplicateOf, groups, countByFirst };
}
export function isSameLatexContent(a, b) {
const keyA = normalizeLatexKey(a);
const keyB = normalizeLatexKey(b);
@@ -107,9 +136,13 @@ export function getLatexFromCellHtml(text) {
div.innerHTML = String(text);
const wmath = div.querySelector('wmath');
if (wmath) {
return (wmath.getAttribute('data-latex') || wmath.textContent || '').trim();
const attr = wmath.getAttribute('data-latex');
if (attr != null && String(attr).trim()) {
return repairImportedLatex(decodeLatexFromAttr(attr));
}
return repairImportedLatex((wmath.textContent || '').trim());
}
return (div.textContent || text || '').trim();
return repairImportedLatex((div.textContent || text || '').trim());
}
export function getLatexKeyFromCellText(text) {
@@ -125,7 +158,10 @@ export function getLatexKeyFromCellText(text) {
export function isLatexDataHeaderRow(row) {
const text = row && row[0] ? row[0].text : '';
return /Latex Data|数字公式/i.test(String(text || ''));
const plain = String(text || '')
.replace(/<[^>]+>/g, '')
.trim();
return /latex\s*data|数字公式/i.test(plain);
}
export function parseFormulaRowsFromTableData(tableData) {
@@ -180,7 +216,7 @@ export function mergeLatexDataTable(existingTableData, incomingTableData) {
export function getLatexDataTableFromList(tables, preferredAmtId) {
const list = (Array.isArray(tables) ? tables : []).filter(
(item) => item && isMathFormulaTableTitle(item.title)
(item) => item && isMathFormulaTableRecord(item)
);
if (!list.length) return null;
if (preferredAmtId != null && preferredAmtId !== '') {
@@ -201,11 +237,13 @@ export function buildFormulaItemList(tables, preferredAmtId) {
? tableItem.table
: parseTableDataFromApiField(tableItem);
return parseFormulaRowsFromTableData(tableData).map((row, index) => {
const html = row[0] && row[0].text ? row[0].text : '';
const rawHtml = row[0] && row[0].text ? row[0].text : '';
const html = normalizeWmathCellHtml(rawHtml);
return {
index: index + 1,
html,
latex: getLatexFromCellHtml(html)
latex: getLatexFromCellHtml(html),
hasIssue: false
};
});
}
@@ -240,7 +278,11 @@ export async function parseWordMathFormulas(file) {
if (!html) {
throw new Error('empty content');
}
return extractWmathTableDataFromHtml(html);
const extracted = extractWmathTableDataFromHtml(html);
return {
...extracted,
previewHtml: buildWordImportPreviewHtml(html)
};
}
export function buildMathFormulaTablePayload(articleId, tableData) {
@@ -254,10 +296,11 @@ export function buildMathFormulaTablePayload(articleId, tableData) {
}
export async function upsertLatexDataTableFromWord(api, articleId, file, existingTable) {
const { tableData: incomingTableData, html: incomingHtml } = await parseWordMathFormulas(file);
const { tableData: incomingTableData, html: incomingHtml, previewHtml } = await parseWordMathFormulas(file);
const incomingRows = parseFormulaRowsFromTableData(incomingTableData);
const parsedCount = incomingRows.length;
const preview = previewHtml || '';
if (!parsedCount) {
return {
@@ -267,7 +310,8 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
addedCount: 0,
totalCount: 0,
tableData: [],
html: ''
html: '',
previewHtml: preview
};
}
@@ -291,6 +335,7 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
allDuplicate: true,
tableData: merged.tableData,
html: merged.html,
previewHtml: preview,
...importMeta
};
}
@@ -309,6 +354,7 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
res,
tableData: merged.tableData,
html: merged.html,
previewHtml: preview,
...importMeta
};
}
@@ -322,6 +368,7 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
res,
tableData: merged.tableData,
html: merged.html,
previewHtml: preview,
...importMeta
};
}
@@ -373,3 +420,94 @@ export function buildFormulaRowFromLatex(latex) {
export async function addMathFormulaTableFromWord(api, articleId, file, existingTable) {
return upsertLatexDataTableFromWord(api, articleId, file, existingTable);
}
/** 判断 Word 解析出的 table 二维数组是否为数字公式表 */
export function isWordTableArrayMathFormula(tableArray) {
if (!Array.isArray(tableArray) || !tableArray.length) return false;
return isMathFormulaTableRecord({ table: tableArray });
}
/** 投稿/校对相关:统一判断接口记录或本地 table 是否为数字公式表 */
export function isMathFormulaTableRecord(item) {
if (!item) return false;
if (isMathFormulaTableTitle(item.title)) return true;
if (item.html_data && isMathFormulaTableTitle(String(item.html_data))) return true;
let table = item.table;
if (!Array.isArray(table)) {
const raw = item.table_data != null && item.table_data !== '' ? item.table_data : item.table;
if (typeof raw === 'string' && raw) {
try {
table = JSON.parse(raw);
} catch (e) {
table = null;
}
}
}
if (Array.isArray(table) && table.length && isLatexDataHeaderRow(table[0])) {
return true;
}
if (Array.isArray(table) && table.length) {
const formulaRows = parseFormulaRowsFromTableData(table);
const dataRows = table.filter((row) => row && row.length && !isLatexDataHeaderRow(row));
if (formulaRows.length > 0 && formulaRows.length === dataRows.length) {
return true;
}
}
return false;
}
/** @deprecated 使用 isMathFormulaTableRecord */
export const isArticleMathFormulaTable = isMathFormulaTableRecord;
export function buildArticleRegularTableItem(tableArray) {
return {
table: JSON.stringify(Array.isArray(tableArray) ? tableArray : []),
type: 0,
html_data: '',
title: ''
};
}
export function buildArticleLatexDataTableItem(tableData) {
const rows = tableData || [];
if (!rows.length) return null;
return {
table: JSON.stringify(rows),
type: 0,
html_data: buildLatexDataHtmlData(),
title: MATH_FORMULA_TABLE_TITLE
};
}
/** 投稿 addArticleTable普通表格 + 可选 Latex Data 公式表(与校对字段一致) */
export function buildArticleAddTableListItems(wordTableArrays, latexTableData) {
const list = (wordTableArrays || [])
.filter((tableArray) => !isWordTableArrayMathFormula(tableArray))
.map((tableArray) => buildArticleRegularTableItem(tableArray));
const latexItem = buildArticleLatexDataTableItem(latexTableData);
if (latexItem) list.push(latexItem);
return list;
}
/** 投稿上传 Word解析物理表格 + 数字公式,生成 addArticleTable 的 list */
export async function buildArticleAddTableListFromWordFile(file, extractWordTablesToArrays) {
if (!file || typeof extractWordTablesToArrays !== 'function') {
return [];
}
const wordTables = await new Promise((resolve) => {
extractWordTablesToArrays(file, resolve);
});
// let latexTableData = [];
// try {
// const parsed = await parseWordMathFormulas(file);
// latexTableData = (parsed && parsed.tableData) || [];
// } catch (err) {
// console.warn('parseWordMathFormulas failed:', err);
// }
return buildArticleAddTableListItems(wordTables, null);
}

View File

@@ -0,0 +1,324 @@
/**
* Word 公式导入源稿:生成可离线打开的独立 HTML 报告
*/
import { normalizeLatexKey } from './mathFormulaModule';
import {
extractLatexFromWmathElement,
isAttrLatexStructurallyBroken
} from './wordMathImport';
const REPORT_STYLES = `
body { margin: 0; font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #303133; background: #f5f7fa; }
.report-wrap { max-width: 920px; margin: 0 auto; padding: 20px 24px 40px; }
.report-header { background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; }
.report-header h1 { margin: 0 0 10px; font-size: 18px; font-weight: 600; }
.report-meta { font-size: 12px; color: #909399; margin-bottom: 12px; }
.report-legend { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; font-size: 12px; }
.legend-ok { padding: 2px 10px; border-radius: 4px; font-weight: 600; color: #67c23a; background: #f0f9eb; border: 1px solid #b3e19d; }
.legend-err { padding: 2px 10px; border-radius: 4px; font-weight: 600; color: #f56c6c; background: #fef0f0; border: 1px solid #fbc4c4; }
.legend-dup { padding: 2px 10px; border-radius: 4px; font-weight: 600; color: #e6a23c; background: #fdf6ec; border: 1px solid #f5dab1; }
.report-issues-sticky { position: sticky; top: 0; z-index: 30; margin: 0 0 12px; padding: 10px 20px 12px; background: #fff; border: 1px solid #fde2e2; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.08); }
.report-issues { margin: 0; font-size: 13px; line-height: 1.5; color: #f56c6c; }
.report-issues a { color: #f56c6c; font-weight: 600; margin: 0 4px; text-decoration: none; }
.report-issues a:hover { text-decoration: underline; }
.report-dup { margin-top: 8px; font-size: 13px; color: #e6a23c; }
.report-dup a { color: #e6a23c; font-weight: 600; margin: 0 4px; }
.report-dup-group { margin: 4px 0 0; font-size: 12px; color: #909399; }
.report-body { background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 20px 24px; }
.report-body p { margin: 0 0 0.65em; }
.report-body table { border-collapse: collapse; margin: 0.5em 0 1em; width: 100%; }
.report-body td, .report-body th { border: 1px solid #dcdfe6; padding: 6px 8px; }
.word-import-formula-anchor { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 4px 6px 4px 0; padding: 4px 8px 4px 6px; vertical-align: middle; border-radius: 6px; border: 2px solid transparent; }
.word-import-formula-index-badge { display: inline-block; min-width: 1.5em; padding: 1px 5px; font-size: 11px; font-weight: 700; text-align: center; color: #409eff; background: #ecf5ff; border-radius: 4px; }
.word-import-dup-tag, .word-import-dup-count-tag { display: inline-block; font-size: 11px; font-weight: 600; line-height: 1.3; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }
.word-import-dup-tag { color: #e6a23c; background: #fdf6ec; border: 1px solid #f5dab1; }
.word-import-dup-count-tag { color: #fff; background: #e6a23c; border: 1px solid #e6a23c; }
.word-import-formula-anchor--nomap { border-color: transparent; background: transparent; padding: 0 2px; }
.word-import-formula-anchor--nomap .word-import-formula-index-badge { display: none; }
.word-import-formula-anchor--nomap::after { display: none !important; }
.word-import-formula-anchor--ok { border-color: #67c23a; background: #f0f9eb; }
.word-import-formula-anchor--ok .word-import-formula-index-badge { color: #fff; background: #67c23a; }
.word-import-formula-anchor--ok:not(.word-import-formula-anchor--dup):not(.word-import-formula-anchor--dup-first)::after { content: "通过"; font-size: 11px; font-weight: 600; color: #67c23a; }
.word-import-formula-anchor--error { border-color: #f56c6c; background: #fef0f0; box-shadow: 0 0 0 1px rgba(245,108,108,.25); }
.word-import-formula-anchor--error .word-import-formula-index-badge { color: #fff; background: #f56c6c; }
.word-import-formula-anchor--error:not(.word-import-formula-anchor--dup):not(.word-import-formula-anchor--dup-first)::after { content: "需修改"; font-size: 11px; font-weight: 600; color: #f56c6c; }
.word-import-formula-anchor--dup { border-color: #e6a23c !important; background: #fdf6ec !important; box-shadow: 0 0 0 1px rgba(230,162,60,.3); }
.word-import-formula-anchor--dup .word-import-formula-index-badge { color: #fff !important; background: #e6a23c !important; }
.word-import-formula-anchor--dup-first { border-color: #e6a23c; background: #fffbf0; }
.word-import-formula-anchor--error.word-import-formula-anchor--dup { border-color: #f56c6c !important; background: linear-gradient(135deg, #fef0f0 0%, #fdf6ec 100%) !important; }
.word-import-orphan-latex { margin: 0.35em 0 0.65em; padding: 8px 10px; border-left: 4px solid #f56c6c; background: #fef0f0; color: #c45656; border-radius: 0 6px 6px 0; }
wmath { display: inline-block; max-width: 100%; }
wmath[data-wrap="block"] { display: block; margin: 0.35em 0; }
`;
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function latexKeyFromWmath(wmath) {
if (!wmath) return '';
const latex = extractLatexFromWmathElement(wmath) || (wmath.textContent || '').trim();
return normalizeLatexKey(latex);
}
/** Word 源稿内按公式序号检测重复(相同 LaTeX后者标为重复 */
export function computeDuplicateMapFromPreviewHtml(previewHtml) {
const div = document.createElement('div');
div.innerHTML = previewHtml || '';
const keyToIndexes = {};
div.querySelectorAll('.word-import-formula-anchor').forEach((anchor) => {
const idx = parseInt(anchor.getAttribute('data-formula-index'), 10);
const key = latexKeyFromWmath(anchor.querySelector('wmath'));
if (!idx || !key) return;
if (!keyToIndexes[key]) keyToIndexes[key] = [];
keyToIndexes[key].push(idx);
});
const duplicateOf = {};
const groups = [];
const countByFirst = {};
Object.keys(keyToIndexes).forEach((key) => {
const sorted = [...keyToIndexes[key]].sort((a, b) => a - b);
if (sorted.length < 2) return;
groups.push(sorted);
const first = sorted[0];
countByFirst[first] = sorted.length;
sorted.forEach((idx) => {
if (idx !== first) duplicateOf[idx] = first;
});
});
return { duplicateOf, groups, countByFirst };
}
/** wmath data-id → Word 稿内序号 */
export function buildWmathIdToWordIndexMap(previewHtml) {
const map = {};
if (!previewHtml) return map;
const div = document.createElement('div');
div.innerHTML = previewHtml;
div.querySelectorAll('.word-import-formula-anchor').forEach((anchor) => {
const wordIdx = parseInt(anchor.getAttribute('data-formula-index'), 10);
const wmath = anchor.querySelector('wmath');
const id = wmath && wmath.getAttribute('data-id');
if (id && wordIdx) map[id] = wordIdx;
});
return map;
}
/** 列表行号 → Word 稿内序号(仅 data-id 能对上的公式) */
export function buildListIndexToWordIndexMap(indexByWmathId, previewHtml) {
const wordById = buildWmathIdToWordIndexMap(previewHtml);
const listToWord = {};
Object.keys(indexByWmathId || {}).forEach((id) => {
const listIdx = indexByWmathId[id];
const wordIdx = wordById[id];
if (listIdx && wordIdx) listToWord[listIdx] = wordIdx;
});
return listToWord;
}
function removeDupTags(anchor) {
anchor.querySelectorAll('.word-import-dup-tag, .word-import-dup-count-tag').forEach((el) => el.remove());
}
function insertDupTag(anchor, className, text) {
if (!className || !text) return;
const tag = document.createElement('span');
tag.className = className;
tag.textContent = text;
const indexBadge = anchor.querySelector('.word-import-formula-index-badge');
if (className === 'word-import-dup-count-tag' && indexBadge) {
indexBadge.after(tag);
return;
}
const wmath = anchor.querySelector('wmath');
anchor.insertBefore(tag, wmath || null);
}
/**
* 标绿/红(列表 renderIssueIndexes+ 橙(重复序号)
* duplicateOf: { 5: 2 } 表示序号 5 与序号 2 重复
*/
export function applyReportMarksToPreviewHtml(previewHtml, options) {
if (!previewHtml) return '';
const {
issueIndexes = {},
indexByWmathId = {},
listDuplicateOf = {},
listDuplicateCountByFirst = {},
dupTagRepeat = '重复#{n}',
dupTagCount = '×{n}'
} = options || {};
const div = document.createElement('div');
div.innerHTML = previewHtml;
const issues = issueIndexes || {};
div.querySelectorAll('.word-import-formula-anchor').forEach((anchor) => {
const wmath = anchor.querySelector('wmath');
const wmathId = wmath && wmath.getAttribute('data-id');
const listIndex = wmathId && indexByWmathId[wmathId] ? indexByWmathId[wmathId] : null;
removeDupTags(anchor);
anchor.classList.remove(
'word-import-formula-anchor--error',
'word-import-formula-anchor--ok',
'word-import-formula-anchor--dup',
'word-import-formula-anchor--dup-first',
'word-import-formula-anchor--nomap'
);
anchor.removeAttribute('data-list-index');
anchor.removeAttribute('id');
const badge = anchor.querySelector('.word-import-formula-index-badge');
if (!listIndex) {
anchor.classList.add('word-import-formula-anchor--nomap');
if (badge) badge.textContent = '';
if (wmath) wmath.removeAttribute('data-import-error');
return;
}
if (badge) badge.textContent = String(listIndex);
anchor.id = 'formula-list-' + listIndex;
anchor.setAttribute('data-list-index', String(listIndex));
const bad = !!issues[listIndex];
const dupFirst = listDuplicateOf[listIndex];
const dupCount = listDuplicateCountByFirst[listIndex];
if (dupFirst) {
anchor.classList.add('word-import-formula-anchor--dup');
const label = String(dupTagRepeat).replace('{n}', String(dupFirst));
insertDupTag(anchor, 'word-import-dup-tag', label);
} else if (dupCount > 1) {
anchor.classList.add('word-import-formula-anchor--dup-first');
const label = String(dupTagCount).replace('{n}', String(dupCount));
insertDupTag(anchor, 'word-import-dup-count-tag', label);
}
if (bad) {
anchor.classList.add('word-import-formula-anchor--error');
if (wmath) wmath.setAttribute('data-import-error', '1');
} else {
anchor.classList.add('word-import-formula-anchor--ok');
if (wmath) wmath.removeAttribute('data-import-error');
}
});
return div.innerHTML;
}
/** @deprecated 使用 applyReportMarksToPreviewHtml */
export function applyIssueMarksToPreviewHtml(previewHtml, issueIndexes, indexByWmathId) {
return applyReportMarksToPreviewHtml(previewHtml, { issueIndexes, indexByWmathId });
}
/** 顶部仅列出有问题的列表行号 */
function buildListIssueLinksHtml(issueIndexes, issueSummaryPrefix) {
const nums = Object.keys(issueIndexes || {})
.filter((k) => issueIndexes[k])
.map((k) => parseInt(k, 10))
.filter((n) => n > 0)
.sort((a, b) => a - b);
if (!nums.length) return '';
const links = nums.map((listIdx) => `<a href="#formula-list-${listIdx}">${listIdx}</a>`).join('、');
return `<div class="report-issues-sticky"><p class="report-issues">${escapeHtml(issueSummaryPrefix)} ${links}</p></div>`;
}
export function buildWordImportReportHtml(options) {
const {
previewBodyHtml = '',
issueIndexes = {},
indexByWmathId = {},
listDuplicateOf = {},
listDuplicateCountByFirst = {},
title = 'Word formula report',
hint = '',
legendOk = 'Green · OK',
legendErr = 'Red · needs fix',
issueSummaryPrefix = 'Rows to fix:',
dupTagRepeat = '重复#{n}',
dupTagCount = '×{n}',
generatedAt = new Date().toLocaleString()
} = options || {};
const bodyInner = applyReportMarksToPreviewHtml(previewBodyHtml, {
issueIndexes,
indexByWmathId,
listDuplicateOf,
listDuplicateCountByFirst,
dupTagRepeat,
dupTagCount
});
const issueBlock = buildListIssueLinksHtml(issueIndexes, issueSummaryPrefix);
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${escapeHtml(title)}</title>
<style>${REPORT_STYLES}</style>
<script>
window.MathJax = {
tex: { inlineMath: [['$','$'], ['\\\\(','\\\\)']], displayMath: [['$$','$$'], ['\\\\[','\\\\]']] },
svg: { fontCache: 'global' }
};
function prepareWmathElements() {
document.querySelectorAll('wmath').forEach(function (el) {
var latex = el.getAttribute('data-latex');
if (!latex) return;
var raw = latex.trim().replace(/^\\$\\$/, '').replace(/\\$\\$$/, '').replace(/^\\$/, '').replace(/\\$$/, '').trim();
if (raw) el.innerHTML = '$$' + raw + '$$';
});
}
document.addEventListener('DOMContentLoaded', function () {
prepareWmathElements();
if (window.MathJax && MathJax.typesetPromise) {
MathJax.typesetPromise().catch(function () {});
}
});
</script>
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
</head>
<body>
<div class="report-wrap">
<header class="report-header">
<h1>${escapeHtml(title)}</h1>
<p class="report-meta">${escapeHtml(generatedAt)}</p>
<div class="report-legend">
<span class="legend-ok">${escapeHtml(legendOk)}</span>
<span class="legend-err">${escapeHtml(legendErr)}</span>
</div>
${hint ? `<p class="report-meta">${escapeHtml(hint)}</p>` : ''}
</header>
${issueBlock}
<main class="report-body">${bodyInner}</main>
</div>
</body>
</html>`;
}
export function downloadHtmlFile(html, filename) {
const name = filename || `word-formula-report-${Date.now()}.html`;
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name;
a.rel = 'noopener';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}

View File

@@ -97,12 +97,389 @@ export function createWmathHtml(latex, wrap) {
function firstByLocal(parent, name) {
if (!parent) return null;
return Array.from(parent.children || []).find((child) => localName(child) === name) || null;
const target = String(name || '').toLowerCase();
return Array.from(parent.children || []).find((child) => localName(child) === target) || null;
}
function allByLocal(parent, name) {
if (!parent) return [];
return Array.from(parent.children || []).filter((child) => localName(child) === name);
const target = String(name || '').toLowerCase();
return Array.from(parent.children || []).filter((child) => localName(child) === target);
}
/** Word OMML m:dPr 上的分隔符begChr / endChr */
function getDelimCharFromD(node, which) {
const dPr = firstByLocal(node, 'dPr');
if (!dPr) return null;
const el = firstByLocal(dPr, which === 'beg' ? 'begChr' : 'endChr');
if (!el) return null;
return el.getAttribute('m:val') || el.getAttribute('val') || el.textContent || null;
}
function isNormDelimiterChar(ch) {
if (ch == null || ch === '') return false;
const c = String(ch).trim();
return c === '|' || c === '‖' || c === '∥' || c === '│' || c === '\u2016' || c === '||';
}
/** matrix 内两行均为 with probability */
function isMatrixWithProbabilityRows(latex) {
const s = String(latex || '');
if (!/\\begin\{matrix\}/.test(s)) return false;
const n = (s.match(/with\s*probability/gi) || []).length;
return n >= 2;
}
/** 分段 matrixif/otherwise 或 with probability */
function isPiecewiseMatrixContent(latex) {
const s = String(latex || '');
if (!/\\begin\{matrix\}/.test(s)) return false;
if (/,\s*(?:&\s*)?if\b/i.test(s) && /\botherwise\b/i.test(s)) return true;
return isMatrixWithProbabilityRows(s);
}
/** 已是标准 matrix/cases 分段(无需再修) */
function isProperPiecewiseLatex(latex) {
const s = String(latex || '');
if (isPiecewiseMatrixContent(s)) {
const hasDelims = /\\left\s*\\?\{/.test(s) && /\\right\s*[\.\)]?/.test(s) && /\\end\{matrix\}/.test(s);
if (!hasDelims) return false;
if (isMatrixWithProbabilityRows(s)) {
return /,\s*&\s*with\s+probability/gi.test(s);
}
return true;
}
if (/\\begin\{(matrix|cases|aligned)\}/.test(s) && /\\end\{(matrix|cases|aligned)\}/.test(s)) {
return true;
}
if (/\\left\s*\\?\{/.test(s) && /\\right\s*[\.\)]?/.test(s) && /\\\\/.test(s)) {
return true;
}
return false;
}
/** Word 将分段压成一行:{…, if …, otherwise} / \atop / \text{if} 等 */
function isFlattenedPiecewiseBrace(latex) {
const inner = String(latex || '');
if (isProperPiecewiseLatex(inner)) return false;
if (/\\atop\b/i.test(inner)) return true;
if (!/(?:\\{|{)/.test(inner)) return false;
const hasIf = /,\s*(?:\\text\{)?if\b/i.test(inner);
const hasOtherwise = /\botherwise\b/i.test(inner);
return hasIf && hasOtherwise;
}
function stripTrailingPiecewiseGarbage(latex) {
return String(latex || '')
.trim()
.replace(/\)\s*$/, '')
.trim();
}
function repairUnicodeMathSymbols(latex) {
return String(latex || '')
.replace(/\u223c/g, ' \\sim ')
.replace(//g, ' \\sim ')
.replace(/\u2212/g, '-');
}
function repairTildeDistribution(latex) {
return repairUnicodeMathSymbols(String(latex || ''))
.replace(/([a-zA-Z])\s*\\sim\s*U\s*\(/g, '$1 \\sim \\mathcal{U}(')
.replace(/([a-zA-Z])\s*\\sim\s*\\mathcal\{U\}\s*\(/g, '$1 \\sim \\mathcal{U}(')
.replace(/([a-zA-Z])\s*~\s*U\s*\(/g, '$1 \\sim \\mathcal{U}(')
.replace(/([a-zA-Z])\s*~\s*N\s*\(/g, '$1 \\sim \\mathcal{N}(');
}
/**
* Word 导出:={\\begin{matrix}…\\end{matrix}),缺 \\left\\{、仅有 \\right.、缺 &、Unicode
*/
function repairMatrixPiecewiseDelimiters(latex) {
let s = normalizeProbabilityText(repairTildeDistribution(String(latex || '').trim()));
s = s.replace(/,\s*otherwise\b/gi, ', & otherwise');
s = s.replace(/,with\s+probability/gi, ', & with probability');
s = s.replace(/,\s+with\s+probability/gi, ', & with probability');
s = s.replace(/\\end\{matrix\}\s*\)\s*$/i, '\\end{matrix} \\right.');
if (!/\\begin\{matrix\}/.test(s) || !isPiecewiseMatrixContent(s)) {
return s;
}
const hasLeft = /\\left\s*\\?\{/.test(s);
const hasRight = /\\right\s*[\.\)]?/.test(s);
if (hasLeft && hasRight && isProperPiecewiseLatex(s)) {
return s;
}
const m = s.match(/^([\s\S]+?)=\s*(?:\\{|{)\s*\\begin\{matrix\}\s*([\s\S]+?)\\end\{matrix\}\s*(?:\\right\.)?\s*$/i);
if (!m) return s;
let inner = m[2].trim();
inner = inner.replace(/,with\s+probability/gi, ', & with probability');
inner = inner.replace(/,\s+with\s+probability/gi, ', & with probability');
return `${m[1].trim()} = \\left\\{ \\begin{matrix} ${inner} \\end{matrix} \\right.`;
}
/** 去掉 Word 在 if/otherwise 两侧插入的 \\quad便于分段正则匹配 */
function normalizePiecewiseSpacing(latex) {
return String(latex || '')
.replace(/,\s*\\quad\s*\\text\{if\s*\}\s*\\quad\s*/g, ', \\text{if } ')
.replace(/,\s*\\quad\s*\\text\{if\s*\}\s*/g, ', \\text{if } ')
.replace(/,\s*\\text\{if\s*\}\s*\\quad\s*/g, ', \\text{if } ')
.replace(/,\s*\\quad\s*\\text\{otherwise\s*\}\s*/g, ', \\text{otherwise} ')
.replace(/,\s*\\text\{otherwise\s*\}\s*\\quad\s*/g, ', \\text{otherwise} ');
}
function normalizeProbabilityText(latex) {
return String(latex || '')
.replace(/withprobability/gi, ' with probability ')
.replace(/with\s+probability\s+/gi, ' with probability ');
}
/** Word 有时输出 \\{^{上}_{下}} 叠字花括号分段 */
function repairStackedBracePiecewise(latex) {
const s = String(latex || '').trim();
const m = s.match(
/^([\s\S]+?)=(?:\\{|{)\^\{([\s\S]+?),\s*\\text\{if\s*\}\s*([\s\S]+?)\}_\{([\s\S]+?)(?:,\s*\\text\{otherwise\s*\})?\}\s*\)?\s*$/
);
if (!m) return s;
const lhs = m[1].trim();
const branch1 = m[2].trim();
const condition = repairTildeDistribution(m[3].trim());
let branch2 = m[4].trim().replace(/,\s*\\text\{otherwise\s*\}\s*$/i, '').trim();
if (!branch1 || !branch2 || !condition) return s;
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & if ${condition} \\\\ ${branch2}, & otherwise \\end{matrix} \\right.`;
}
/** with probability 分段:\\{ expr, with probability p1) expr2, with probability p2 */
function repairProbabilityPiecewise(latex) {
const s = normalizeProbabilityText(String(latex || '').trim());
const m = s.match(
/^([\s\S]+?)=(?:\\{|{)([\s\S]+?),\s*with probability\s+([^,]+?)\)\s*([^,]+?),\s*with probability\s+(\S+)\s*(?:\\}|})?\s*$/
);
if (!m) return String(latex || '').trim();
const lhs = m[1].trim();
const branch1 = m[2].trim();
const prob1 = m[3].trim();
const branch2 = m[4].trim();
const prob2 = m[5].trim();
if (!branch1 || !branch2) return String(latex || '').trim();
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & with probability ${prob1} \\\\ ${branch2}, & with probability ${prob2} \\end{matrix} \\right.`;
}
/** 修复范数:// → \\|,以及 \\| ... \\| _{2} 间距 */
function repairNormDelimiters(latex) {
let s = String(latex || '');
s = s.replace(/\/\/\s*(?=\\?[a-zA-Z_{\\])/g, '\\| ');
s = s.replace(/\/\/\s*_\{/g, '\\|_{');
s = s.replace(/,\s*if\s+\/\/\s*/gi, ', & if \\| ');
s = s.replace(/,\s*if\s+(?=\\?[a-zA-Z_\\|])/gi, ', & if ');
s = s.replace(/\\\|\s+([\s\S]+?)\s+\\\|\s*_\{2\}/g, '\\left\\|$1\\right\\|_2');
s = s.replace(/\\\|\s+([\s\S]+?)\s+\\\|_2/g, '\\left\\|$1\\right\\|_2');
s = s.replace(/\\\|\s*_\{2\}/g, '\\|_2');
return s;
}
/** 未闭合的 \\begin{matrix}:尝试收成单行分式 */
function repairIncompleteMatrix(latex) {
const s = String(latex || '').trim();
if (!/\\begin\{matrix\}/.test(s) || /\\end\{matrix\}/.test(s)) return s;
const m = s.match(
/^([\s\S]+?)=\{\\begin\{matrix\}\s*\\frac\{([\s\S]+?)\}\{([^}]+)\}(?:,\s*(?:&\s*)?if\s+([\s\S]+))?\s*$/
);
if (!m) return s;
const lhs = m[1].trim();
const num = m[2].trim();
const den = m[3].trim();
const cond = m[4] ? m[4].trim() : '';
if (cond) {
return `${lhs} = \\frac{${num}}{${den}}, \\quad \\text{if } ${cond}`;
}
return `${lhs} = \\frac{${num}}{${den}}`;
}
/** \\min_{i}L、^{\\left(t \\ 1\\right)} 等 */
function repairMiscLatexGlitches(latex) {
return String(latex || '')
.replace(/\bmin_\{1\}L/g, '\\min L')
.replace(/\\min_\{([^}]+)\}L/g, '\\min_$1 L')
.replace(/\\left\(\s*([a-zA-Z])\s*\\\s*(\d+)\s*\\right\)/g, '($1+$2)')
.replace(/\^\{\\left\(([^)]+)\s*\\\s*(\d+)\s*\\right\)\}/g, '^{($1+$2)}')
.replace(/(^|[^\\])left\(/g, '$1\\left(')
.replace(/(^|[^\\])right\)/g, '$1\\right)');
}
/** Word 导出 \\atop 堆叠分段:{ expr1, if cond \\atop expr2, otherwise } */
function repairAtopPiecewise(latex) {
let s = stripTrailingPiecewiseGarbage(String(latex || '').trim());
if (!/\\atop\b/i.test(s)) return String(latex || '').trim();
const m = s.match(
/^([\s\S]+?)=(?:\\{|{)([\s\S]+?),\s*(?:\\text\{if\s*\}|if)\s+([\s\S]+?)\s+\\atop\s+([\s\S]+?)(?:,\s*(?:\\text\{)?otherwise)?\s*$/i
);
if (!m) return String(latex || '').trim();
const lhs = m[1].trim();
const branch1 = m[2].trim();
const condition = repairTildeDistribution(m[3].trim());
const branch2 = m[4].trim().replace(/,\s*otherwise\s*$/i, '').trim();
if (!branch1 || !branch2 || !condition) return String(latex || '').trim();
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & if ${condition} \\\\ ${branch2}, & otherwise \\end{matrix} \\right.`;
}
function buildPiecewiseMatrix(lhs, branch1, condition, branch2) {
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & if ${condition} \\\\ ${branch2}, & otherwise \\end{matrix} \\right.`;
}
/**
* 将压扁的两段式分段函数还原为 matrix + \left\{ \right.
* 支持 \\text{if}、裸 if、otherwise
*/
function repairFlattenedPiecewise(latex) {
const original = String(latex || '').trim();
let s = stripTrailingPiecewiseGarbage(normalizePiecewiseSpacing(original));
if (!isFlattenedPiecewiseBrace(s)) return original;
const atopFixed = repairAtopPiecewise(s);
if (atopFixed !== s && !/\\atop\b/i.test(atopFixed)) return atopFixed;
const m = s.match(
/^([\s\S]+?)=(?:\\{|{)([\s\S]+?),\s*(?:\\text\{if\s*\}|if)\s*([\s\S]+?),\s*(?:\\text\{otherwise\s*\}|otherwise)\s*$/i
);
if (!m) return original;
const lhs = m[1].trim();
const branch1 = m[2].trim();
let mid = repairTildeDistribution(m[3].trim());
const split = mid.match(/^([\s\S]+?)(<|>|<=|>=)(-?\d*\.?\d+)\s*([\s\S]+)$/);
if (!split) return original;
const condition = `${split[1].trim()}${split[2]}${split[3]}`.trim();
const branch2 = split[4].trim();
if (!branch1 || !branch2 || !condition) return original;
return buildPiecewiseMatrix(lhs, branch1, condition, branch2);
}
/**
* 修复 Word 导入常见 LaTeX 瑕疵(范数变 //、裸 if、重复 $$ 等)
*/
export function repairImportedLatex(latex) {
let s = String(latex || '').trim();
if (!s) return '';
s = s.replace(/^\$+/, '').replace(/\$+$/, '').trim();
s = repairMatrixPiecewiseDelimiters(s);
s = repairAtopPiecewise(s);
s = repairStackedBracePiecewise(s);
s = repairProbabilityPiecewise(s);
s = repairFlattenedPiecewise(s);
s = repairNormDelimiters(s);
s = repairIncompleteMatrix(s);
s = repairTildeDistribution(s);
s = repairMiscLatexGlitches(s);
s = s.replace(/\s+/g, ' ').trim();
return s;
}
function normalizeLatexCompareKey(latex) {
return String(latex || '')
.trim()
.replace(/^\$+/, '')
.replace(/\$+$/, '')
.replace(/\s+/g, ' ');
}
/** Word 导入后仍残留的常见异常片段(如范数变成 // */
export function hasLatexImportArtifacts(latex) {
const raw = String(latex || '');
if (/\/\//.test(raw)) return true;
const inner = normalizeLatexCompareKey(latex);
if (!inner) return false;
if (/\/\/\s*(?=\\?[a-zA-Z_])/.test(inner)) return true;
if (/\/\/\s*_\{/.test(inner)) return true;
if (/\bmin_\{1\}L/.test(inner)) return true;
if (/(^|[^\\])left\(/.test(inner)) return true;
if (/(^|[^\\])right\)/.test(inner)) return true;
if (isFlattenedPiecewiseBrace(inner)) return true;
if (/[a-zA-Z]\s*~\s*U\s*\(/.test(inner)) return true;
if (/[a-zA-Z]\s*\\sim\s*U\s*\(/.test(inner)) return true;
if (/^\$\$[\s\S]+\$\$$/.test(String(latex || '').trim())) return true;
if (/(?:\\{|{)\^\{[\s\S]*\\text\{if/.test(inner)) return true;
if (/withprobability/i.test(inner)) return true;
if (/\\begin\{matrix\}/.test(inner) && !/\\end\{matrix\}/.test(inner)) return true;
if (/\\left\(\s*[a-zA-Z]\s*\\\s*\d+\s*\\right\)/.test(inner)) return true;
if (/\\atop\b/i.test(inner)) return true;
if (isPiecewiseMatrixContent(inner) && !/\\left\s*\\?\{/.test(inner)) return true;
if (/\\right\s*[\.\)]?/.test(inner) && !/\\left\s*\\?\{/.test(inner) && /\\begin\{matrix\}/.test(inner)) {
return true;
}
if (/with\s*probability/i.test(inner) && !/,\s*&\s*with\s+probability/i.test(inner)) return true;
if (/,\s*otherwise\b/i.test(inner) && !/,\s*&\s*otherwise\b/i.test(inner)) return true;
if (//.test(raw) || /\u223c/.test(raw)) return true;
return false;
}
/** data-latex 是否明显损坏(不含合法的 \\left/\\frac 等) */
export function isAttrLatexStructurallyBroken(latex) {
const t = String(latex || '').trim();
if (!t) return true;
if (/^\$\$/.test(t) || /\$\$$/.test(t)) return true;
if (/\\\\/.test(t) && t.length > 12) return true;
if (/\/\/\s*(_|[\\a-zA-Z])/.test(t)) return true;
return false;
}
/** 文本是否像未渲染的 LaTeX 源码DOM 内可见 $$、\\frac 等) */
export function isRawLatexTextProblematic(text) {
const t = String(text || '').trim();
if (!t) return true;
if (isAttrLatexStructurallyBroken(t)) return true;
if (/\\(begin|frac|left|right|mathcal|atop|end|sim)\b/i.test(t)) return true;
return false;
}
/**
* 列表/预览 DOM是否应标红露出源码、MathJax 报错、或导入时已标记)
*/
export function isFormulaDisplayedAsRawSource(wmath) {
if (!wmath) return true;
if (wmath.getAttribute('data-import-error') === '1') return true;
if (wmath.querySelector('mjx-merror')) return true;
const mjx = wmath.querySelector('mjx-container');
if (mjx) {
const text = (wmath.textContent || '').trim();
if (text && text !== '1' && isRawLatexTextProblematic(text)) return true;
return false;
}
const text = (wmath.textContent || '').trim();
if (!text || text === '1') {
const fromAttr = extractLatexFromWmathElement(wmath);
return isAttrLatexStructurallyBroken(fromAttr);
}
return isRawLatexTextProblematic(text);
}
/**
* @deprecated 标红请用 isFormulaDisplayedAsRawSource(wmath);此处仅判断缺少 wmath 标签
*/
export function isProblematicLatexFormula(rawCellHtml) {
const div = document.createElement('div');
div.innerHTML = String(rawCellHtml || '');
return !div.querySelector('wmath');
}
function walkOmml(node) {
@@ -157,17 +534,48 @@ function walkOmml(node) {
}
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))
const begChr = getDelimCharFromD(node, 'beg') || node.getAttribute('m:begChr') || node.getAttribute('begChr') || '(';
const endChr = getDelimCharFromD(node, 'end') || node.getAttribute('m:endChr') || node.getAttribute('endChr') || ')';
const elems = allByLocal(node, 'e');
let inner;
if (elems.length > 1) {
inner = elems.map((item) => walkOmmlChildren(item)).join(' \\\\ ');
} else if (elems.length === 1) {
inner = walkOmmlChildren(elems[0]);
} else {
inner = Array.from(node.children || [])
.filter((child) => localName(child) !== 'dpr')
.map((child) => walkOmml(child))
.join('');
}
if (isNormDelimiterChar(begChr) && isNormDelimiterChar(endChr)) {
return `\\left\\|${inner}\\right\\|`;
}
if (begChr === '(' && endChr === ')') return `\\left(${inner}\\right)`;
if (begChr === '[' && endChr === ']') return `\\left[${inner}\\right]`;
if (begChr === '{' && endChr === '}') return `\\left\\{${inner}\\right\\}`;
if (begChr === '{' && endChr === '}') {
if (elems.length > 1 && !/\\begin\{(matrix|cases)\}/.test(inner)) {
inner = `\\begin{matrix} ${inner} \\end{matrix}`;
}
if (inner.includes('\\\\') || inner.includes('\\begin{matrix}')) {
return `\\left\\{ ${inner} \\right.`;
}
return `\\left\\{${inner}\\right\\}`;
}
if (begChr === '|' && endChr === '|') return `\\left|${inner}\\right|`;
return `${begChr}${inner}${endChr}`;
}
if (name === 'func') {
const fNameEl = firstByLocal(node, 'fName');
const fname = (fNameEl && (fNameEl.getAttribute('m:val') || fNameEl.getAttribute('val') || fNameEl.textContent)) || '';
const fnameTrim = String(fname).trim();
const body = walkOmmlChildren(firstByLocal(node, 'e'));
if (!fnameTrim) return body;
const cmd = fnameTrim.replace(/\s+/g, '');
return `\\${cmd}${body ? ` ${body}` : ''}`;
}
if (name === 'nary') {
const chr = node.getAttribute('m:chr') || node.getAttribute('chr') || '\\int';
const sub = walkOmmlChildren(firstByLocal(node, 'sub'));
@@ -184,7 +592,34 @@ function walkOmml(node) {
return result;
}
if (name === 'eqarr' || name === 'm') {
if (name === 'eqarr') {
const rows = allByLocal(node, 'e');
if (rows.length > 1) {
const latexRows = rows.map((row) => walkOmmlChildren(row));
return `\\begin{matrix} ${latexRows.join(' \\\\ ')} \\end{matrix}`;
}
return walkOmmlChildren(node);
}
if (name === 'm') {
const matrixRows = allByLocal(node, 'mr');
if (matrixRows.length > 0) {
const latexRows = matrixRows.map((mr) => {
const cols = allByLocal(mr, 'mc');
if (cols.length > 0) {
return cols.map((mc) => walkOmmlChildren(mc)).join(' & ');
}
return walkOmmlChildren(mr);
});
if (latexRows.length > 1) {
return `\\begin{matrix} ${latexRows.join(' \\\\ ')} \\end{matrix}`;
}
return latexRows[0] || walkOmmlChildren(node);
}
return walkOmmlChildren(node);
}
if (name === 'mc') {
return walkOmmlChildren(node);
}
@@ -208,7 +643,7 @@ function walkOmmlChildren(node) {
function ommlNodeToLatex(node) {
if (!node) return '';
return walkOmml(node).replace(/\s+/g, ' ').trim();
return repairImportedLatex(walkOmml(node).replace(/\s+/g, ' ').trim());
}
function isRunBold(run) {
@@ -345,16 +780,14 @@ export const MATH_FORMULA_TABLE_TITLE = 'Latex Data';
export const LEGACY_MATH_FORMULA_TABLE_TITLE = '数字公式';
export function isMathFormulaTableTitle(title) {
return title === MATH_FORMULA_TABLE_TITLE || title === LEGACY_MATH_FORMULA_TABLE_TITLE;
const t = String(title || '').trim();
if (!t) return false;
if (t === LEGACY_MATH_FORMULA_TABLE_TITLE) return true;
return t.replace(/\s+/g, ' ').toLowerCase() === 'latex data';
}
function normalizeWmathLatex(latex) {
let value = String(latex || '').trim();
if (!value) return '';
if (/^\$\$[\s\S]+\$\$$/.test(value) || /^\$[\s\S]+\$$/.test(value)) {
return value;
}
return `$$${value}$$`;
return repairImportedLatex(latex);
}
function buildWmathHtml(latex, wrap, id) {
@@ -363,7 +796,7 @@ function buildWmathHtml(latex, wrap, id) {
const uid = id || `wmath-${Math.random().toString(36).substr(2, 9)}`;
const mode = wrap === 'inline' || wrap === 'block' ? wrap : 'block';
const safe = escapeLatexForAttr(normalized);
return `<wmath contenteditable="false" data-id="${uid}" data-latex="${safe}" data-wrap="${mode}">${normalized}</wmath>`;
return `<wmath contenteditable="false" data-id="${uid}" data-latex="${safe}" data-wrap="${mode}">1</wmath>`;
}
export function buildWmathCellHtml(latex, wrap, id) {
@@ -377,10 +810,65 @@ export function getLatexFromCellHtml(html) {
const wmath = div.querySelector('wmath');
if (wmath) {
const fromAttr = extractLatexFromWmathElement(wmath);
if (fromAttr) return fromAttr;
return (wmath.textContent || '').trim();
if (fromAttr) return repairImportedLatex(fromAttr);
return repairImportedLatex((wmath.textContent || '').trim());
}
return (div.textContent || '').trim();
return repairImportedLatex((div.textContent || '').trim());
}
/** 展示/编辑前:修复 LaTeX 并重建 wmath 单元格 HTML兼容历史脏数据 */
export function normalizeWmathCellHtml(html) {
const div = document.createElement('div');
div.innerHTML = String(html || '');
const wmath = div.querySelector('wmath');
if (!wmath) return html || '';
const raw = extractLatexFromWmathElement(wmath) || (wmath.textContent || '').trim();
const repaired = repairImportedLatex(raw);
if (!repaired) return html || '';
const wrap = wmath.getAttribute('data-wrap') || 'block';
const id = wmath.getAttribute('data-id') || '';
return buildWmathHtml(repaired, wrap, id) || html || '';
}
/**
* 为 Word 导入预览标注公式序号(与 Latex Data 列表行号一致,仅计有 LaTeX 的 wmath
*/
function markOrphanLatexBlocksInPreview(root) {
root.querySelectorAll('p, td, th, li').forEach((block) => {
if (block.querySelector('wmath')) return;
const text = (block.textContent || '').trim();
if (!text) return;
if (/\$\$[\s\S]+?\$\$/.test(text) || /\\begin\{/.test(text) || /\\frac\s*\{/.test(text)) {
block.classList.add('word-import-orphan-latex');
}
});
}
export function buildWordImportPreviewHtml(html) {
if (!html) return '';
const div = document.createElement('div');
div.innerHTML = String(html);
let index = 0;
div.querySelectorAll('wmath').forEach((el) => {
const latex = extractLatexFromWmathElement(el);
if (!latex) return;
index += 1;
const idx = String(index);
el.setAttribute('data-formula-index', idx);
const anchor = document.createElement('span');
anchor.className = 'word-import-formula-anchor';
anchor.setAttribute('data-formula-index', idx);
const badge = document.createElement('span');
badge.className = 'word-import-formula-index-badge';
badge.textContent = idx;
const parent = el.parentNode;
if (!parent) return;
parent.insertBefore(anchor, el);
anchor.appendChild(badge);
anchor.appendChild(el);
});
markOrphanLatexBlocksInPreview(div);
return div.innerHTML;
}
/** 将解析后的 HTML 中的 wmath 提取为单个表格 table_data表头 + 公式行) */
@@ -407,8 +895,8 @@ export function extractWmathTableDataFromHtml(html) {
}
return {
tableData: [
[{ text: '<b>Latex Data</b>', colspan: 1, rowspan: 1 }],
tableData: [
[{ text: '<b>Latex Data</b>', colspan: 1, rowspan: 1 }],
...formulaRows
],
html: htmlParts.join('')