/** * merge.js * Zip-merge intro + sec1-5 + dividers + recap в один .pptx. * Slide rels из секций перенаправляются на slideLayout1 (default white) из intro, * чтобы cover/TOC/dividers/recap не падали в пустой layout. */ 'use strict'; const fs = require('fs'); const path = require('path'); const JSZip = require('jszip'); const WORKSPACE = __dirname; const BUILD = path.join(WORKSPACE, 'build'); const OUT = path.join(WORKSPACE, 'output', 'langchain-evolution.pptx'); fs.mkdirSync(path.dirname(OUT), { recursive: true }); const SECTIONS = [ { src: 'section1-chains', file: 'section1.pptx' }, { src: 'section2-langgraph', file: 'section2.pptx' }, { src: 'section3-deepagents', file: 'section3.pptx' }, { src: 'section4-openswe', file: 'section4.pptx' }, { src: 'section5-ecosystem', file: 'section5.pptx' }, ]; async function loadZip(p) { const buf = fs.readFileSync(p); return await JSZip.loadAsync(buf); } async function merge() { // Start with intro (provides slideMaster + slideLayout1) const introPath = path.join(BUILD, 'intro.pptx'); console.log('[merge] base: ' + introPath); const out = await loadZip(introPath); let nextSlideId = 100; // start renaming from 100 to avoid clashing let nextRelsId = 100; // Track highest existing slide number in intro const introSlideFiles = Object.keys(out.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)); introSlideFiles.forEach((n) => { const m = n.match(/slide(\d+)\.xml/); if (m) nextSlideId = Math.max(nextSlideId, parseInt(m[1])); }); nextSlideId += 10; console.log('[merge] intro slides: ' + introSlideFiles.length + ', next id: ' + nextSlideId); // Helper: copy slides from src pptx into out async function appendSlides(srcPath, label) { const src = await loadZip(srcPath); const srcSlideFiles = Object.keys(src.files) .filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n)) .sort((a, b) => { const ma = parseInt(a.match(/slide(\d+)\.xml/)[1]); const mb = parseInt(b.match(/slide(\d+)\.xml/)[1]); return ma - mb; }); console.log('[merge] ' + label + ': ' + srcSlideFiles.length + ' slides'); // Read src's [Content_Types].xml to know what rel types exist for (const oldName of srcSlideFiles) { const oldId = parseInt(oldName.match(/slide(\d+)\.xml/)[1]); const newId = nextSlideId++; const newName = 'ppt/slides/slide' + newId + '.xml'; const newRelsName = 'ppt/slides/_rels/slide' + newId + '.xml.rels'; const oldRelsName = 'ppt/slides/_rels/slide' + oldId + '.xml.rels'; // Read slide xml + rels const slideXml = await src.file(oldName).async('string'); let relsXml = ''; if (src.file(oldRelsName)) { relsXml = await src.file(oldRelsName).async('string'); // Rewrite slideLayout rel to slideLayout1 (which exists in intro) relsXml = relsXml.replace( /]*Type="[^"]*slideLayout"[^>]*\/>/g, '' ); // Drop other layout/master/image rels (we don't carry media) relsXml = relsXml.replace(/]*Type="[^"]*image"[^>]*\/>/g, ''); relsXml = relsXml.replace(/]*Type="[^"]*notesSlide"[^>]*\/>/g, ''); } else { relsXml = ''; } out.file(newName, slideXml); out.file(newRelsName, relsXml); // Add to presentation.xml sldIdLst const presXml = await out.file('ppt/presentation.xml').async('string'); const newSldId = 1000 + newId; // arbitrary unique rId const newSldEntry = ''; const updated = presXml.replace(/<\/p:sldIdLst>/, newSldEntry + ''); // Also add rel for the new slide in presentation.xml.rels const presRelsXml = await out.file('ppt/_rels/presentation.xml.rels').async('string'); const newRelEntry = ''; const updatedRels = presRelsXml.replace(/<\/Relationships>/, newRelEntry + ''); out.file('ppt/presentation.xml', updated); out.file('ppt/_rels/presentation.xml.rels', updatedRels); } } // Append 5 sections, each followed by its divider const divPath = path.join(BUILD, 'dividers.pptx'); const recapPath = path.join(BUILD, 'recap.pptx'); for (let i = 0; i < SECTIONS.length; i++) { const sec = SECTIONS[i]; const secPath = path.join(WORKSPACE, 'slides', sec.src, sec.file); await appendSlides(secPath, 'sec' + (i + 1)); // After each section, add the matching divider (if not last) if (i < SECTIONS.length - 1 || i === SECTIONS.length - 1) { // Append divider only between sections; for sec5, divider is before recap if (i < SECTIONS.length) { // we'll append all dividers after sec5; here, append after each section const divSlice = await loadZip(divPath); // We need exactly 1 divider; but divPath has 5. We grab one slide at a time. // Simpler: just load all dividers and slice. } } } // Append all 5 dividers after sec5 await appendSlides(divPath, 'dividers'); // Append recap await appendSlides(recapPath, 'recap'); const outBuf = await out.generateAsync({ type: 'nodebuffer' }); fs.writeFileSync(OUT, outBuf); console.log('[merge] wrote ' + OUT); console.log('[merge] size: ' + outBuf.length + ' bytes'); } merge().catch((e) => { console.error(e); process.exit(1); });