import { Document, Paragraph, TextRun, CommentRangeStart, CommentRangeEnd, CommentReference, LevelFormat, LevelSuffix, AlignmentType, Packer } from 'docx'; import JSZip from 'jszip'; import fs from 'fs'; function htmlToPlainText(html) { return String(html || '') .replace(/ /g, ' ') .replace(//gi, ' ') .replace(/<[^>]*>/g, '') .replace(/\s+/g, ' ') .trim(); } function parseSingleAuthorName(nameStr) { const token = String(nameStr || '').trim(); if (!token || /^et al\.?$/i.test(token)) return null; const parts = token.split(/\s+/).filter(Boolean); if (!parts.length) return null; if (parts.length === 1) return { first: '', last: parts[0] }; return { first: parts.slice(0, -1).join(' '), last: parts[parts.length - 1] }; } function firstNameToAmaInitials(first) { return String(first || '') .split(/[\s-]+/) .filter(Boolean) .map((part) => part.charAt(0).toUpperCase()) .join(''); } function formatOneAmaAuthor(nameStr) { const parsed = parseSingleAuthorName(nameStr); if (!parsed || !parsed.last) return ''; const initials = firstNameToAmaInitials(parsed.first); return initials ? parsed.last + ' ' + initials : parsed.last; } function splitAuthorList(authorText) { return String(authorText || '') .split(/\s*,\s*/) .map((part) => part.trim()) .filter(Boolean); } function isAmaAbbreviatedAuthorList(authorText) { const authors = splitAuthorList(htmlToPlainText(authorText)); if (!authors.length) return true; return authors.every((token) => { if (/^et al\.?$/i.test(token)) return true; return /^[\p{L}][\p{L}\-'']*\s+[A-Z](?:-?[A-Z])*\.?$/u.test(token); }); } function needsAmaAuthorAbbreviation(authorText) { const plain = htmlToPlainText(authorText).trim(); if (!plain || isAmaAbbreviatedAuthorList(plain)) return false; return splitAuthorList(plain).some((token) => { const parts = token.split(/\s+/).filter(Boolean); if (parts.length < 2) return false; return parts[0].length > 2; }); } function buildAmaAuthorAbbreviation(authorText) { const authors = splitAuthorList(htmlToPlainText(authorText)); return authors .map((token) => (/^et al\.?$/i.test(token) ? 'et al' : formatOneAmaAuthor(token))) .filter(Boolean) .join(', '); } function buildBookAmaAuthorComment(ref) { if (!ref || ref.refer_type !== 'book') return ''; const authorSource = ref.author ? htmlToPlainText(ref.author) : ''; if (!authorSource || !needsAmaAuthorAbbreviation(authorSource)) return ''; const amaAuthors = buildAmaAuthorAbbreviation(authorSource); return amaAuthors ? 'AMA格式作者缩写:' + amaAuthors : ''; } const ref = { refer_type: 'book', author: 'Jacqueline Fawcett, Susan DeSanto-Madeya', title: 'Contemporary Nursing Knowledge', dateno: '(3rd Edition). Philadelphia, PA: F. A. Davis Company; 2013.', isbn: '978-0-8036-2765-9' }; const commentText = buildBookAmaAuthorComment(ref); console.log('comment text:', commentText); const commentId = 0; const doc = new Document({ numbering: { config: [{ reference: 'ref-num', levels: [{ level: 0, format: LevelFormat.DECIMAL, text: '%1.', alignment: AlignmentType.START, suffix: LevelSuffix.TAB, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }] }, comments: { children: [{ id: commentId, author: 'Editor', initials: 'Ed', date: new Date(), children: [new Paragraph({ children: [new TextRun(commentText)] })] }] }, sections: [{ children: [new Paragraph({ numbering: { reference: 'ref-num', level: 0 }, children: [ new CommentRangeStart(commentId), new TextRun('Jacqueline Fawcett, Susan DeSanto-Madeya. Contemporary Nursing Knowledge.'), new CommentRangeEnd(commentId), new TextRun({ children: [new CommentReference(commentId)] }) ] })] }] }); const buf = await Packer.toBuffer(doc); fs.writeFileSync('test-ref-comment-standalone.docx', buf); const zip = await JSZip.loadAsync(buf); const docXml = await zip.file('word/document.xml').async('string'); const commentsXml = zip.file('word/comments.xml'); const relsXml = zip.file('word/_rels/document.xml.rels'); const contentTypes = zip.file('[Content_Types].xml'); console.log('has comments.xml:', !!commentsXml); console.log('has commentReference in document:', docXml.includes('commentReference')); console.log('has comments rel in document.xml.rels:', relsXml ? (await relsXml.async('string')).includes('comments') : false); console.log('has comments in Content_Types:', contentTypes ? (await contentTypes.async('string')).includes('comments') : false); if (commentsXml) { console.log('comments preview:', (await commentsXml.async('string')).slice(0, 800)); }