Files
tougao_web/src/utils/parseReferenceFields.js
2026-07-10 09:12:09 +08:00

543 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 || ''
});
}