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(/]*\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(/]*\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 || '' }); }