543 lines
17 KiB
JavaScript
543 lines
17 KiB
JavaScript
import { htmlToPlainText } from '@/utils/manuscriptCitationRelevance';
|
||
import { parseBookReferenceContent } from '@/utils/parseBookReference';
|
||
|
||
/** 导入时去掉 (Chinese) 并规整空格 */
|
||
export function normalizeImportedReferenceText(text) {
|
||
return String(text || '')
|
||
.replace(/\(\s*Chinese\s*\)/gi, '')
|
||
.replace(/\u00a0/g, ' ')
|
||
.replace(/\s+\./g, '.')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
function sanitizeImportedReferenceHtml(html) {
|
||
return String(html || '')
|
||
.replace(/\(\s*Chinese\s*\)/gi, '')
|
||
.replace(/\(\s*Chinese\s*\)\s*\./gi, '.')
|
||
.replace(/\u00a0/g, ' ')
|
||
.replace(/\s+\./g, '.')
|
||
.replace(/\s{2,}/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
export function normalizeImportedReferenceFields(fields) {
|
||
if (!fields) return fields;
|
||
const keys = ['author', 'title', 'joura', 'dateno', 'doilink', 'doi', 'isbn', 'content'];
|
||
const out = Object.assign({}, fields);
|
||
keys.forEach(function (key) {
|
||
if (out[key] != null && out[key] !== '') {
|
||
out[key] = normalizeImportedReferenceText(out[key]);
|
||
}
|
||
});
|
||
return swapMisassignedYearAndSource(out);
|
||
}
|
||
|
||
/** 年份误写入 joura、来源误写入 dateno 时纠正(web/news 引用常见) */
|
||
function swapMisassignedYearAndSource(fields) {
|
||
if (!fields) return fields;
|
||
const joura = String(fields.joura || '').trim();
|
||
const dateno = String(fields.dateno || '').trim();
|
||
if (
|
||
looksLikePlainYear(joura) &&
|
||
dateno &&
|
||
!looksLikePlainYear(dateno) &&
|
||
!looksLikeDateno(dateno)
|
||
) {
|
||
return Object.assign({}, fields, {
|
||
joura: dateno,
|
||
dateno: normalizePlainYearDateno(joura)
|
||
});
|
||
}
|
||
return fields;
|
||
}
|
||
|
||
function splitAtFirstPeriod(text) {
|
||
const raw = String(text || '').trim();
|
||
if (!raw) return { before: '', after: '' };
|
||
const match = raw.match(/\s*\.\s+/);
|
||
if (!match || match.index == null) {
|
||
return { before: raw, after: '' };
|
||
}
|
||
return {
|
||
before: raw.slice(0, match.index).trim(),
|
||
after: raw.slice(match.index + match[0].length).trim()
|
||
};
|
||
}
|
||
|
||
function withAuthorPeriod(name) {
|
||
const t = String(name || '').trim();
|
||
if (!t) return '';
|
||
return t.endsWith('.') ? t : t + '.';
|
||
}
|
||
|
||
function trimField(text) {
|
||
return normalizeImportedReferenceText(
|
||
String(text || '')
|
||
.replace(/\s+\.\s*$/, '')
|
||
.replace(/\.\s*$/, '')
|
||
);
|
||
}
|
||
|
||
function stripLeadingReferenceNumber(text) {
|
||
return String(text || '')
|
||
.replace(/^\s*(?:<[^>]+>\s*)*\[\d+\]\s*/i, '')
|
||
.replace(/^\s*(?:<[^>]+>\s*)*\d+\.\s*/i, '')
|
||
.trim();
|
||
}
|
||
|
||
function fixMissingSpaceAfterPeriod(plain) {
|
||
return String(plain || '').replace(/([A-Za-z)\]])\.([A-Za-z])/g, '$1. $2');
|
||
}
|
||
|
||
function looksLikeDateno(text) {
|
||
const value = String(text || '').trim();
|
||
return /^\d{4}[;:]\d+(?:\([^)]*\))?(?::[\w\-–—eE.]+(?:[\-–—][\w\-–—eE.]+)?)?\.?$/.test(value);
|
||
}
|
||
|
||
function looksLikePlainYear(text) {
|
||
return /^\d{4}\.?$/.test(String(text || '').trim());
|
||
}
|
||
|
||
function normalizePlainYearDateno(text) {
|
||
return String(text || '').trim().replace(/\.$/, '');
|
||
}
|
||
|
||
/** 提取末尾纯年份(如 web/news 引用:Source. 2025.) */
|
||
function extractPlainYearFromTail(plain) {
|
||
const text = String(plain || '').trim();
|
||
const match = text.match(/(?:^|\.\s+)(\d{4})\.\s*$/);
|
||
if (!match) {
|
||
return { body: text, dateno: '' };
|
||
}
|
||
const year = match[1];
|
||
let body = text.slice(0, match.index).trim().replace(/\.\s*$/, '');
|
||
return {
|
||
body: body,
|
||
dateno: normalizePlainYearDateno(year)
|
||
};
|
||
}
|
||
|
||
function normalizeDatenoField(dateno) {
|
||
return normalizeImportedReferenceText(String(dateno || '').trim()).replace(/^(\d{4}):/, '$1;');
|
||
}
|
||
|
||
function extractDoilink(html) {
|
||
const raw = String(html || '');
|
||
let body = raw.trim();
|
||
let doilink = '';
|
||
|
||
const availMatch = raw.match(/Available\s+at:\s*([\s\S]*)$/i);
|
||
if (availMatch) {
|
||
const tail = availMatch[1];
|
||
const anchorMatch = tail.match(/<a\b[^>]*\bhref=["']([^"']+)["'][^>]*>/i);
|
||
doilink = anchorMatch
|
||
? anchorMatch[1].trim()
|
||
: htmlToPlainText(tail).replace(/\s+/g, ' ').trim();
|
||
body = raw.slice(0, availMatch.index).trim();
|
||
} else {
|
||
const anchorMatch = raw.match(/<a\b[^>]*\bhref=["']([^"']+)["'][^>]*>/i);
|
||
if (anchorMatch) {
|
||
doilink = anchorMatch[1].trim();
|
||
body = raw.replace(anchorMatch[0], '').trim();
|
||
} else {
|
||
const urlMatch = raw.match(/(https?:\/\/(?:dx\.)?doi\.org\/[^\s<]+|10\.\d{4,9}\/[^\s<]+)\s*$/i);
|
||
if (urlMatch) {
|
||
doilink = htmlToPlainText(urlMatch[1]).replace(/\s+/g, ' ').trim();
|
||
body = raw.slice(0, urlMatch.index).trim();
|
||
}
|
||
}
|
||
}
|
||
|
||
if (doilink && !/^https?:\/\//i.test(doilink)) {
|
||
doilink = 'https://doi.org/' + doilink.replace(/^https?:\/\/doi\.org\//i, '');
|
||
}
|
||
|
||
return { body: body, doilink: doilink };
|
||
}
|
||
|
||
function mergeAdjacentItalicHtml(html) {
|
||
let result = String(html || '');
|
||
let prev = '';
|
||
while (prev !== result) {
|
||
prev = result;
|
||
result = result.replace(/<\/(?:i|em)>\s*<(?:i|em)(?:\s[^>]*)?>/gi, '');
|
||
}
|
||
return result;
|
||
}
|
||
|
||
function extractAllItalicTexts(html) {
|
||
const merged = mergeAdjacentItalicHtml(html);
|
||
const texts = [];
|
||
const re = /<(?:i|em)\b[^>]*>([\s\S]*?)<\/(?:i|em)>/gi;
|
||
let match;
|
||
while ((match = re.exec(merged)) !== null) {
|
||
const text = htmlToPlainText(match[1]).replace(/\s+/g, ' ').trim();
|
||
if (text) {
|
||
texts.push({ text: text, index: match.index, length: match[0].length });
|
||
}
|
||
}
|
||
return { merged: merged, texts: texts };
|
||
}
|
||
|
||
function cleanJournalName(name) {
|
||
return trimField(String(name || ''))
|
||
.replace(/\s*\.?\s*-+\.?\s*$/g, '')
|
||
.replace(/\s*\.\s*$/g, '')
|
||
.trim();
|
||
}
|
||
|
||
function extractJournalFromPlainBeforeDateno(plain) {
|
||
const text = String(plain || '').trim();
|
||
const patterns = [
|
||
/\.\s*([^.\d][^.]*?)\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?:)/,
|
||
/\.\s*([^.\d][^.]+?)\s+(\d{4}[;:]\d+(?:\([^)]*\))?:)/,
|
||
/\s([A-Za-z][A-Za-z0-9 .&'-]{1,80})\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?:)/,
|
||
/\.\s*([^.\d][^.]*?)\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?)\.?\s*$/,
|
||
/\s([A-Za-z][A-Za-z0-9 .&'-]{1,80})\.\s*(\d{4}[;:]\d+(?:\([^)]*\))?)\.?\s*$/
|
||
];
|
||
for (let i = 0; i < patterns.length; i++) {
|
||
const match = text.match(patterns[i]);
|
||
if (match && match[1] && !looksLikeDateno(match[1])) {
|
||
return cleanJournalName(match[1]);
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
|
||
function extractJouraFromHead(head) {
|
||
const parts = String(head || '')
|
||
.split(/\.\s+/)
|
||
.map(function (part) {
|
||
return part.trim();
|
||
})
|
||
.filter(Boolean);
|
||
if (parts.length < 2) {
|
||
return { joura: '', rest: String(head || '').trim(), plainYear: '' };
|
||
}
|
||
|
||
while (parts.length && looksLikeDateno(parts[parts.length - 1])) {
|
||
parts.pop();
|
||
}
|
||
|
||
let plainYear = '';
|
||
if (parts.length && looksLikePlainYear(parts[parts.length - 1])) {
|
||
plainYear = normalizePlainYearDateno(parts.pop());
|
||
}
|
||
|
||
if (parts.length < 2) {
|
||
return { joura: '', rest: parts.join('. '), plainYear: plainYear };
|
||
}
|
||
|
||
let joura = cleanJournalName(parts.pop());
|
||
if (!joura || looksLikeDateno(joura) || looksLikePlainYear(joura)) {
|
||
if (joura && looksLikePlainYear(joura) && !plainYear) {
|
||
plainYear = normalizePlainYearDateno(joura);
|
||
}
|
||
return { joura: '', rest: parts.join('. '), plainYear: plainYear };
|
||
}
|
||
return { joura: joura, rest: parts.join('. '), plainYear: plainYear };
|
||
}
|
||
|
||
function extractItalicJournal(html) {
|
||
const raw = String(html || '');
|
||
const italicInfo = extractAllItalicTexts(raw);
|
||
if (!italicInfo.texts.length) {
|
||
return { body: raw.trim(), joura: '' };
|
||
}
|
||
|
||
const plainForPos = htmlToPlainText(italicInfo.merged).replace(/\s+/g, ' ').trim();
|
||
const datenoMatch = plainForPos.match(/\d{4}[;:]\d+(?:\([^)]*\))?:/);
|
||
const datenoPos = datenoMatch ? datenoMatch.index : plainForPos.length;
|
||
|
||
let joura = '';
|
||
for (let i = italicInfo.texts.length - 1; i >= 0; i--) {
|
||
const item = italicInfo.texts[i];
|
||
const pos = htmlToPlainText(italicInfo.merged.slice(0, item.index)).replace(/\s+/g, ' ').length;
|
||
if (pos <= datenoPos && item.text.length <= 120) {
|
||
joura = item.text;
|
||
break;
|
||
}
|
||
}
|
||
if (!joura) {
|
||
joura = italicInfo.texts[italicInfo.texts.length - 1].text;
|
||
}
|
||
joura = cleanJournalName(joura);
|
||
|
||
const body = italicInfo.merged
|
||
.replace(/<(?:i|em)\b[^>]*>([\s\S]*?)<\/(?:i|em)>/gi, ' ')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
return { body: body, joura: joura };
|
||
}
|
||
|
||
function extractDateno(plain) {
|
||
const text = String(plain || '').trim();
|
||
const patterns = [
|
||
/(\d{4}[;:]\d+(?:\([^)]*\))?:[\w\-–—eE.]+(?:[\-–—][\w\-–—eE.]+)?)\.?\s*$/,
|
||
/(\d{4}[;:]\d+(?:\([^)]*\))?:[\w\-–—eE]+)\.?\s*$/,
|
||
/(\d{4}[;:]\d+(?:\([^)]*\))?)\.?\s*$/
|
||
];
|
||
for (let i = 0; i < patterns.length; i++) {
|
||
const match = text.match(patterns[i]);
|
||
if (match) {
|
||
return {
|
||
body: text.slice(0, match.index).trim().replace(/\.\s*$/, ''),
|
||
dateno: normalizeDatenoField(match[1])
|
||
};
|
||
}
|
||
}
|
||
return { body: text, dateno: '' };
|
||
}
|
||
|
||
function parseJournalReferenceContent(content) {
|
||
let html = stripLeadingReferenceNumber(String(content || '').trim());
|
||
if (!html) return null;
|
||
|
||
const linkPart = extractDoilink(html);
|
||
html = linkPart.body;
|
||
const italicPart = extractItalicJournal(html);
|
||
html = italicPart.body;
|
||
|
||
let plain = fixMissingSpaceAfterPeriod(normalizeImportedReferenceText(htmlToPlainText(html)));
|
||
plain = plain.replace(/\.\s*$/, '');
|
||
|
||
const datenoPart = extractDateno(plain);
|
||
let dateno = datenoPart.dateno;
|
||
let head = datenoPart.body;
|
||
|
||
if (!dateno) {
|
||
const plainYearPart = extractPlainYearFromTail(head || plain);
|
||
if (plainYearPart.dateno) {
|
||
dateno = plainYearPart.dateno;
|
||
head = plainYearPart.body;
|
||
}
|
||
}
|
||
|
||
let joura = cleanJournalName(italicPart.joura || '');
|
||
if (looksLikeDateno(joura) || looksLikePlainYear(joura)) {
|
||
if (looksLikePlainYear(joura) && !dateno) {
|
||
dateno = normalizePlainYearDateno(joura);
|
||
}
|
||
joura = '';
|
||
}
|
||
|
||
if (!joura) {
|
||
joura = extractJournalFromPlainBeforeDateno(head || plain);
|
||
}
|
||
|
||
const jouraFromHead = extractJouraFromHead(head);
|
||
if (!joura && jouraFromHead.joura) {
|
||
joura = jouraFromHead.joura;
|
||
head = jouraFromHead.rest;
|
||
} else if (joura && head) {
|
||
const jouraEsc = joura.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
head = head
|
||
.replace(new RegExp('\\.\s*' + jouraEsc + '\.?\s*$', 'i'), '')
|
||
.replace(/\s*\.?\s*-+\.?\s*$/g, '')
|
||
.trim();
|
||
}
|
||
if (!dateno && jouraFromHead.plainYear) {
|
||
dateno = jouraFromHead.plainYear;
|
||
}
|
||
|
||
if (looksLikeDateno(joura) && !dateno) {
|
||
dateno = normalizeDatenoField(joura);
|
||
const recovered = extractJouraFromHead(head);
|
||
joura = extractJournalFromPlainBeforeDateno(head) || recovered.joura;
|
||
head = recovered.rest;
|
||
if (!dateno && recovered.plainYear) {
|
||
dateno = recovered.plainYear;
|
||
}
|
||
}
|
||
|
||
if (!dateno) {
|
||
const trailingYear = extractPlainYearFromTail(head);
|
||
if (trailingYear.dateno) {
|
||
dateno = trailingYear.dateno;
|
||
head = trailingYear.body;
|
||
}
|
||
}
|
||
|
||
joura = cleanJournalName(joura);
|
||
|
||
const authorSplit = splitAtFirstPeriod(head);
|
||
const author = withAuthorPeriod(authorSplit.before);
|
||
const title = trimField(authorSplit.after);
|
||
|
||
if (!author && !title && !joura && !dateno && !linkPart.doilink) {
|
||
return null;
|
||
}
|
||
|
||
return {
|
||
refer_type: 'journal',
|
||
author,
|
||
title,
|
||
joura,
|
||
dateno: dateno,
|
||
doilink: linkPart.doilink,
|
||
doi: linkPart.doilink ? linkPart.doilink.replace(/^https?:\/\/doi\.org\//i, '').toLowerCase() : '',
|
||
isbn: '',
|
||
content: ''
|
||
};
|
||
}
|
||
|
||
function hasReferenceDoiLink(fields, rawHtml) {
|
||
const link = String((fields && (fields.doilink || fields.doi)) || '').trim();
|
||
if (link) {
|
||
if (/^https?:\/\/(?:dx\.)?doi\.org\//i.test(link)) {
|
||
return true;
|
||
}
|
||
if (/^doi:/i.test(link)) {
|
||
return true;
|
||
}
|
||
if (/^10\.\d{4,9}\//i.test(link)) {
|
||
return true;
|
||
}
|
||
}
|
||
return /doi\.org\/10\.\d{4,9}\//i.test(String(rawHtml || ''));
|
||
}
|
||
|
||
function hasReferenceIsbn(fields, rawHtml) {
|
||
const isbn = String((fields && fields.isbn) || '').trim();
|
||
if (isbn) {
|
||
return true;
|
||
}
|
||
return /ISBN\s*:/i.test(String(rawHtml || ''));
|
||
}
|
||
|
||
function buildReferenceContentFromPartialFields(fields) {
|
||
const parts = [];
|
||
if (fields && fields.author) {
|
||
parts.push(String(fields.author).trim());
|
||
}
|
||
if (fields && fields.title) {
|
||
parts.push(String(fields.title).trim());
|
||
}
|
||
if (fields && fields.joura) {
|
||
parts.push(String(fields.joura).trim());
|
||
}
|
||
if (fields && fields.dateno) {
|
||
parts.push(String(fields.dateno).trim());
|
||
}
|
||
|
||
let content = parts
|
||
.filter(Boolean)
|
||
.join(' ')
|
||
.replace(/\s+\./g, '.')
|
||
.replace(/\s+/g, ' ')
|
||
.trim();
|
||
|
||
const link = String((fields && fields.doilink) || '').trim();
|
||
if (link && !/Available at:/i.test(content)) {
|
||
content = content + '. Available at: ' + link;
|
||
}
|
||
|
||
return normalizeImportedReferenceText(content);
|
||
}
|
||
|
||
/** 无 DOI 且无 ISBN 的条目归为 other;有 DOI 为 journal,有 ISBN 为 book */
|
||
export function finalizeReferenceFields(fields, html) {
|
||
const base = Object.assign({}, fields || {});
|
||
const rawHtml = String(html || '').trim();
|
||
const plainFromHtml = normalizeImportedReferenceText(htmlToPlainText(rawHtml));
|
||
|
||
if (hasReferenceIsbn(base, rawHtml)) {
|
||
return normalizeImportedReferenceFields(
|
||
Object.assign({}, base, {
|
||
refer_type: 'book'
|
||
})
|
||
);
|
||
}
|
||
|
||
if (hasReferenceDoiLink(base, rawHtml)) {
|
||
return normalizeImportedReferenceFields(
|
||
Object.assign({}, base, {
|
||
refer_type: 'journal'
|
||
})
|
||
);
|
||
}
|
||
|
||
const content =
|
||
base.content ||
|
||
plainFromHtml ||
|
||
buildReferenceContentFromPartialFields(base);
|
||
|
||
return normalizeImportedReferenceFields({
|
||
refer_type: 'other',
|
||
author: '',
|
||
title: '',
|
||
joura: '',
|
||
dateno: '',
|
||
doilink: base.doilink || '',
|
||
doi: base.doi || '',
|
||
isbn: '',
|
||
content: content
|
||
});
|
||
}
|
||
|
||
/** 将单条参考文献 HTML/文本拆解为结构化字段 */
|
||
export function parseReferenceFieldsFromHtml(html, referTypeHint) {
|
||
const raw = sanitizeImportedReferenceHtml(stripLeadingReferenceNumber(String(html || '').trim()));
|
||
if (!raw) return null;
|
||
|
||
const hinted = String(referTypeHint || '').toLowerCase();
|
||
if (hinted === 'book' || /ISBN\s*:/i.test(raw)) {
|
||
const plain = normalizeImportedReferenceText(htmlToPlainText(raw));
|
||
const parsed = parseBookReferenceContent(plain);
|
||
if (parsed && parsed.isbn) {
|
||
return finalizeReferenceFields(
|
||
{
|
||
refer_type: 'book',
|
||
author: parsed.author || '',
|
||
title: parsed.title || '',
|
||
joura: '',
|
||
dateno: parsed.dateno || '',
|
||
doilink: '',
|
||
doi: '',
|
||
isbn: parsed.isbn || '',
|
||
content: ''
|
||
},
|
||
raw
|
||
);
|
||
}
|
||
}
|
||
|
||
const journalParsed = parseJournalReferenceContent(raw);
|
||
if (journalParsed) {
|
||
return finalizeReferenceFields(journalParsed, raw);
|
||
}
|
||
|
||
return finalizeReferenceFields(
|
||
{
|
||
refer_type: hinted || 'other',
|
||
author: '',
|
||
title: '',
|
||
joura: '',
|
||
dateno: '',
|
||
doilink: '',
|
||
doi: '',
|
||
isbn: '',
|
||
content: normalizeImportedReferenceText(htmlToPlainText(raw))
|
||
},
|
||
raw
|
||
);
|
||
}
|
||
|
||
export function applyDealContentToFields(fields, dealData) {
|
||
const base = fields || {};
|
||
const data = dealData || {};
|
||
if (!data || typeof data !== 'object') return normalizeImportedReferenceFields(base);
|
||
return normalizeImportedReferenceFields({
|
||
refer_type: base.refer_type || 'journal',
|
||
author: data.author || base.author || '',
|
||
title: data.title || base.title || '',
|
||
joura: data.joura || base.joura || '',
|
||
dateno: data.dateno || base.dateno || '',
|
||
doilink: data.doilink || data.doi || base.doilink || '',
|
||
doi: data.doi || data.doilink || base.doi || '',
|
||
isbn: data.isbn || base.isbn || '',
|
||
content: base.content || ''
|
||
});
|
||
}
|