/** * Builds the pptxtojson bundle the PowerPoint viewer (/pptx-viewer) loads from * /vendor/local/pptxtojson@2.2.0-jm1/pptxtojson.mjs. * * Why a local build rather than vendor.json: upstream parses every element of * every slide inside one promise, so a single element it cannot handle (an * unusual picture fill, a scatter chart without a cache) throws and the whole * presentation fails to open. The patch below wraps each element and each * slide in try/catch: what cannot be read is left out and counted * (result.skipped), and the rest of the deck still shows. * * Versions are pinned by package-lock.json (integrity hashes). Run: * cd frontend/vendor-src/pptxtojson && npm ci --ignore-scripts && node build.mjs */ import { build } from '../../node_modules/esbuild/lib/main.js'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const here = path.dirname(fileURLToPath(import.meta.url)); const pkg = path.join(here, 'node_modules/pptxtojson'); const version = JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'), 'utf8')).version; if (version !== '2.2.0') throw new Error(`expected pptxtojson 2.2.0, found ${version}: update the patch and the output folder together`); const work = path.join(here, 'node_modules/.patched-src'); fs.rmSync(work, { recursive: true, force: true }); fs.cpSync(path.join(pkg, 'src'), work, { recursive: true }); function patch(file, from, to) { const p = path.join(work, file); const s = fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n'); if (s.split(from).length !== 2) throw new Error(`patch target not found exactly once in ${file}: ${from.slice(0, 60)}`); fs.writeFileSync(p, s.replace(from, to)); } // A counter for what was left out, reset per parse and returned with the result. patch('pptxtojson.js', 'export async function parse(file, options = {}) {\n const slides = []', 'let skipped = 0\n\nexport async function parse(file, options = {}) {\n skipped = 0\n const slides = []'); patch('pptxtojson.js', ' for (const filename of filesInfo.slides) {\n const singleSlide = await processSingleSlide(zip, filename, themeContent, defaultTextStyle, loadedImages, loadedVideos, loadedAudios, parseOptions, xmlCache)\n slides.push(singleSlide)\n }', ' for (const filename of filesInfo.slides) {\n try {\n const singleSlide = await processSingleSlide(zip, filename, themeContent, defaultTextStyle, loadedImages, loadedVideos, loadedAudios, parseOptions, xmlCache)\n slides.push(singleSlide)\n }\n catch {\n skipped++\n slides.push({ fill: null, elements: [], layoutElements: [], note: \'\', failed: true })\n }\n }'); patch('pptxtojson.js', ' return {\n slides,\n usedFonts,', ' return {\n slides,\n skipped,\n usedFonts,'); // Each element on its own: one that throws is left out, not the deck. patch('pptxtojson.js', ' let json\n\n switch (nodeKey) {', ' let json\n\n try {\n switch (nodeKey) {'); patch('pptxtojson.js', ' default:\n }\n\n if (json && typeof json === \'object\' && !json.id) {', ' default:\n }\n }\n catch {\n skipped++\n json = undefined\n }\n\n if (json && typeof json === \'object\' && !json.id) {'); // A placeholder with no type is a body placeholder in PowerPoint ("obj"), styled // by the master's body text style - where bullets, sizes and spacing for // content slides live. Upstream lumps it with plain shapes, so it loses them. patch('pptxtojson.js', " if (source === 'diagramBg') type = 'diagram'\n else type = 'obj'", " if (source === 'diagramBg') type = 'diagram'\n else if (getTextByPathList(node, ['p:nvSpPr', 'p:nvPr', 'p:ph'])) type = 'body'\n else type = 'obj'"); // Bullets are inherited like any other paragraph property (text box list // style, layout, master, master text styles); upstream only looked at the // paragraph itself, so nearly every bulleted slide lost its bullets. patch('paragraph.js', 'function getParagraphStyleNodes(', 'export function getParagraphStyleNodes('); patch('text.js', 'export function getListType(node) {\n const pPrNode = node[\'a:pPr\']\n if (!pPrNode) return \'\'\n\n if (pPrNode[\'a:buChar\']) return \'ul\'\n if (pPrNode[\'a:buAutoNum\']) return \'ol\'\n \n return \'\'\n}', 'export function getListType(node, styleNodes) {\n for (const s of styleNodes || [node[\'a:pPr\']]) {\n if (!s) continue\n if (s[\'a:buNone\']) return \'\'\n if (s[\'a:buChar\']) return \'ul\'\n if (s[\'a:buAutoNum\']) return \'ol\'\n }\n return \'\'\n}'); patch('text.js', ' const listType = getListType(pNode)', ' const listType = getListType(pNode, getParagraphStyleNodes(pNode, textBodyNode, slideLayoutSpNode, slideMasterSpNode, type, slideMasterTextStyles, warpObj))'); { const p = path.join(work, 'text.js'); const s = fs.readFileSync(p, 'utf8'); if (!/import \{[^}]*getParagraphStyleNodes/.test(s)) { const m = /import \{([^}]*)\} from '\.\/paragraph'/.exec(s); if (!m) throw new Error('text.js does not import from ./paragraph'); fs.writeFileSync(p, s.replace(m[0], `import {${m[1].replace(/\s*$/, '')}, getParagraphStyleNodes } from './paragraph'`)); } } // Space before and after given as a percentage (val="20000" is 20% of a line) // came out as "20em", a hundred times too much. patch('paragraph.js', " if (spcPct) return parseInt(spcPct) / 1000 + 'em'", " if (spcPct) return (String(spcPct).trim().endsWith('%') ? parseFloat(spcPct) / 100 : parseInt(spcPct) / 100000) * 1.2 + 'em'"); const licence = (dir) => { for (const n of ['LICENSE', 'LICENSE.md', 'LICENSE.markdown', 'license']) { const p = path.join(here, 'node_modules', dir, n); if (fs.existsSync(p)) return fs.readFileSync(p, 'utf8').trim(); } return 'MIT (see package)'; }; const lock = JSON.parse(fs.readFileSync(path.join(here, 'package-lock.json'), 'utf8')); const deps = Object.entries(lock.packages).filter(([k]) => k).map(([k, v]) => `${k.replace('node_modules/', '')}@${v.version}`); const banner = [ '/*!', ` * pptxtojson ${version} (https://github.com/pipipi-pikachu/pptxtojson), patched by johnmcm.com`, ' * to leave out elements and slides it cannot read instead of failing; see', ' * https://johnmcm.com/licenses. Bundled with: ' + deps.join(', '), ' *', ...licence('pptxtojson').split('\n').map((l) => ' * ' + l), ' *', ' * JSZip: MIT or GPLv3 (used here under MIT). pako: MIT and Zlib. txml, tinycolor2: MIT.', ' */', ].join('\n'); const out = path.join(here, '../../vendor/local/pptxtojson@2.2.0-jm1/pptxtojson.mjs'); await build({ entryPoints: [path.join(work, 'pptxtojson.js')], bundle: true, format: 'esm', platform: 'browser', target: 'es2020', minify: true, legalComments: 'none', banner: { js: banner }, outfile: out, logLevel: 'warning', }); console.log(`wrote ${path.relative(process.cwd(), out)} (${fs.statSync(out).size} bytes)`);