569 lines
19 KiB
JavaScript
569 lines
19 KiB
JavaScript
import { htmlToPlainText } from '@/utils/exportTableWord';
|
||
import { normalizeReferencePublicationDetails } from '@/utils/exportManuscriptWord';
|
||
|
||
const REF_COPY_DOI_COLOR = '0082AA';
|
||
const REF_COPY_FONT = 'Charis SIL, Georgia, Times New Roman, serif';
|
||
|
||
function escapeHtml(text) {
|
||
return String(text || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"');
|
||
}
|
||
|
||
function hasItalicHtmlTag(html) {
|
||
return /<i\b|<em\b/i.test(String(html || ''));
|
||
}
|
||
|
||
function stripAvailableAtSuffix(html) {
|
||
return String(html || '')
|
||
.replace(/\s*Available at:\s*[\s\S]*$/i, '')
|
||
.trim();
|
||
}
|
||
|
||
function normalizeReferenceLink(url) {
|
||
const raw = String(url || '').trim();
|
||
if (!raw) {
|
||
return '';
|
||
}
|
||
if (/^https?:\/\//i.test(raw)) {
|
||
return raw;
|
||
}
|
||
if (/^10\./.test(raw)) {
|
||
return 'https://doi.org/' + raw;
|
||
}
|
||
return raw;
|
||
}
|
||
|
||
function getReferenceIsbnValue(ref) {
|
||
if (!ref) {
|
||
return '';
|
||
}
|
||
return String(ref.isbn || ref.doilink || '').trim();
|
||
}
|
||
|
||
function appendHtmlField(parts, html, suffix, options) {
|
||
const opts = options || {};
|
||
const raw = String(html || '').trim();
|
||
if (!raw) {
|
||
return;
|
||
}
|
||
if (opts.italics && !hasItalicHtmlTag(raw)) {
|
||
parts.push('<i>' + raw + '</i>');
|
||
} else {
|
||
parts.push(raw);
|
||
}
|
||
if (suffix) {
|
||
parts.push(escapeHtml(suffix));
|
||
}
|
||
}
|
||
|
||
/** 将接口参考文献转为可编辑 HTML(支持后续粘贴带 <i> 的内容) */
|
||
export function referenceRecordToEditableHtml(ref) {
|
||
if (!ref) {
|
||
return '';
|
||
}
|
||
|
||
const referType = String(ref.refer_type || 'journal').toLowerCase();
|
||
|
||
if (!ref.author && ref.refer_frag) {
|
||
let html = stripAvailableAtSuffix(ref.refer_frag);
|
||
const link = normalizeReferenceLink(ref.doilink || getReferenceIsbnValue(ref));
|
||
if (link) {
|
||
html += '<br> Available at:<br>' + escapeHtml(ref.doilink || ref.isbn || link);
|
||
}
|
||
return html;
|
||
}
|
||
|
||
const parts = [];
|
||
|
||
if (referType === 'book') {
|
||
if (ref.author) {
|
||
appendHtmlField(parts, ref.author, ' ');
|
||
}
|
||
if (ref.title) {
|
||
appendHtmlField(parts, ref.title, '. ', { italics: true });
|
||
}
|
||
if (ref.dateno) {
|
||
appendHtmlField(parts, ref.dateno, '. ');
|
||
}
|
||
const isbnValue = getReferenceIsbnValue(ref);
|
||
if (isbnValue) {
|
||
parts.push(' Available at:<br>ISBN: ' + escapeHtml(isbnValue));
|
||
}
|
||
return parts.join('');
|
||
}
|
||
|
||
if (referType === 'other') {
|
||
let html = stripAvailableAtSuffix(ref.refer_frag);
|
||
const link = normalizeReferenceLink(ref.doilink);
|
||
if (link) {
|
||
html += '<br> Available at:<br>' + escapeHtml(ref.doilink || link);
|
||
}
|
||
return html;
|
||
}
|
||
|
||
if (ref.author) {
|
||
appendHtmlField(parts, ref.author, ' ');
|
||
}
|
||
if (ref.title) {
|
||
appendHtmlField(parts, ref.title, '. ');
|
||
}
|
||
if (ref.joura) {
|
||
appendHtmlField(parts, ref.joura, '. ', { italics: !hasItalicHtmlTag(ref.joura) });
|
||
}
|
||
if (ref.dateno) {
|
||
appendHtmlField(parts, ref.dateno, '. ');
|
||
}
|
||
const doiLink = normalizeReferenceLink(ref.doilink);
|
||
if (doiLink) {
|
||
parts.push(' Available at:<br>' + escapeHtml(ref.doilink || doiLink));
|
||
}
|
||
|
||
return parts.join('');
|
||
}
|
||
|
||
export function buildReferencesEditableHtml(references, labels) {
|
||
const list = (references || []).filter(Boolean);
|
||
const L = labels || {};
|
||
const pageTitle = L.pageTitle || 'References Editor';
|
||
const intro = L.intro || '';
|
||
const saveTip = L.saveTip || '';
|
||
const downloadBtn = L.downloadEdited || 'Download edited HTML';
|
||
const refTitle = L.referencesTitle || 'References';
|
||
const bulkHint = L.bulkHint || '';
|
||
const bulkContent = buildReferencesBulkHtml(list);
|
||
|
||
return (
|
||
'<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8">' +
|
||
'<meta name="viewport" content="width=device-width,initial-scale=1">' +
|
||
'<title>' + escapeHtml(pageTitle) + '</title>' +
|
||
'<style>' +
|
||
'*{box-sizing:border-box}body{margin:0;font-family:Charis SIL,Georgia,"Times New Roman",serif;background:#f5f7fa;color:#303133;line-height:1.5}' +
|
||
'.page{max-width:1400px;margin:0 auto;padding:20px}' +
|
||
'.header{background:#fff;border-radius:8px;padding:16px 20px;margin-bottom:12px;box-shadow:0 1px 4px rgba(0,0,0,.08)}' +
|
||
'.header h1{margin:0 0 8px;font-size:20px;text-align:center;font-style:italic;font-weight:bold;color:#008080}' +
|
||
'.intro{color:#606266;font-size:14px;margin-bottom:12px;white-space:pre-wrap}' +
|
||
'.actions{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:8px}' +
|
||
'.btn{border:1px solid #409eff;background:#409eff;color:#fff;border-radius:4px;padding:8px 16px;font-size:13px;cursor:pointer}' +
|
||
'.ref-panel{background:#fff;border-radius:8px;padding:20px 24px;box-shadow:0 1px 4px rgba(0,0,0,.08);font-size:12px}' +
|
||
'.ref-bulk{min-height:480px;padding:12px 14px;outline:none;border:1px solid #dcdfe6;border-radius:4px;line-height:1.6;white-space:pre-wrap;word-break:break-word}' +
|
||
'.ref-bulk:focus{border-color:#409eff;box-shadow:0 0 0 2px rgba(64,158,255,.15)}' +
|
||
'.ref-bulk i,.ref-bulk em{font-style:italic}' +
|
||
'.bulk-hint{margin:0 0 10px;color:#909399;font-size:12px}' +
|
||
'</style></head><body><div class="page">' +
|
||
'<div class="header"><h1>' + escapeHtml(refTitle) + '</h1>' +
|
||
'<div class="intro">' + escapeHtml(intro) + '</div>' +
|
||
'<div class="actions">' +
|
||
'<button type="button" class="btn" id="download-edited-btn">' + escapeHtml(downloadBtn) + '</button>' +
|
||
'</div>' +
|
||
'<div class="intro" style="font-size:12px;color:#909399">' + escapeHtml(saveTip) + '</div>' +
|
||
'</div>' +
|
||
'<div class="ref-panel">' +
|
||
'<p class="bulk-hint">' + escapeHtml(bulkHint) + '</p>' +
|
||
'<div id="ref-bulk" class="ref-bulk" contenteditable="true" spellcheck="false">' +
|
||
bulkContent +
|
||
'</div></div></div>' +
|
||
'<script>(function(){' +
|
||
'function serializePage(){var clone=document.documentElement.cloneNode(true);' +
|
||
'var btn=clone.querySelector("#download-edited-btn");if(btn){btn.remove();}' +
|
||
'return "<!DOCTYPE html>"+clone.outerHTML;}' +
|
||
'document.getElementById("download-edited-btn").addEventListener("click",function(){' +
|
||
'var html=serializePage();var blob=new Blob([html],{type:"text/html;charset=utf-8"});' +
|
||
'var url=URL.createObjectURL(blob);var a=document.createElement("a");' +
|
||
'a.href=url;a.download=' + JSON.stringify(L.downloadFileName || 'references-edited.html') + ';' +
|
||
'document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);});' +
|
||
'})();<' +
|
||
'/script></body></html>'
|
||
);
|
||
}
|
||
|
||
/** 整段参考文献 HTML(用于可编辑区初始内容) */
|
||
export function buildReferencesBulkHtml(references) {
|
||
const list = (references || []).filter(Boolean);
|
||
return list
|
||
.map(function (ref, index) {
|
||
const content = buildReferenceCopyItemHtml(ref);
|
||
if (!content) {
|
||
return '';
|
||
}
|
||
return (index + 1) + '. ' + content;
|
||
})
|
||
.filter(Boolean)
|
||
.join('<br><br>');
|
||
}
|
||
|
||
function stripLeadingReferenceNumber(text) {
|
||
return String(text || '')
|
||
.replace(/^\s*(?:<[^>]+>\s*)*\[\d+\]\s*/i, '')
|
||
.replace(/^\s*(?:<[^>]+>\s*)*\d+\.\s*/i, '')
|
||
.trim();
|
||
}
|
||
|
||
function unwrapReferenceHtmlSegment(segment) {
|
||
let seg = String(segment || '').trim();
|
||
if (!seg) {
|
||
return '';
|
||
}
|
||
seg = seg.replace(/^<p[^>]*>/i, '').replace(/<\/p>$/i, '');
|
||
seg = seg.replace(/^<div[^>]*>/i, '').replace(/<\/div>$/i, '');
|
||
return seg.trim();
|
||
}
|
||
|
||
/** contenteditable 编辑后常见 div 换行,统一成 br 便于拆分 */
|
||
function normalizeReferenceBulkHtml(html) {
|
||
return String(html || '')
|
||
.replace(/\r\n/g, '\n')
|
||
.replace(/<div>\s*<br\s*\/?>\s*<\/div>/gi, '<br><br>')
|
||
.replace(/<\/div>\s*<div[^>]*>/gi, '<br><br>')
|
||
.replace(/^<div[^>]*>/i, '')
|
||
.replace(/<\/div>$/i, '');
|
||
}
|
||
|
||
function splitReferenceSegmentsByBlockElements(raw) {
|
||
if (typeof DOMParser === 'undefined') {
|
||
return [];
|
||
}
|
||
|
||
const doc = new DOMParser().parseFromString('<div id="ref-root">' + raw + '</div>', 'text/html');
|
||
const root = doc.getElementById('ref-root');
|
||
if (!root) {
|
||
return [];
|
||
}
|
||
|
||
const directBlocks = Array.from(root.children).filter(function (el) {
|
||
const tag = el.tagName.toLowerCase();
|
||
return tag === 'p' || tag === 'div';
|
||
});
|
||
|
||
if (directBlocks.length > 1) {
|
||
return directBlocks
|
||
.map(function (node) {
|
||
return unwrapReferenceHtmlSegment(node.innerHTML || node.textContent || '');
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
if (/<(?:p|div)\b/i.test(raw)) {
|
||
const blocks = doc.querySelectorAll('#ref-root p, #ref-root div');
|
||
if (blocks.length > 1) {
|
||
return Array.from(blocks)
|
||
.map(function (node) {
|
||
return unwrapReferenceHtmlSegment(node.innerHTML || node.textContent || '');
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
}
|
||
|
||
return [];
|
||
}
|
||
|
||
function splitReferenceSegments(body) {
|
||
let raw = normalizeReferenceBulkHtml(body);
|
||
raw = String(raw || '').trim();
|
||
if (!raw) {
|
||
return [];
|
||
}
|
||
|
||
const blockSegments = splitReferenceSegmentsByBlockElements(raw);
|
||
if (blockSegments.length > 1) {
|
||
return blockSegments;
|
||
}
|
||
|
||
if (/<br\s*\/?>\s*<br\s*\/?>/i.test(raw)) {
|
||
return raw.split(/<br\s*\/?>\s*<br\s*\/?>/i).map(function (part) {
|
||
return part.trim();
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
const normalized = raw.replace(/\r\n/g, '\n').replace(/<br\s*\/?>/gi, '\n');
|
||
|
||
if (/\n(?=\d+\.\s)/.test(normalized)) {
|
||
return normalized.split(/\n(?=\d+\.\s)/).map(function (part) {
|
||
return part.trim();
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
if (/\n(?=\[\d+\])/.test(normalized)) {
|
||
return normalized.split(/\n(?=\[\d+\])/).map(function (part) {
|
||
return part.trim();
|
||
}).filter(Boolean);
|
||
}
|
||
|
||
const parts = normalized.split(/\n\s*\n+/);
|
||
if (parts.length > 1) {
|
||
const merged = [];
|
||
parts.forEach(function (part) {
|
||
const trimmed = part.trim();
|
||
if (!trimmed) {
|
||
return;
|
||
}
|
||
if (/^\d+\.\s/.test(trimmed) || /^\[\d+\]/.test(trimmed) || !merged.length) {
|
||
merged.push(trimmed);
|
||
return;
|
||
}
|
||
merged[merged.length - 1] += '\n' + trimmed;
|
||
});
|
||
return merged.filter(Boolean);
|
||
}
|
||
|
||
return blockSegments.length ? blockSegments : [raw];
|
||
}
|
||
|
||
function inferReferenceTypeFromHtml(html) {
|
||
const raw = String(html || '');
|
||
if (/ISBN\s*:/i.test(raw)) {
|
||
return 'book';
|
||
}
|
||
return 'journal';
|
||
}
|
||
|
||
function plainSegmentToReferenceHtml(segment) {
|
||
let seg = stripLeadingReferenceNumber(segment);
|
||
if (!seg) {
|
||
return '';
|
||
}
|
||
if (/<[a-z][\s\S]*>/i.test(seg)) {
|
||
return seg.replace(/\n/g, '<br>');
|
||
}
|
||
return escapeHtml(seg).replace(/\n/g, '<br>');
|
||
}
|
||
|
||
function segmentToReferenceItem(segment) {
|
||
const html = plainSegmentToReferenceHtml(segment);
|
||
if (!html) {
|
||
return null;
|
||
}
|
||
return {
|
||
refer_type: inferReferenceTypeFromHtml(html),
|
||
html: html
|
||
};
|
||
}
|
||
|
||
/** 将整段参考文献内容拆分为 Word 导出条目 */
|
||
export function parseReferencesBulkContent(content) {
|
||
const raw = String(content || '').trim();
|
||
if (!raw) {
|
||
return [];
|
||
}
|
||
|
||
let body = raw.replace(/^(\s*<[^>]+>\s*)*References(\s*<[^>]+>\s*)*\s*/i, '');
|
||
body = body.replace(/^References\s*\n+/i, '').trim();
|
||
if (!body) {
|
||
return [];
|
||
}
|
||
|
||
if (/<html[\s>]/i.test(body) && typeof DOMParser !== 'undefined') {
|
||
const doc = new DOMParser().parseFromString(body, 'text/html');
|
||
const bulkNode = doc.querySelector('#ref-bulk');
|
||
if (bulkNode) {
|
||
body = bulkNode.innerHTML || bulkNode.textContent || '';
|
||
} else if (doc.body) {
|
||
body = doc.body.innerHTML || doc.body.textContent || body;
|
||
}
|
||
}
|
||
|
||
return splitReferenceSegments(body)
|
||
.map(function (segment) {
|
||
return segmentToReferenceItem(segment);
|
||
})
|
||
.filter(Boolean);
|
||
}
|
||
|
||
/** 预览整段参考文献可解析条数(粘贴弹窗用) */
|
||
export function countParsedReferences(content) {
|
||
return parseReferencesBulkContent(content).length;
|
||
}
|
||
|
||
export function parseReferencesEditableHtml(htmlText) {
|
||
const raw = String(htmlText || '');
|
||
if (!raw.trim()) {
|
||
return [];
|
||
}
|
||
|
||
const doc = new DOMParser().parseFromString(raw, 'text/html');
|
||
const bulkNode = doc.querySelector('#ref-bulk');
|
||
if (bulkNode) {
|
||
return parseReferencesBulkContent(bulkNode.innerHTML || bulkNode.textContent || '');
|
||
}
|
||
|
||
const nodes = doc.querySelectorAll('#ref-list .ref-item, #ref-list li[data-ref-index], ol.ref-list li');
|
||
if (nodes.length) {
|
||
const items = [];
|
||
nodes.forEach(function (node) {
|
||
const html = (node.innerHTML || '').trim();
|
||
if (!html) {
|
||
return;
|
||
}
|
||
items.push({
|
||
refer_type: String(node.getAttribute('data-ref-type') || 'journal').toLowerCase(),
|
||
html: html
|
||
});
|
||
});
|
||
if (items.length) {
|
||
return items;
|
||
}
|
||
}
|
||
|
||
return parseReferencesBulkContent(raw);
|
||
}
|
||
|
||
export function downloadReferencesEditableHtml(references, labels, fileName) {
|
||
const html = buildReferencesEditableHtml(references, labels);
|
||
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
||
const url = URL.createObjectURL(blob);
|
||
const link = document.createElement('a');
|
||
link.href = url;
|
||
link.download = fileName || 'references-editor.html';
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
export function buildReferencesHtmlLabels(translate) {
|
||
const t = translate || function (key) {
|
||
return key;
|
||
};
|
||
return {
|
||
pageTitle: t('commonTable.refHtmlPageTitle'),
|
||
referencesTitle: t('commonTable.refHtmlReferencesTitle'),
|
||
intro: t('commonTable.refHtmlIntro'),
|
||
saveTip: t('commonTable.refHtmlSaveTip'),
|
||
downloadEdited: t('commonTable.refHtmlDownloadEdited'),
|
||
downloadFileName: t('commonTable.refHtmlDownloadFileName'),
|
||
bulkHint: t('commonTable.refHtmlBulkHint')
|
||
};
|
||
}
|
||
|
||
function buildReferenceCopyItemHtml(ref) {
|
||
let html = referenceRecordToEditableHtml(ref);
|
||
if (!html) {
|
||
return '';
|
||
}
|
||
html = normalizeReferencePublicationDetails(html);
|
||
html = html.replace(/\s*Available at:\s*<br\s*\/?>/gi, ' Available at:<br>');
|
||
html = html.replace(/( Available at:<br\s*\/?>)([\s\S]*)$/i, function (_, prefix, linkPart) {
|
||
const trimmed = String(linkPart || '').trim();
|
||
if (!trimmed) {
|
||
return prefix;
|
||
}
|
||
if (/^<(a|span)\b/i.test(trimmed)) {
|
||
return prefix + trimmed;
|
||
}
|
||
return prefix + '<span style="color:#' + REF_COPY_DOI_COLOR + '">' + trimmed + '</span>';
|
||
});
|
||
return html;
|
||
}
|
||
|
||
/** 按 Word 导出格式生成可复制 HTML(编号、期刊斜体、DOI 蓝色、Available at 换行) */
|
||
export function buildReferencesCopyHtml(references) {
|
||
const list = (references || []).filter(Boolean);
|
||
return list
|
||
.map(function (ref, index) {
|
||
const html = buildReferenceCopyItemHtml(ref);
|
||
if (!html) {
|
||
return '';
|
||
}
|
||
return (
|
||
'<p style="margin:0 0 8pt;line-height:1.5;font-family:' +
|
||
REF_COPY_FONT +
|
||
';font-size:12pt">' +
|
||
(index + 1) +
|
||
'. ' +
|
||
html +
|
||
'</p>'
|
||
);
|
||
})
|
||
.filter(Boolean)
|
||
.join('');
|
||
}
|
||
|
||
export function buildReferencesCopyHtmlDocument(references) {
|
||
return (
|
||
'<!DOCTYPE html><html><head><meta charset="utf-8"></head><body style="font-family:' +
|
||
REF_COPY_FONT +
|
||
';font-size:12pt">' +
|
||
buildReferencesCopyHtml(references) +
|
||
'</body></html>'
|
||
);
|
||
}
|
||
|
||
/** 按 Word 导出格式生成可复制纯文本(编号 + 换行) */
|
||
export function buildReferencesCopyText(references) {
|
||
const list = (references || []).filter(Boolean);
|
||
return list
|
||
.map(function (ref, index) {
|
||
const html = buildReferenceCopyItemHtml(ref);
|
||
const text = htmlToPlainText(String(html).replace(/<br\s*\/?>/gi, '\n')).trim();
|
||
if (!text) {
|
||
return '';
|
||
}
|
||
return index + 1 + '. ' + text;
|
||
})
|
||
.filter(Boolean)
|
||
.join('\n\n');
|
||
}
|
||
|
||
function copyTextToClipboard(text) {
|
||
const value = String(text || '').trim();
|
||
if (!value) {
|
||
return Promise.reject(new Error('EMPTY'));
|
||
}
|
||
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
return navigator.clipboard.writeText(value).catch(function () {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = value;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
});
|
||
}
|
||
|
||
const ta = document.createElement('textarea');
|
||
ta.value = value;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
return Promise.resolve();
|
||
}
|
||
|
||
function copyRichTextToClipboard(plainText, htmlText) {
|
||
const plain = String(plainText || '').trim();
|
||
const html = String(htmlText || '').trim();
|
||
if (!plain) {
|
||
return Promise.reject(new Error('EMPTY'));
|
||
}
|
||
|
||
if (typeof navigator !== 'undefined' && navigator.clipboard && window.ClipboardItem && html) {
|
||
const htmlBlob = new Blob([html], { type: 'text/html' });
|
||
const plainBlob = new Blob([plain], { type: 'text/plain' });
|
||
return navigator.clipboard
|
||
.write([
|
||
new ClipboardItem({
|
||
'text/html': htmlBlob,
|
||
'text/plain': plainBlob
|
||
})
|
||
])
|
||
.catch(function () {
|
||
return copyTextToClipboard(plain);
|
||
});
|
||
}
|
||
|
||
return copyTextToClipboard(plain);
|
||
}
|
||
|
||
/** 复制参考文献到剪贴板(Word 导出同款格式,含斜体与蓝色 DOI) */
|
||
export function copyReferencesToClipboard(references) {
|
||
const text = buildReferencesCopyText(references);
|
||
const html = buildReferencesCopyHtmlDocument(references);
|
||
if (!text) {
|
||
return Promise.reject(new Error('EMPTY'));
|
||
}
|
||
return copyRichTextToClipboard(text, html);
|
||
}
|