75601988c2
- 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
862 lines
22 KiB
JavaScript
862 lines
22 KiB
JavaScript
/**
|
|
* design-system.js
|
|
* ----------------------------------------------------------------------------
|
|
* Design tokens + slide helpers for the LangChain Evolution deck
|
|
* (lc-evo-deck, 100+ slides, code-heavy, dark theme, 16:9).
|
|
*
|
|
* Audience: engineers familiar with LLMs. No introductory filler.
|
|
* Visual register: developer-tool aesthetic, deep navy, JetBrains Mono for
|
|
* code, Inter for UI, Arial as the universal fallback (covers Cyrillic).
|
|
*
|
|
* USAGE
|
|
* -----
|
|
* // 1. Import the module
|
|
* const ds = require('./design-system');
|
|
* const { theme, helpers, layouts, fonts, sizes, spacing } = ds;
|
|
*
|
|
* // 2. Create a new presentation with the built-in 16:9 layout
|
|
* const pptxgen = require('pptxgenjs');
|
|
* const pres = new pptxgen();
|
|
* pres.layout = 'LAYOUT_16x9'; // 10 x 5.625 inches
|
|
*
|
|
* // 3. Add a slide
|
|
* const slide = pres.addSlide();
|
|
* helpers.slideBase(slide, pres, theme);
|
|
* helpers.addHeader(slide, pres, theme, {
|
|
* section: 'Stage 2: Chains',
|
|
* sectionNumber: 2,
|
|
* title: 'LCEL: a composable expression language',
|
|
* eyebrow: 'STAGE 2',
|
|
* });
|
|
*
|
|
* // 4. Add a code block
|
|
* helpers.addCodeBlock(slide, pres, theme, {
|
|
* x: 0.5, y: layouts.CONTENT_TOP,
|
|
* w: 5.0, h: 3.2,
|
|
* language: 'python',
|
|
* code: [
|
|
* 'from langchain_core.prompts import ChatPromptTemplate',
|
|
* 'from langchain_openai import ChatOpenAI',
|
|
* '',
|
|
* 'prompt = ChatPromptTemplate.from_messages([',
|
|
* ' ("system", "You are a helpful assistant."),',
|
|
* ' ("human", "{question}"),',
|
|
* '])',
|
|
* 'model = ChatOpenAI(model="gpt-4o-mini")',
|
|
* 'chain = prompt | model',
|
|
* 'print(chain.invoke({"question": "What is LCEL?"}))',
|
|
* ].join('\n'),
|
|
* filePath: 'examples/lcel_basic.py',
|
|
* startLine: 1,
|
|
* });
|
|
*
|
|
* // 5. Add a callout and a pros/cons panel
|
|
* helpers.addCallout(slide, pres, theme, {
|
|
* x: 5.8, y: layouts.CONTENT_TOP, w: 3.7, h: 1.2,
|
|
* kind: 'info',
|
|
* text: 'LCEL is the default composition language from v0.1 onward.',
|
|
* });
|
|
*
|
|
* helpers.addProsCons(slide, pres, theme, {
|
|
* x: 5.8, y: 2.8, w: 3.7, h: 2.0,
|
|
* pros: ['Composable via | operator', 'Streaming, async, batched for free'],
|
|
* cons: ['Verbose for simple chains', 'Mental model differs from LangChain v0'],
|
|
* });
|
|
*
|
|
* // 6. Number the slide and add a source line
|
|
* helpers.addPageNumber(slide, pres, theme, 7);
|
|
* helpers.addSourceLine(slide, pres, theme, {
|
|
* x: 0.5, y: layouts.FOOTER_Y + 0.1, w: 6.0,
|
|
* source: 'python.langchain.com/docs/concepts/lcel',
|
|
* });
|
|
*
|
|
* // 7. Save
|
|
* await pres.writeFile({ fileName: 'lc-evolution.pptx' });
|
|
*
|
|
* LAYOUTS (PPTX 16:9, units = inches)
|
|
* -----------------------------------
|
|
* HEADER_Y = 0.4
|
|
* CONTENT_TOP = 1.4
|
|
* CONTENT_BOTTOM = 5.05
|
|
* FOOTER_Y = 5.25
|
|
*
|
|
* Vertical regions:
|
|
* - Header band : [0.0 .. 1.4] eyebrow + h1 title
|
|
* - Content body : [1.4 .. 5.05] main slide content
|
|
* - Footer band : [5.05 .. 5.625] page number + source line
|
|
*
|
|
* Horizontal margins: 0.5 inches left/right by default.
|
|
*
|
|
* RULES
|
|
* -----
|
|
* - No em-dash (--), en-dash (-), no smart quotes, no ellipsis (...)
|
|
* - All source files are pure ASCII except inside string literals where
|
|
* Cyrillic is allowed (Arial fallback guarantees rendering).
|
|
* - Code blocks use JetBrains Mono; body uses Inter; fallback Arial.
|
|
*
|
|
* @module design-system
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. PALETTE
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const palette = {
|
|
bg: {
|
|
primary: '#0A1A2A', // main slide background
|
|
elevated: '#142B3F', // cards, callouts, panels
|
|
code: '#0F1E2E', // code block background (slightly darker)
|
|
overlay: '#1B2F44', // hover / focus surfaces
|
|
},
|
|
text: {
|
|
primary: '#E6F0F7',
|
|
secondary: '#B5C4D1',
|
|
muted: '#8A9AAB',
|
|
inverse: '#0A1A2A', // for text on bright accents
|
|
},
|
|
accent: {
|
|
primary: '#219EBC', // teal, default accent
|
|
secondary: '#FFB703', // gold, important emphasis
|
|
tertiary: '#8ECAE6', // light blue, soft accent
|
|
},
|
|
border: {
|
|
subtle: '#233A4F',
|
|
strong: '#3A5670',
|
|
accent: '#219EBC',
|
|
},
|
|
code: {
|
|
keyword: '#C586C0', // def, class, import, return
|
|
string: '#CE9178',
|
|
number: '#B5CEA8',
|
|
comment: '#6A9955',
|
|
function: '#DCDCAA',
|
|
builtin: '#4EC9B0',
|
|
text: '#D4D4D4', // default code body
|
|
bg: '#0F1E2E',
|
|
lineHighlight: '#1F2F44',
|
|
},
|
|
state: {
|
|
info: '#219EBC',
|
|
success: '#4EC9B0',
|
|
warning: '#FFB703',
|
|
danger: '#F48771',
|
|
infoBg: '#102A38',
|
|
successBg: '#0F2A28',
|
|
warningBg: '#3A2A0F',
|
|
dangerBg: '#3A1A14',
|
|
},
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. TYPOGRAPHY
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const fonts = {
|
|
code: 'JetBrains Mono',
|
|
ui: 'Inter',
|
|
fallback: 'Arial', // universal fallback, supports Cyrillic
|
|
};
|
|
|
|
const sizes = {
|
|
h1: 36,
|
|
h2: 28,
|
|
h3: 20,
|
|
body: 14,
|
|
code: 10,
|
|
caption: 9,
|
|
eyebrow: 10,
|
|
};
|
|
|
|
const spacing = {
|
|
page: 0.5,
|
|
card_pad: 0.25,
|
|
gap: 0.15,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. LAYOUTS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const layouts = {
|
|
LAYOUT_16x9: 'LAYOUT_16x9',
|
|
HEADER_Y: 0.4,
|
|
CONTENT_TOP: 1.45,
|
|
CONTENT_BOTTOM: 5.05,
|
|
FOOTER_Y: 5.25,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. THEME (bundled export of the above)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const theme = {
|
|
palette,
|
|
fonts,
|
|
sizes,
|
|
spacing,
|
|
layouts,
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. HELPERS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Resolve a font that always carries the Arial fallback.
|
|
* pptxgenjs lets us pass an array; PowerPoint will pick the first available.
|
|
*/
|
|
function withFallback(family) {
|
|
return [family, fonts.fallback];
|
|
}
|
|
|
|
/**
|
|
* Paint the slide background.
|
|
* @param {object} slide - pptxgenjs slide instance
|
|
* @param {object} _pres - pptxgenjs pres (kept for signature symmetry)
|
|
* @param {object} t - theme bundle
|
|
*/
|
|
function slideBase(slide, _pres, t) {
|
|
slide.background = { color: t.palette.bg.primary };
|
|
}
|
|
|
|
/**
|
|
* Add a header band: eyebrow (small caps, accent) + section number + title.
|
|
* @param {object} slide
|
|
* @param {object} pres
|
|
* @param {object} t - theme
|
|
* @param {object} opts
|
|
* @param {string} [opts.section] - small caps section label (e.g. "Stage 2")
|
|
* @param {number} [opts.sectionNumber] - large section number shown on the right
|
|
* @param {string} opts.title - main title (h1)
|
|
* @param {string} [opts.eyebrow] - eyebrow text (e.g. "STAGE 2: CHAINS")
|
|
*/
|
|
function addHeader(slide, pres, t, opts) {
|
|
const o = opts || {};
|
|
const margin = t.spacing.page;
|
|
const titleY = t.layouts.HEADER_Y + 0.4;
|
|
const titleSize = o.titleSize || t.sizes.h2; // default to 28pt -- fits two-line titles without overflow
|
|
const titleW = o.sectionNumber != null ? 8.4 : 9.0;
|
|
|
|
if (o.eyebrow) {
|
|
slide.addText(o.eyebrow, {
|
|
x: margin,
|
|
y: t.layouts.HEADER_Y,
|
|
w: 6.0,
|
|
h: 0.3,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.eyebrow,
|
|
color: t.palette.accent.primary,
|
|
bold: true,
|
|
charSpacing: 4,
|
|
});
|
|
} else if (o.section) {
|
|
slide.addText(o.section, {
|
|
x: margin,
|
|
y: t.layouts.HEADER_Y,
|
|
w: 6.0,
|
|
h: 0.3,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.eyebrow,
|
|
color: t.palette.accent.primary,
|
|
bold: true,
|
|
charSpacing: 4,
|
|
});
|
|
}
|
|
|
|
slide.addText(o.title || '', {
|
|
x: margin,
|
|
y: titleY,
|
|
w: titleW,
|
|
h: 0.75,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: titleSize,
|
|
color: t.palette.text.primary,
|
|
bold: true,
|
|
valign: 'middle',
|
|
fit: 'shrink',
|
|
});
|
|
|
|
if (o.sectionNumber != null) {
|
|
slide.addText(String(o.sectionNumber), {
|
|
x: 9.0,
|
|
y: t.layouts.HEADER_Y - 0.05,
|
|
w: 0.7,
|
|
h: 1.0,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: 56,
|
|
color: t.palette.accent.secondary,
|
|
bold: true,
|
|
align: 'right',
|
|
valign: 'top',
|
|
});
|
|
}
|
|
|
|
// Hairline separator below the header band
|
|
slide.addShape(pres.ShapeType.line, {
|
|
x: margin,
|
|
y: 1.3,
|
|
w: 10.0 - margin * 2,
|
|
h: 0,
|
|
line: { color: t.palette.border.subtle, width: 0.75 },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Render a code block as a card with monospaced text.
|
|
* @param {object} slide
|
|
* @param {object} pres
|
|
* @param {object} t - theme
|
|
* @param {object} opts
|
|
* @param {string} opts.code
|
|
* @param {number} opts.x
|
|
* @param {number} opts.y
|
|
* @param {number} opts.w
|
|
* @param {number} opts.h
|
|
* @param {string} [opts.language='python']
|
|
* @param {string} [opts.filePath] - optional, renders as "// path:line" header
|
|
* @param {number} [opts.startLine=1]
|
|
* @param {Array<number>} [opts.highlightLines] - 1-based line numbers
|
|
*/
|
|
function addCodeBlock(slide, pres, t, opts) {
|
|
const o = opts || {};
|
|
const code = String(o.code || '');
|
|
const x = o.x;
|
|
const y = o.y;
|
|
const w = o.w;
|
|
const h = o.h;
|
|
const headerH = o.filePath ? 0.3 : 0;
|
|
const radius = 0.08;
|
|
|
|
// Card background
|
|
slide.addShape(pres.ShapeType.roundRect, {
|
|
x: x,
|
|
y: y,
|
|
w: w,
|
|
h: h,
|
|
fill: { color: t.palette.code.bg },
|
|
line: { color: t.palette.border.subtle, width: 0.75 },
|
|
rectRadius: radius,
|
|
});
|
|
|
|
// Optional header strip with file path
|
|
if (o.filePath) {
|
|
const startLine = o.startLine || 1;
|
|
const lineCount = code.split('\n').length;
|
|
const endLine = startLine + lineCount - 1;
|
|
slide.addText(
|
|
'// ' + o.filePath + ':' + startLine + '-' + endLine,
|
|
{
|
|
x: x + t.spacing.card_pad,
|
|
y: y + 0.05,
|
|
w: w - t.spacing.card_pad * 2,
|
|
h: 0.22,
|
|
fontFace: withFallback(t.fonts.code),
|
|
fontSize: t.sizes.caption,
|
|
color: t.palette.text.muted,
|
|
italic: true,
|
|
}
|
|
);
|
|
}
|
|
|
|
// Body code
|
|
const lines = code.split('\n');
|
|
const hasHighlight =
|
|
Array.isArray(o.highlightLines) && o.highlightLines.length > 0;
|
|
const highlightedSet = hasHighlight
|
|
? new Set(o.highlightLines.map(function (n) { return n; }))
|
|
: null;
|
|
|
|
// Pre-compute line height to fit within available area.
|
|
const codeAreaY = y + headerH + 0.05;
|
|
const codeAreaH = h - headerH - 0.1;
|
|
// Use a tighter font + leading so 20-line snippets fit in 3-inch cards.
|
|
const fontSize = o.fontSize || 11;
|
|
const lineH = (fontSize / 72) * 1.25; // inches per line at 1.25 leading
|
|
|
|
const text = lines.map(function (line, idx) {
|
|
const lineNum = idx + 1;
|
|
const isHi = highlightedSet && highlightedSet.has(lineNum);
|
|
const prefix = isHi ? '> ' : ' ';
|
|
return {
|
|
text: prefix + (line.length === 0 ? ' ' : line) + '\n',
|
|
options: {
|
|
fontFace: withFallback(t.fonts.code),
|
|
fontSize: fontSize,
|
|
color: isHi ? t.palette.text.primary : t.palette.code.text,
|
|
bold: false,
|
|
highlight: isHi ? t.palette.code.lineHighlight : undefined,
|
|
},
|
|
};
|
|
});
|
|
|
|
slide.addText(text, {
|
|
x: x + t.spacing.card_pad,
|
|
y: codeAreaY,
|
|
w: w - t.spacing.card_pad * 2,
|
|
h: codeAreaH,
|
|
valign: 'top',
|
|
paraSpaceBefore: 0,
|
|
paraSpaceAfter: 0,
|
|
lineSpacingMultiple: 1.0,
|
|
isTextBox: true,
|
|
});
|
|
|
|
// Caption warning if content may overflow.
|
|
const maxLines = Math.floor(codeAreaH / lineH);
|
|
if (lines.length > maxLines) {
|
|
// Best-effort: append a small note. PowerPoint won't clip the text, but
|
|
// it will overflow the card visually. Caller should reduce fontSize or
|
|
// split the snippet.
|
|
slide.addText(
|
|
'// note: snippet has ' + lines.length + ' lines, card fits ~' + maxLines,
|
|
{
|
|
x: x + t.spacing.card_pad,
|
|
y: y + h - 0.22,
|
|
w: w - t.spacing.card_pad * 2,
|
|
h: 0.18,
|
|
fontFace: withFallback(t.fonts.code),
|
|
fontSize: t.sizes.caption,
|
|
color: t.palette.state.warning,
|
|
italic: true,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Render a code block with line highlighting.
|
|
* Convenience wrapper over addCodeBlock.
|
|
*/
|
|
function addCodeBlockWithHighlight(slide, pres, t, opts) {
|
|
return addCodeBlock(slide, pres, t, Object.assign({}, opts, {
|
|
highlightLines: opts.lines || opts.highlightLines,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Add a callout panel (info / warning / success / danger).
|
|
* @param {object} slide
|
|
* @param {object} pres
|
|
* @param {object} t - theme
|
|
* @param {object} opts
|
|
* @param {number} opts.x
|
|
* @param {number} opts.y
|
|
* @param {number} opts.w
|
|
* @param {number} opts.h
|
|
* @param {('info'|'warning'|'success'|'danger')} [opts.kind='info']
|
|
* @param {string} opts.text
|
|
* @param {string} [opts.title]
|
|
*/
|
|
function addCallout(slide, pres, t, opts) {
|
|
const o = opts || {};
|
|
const kind = o.kind || 'info';
|
|
const accent = t.palette.state[kind] || t.palette.state.info;
|
|
const bgKey = kind + 'Bg';
|
|
const bg = t.palette.state[bgKey] || t.palette.state.infoBg;
|
|
|
|
// Background card
|
|
slide.addShape(pres.ShapeType.roundRect, {
|
|
x: o.x,
|
|
y: o.y,
|
|
w: o.w,
|
|
h: o.h,
|
|
fill: { color: bg },
|
|
line: { color: accent, width: 1 },
|
|
rectRadius: 0.08,
|
|
});
|
|
|
|
// Left accent bar
|
|
slide.addShape(pres.ShapeType.rect, {
|
|
x: o.x,
|
|
y: o.y,
|
|
w: 0.08,
|
|
h: o.h,
|
|
fill: { color: accent },
|
|
line: { type: 'none' },
|
|
});
|
|
|
|
// Label
|
|
slide.addText((kind || 'info').toUpperCase(), {
|
|
x: o.x + 0.25,
|
|
y: o.y + 0.1,
|
|
w: o.w - 0.35,
|
|
h: 0.25,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.eyebrow,
|
|
color: accent,
|
|
bold: true,
|
|
charSpacing: 4,
|
|
});
|
|
|
|
// Optional title
|
|
let bodyY = o.y + 0.4;
|
|
let bodyH = o.h - 0.5;
|
|
if (o.title) {
|
|
slide.addText(o.title, {
|
|
x: o.x + 0.25,
|
|
y: o.y + 0.35,
|
|
w: o.w - 0.35,
|
|
h: 0.3,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.h3,
|
|
color: t.palette.text.primary,
|
|
bold: true,
|
|
});
|
|
bodyY = o.y + 0.7;
|
|
bodyH = o.h - 0.8;
|
|
}
|
|
|
|
slide.addText(o.text || '', {
|
|
x: o.x + 0.25,
|
|
y: bodyY,
|
|
w: o.w - 0.35,
|
|
h: bodyH,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.body,
|
|
color: t.palette.text.secondary,
|
|
valign: 'top',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Two-column pros / cons panel.
|
|
* @param {object} slide
|
|
* @param {object} pres
|
|
* @param {object} t - theme
|
|
* @param {object} opts
|
|
* @param {number} opts.x
|
|
* @param {number} opts.y
|
|
* @param {number} opts.w
|
|
* @param {number} opts.h
|
|
* @param {string[]} opts.pros
|
|
* @param {string[]} opts.cons
|
|
*/
|
|
function addProsCons(slide, pres, t, opts) {
|
|
const o = opts || {};
|
|
const gap = 0.2;
|
|
const colW = (o.w - gap) / 2;
|
|
|
|
// Pros card
|
|
slide.addShape(pres.ShapeType.roundRect, {
|
|
x: o.x,
|
|
y: o.y,
|
|
w: colW,
|
|
h: o.h,
|
|
fill: { color: t.palette.state.successBg },
|
|
line: { color: t.palette.state.success, width: 1 },
|
|
rectRadius: 0.08,
|
|
});
|
|
slide.addText('PROS', {
|
|
x: o.x + 0.2,
|
|
y: o.y + 0.1,
|
|
w: colW - 0.3,
|
|
h: 0.3,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.eyebrow,
|
|
color: t.palette.state.success,
|
|
bold: true,
|
|
charSpacing: 4,
|
|
});
|
|
|
|
const prosBody = (o.pros || []).map(function (item) {
|
|
return {
|
|
text: '+ ' + item + '\n',
|
|
options: {
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.body,
|
|
color: t.palette.text.primary,
|
|
},
|
|
};
|
|
});
|
|
slide.addText(prosBody, {
|
|
x: o.x + 0.2,
|
|
y: o.y + 0.45,
|
|
w: colW - 0.3,
|
|
h: o.h - 0.55,
|
|
valign: 'top',
|
|
paraSpaceAfter: 4,
|
|
});
|
|
|
|
// Cons card
|
|
const cx = o.x + colW + gap;
|
|
slide.addShape(pres.ShapeType.roundRect, {
|
|
x: cx,
|
|
y: o.y,
|
|
w: colW,
|
|
h: o.h,
|
|
fill: { color: t.palette.state.dangerBg },
|
|
line: { color: t.palette.state.danger, width: 1 },
|
|
rectRadius: 0.08,
|
|
});
|
|
slide.addText('CONS', {
|
|
x: cx + 0.2,
|
|
y: o.y + 0.1,
|
|
w: colW - 0.3,
|
|
h: 0.3,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.eyebrow,
|
|
color: t.palette.state.danger,
|
|
bold: true,
|
|
charSpacing: 4,
|
|
});
|
|
|
|
const consBody = (o.cons || []).map(function (item) {
|
|
return {
|
|
text: '- ' + item + '\n',
|
|
options: {
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.body,
|
|
color: t.palette.text.primary,
|
|
},
|
|
};
|
|
});
|
|
slide.addText(consBody, {
|
|
x: cx + 0.2,
|
|
y: o.y + 0.45,
|
|
w: colW - 0.3,
|
|
h: o.h - 0.55,
|
|
valign: 'top',
|
|
paraSpaceAfter: 4,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a page number in the bottom-right corner.
|
|
*/
|
|
function addPageNumber(slide, _pres, t, num) {
|
|
slide.addText(String(num), {
|
|
x: 9.2,
|
|
y: t.layouts.FOOTER_Y,
|
|
w: 0.6,
|
|
h: 0.25,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.caption,
|
|
color: t.palette.text.muted,
|
|
align: 'right',
|
|
valign: 'middle',
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a section divider slide: a large numeric label + title + intro paragraph.
|
|
* @param {object} slide
|
|
* @param {object} pres
|
|
* @param {object} t - theme
|
|
* @param {object} opts
|
|
* @param {number} opts.number
|
|
* @param {string} opts.title
|
|
* @param {string} [opts.intro]
|
|
* @param {string} [opts.eyebrow]
|
|
*/
|
|
function addSectionDivider(slide, pres, t, opts) {
|
|
const o = opts || {};
|
|
|
|
slideBase(slide, pres, t);
|
|
|
|
// Giant number on the left
|
|
slide.addText(String(o.number), {
|
|
x: 0.5,
|
|
y: 1.0,
|
|
w: 3.5,
|
|
h: 3.5,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: 220,
|
|
color: t.palette.accent.primary,
|
|
bold: true,
|
|
valign: 'middle',
|
|
});
|
|
|
|
// Vertical divider
|
|
slide.addShape(pres.ShapeType.line, {
|
|
x: 4.2,
|
|
y: 1.4,
|
|
w: 0,
|
|
h: 2.8,
|
|
line: { color: t.palette.border.subtle, width: 1 },
|
|
});
|
|
|
|
// Title
|
|
if (o.eyebrow) {
|
|
slide.addText(o.eyebrow, {
|
|
x: 4.5,
|
|
y: 1.4,
|
|
w: 5.0,
|
|
h: 0.3,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.eyebrow,
|
|
color: t.palette.accent.secondary,
|
|
bold: true,
|
|
charSpacing: 4,
|
|
});
|
|
}
|
|
|
|
slide.addText(o.title || '', {
|
|
x: 4.5,
|
|
y: o.eyebrow ? 1.7 : 1.4,
|
|
w: 5.0,
|
|
h: 1.0,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.h1,
|
|
color: t.palette.text.primary,
|
|
bold: true,
|
|
valign: 'top',
|
|
});
|
|
|
|
if (o.intro) {
|
|
slide.addText(o.intro, {
|
|
x: 4.5,
|
|
y: 2.8,
|
|
w: 5.0,
|
|
h: 1.6,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.body,
|
|
color: t.palette.text.secondary,
|
|
valign: 'top',
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add an italic source line, e.g. "source: python.langchain.com/..."
|
|
* @param {object} slide
|
|
* @param {object} _pres
|
|
* @param {object} t
|
|
* @param {object} opts
|
|
* @param {string} opts.source
|
|
* @param {number} [opts.x]
|
|
* @param {number} [opts.y]
|
|
* @param {number} [opts.w]
|
|
*/
|
|
function addSourceLine(slide, _pres, t, opts) {
|
|
const o = opts || {};
|
|
slide.addText('source: ' + (o.source || ''), {
|
|
x: o.x != null ? o.x : t.spacing.page,
|
|
y: o.y != null ? o.y : t.layouts.FOOTER_Y + 0.05,
|
|
w: o.w != null ? o.w : 7.0,
|
|
h: 0.25,
|
|
fontFace: withFallback(t.fonts.ui),
|
|
fontSize: t.sizes.caption,
|
|
color: t.palette.text.muted,
|
|
italic: true,
|
|
valign: 'middle',
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. OPTIONAL: PYTHON SYNTAX HIGHLIGHTING (pygments via subprocess)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Best-effort Python syntax highlighter.
|
|
* Calls the `pygmentize` CLI from pygments if available.
|
|
* Returns an array of { text, color } tokens.
|
|
* On any failure (missing binary, non-zero exit, parse error) returns a
|
|
* single-token array with the whole snippet in the default code text color
|
|
* so that the slide still renders something.
|
|
*
|
|
* @param {string} code - Python source code
|
|
* @returns {Array<{text: string, color: string}>}
|
|
*/
|
|
function highlightPython(code) {
|
|
const defaultColor = palette.code.text;
|
|
const text = String(code || '');
|
|
if (!text) return [{ text: '', color: defaultColor }];
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
let result;
|
|
try {
|
|
result = spawnSync(
|
|
'pygmentize',
|
|
['-f', 'json', '-l', 'python'],
|
|
{ input: text, encoding: 'utf8', timeout: 4000 }
|
|
);
|
|
} catch (e) {
|
|
return [{ text: text, color: defaultColor }];
|
|
}
|
|
|
|
if (result.error || result.status !== 0) {
|
|
return [{ text: text, color: defaultColor }];
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(result.stdout);
|
|
} catch (e) {
|
|
return [{ text: text, color: defaultColor }];
|
|
}
|
|
|
|
// Pygments JSON output: { tokens: [ [type, value], ... ] }
|
|
const tokens = Array.isArray(parsed.tokens) ? parsed.tokens : [];
|
|
const out = [];
|
|
for (const tok of tokens) {
|
|
if (!Array.isArray(tok) || tok.length < 2) continue;
|
|
const [type, value] = tok;
|
|
out.push({
|
|
text: String(value),
|
|
color: mapPygmentsTokenToColor(type) || defaultColor,
|
|
});
|
|
}
|
|
return out.length > 0 ? out : [{ text: text, color: defaultColor }];
|
|
}
|
|
|
|
/**
|
|
* Map a pygments token type to a palette color.
|
|
* Falls back to the default code text color.
|
|
*/
|
|
function mapPygmentsTokenToColor(type) {
|
|
if (!type) return null;
|
|
// Pygments short token types we care about for Python:
|
|
// Keyword, Keyword.Namespace, Keyword.Constant, String, Number,
|
|
// Comment, Comment.Single, Name.Function, Name.Builtin,
|
|
// Operator, Punctuation, Text, Error
|
|
if (type === 'Keyword' || type.startsWith('Keyword.')) {
|
|
return palette.code.keyword;
|
|
}
|
|
if (type.startsWith('String')) {
|
|
return palette.code.string;
|
|
}
|
|
if (type === 'Number') {
|
|
return palette.code.number;
|
|
}
|
|
if (type.startsWith('Comment')) {
|
|
return palette.code.comment;
|
|
}
|
|
if (type === 'Name.Function' || type === 'Name.Function.Magic') {
|
|
return palette.code.function;
|
|
}
|
|
if (type === 'Name.Builtin' || type === 'Name.Builtin.Pseudo') {
|
|
return palette.code.builtin;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. EXPORTS
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const helpers = {
|
|
slideBase,
|
|
addHeader,
|
|
addCodeBlock,
|
|
addCodeBlockWithHighlight,
|
|
addCallout,
|
|
addProsCons,
|
|
addPageNumber,
|
|
addSectionDivider,
|
|
addSourceLine,
|
|
withFallback,
|
|
highlightPython,
|
|
};
|
|
|
|
module.exports = {
|
|
theme: theme,
|
|
palette: palette,
|
|
fonts: fonts,
|
|
sizes: sizes,
|
|
spacing: spacing,
|
|
layouts: layouts,
|
|
helpers: helpers,
|
|
}; |