Files
petya 75601988c2 Initial commit: LangChain evolution tutorial deck (132 slides)
- Cover, TOC, 5 dividers, 3 recap slides
- 5 sections (chains, langgraph, deepagents, openswe, ecosystem)
- design-system.js with theme tokens + 9 helper functions
- research/: timeline + sources + per-tech notes
- final-compile.js + merge.js for rebuild pipeline
- output/: langchain-evolution.pptx (2.3 MB) + langchain-evolution.pdf (1.1 MB) + 7 sample previews
2026-06-22 11:29:03 +03:00

129 lines
6.0 KiB
JavaScript

/**
* 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(
/<Relationship\s+[^>]*Type="[^"]*slideLayout"[^>]*\/>/g,
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/>'
);
// Drop other layout/master/image rels (we don't carry media)
relsXml = relsXml.replace(/<Relationship\s+[^>]*Type="[^"]*image"[^>]*\/>/g, '');
relsXml = relsXml.replace(/<Relationship\s+[^>]*Type="[^"]*notesSlide"[^>]*\/>/g, '');
} else {
relsXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/></Relationships>';
}
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 = '<p:sldId id="' + newSldId + '" r:id="rId' + newSldId + '"/>';
const updated = presXml.replace(/<\/p:sldIdLst>/, newSldEntry + '</p:sldIdLst>');
// 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 = '<Relationship Id="rId' + newSldId + '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide' + newId + '.xml"/>';
const updatedRels = presRelsXml.replace(/<\/Relationships>/, newRelEntry + '</Relationships>');
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); });