'use strict'; const fs = require('fs'); const path = require('path'); const JSZip = require('jszip'); const REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'; function generatedRId(idx) { return `rIdFont${idx}`; } async function patchZipWithFonts(inputPath, fonts) { const buf = fs.readFileSync(inputPath); const zip = await JSZip.loadAsync(buf); const existingFontNames = new Set(); // Detect prior embeds (idempotency: skip TTF files already present). for (const f of Object.keys(zip.files)) { if (zip.files[f].name && /^word\/fonts\//.test(zip.files[f].name)) { existingFontNames.add(path.basename(f)); } } for (let i = 0; i < fonts.length; i++) { const { path: fontPath } = fonts[i]; const fname = path.basename(fontPath); const wordPath = `word/fonts/${fname}`; if (!existingFontNames.has(fname)) { zip.file(wordPath, fs.readFileSync(fontPath)); existingFontNames.add(fname); } } // Build/replace word/fontTable.xml so Word knows the family and where to // fetch the embedded TTF data. const fontTableXml = `\n` + `\n` + fonts .map( (f, i) => ` ` ) .join('\n') + `\n\n`; zip.file('word/fontTable.xml', fontTableXml); // Patch [Content_Types].xml — add Override for /word/fontTable.xml and each TTF. const ctPath = '[Content_Types].xml'; let ct = await zip.file(ctPath).async('string'); if (!ct.includes('PartName="/word/fontTable.xml"')) { ct = ct.replace( '', '' ); } for (const f of fonts) { const ttfCt = 'application/x-font-ttf'; const filePart = `/word/fonts/${path.basename(f.path)}`; if (!ct.includes(`PartName="${filePart}"`)) { ct = ct.replace( '', `` ); } } if (!ct.includes('Default Extension="ttf"')) { ct = ct.replace( '', '' ); } zip.file(ctPath, ct); // Patch word/_rels/document.xml.rels — relationships for fontTable + each font. const relsPath = 'word/_rels/document.xml.rels'; if (!zip.files[relsPath]) { zip.file( relsPath, `\n` ); } let rels = await zip.file(relsPath).async('string'); if (!rels.includes('fontTable.xml')) { rels = rels.replace( '', `` ); } for (let i = 0; i < fonts.length; i++) { const fname = path.basename(fonts[i].path); if (!rels.includes(fname)) { rels = rels.replace( '', `` ); } } zip.file(relsPath, rels); // Patch word/styles.xml — bind SourceCode/VerbatimChar styles to the family. const stylesPath = 'word/styles.xml'; if (zip.files[stylesPath]) { let styles = await zip.file(stylesPath).async('string'); const family = fonts[0].family; if (!styles.includes(`w:ascii="${family}"`)) { styles = styles.replace( /(]*w:styleId="(?:SourceCode|VerbatimChar)"[^>]*>)/, `$1` ); } zip.file(stylesPath, styles); } const outBuf = await zip.generateAsync({ type: 'nodebuffer' }); fs.writeFileSync(inputPath, outBuf); return inputPath; } async function embed(docxPath, fonts) { return patchZipWithFonts(docxPath, fonts); } module.exports = { embed };