1023 lines
29 KiB
JavaScript
1023 lines
29 KiB
JavaScript
import {
|
||
AlignmentType,
|
||
BorderStyle,
|
||
CommentRangeEnd,
|
||
CommentRangeStart,
|
||
CommentReference,
|
||
Document,
|
||
LineRuleType,
|
||
Packer,
|
||
Paragraph,
|
||
ShadingType,
|
||
Table,
|
||
TableCell,
|
||
TableLayoutType,
|
||
TableRow,
|
||
TextRun,
|
||
VerticalAlign,
|
||
WidthType
|
||
} from 'docx';
|
||
import { saveAs } from 'file-saver';
|
||
import JSZip from 'jszip';
|
||
import { TableUtils } from '@/common/js/TableUtils';
|
||
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
|
||
import { expandCitationBracket } from '@/utils/manuscriptCitationRelevance';
|
||
import { tokenizeReferenceHighlightSegments } from '@/utils/manuscriptMediaReferences';
|
||
|
||
/** 斑马纹 rgb(250, 231, 232) */
|
||
const ODD_ROW_FILL = 'FAE7E8';
|
||
/** 标题 rgb(210, 90, 90) */
|
||
const TITLE_COLOR = 'D25A5A';
|
||
/** 引用 blue rgb(0, 130, 170) */
|
||
const BLUE_COLOR = '0082AA';
|
||
const FONT_NAME = 'Charis SIL';
|
||
/** 六号 = 7.5pt */
|
||
const TABLE_FONT_SIZE = 15;
|
||
const TITLE_FONT_SIZE = TABLE_FONT_SIZE;
|
||
/** Word「为字体调整字间距」:1 磅或更大(w:kern 以半磅为单位) */
|
||
const FONT_KERN_MIN_1PT = 2;
|
||
/** Word「为字体调整字间距」:小二(18pt)或更大 */
|
||
const FONT_KERN_MIN_XIAO_ER = 36;
|
||
|
||
function cmToTwips(cm) {
|
||
return Math.round((cm * 1440) / 2.54);
|
||
}
|
||
|
||
/** 页面边距:上/下 2.54cm,左/右 1.91cm */
|
||
const PAGE_MARGINS = {
|
||
top: cmToTwips(2.54),
|
||
bottom: cmToTwips(2.54),
|
||
left: cmToTwips(1.91),
|
||
right: cmToTwips(1.91)
|
||
};
|
||
|
||
/** 表格属性:宽 98.3%,整体居中,无环绕 */
|
||
const TABLE_WIDTH_PERCENT = 98.3;
|
||
/** 表格选项(图二):单元格边距 上/下 0、左/右 0.19cm,自动适应内容 */
|
||
const TABLE_CELL_MARGIN_TWIPS = cmToTwips(0.19);
|
||
const TABLE_CELL_MARGINS = {
|
||
top: 0,
|
||
bottom: 0,
|
||
left: TABLE_CELL_MARGIN_TWIPS,
|
||
right: TABLE_CELL_MARGIN_TWIPS
|
||
};
|
||
/** 行属性:允许跨页断行,不指定行高 */
|
||
const TABLE_ROW_OPTIONS = {
|
||
cantSplit: false
|
||
};
|
||
/** 全篇段落:固定行距 10 磅(1pt = 20 twips → 10pt = 200) */
|
||
const WORD_PARAGRAPH_SPACING = {
|
||
before: 0,
|
||
after: 0,
|
||
line: 200,
|
||
lineRule: LineRuleType.EXACT,
|
||
beforeAutoSpacing: false,
|
||
afterAutoSpacing: false
|
||
};
|
||
|
||
const WORD_DOCUMENT_STYLES = {
|
||
default: {
|
||
document: {
|
||
paragraph: {
|
||
spacing: WORD_PARAGRAPH_SPACING
|
||
},
|
||
run: {
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
kern: FONT_KERN_MIN_1PT
|
||
}
|
||
}
|
||
},
|
||
paragraphStyles: [
|
||
{
|
||
id: 'Normal',
|
||
name: 'Normal',
|
||
quickFormat: true,
|
||
spacing: WORD_PARAGRAPH_SPACING,
|
||
run: {
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
kern: FONT_KERN_MIN_1PT
|
||
}
|
||
},
|
||
{
|
||
id: 'Heading1',
|
||
name: 'Heading 1',
|
||
basedOn: 'Normal',
|
||
next: 'Normal',
|
||
quickFormat: true,
|
||
spacing: WORD_PARAGRAPH_SPACING,
|
||
run: {
|
||
font: FONT_NAME,
|
||
size: TITLE_FONT_SIZE,
|
||
bold: true,
|
||
color: TITLE_COLOR,
|
||
kern: FONT_KERN_MIN_1PT
|
||
}
|
||
}
|
||
]
|
||
};
|
||
|
||
/** 每个表格块末尾空行数 */
|
||
const TABLE_BLOCK_EMPTY_LINE_COUNT = 4;
|
||
|
||
function createWordParagraph(children, options) {
|
||
const opts = options || {};
|
||
let alignment = AlignmentType.JUSTIFIED;
|
||
if (opts.alignment !== undefined && opts.alignment !== null) {
|
||
alignment = opts.alignment;
|
||
}
|
||
|
||
const paragraphOptions = {
|
||
spacing: WORD_PARAGRAPH_SPACING,
|
||
indent: {
|
||
left: 0,
|
||
right: 0
|
||
},
|
||
children
|
||
};
|
||
|
||
if (opts.title) {
|
||
paragraphOptions.alignment = AlignmentType.CENTER;
|
||
paragraphOptions.style = 'Normal';
|
||
} else {
|
||
paragraphOptions.alignment = alignment;
|
||
paragraphOptions.style = 'Normal';
|
||
}
|
||
|
||
return new Paragraph(paragraphOptions);
|
||
}
|
||
|
||
function appendTableBlockEmptyLines(children) {
|
||
let i = 0;
|
||
for (i = 0; i < TABLE_BLOCK_EMPTY_LINE_COUNT; i += 1) {
|
||
children.push(createTableParagraph([new TextRun('')], AlignmentType.JUSTIFIED));
|
||
}
|
||
}
|
||
|
||
/** 表格标题段落:居中,固定行距 10pt,段前段后 0,无缩进(非大纲一级) */
|
||
function createTitleParagraph(children) {
|
||
return createWordParagraph(children, { alignment: AlignmentType.CENTER });
|
||
}
|
||
|
||
function decodeTableCaptionXmlText(text) {
|
||
return String(text || '')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, "'")
|
||
.replace(/&/g, '&');
|
||
}
|
||
|
||
function extractTableCaptionParagraphPlainText(pInner) {
|
||
const parts = [];
|
||
String(pInner || '').replace(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g, function (_match, value) {
|
||
parts.push(decodeTableCaptionXmlText(value));
|
||
});
|
||
return parts.join('').replace(/[\u200B-\u200D\uFEFF]/g, '').trim();
|
||
}
|
||
|
||
function isBodyTableCaptionPlainText(text) {
|
||
return /^Table\s+\d+/i.test(String(text || '').trim());
|
||
}
|
||
|
||
function applyTableCaptionCenterParagraphPr(pInner) {
|
||
if (/<w:pPr>/.test(pInner)) {
|
||
let inner = pInner
|
||
.replace(/<w:jc\b[^>]*\/>/g, '')
|
||
.replace(/<w:jc\b[^>]*>[\s\S]*?<\/w:jc>/g, '');
|
||
return inner.replace(/<w:pPr>/, '<w:pPr><w:jc w:val="center"/>');
|
||
}
|
||
return '<w:pPr><w:jc w:val="center"/></w:pPr>' + pInner;
|
||
}
|
||
|
||
/** 从 tbl 前向前收集紧邻的 w:p(避免全文正则灾难性回溯) */
|
||
function collectParagraphBlocksImmediatelyBefore(xml, tblIdx, minIndex) {
|
||
const blocks = [];
|
||
let end = tblIdx;
|
||
|
||
while (end > minIndex && blocks.length < 8) {
|
||
let ws = end;
|
||
while (ws > minIndex && /\s/.test(xml.charAt(ws - 1))) {
|
||
ws -= 1;
|
||
}
|
||
if (ws <= minIndex) {
|
||
break;
|
||
}
|
||
const closeIdx = xml.lastIndexOf('</w:p>', ws - 1);
|
||
if (closeIdx < minIndex) {
|
||
break;
|
||
}
|
||
const openIdx = xml.lastIndexOf('<w:p', closeIdx);
|
||
if (openIdx < minIndex) {
|
||
break;
|
||
}
|
||
const raw = xml.slice(openIdx, ws);
|
||
if (!/<\/w:p>\s*$/i.test(raw)) {
|
||
break;
|
||
}
|
||
blocks.unshift({
|
||
start: openIdx,
|
||
raw: raw,
|
||
inner: raw.replace(/^<w:p\b[^>]*>/i, '').replace(/<\/w:p>\s*$/i, '')
|
||
});
|
||
end = openIdx;
|
||
}
|
||
|
||
return blocks;
|
||
}
|
||
|
||
function isHomePageMetadataTableBlock(block) {
|
||
const text = String(block || '');
|
||
return /Author/i.test(text) && /contributions/i.test(text) && /Abstract/i.test(text);
|
||
}
|
||
|
||
/** 正文表格标题(Table N…)紧挨 w:tbl 前的段落强制居中,不含首页元数据表格 */
|
||
export function patchBodyTableCaptionCenterToXml(xml) {
|
||
if (!xml || xml.indexOf('<w:tbl') < 0) {
|
||
return xml;
|
||
}
|
||
|
||
const parts = [];
|
||
let cursor = 0;
|
||
const tblToken = '<w:tbl';
|
||
const closeTag = '</w:tbl>';
|
||
|
||
while (cursor < xml.length) {
|
||
const tblIdx = xml.indexOf(tblToken, cursor);
|
||
if (tblIdx < 0) {
|
||
parts.push(xml.slice(cursor));
|
||
break;
|
||
}
|
||
|
||
const tblEnd = xml.indexOf(closeTag, tblIdx);
|
||
if (tblEnd < 0) {
|
||
parts.push(xml.slice(cursor));
|
||
break;
|
||
}
|
||
|
||
const tblEndExclusive = tblEnd + closeTag.length;
|
||
const tblBlock = xml.slice(tblIdx, tblEndExclusive);
|
||
|
||
if (isHomePageMetadataTableBlock(tblBlock)) {
|
||
parts.push(xml.slice(cursor, tblEndExclusive));
|
||
cursor = tblEndExclusive;
|
||
continue;
|
||
}
|
||
|
||
const blocks = collectParagraphBlocksImmediatelyBefore(xml, tblIdx, cursor);
|
||
const isCaption =
|
||
blocks.length &&
|
||
isBodyTableCaptionPlainText(extractTableCaptionParagraphPlainText(blocks[0].inner));
|
||
|
||
if (isCaption) {
|
||
parts.push(xml.slice(cursor, blocks[0].start));
|
||
blocks.forEach(function (entry) {
|
||
parts.push(
|
||
entry.raw.replace(/(<w:p\b[^>]*>)([\s\S]*?)(<\/w:p>)/i, function (_full, open, inner, close) {
|
||
return open + applyTableCaptionCenterParagraphPr(inner) + close;
|
||
})
|
||
);
|
||
});
|
||
const afterBlocks = blocks[blocks.length - 1].start + blocks[blocks.length - 1].raw.length;
|
||
parts.push(xml.slice(afterBlocks, tblIdx));
|
||
} else {
|
||
parts.push(xml.slice(cursor, tblIdx));
|
||
}
|
||
|
||
parts.push(tblBlock);
|
||
cursor = tblEndExclusive;
|
||
}
|
||
|
||
return parts.join('');
|
||
}
|
||
|
||
/** 导出后处理:除首页表格外,正文表格标题居中 */
|
||
async function patchBodyTableCaptionCenter(blob) {
|
||
if (!blob) {
|
||
return blob;
|
||
}
|
||
|
||
const zip = await JSZip.loadAsync(await blob.arrayBuffer());
|
||
const documentFile = zip.file('word/document.xml');
|
||
if (!documentFile) {
|
||
return blob;
|
||
}
|
||
|
||
let xml = await documentFile.async('string');
|
||
if (!/\bTable\s+\d+/i.test(xml)) {
|
||
return blob;
|
||
}
|
||
|
||
xml = patchBodyTableCaptionCenterToXml(xml);
|
||
|
||
zip.file('word/document.xml', xml);
|
||
return zip.generateAsync({
|
||
type: 'blob',
|
||
mimeType: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||
});
|
||
}
|
||
|
||
/** 图片标题:与表格标题相同字号/颜色/段落,但两端对齐;末尾无句点则补 . */
|
||
function ensureFigureTitleTrailingPeriod(text) {
|
||
const trimmed = String(text || '').trim();
|
||
if (!trimmed) {
|
||
return '';
|
||
}
|
||
if (trimmed.endsWith('.')) {
|
||
return trimmed;
|
||
}
|
||
return trimmed + '.';
|
||
}
|
||
|
||
function createFigureTitleParagraph(children, alignment) {
|
||
return createWordParagraph(children, { alignment: alignment || AlignmentType.CENTER });
|
||
}
|
||
|
||
function hasFigureAnnotation(noteHtml) {
|
||
return !!htmlToPlainText(String(noteHtml || '')).trim();
|
||
}
|
||
|
||
function appendFigureTitleParagraphs(children, titleHtml, options) {
|
||
const opts = options || {};
|
||
const alignment = hasFigureAnnotation(opts.note) ? AlignmentType.JUSTIFIED : AlignmentType.CENTER;
|
||
const segments = splitHtmlSegments(titleHtml)
|
||
.map(function (segment) {
|
||
return htmlToPlainText(segment).trim();
|
||
})
|
||
.filter(Boolean);
|
||
if (!segments.length) {
|
||
return;
|
||
}
|
||
|
||
segments.forEach(function (segment, index) {
|
||
const text =
|
||
index === segments.length - 1 ? ensureFigureTitleTrailingPeriod(segment) : segment;
|
||
children.push(
|
||
createFigureTitleParagraph(
|
||
[
|
||
new TextRun({
|
||
text: text,
|
||
bold: true,
|
||
color: TITLE_COLOR,
|
||
font: FONT_NAME,
|
||
size: TITLE_FONT_SIZE,
|
||
kern: FONT_KERN_MIN_1PT
|
||
})
|
||
],
|
||
alignment
|
||
)
|
||
);
|
||
});
|
||
}
|
||
|
||
function createTableParagraph(children, alignment) {
|
||
return createWordParagraph(children, { alignment });
|
||
}
|
||
|
||
function splitHtmlSegments(html) {
|
||
const raw = String(html || '');
|
||
if (!raw) {
|
||
return [''];
|
||
}
|
||
const parts = raw.split(/<br\s*\/?>/gi);
|
||
if (!parts.length) {
|
||
return [''];
|
||
}
|
||
return parts;
|
||
}
|
||
|
||
function decodeHtmlEntities(text) {
|
||
return String(text || '')
|
||
.replace(/ /g, ' ')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function htmlToPlainText(html) {
|
||
const raw = String(html || '');
|
||
if (typeof document !== 'undefined') {
|
||
const node = document.createElement('div');
|
||
node.innerHTML = raw;
|
||
return (node.textContent || node.innerText || '').replace(/\u00a0/g, ' ').trim();
|
||
}
|
||
return decodeHtmlEntities(
|
||
raw
|
||
.replace(/<br\s*\/?>/gi, '\n')
|
||
.replace(/<[^>]*>/g, '')
|
||
).trim();
|
||
}
|
||
|
||
function isBlueElement(node) {
|
||
if (!node || node.nodeType !== Node.ELEMENT_NODE) {
|
||
return false;
|
||
}
|
||
const tag = node.tagName.toLowerCase();
|
||
if (tag === 'blue') {
|
||
return true;
|
||
}
|
||
if (tag === 'span' && node.classList && node.classList.contains('blue')) {
|
||
return true;
|
||
}
|
||
return !!(node.classList && node.classList.contains('color-highlight'));
|
||
}
|
||
|
||
/** 参考文献标号 [1] / [1, 2]:逗号后补空格(与 HTML 排版规则一致,重复 3 次) */
|
||
function normalizeCitationBracketCommaSpacing(text) {
|
||
return String(text || '').replace(/\[([^\]]*)\]/g, function (match, inner) {
|
||
let fixed = inner;
|
||
for (let i = 0; i < 3; i++) {
|
||
fixed = fixed.replace(/(\d),(\d)/g, '$1, $2');
|
||
}
|
||
return '[' + fixed + ']';
|
||
});
|
||
}
|
||
|
||
|
||
function createCitationCommentMatcher(citationComments) {
|
||
const byRef = {};
|
||
(citationComments || []).forEach(function (comment) {
|
||
if (!comment || comment.refNo == null) {
|
||
return;
|
||
}
|
||
const key = String(comment.refNo);
|
||
if (!byRef[key]) {
|
||
byRef[key] = [];
|
||
}
|
||
byRef[key].push(comment);
|
||
});
|
||
Object.keys(byRef).forEach(function (key) {
|
||
byRef[key].sort(function (a, b) {
|
||
return Number(a.occurrence || 0) - Number(b.occurrence || 0);
|
||
});
|
||
});
|
||
|
||
const nextIndex = {};
|
||
|
||
return {
|
||
matchBracket: function (inner) {
|
||
const nums = expandCitationBracket(inner);
|
||
for (let i = 0; i < nums.length; i += 1) {
|
||
const refNo = nums[i];
|
||
const key = String(refNo);
|
||
const idx = nextIndex[key] || 0;
|
||
nextIndex[key] = idx + 1;
|
||
const list = byRef[key];
|
||
if (list && list[idx]) {
|
||
return list[idx].commentId;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
};
|
||
}
|
||
|
||
function appendRunsForReferenceHighlightText(text, style, appendRun, commentMatcher) {
|
||
const normalized = normalizeCitationBracketCommaSpacing(text);
|
||
if (!normalized) {
|
||
return;
|
||
}
|
||
|
||
const segments = tokenizeReferenceHighlightSegments(normalized);
|
||
segments.forEach(function (segment) {
|
||
if (!segment || !segment.text) {
|
||
return;
|
||
}
|
||
if (segment.blue && /^\[/.test(segment.text)) {
|
||
const bracketText = segment.text;
|
||
const inner = bracketText.slice(1, -1);
|
||
const commentId = commentMatcher ? commentMatcher.matchBracket(inner) : null;
|
||
if (commentId != null && typeof appendRun.onCommentBracket === 'function') {
|
||
appendRun.onCommentBracket(bracketText, Object.assign({}, style, { blue: true }), commentId);
|
||
} else {
|
||
appendRun(bracketText, Object.assign({}, style, { blue: true }));
|
||
}
|
||
return;
|
||
}
|
||
if (segment.blue) {
|
||
appendRun(segment.text, Object.assign({}, style, { blue: true }));
|
||
return;
|
||
}
|
||
appendRun(segment.text, style);
|
||
});
|
||
}
|
||
|
||
function createStyledExportTextRun(text, style, boldDefault) {
|
||
return new TextRun({
|
||
text,
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
bold: boldDefault || (style && style.bold),
|
||
italics: style && style.italic,
|
||
superScript: style && style.sup,
|
||
subScript: style && style.sub,
|
||
color: style && style.blue ? BLUE_COLOR : undefined,
|
||
kern: FONT_KERN_MIN_1PT
|
||
});
|
||
}
|
||
|
||
function createCitationAppendRun(runs, boldDefault, commentMatcher) {
|
||
const appendRun = function (text, style) {
|
||
if (!text) {
|
||
return;
|
||
}
|
||
runs.push(createStyledExportTextRun(text, style, boldDefault));
|
||
};
|
||
if (commentMatcher) {
|
||
appendRun.onCommentBracket = function (bracketText, style, commentId) {
|
||
runs.push(new CommentRangeStart(commentId));
|
||
runs.push(createStyledExportTextRun(bracketText, style, boldDefault));
|
||
runs.push(new CommentRangeEnd(commentId));
|
||
runs.push(
|
||
new TextRun({
|
||
children: [new CommentReference(commentId)]
|
||
})
|
||
);
|
||
};
|
||
}
|
||
return appendRun;
|
||
}
|
||
|
||
function htmlToTextRuns(html, options) {
|
||
const opts = options || {};
|
||
const bold = opts.bold === true;
|
||
const citationComments = opts.citationComments || null;
|
||
const commentMatcher =
|
||
citationComments && citationComments.length ? createCitationCommentMatcher(citationComments) : null;
|
||
const runs = [];
|
||
const raw = String(html || '');
|
||
|
||
if (!raw) {
|
||
return [
|
||
new TextRun({
|
||
text: '',
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
bold,
|
||
kern: FONT_KERN_MIN_1PT
|
||
})
|
||
];
|
||
}
|
||
|
||
if (typeof document === 'undefined') {
|
||
const plainRuns = [];
|
||
const appendRun = createCitationAppendRun(plainRuns, bold, commentMatcher);
|
||
appendRunsForReferenceHighlightText(
|
||
htmlToPlainText(raw),
|
||
{ bold, italics: false, sup: false, sub: false, blue: false },
|
||
appendRun,
|
||
commentMatcher
|
||
);
|
||
if (plainRuns.length) {
|
||
return plainRuns;
|
||
}
|
||
return [
|
||
new TextRun({
|
||
text: htmlToPlainText(raw),
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
bold,
|
||
kern: FONT_KERN_MIN_1PT
|
||
})
|
||
];
|
||
}
|
||
|
||
const root = document.createElement('div');
|
||
root.innerHTML = raw;
|
||
const appendRun = createCitationAppendRun(runs, bold, commentMatcher);
|
||
|
||
function walk(node, style) {
|
||
if (node.nodeType === Node.TEXT_NODE) {
|
||
appendRunsForReferenceHighlightText(
|
||
decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' '),
|
||
style,
|
||
appendRun,
|
||
commentMatcher
|
||
);
|
||
return;
|
||
}
|
||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||
return;
|
||
}
|
||
|
||
const tag = node.tagName.toLowerCase();
|
||
if (tag === 'br') {
|
||
appendRun('\n', style);
|
||
return;
|
||
}
|
||
|
||
const nextStyle = {
|
||
bold: style.bold,
|
||
italic: style.italic,
|
||
sup: style.sup,
|
||
sub: style.sub,
|
||
blue: style.blue
|
||
};
|
||
if (tag === 'b' || tag === 'strong') {
|
||
nextStyle.bold = true;
|
||
}
|
||
if (tag === 'i' || tag === 'em') {
|
||
nextStyle.italic = true;
|
||
}
|
||
if (tag === 'sup') {
|
||
nextStyle.sup = true;
|
||
}
|
||
if (tag === 'sub') {
|
||
nextStyle.sub = true;
|
||
}
|
||
if (isBlueElement(node)) {
|
||
nextStyle.blue = true;
|
||
}
|
||
|
||
Array.from(node.childNodes).forEach((child) => walk(child, nextStyle));
|
||
}
|
||
|
||
walk(root, {
|
||
bold: false,
|
||
italic: false,
|
||
sup: false,
|
||
sub: false,
|
||
blue: false
|
||
});
|
||
|
||
if (!runs.length) {
|
||
return [
|
||
new TextRun({
|
||
text: htmlToPlainText(raw),
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
bold,
|
||
kern: FONT_KERN_MIN_1PT
|
||
})
|
||
];
|
||
}
|
||
|
||
return runs;
|
||
}
|
||
|
||
function htmlToTextRunsWithCitationComments(html, options, citationComments) {
|
||
return htmlToTextRuns(html, Object.assign({}, options || {}, { citationComments: citationComments || [] }));
|
||
}
|
||
|
||
/** 表头:相邻片段之间补空格,再规范为“一词一空” */
|
||
function formatHeaderWordSpacing(html) {
|
||
const raw = String(html || '');
|
||
if (!raw) {
|
||
return '';
|
||
}
|
||
if (typeof document !== 'undefined') {
|
||
const root = document.createElement('div');
|
||
root.innerHTML = raw;
|
||
const parts = [];
|
||
|
||
function collect(node) {
|
||
if (node.nodeType === Node.TEXT_NODE) {
|
||
const text = decodeHtmlEntities(node.textContent).replace(/\u00a0/g, ' ').trim();
|
||
if (text) {
|
||
parts.push(text);
|
||
}
|
||
return;
|
||
}
|
||
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||
return;
|
||
}
|
||
if (node.tagName.toLowerCase() === 'br') {
|
||
parts.push('\n');
|
||
return;
|
||
}
|
||
Array.from(node.childNodes).forEach(collect);
|
||
}
|
||
|
||
collect(root);
|
||
return parts
|
||
.join(' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
return htmlToPlainText(raw)
|
||
.split(/\s+/)
|
||
.filter(Boolean)
|
||
.join(' ');
|
||
}
|
||
|
||
function htmlToHeaderTextRuns(html) {
|
||
return [
|
||
new TextRun({
|
||
text: formatHeaderWordSpacing(html),
|
||
font: FONT_NAME,
|
||
size: TABLE_FONT_SIZE,
|
||
bold: true,
|
||
kern: FONT_KERN_MIN_1PT
|
||
})
|
||
];
|
||
}
|
||
|
||
/** 表格边框:黑色 0.5 磅(OOXML sz 单位为 1/8 pt → 0.5pt = 4) */
|
||
const TABLE_BORDER = {
|
||
style: BorderStyle.SINGLE,
|
||
size: 4,
|
||
color: '000000'
|
||
};
|
||
const TABLE_BORDER_NONE = {
|
||
style: BorderStyle.NONE,
|
||
size: 0,
|
||
color: 'FFFFFF'
|
||
};
|
||
|
||
function createBorder(show) {
|
||
return show ? TABLE_BORDER : TABLE_BORDER_NONE;
|
||
}
|
||
|
||
function createTableCell(cell, options) {
|
||
const { header = false, odd = false, lastRow = false } = options;
|
||
if (!cell || cell.rowspan === 0 || cell.colspan === 0) {
|
||
return null;
|
||
}
|
||
|
||
const cellOptions = {
|
||
verticalAlign: VerticalAlign.CENTER,
|
||
borders: {
|
||
top: createBorder(header),
|
||
bottom: createBorder(header || lastRow),
|
||
left: createBorder(false),
|
||
right: createBorder(false)
|
||
},
|
||
children: splitHtmlSegments(cell.text).map((segment) =>
|
||
createTableParagraph(
|
||
header ? htmlToHeaderTextRuns(segment) : htmlToTextRuns(segment, { bold: false }),
|
||
AlignmentType.LEFT
|
||
)
|
||
)
|
||
};
|
||
|
||
if (cell.colspan > 1) {
|
||
cellOptions.columnSpan = cell.colspan;
|
||
}
|
||
if (cell.rowspan > 1) {
|
||
cellOptions.rowSpan = cell.rowspan;
|
||
}
|
||
if (odd) {
|
||
cellOptions.shading = {
|
||
fill: ODD_ROW_FILL,
|
||
type: ShadingType.CLEAR,
|
||
color: 'auto'
|
||
};
|
||
}
|
||
|
||
return new TableCell(cellOptions);
|
||
}
|
||
|
||
function createTableRow(cells) {
|
||
return new TableRow({
|
||
...TABLE_ROW_OPTIONS,
|
||
children: cells
|
||
});
|
||
}
|
||
|
||
function buildTableRows(table) {
|
||
const headers = (table && table.tableHeader) || [];
|
||
const contents = (table && table.tableContent) || [];
|
||
const oddRowIds = (table && table.oddRowIds) || [];
|
||
const rows = [];
|
||
|
||
headers.forEach((row) => {
|
||
const cells = (row || []).map((cell) => createTableCell(cell, { header: true })).filter(Boolean);
|
||
if (cells.length) {
|
||
rows.push(createTableRow(cells));
|
||
}
|
||
});
|
||
|
||
contents.forEach((row, rowIndex) => {
|
||
const odd = oddRowIds.length ? oddRowIds.includes(row.rowId) : rowIndex % 2 === 0;
|
||
const cells = (row || [])
|
||
.map((cell) =>
|
||
createTableCell(cell, {
|
||
odd,
|
||
lastRow: rowIndex === contents.length - 1
|
||
})
|
||
)
|
||
.filter(Boolean);
|
||
if (cells.length) {
|
||
rows.push(createTableRow(cells));
|
||
}
|
||
});
|
||
|
||
return rows;
|
||
}
|
||
|
||
function createWordDocument(children) {
|
||
return new Document({
|
||
styles: WORD_DOCUMENT_STYLES,
|
||
sections: [
|
||
{
|
||
properties: {
|
||
page: {
|
||
margin: PAGE_MARGINS
|
||
},
|
||
grid: {
|
||
linePitch: 200
|
||
}
|
||
},
|
||
children
|
||
}
|
||
]
|
||
});
|
||
}
|
||
|
||
export function prepareTableExportItem(item) {
|
||
if (!item || isMathFormulaTableRecord(item)) {
|
||
return null;
|
||
}
|
||
|
||
if (item.table && item.table.tableHeader) {
|
||
return {
|
||
title: item.title || '',
|
||
note: item.note || '',
|
||
table: item.table
|
||
};
|
||
}
|
||
|
||
try {
|
||
const tableList = typeof item.table === 'string' ? JSON.parse(item.table) : item.table;
|
||
const splitResult = TableUtils.splitTable(tableList);
|
||
const rowResult = TableUtils.addRowIdToData(splitResult.content);
|
||
return {
|
||
title: item.title || '',
|
||
note: item.note || '',
|
||
table: {
|
||
tableHeader: splitResult.header,
|
||
tableContent: rowResult.rowData,
|
||
oddRowIds: rowResult.rowIds
|
||
}
|
||
};
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function buildTableWordChildren(processedItem, options) {
|
||
const opts = options || {};
|
||
const appendTrailingEmptyLines = opts.appendTrailingEmptyLines !== false;
|
||
const title = (processedItem && processedItem.title) || '';
|
||
const note = (processedItem && processedItem.note) || '';
|
||
const table = (processedItem && processedItem.table) || {};
|
||
const children = [];
|
||
|
||
if (title) {
|
||
splitHtmlSegments(title).forEach((segment) => {
|
||
children.push(
|
||
createTitleParagraph([
|
||
new TextRun({
|
||
text: htmlToPlainText(segment),
|
||
bold: true,
|
||
color: TITLE_COLOR,
|
||
font: FONT_NAME,
|
||
size: TITLE_FONT_SIZE,
|
||
kern: FONT_KERN_MIN_1PT
|
||
})
|
||
])
|
||
);
|
||
});
|
||
}
|
||
|
||
const tableRows = buildTableRows(table);
|
||
if (tableRows.length) {
|
||
children.push(
|
||
new Table({
|
||
width: {
|
||
size: TABLE_WIDTH_PERCENT,
|
||
type: WidthType.PERCENTAGE
|
||
},
|
||
alignment: AlignmentType.CENTER,
|
||
layout: TableLayoutType.AUTOFIT,
|
||
margins: TABLE_CELL_MARGINS,
|
||
borders: {
|
||
top: TABLE_BORDER_NONE,
|
||
bottom: TABLE_BORDER_NONE,
|
||
left: TABLE_BORDER_NONE,
|
||
right: TABLE_BORDER_NONE,
|
||
insideHorizontal: TABLE_BORDER_NONE,
|
||
insideVertical: TABLE_BORDER_NONE
|
||
},
|
||
rows: tableRows
|
||
})
|
||
);
|
||
}
|
||
|
||
if (note) {
|
||
splitHtmlSegments(note).forEach((segment) => {
|
||
children.push(
|
||
createTableParagraph(htmlToTextRuns(segment, { bold: false }), AlignmentType.JUSTIFIED)
|
||
);
|
||
});
|
||
}
|
||
|
||
if (appendTrailingEmptyLines) {
|
||
appendTableBlockEmptyLines(children);
|
||
}
|
||
|
||
return children;
|
||
}
|
||
|
||
/** 稿件导出:标题 + 表格 + 表注均为单栏(与图片块相同,由 exportManuscriptWord 分节) */
|
||
export function buildTableWordManuscriptParts(processedItem, options) {
|
||
const blockChildren = buildTableWordChildren(processedItem, options);
|
||
const tableIndex = blockChildren.findIndex(function (child) {
|
||
return child instanceof Table;
|
||
});
|
||
|
||
if (tableIndex < 0) {
|
||
return {
|
||
titleChildren: blockChildren,
|
||
tableChildren: [],
|
||
noteChildren: []
|
||
};
|
||
}
|
||
|
||
return {
|
||
titleChildren: blockChildren.slice(0, tableIndex),
|
||
tableChildren: blockChildren.slice(tableIndex, tableIndex + 1),
|
||
noteChildren: blockChildren.slice(tableIndex + 1)
|
||
};
|
||
}
|
||
|
||
export function buildTableWordDocument(processedItem) {
|
||
return createWordDocument(buildTableWordChildren(processedItem));
|
||
}
|
||
|
||
export function buildAllTablesWordDocument(items) {
|
||
const children = [];
|
||
(items || []).forEach((item) => {
|
||
const prepared = prepareTableExportItem(item);
|
||
if (!prepared) {
|
||
return;
|
||
}
|
||
const blockChildren = buildTableWordChildren(prepared);
|
||
blockChildren.forEach((child) => {
|
||
children.push(child);
|
||
});
|
||
});
|
||
return createWordDocument(children);
|
||
}
|
||
|
||
function sanitizeFileName(name) {
|
||
const base = String(name || 'table')
|
||
.replace(/<[^>]+>/g, '')
|
||
.replace(/[\\/:*?"<>|]/g, '')
|
||
.trim()
|
||
.slice(0, 80);
|
||
return base || 'table';
|
||
}
|
||
|
||
export async function downloadTableWord(processedItem, fileName) {
|
||
const doc = buildTableWordDocument(processedItem);
|
||
let blob = await Packer.toBlob(doc);
|
||
blob = await patchBodyTableCaptionCenter(blob);
|
||
saveAs(blob, `${sanitizeFileName(fileName || processedItem.title)}.docx`);
|
||
}
|
||
|
||
export async function downloadAllTablesWord(items, fileName) {
|
||
const exportItems = [];
|
||
(items || []).forEach((item) => {
|
||
const prepared = prepareTableExportItem(item);
|
||
if (prepared) {
|
||
exportItems.push(prepared);
|
||
}
|
||
});
|
||
|
||
if (!exportItems.length) {
|
||
const error = new Error('NO_TABLES');
|
||
throw error;
|
||
}
|
||
|
||
const doc = buildAllTablesWordDocument(exportItems);
|
||
let blob = await Packer.toBlob(doc);
|
||
blob = await patchBodyTableCaptionCenter(blob);
|
||
saveAs(blob, `${sanitizeFileName(fileName || 'tables')}.docx`);
|
||
}
|
||
|
||
export {
|
||
appendFigureTitleParagraphs,
|
||
appendTableBlockEmptyLines,
|
||
buildTableWordChildren,
|
||
createFigureTitleParagraph,
|
||
createTableParagraph,
|
||
createWordDocument,
|
||
ensureFigureTitleTrailingPeriod,
|
||
FONT_KERN_MIN_1PT,
|
||
FONT_KERN_MIN_XIAO_ER,
|
||
FONT_NAME,
|
||
htmlToPlainText,
|
||
htmlToTextRuns,
|
||
htmlToTextRunsWithCitationComments,
|
||
PAGE_MARGINS,
|
||
patchBodyTableCaptionCenter,
|
||
splitHtmlSegments,
|
||
TABLE_FONT_SIZE,
|
||
TITLE_COLOR,
|
||
WORD_PARAGRAPH_SPACING
|
||
};
|