@entropicwarrior/sdoc 0.2.17 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,514 @@
1
+ // SDOC Slide Layouts — structured layout builders.
2
+ //
3
+ // A slide's `config:` line names a layout. The default, `center` and
4
+ // `two-column` layouts need no structure beyond the content itself; the
5
+ // layouts in this module consume the slide's *child scopes* and arrange them
6
+ // into a known shape (columns, stats, a pipeline, a comparison matrix, and so
7
+ // on). Every builder emits plain semantic HTML with stable class names; all
8
+ // visual decisions belong to the theme.
9
+ //
10
+ // Layouts compose. `split` renders two child scopes as panes and `stack`
11
+ // renders each child scope as a block, and in both cases the child scope may
12
+ // name its own layout with its own `config:` line. That is how a slide carries
13
+ // a pipeline above a set of numbered columns without a bespoke layout for the
14
+ // combination.
15
+ //
16
+ // The renderer injects its own callbacks (renderChildren, renderInline,
17
+ // escapeHtml) so this module stays free of parser and KaTeX dependencies.
18
+
19
+ // Configuration is a leading run of `key: value` paragraphs. Which keys a
20
+ // scope actually understands depends on where it sits, and a key it does not
21
+ // understand stays content rather than disappearing — a slide opening with
22
+ // "Value: the customer keeps their data." is prose, not configuration.
23
+ //
24
+ // COMMON_KEYS every scope understands, whatever layout it names
25
+ // LAYOUT_KEYS a scope understands because of the layout it names itself
26
+ // CELL_KEYS a scope understands because of the layout its *parent* names
27
+ const COMMON_KEYS = new Set([
28
+ "config",
29
+ "layout",
30
+ "kicker",
31
+ "lede",
32
+ "footnote",
33
+ "accent",
34
+ "status",
35
+ ]);
36
+
37
+ const LAYOUT_KEYS = {
38
+ columns: ["numbered", "variant"],
39
+ "two-column": ["numbered", "variant"],
40
+ stats: [],
41
+ pipeline: ["arrow"],
42
+ matrix: ["highlight"],
43
+ rows: ["numbered", "variant"],
44
+ bars: [],
45
+ split: ["weights"],
46
+ stack: ["rule"],
47
+ };
48
+
49
+ const CELL_KEYS = {
50
+ columns: ["caption"],
51
+ "two-column": ["caption"],
52
+ rows: ["value"],
53
+ bars: ["value", "fill"],
54
+ };
55
+
56
+ // Every key any scope might understand. A leading paragraph opening with one
57
+ // of these is a configuration *candidate*; whether it is kept depends on the
58
+ // context resolved below.
59
+ const CONFIG_KEYS = new Set([
60
+ ...COMMON_KEYS,
61
+ ...Object.values(LAYOUT_KEYS).flat(),
62
+ ...Object.values(CELL_KEYS).flat(),
63
+ ]);
64
+
65
+ // Layouts that consume child scopes as structure rather than rendering them
66
+ // inline as nested sections.
67
+ const STRUCTURED_LAYOUTS = new Set(Object.keys(LAYOUT_KEYS));
68
+
69
+ function truthy(value) {
70
+ if (value === true) return true;
71
+ if (typeof value !== "string") return false;
72
+ const v = value.trim().toLowerCase();
73
+ return v === "true" || v === "yes" || v === "on" || v === "1";
74
+ }
75
+
76
+ function escapeAttrValue(value) {
77
+ return String(value).replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
78
+ }
79
+
80
+ // An author-supplied word reduced to something safe to concatenate into a
81
+ // class name. Anything outside [a-z0-9-] is dropped, so no configuration value
82
+ // can close the attribute it lands in.
83
+ function slug(value) {
84
+ if (!value) return "";
85
+ return String(value).trim().toLowerCase().replace(/[^a-z0-9-]/g, "");
86
+ }
87
+
88
+ // The class for a named variant of a layout, or "" when none was named.
89
+ function variantClass(value) {
90
+ const name = slug(value);
91
+ return name ? ` variant-${name}` : "";
92
+ }
93
+
94
+ // Splits a leading run of `key: value` paragraphs off a scope's children.
95
+ //
96
+ // Two passes, because which keys are meaningful depends on the layout the
97
+ // scope names and the layout its parent named, and `config:` may come after
98
+ // another key. Pass one collects candidates; pass two keeps the ones this
99
+ // context understands and hands the rest back as content, in source order.
100
+ // Nothing an author wrote is ever dropped on the floor.
101
+ //
102
+ // `parentLayout` is the layout of the scope this one is a cell of, or null
103
+ // for a slide (which is nobody's cell).
104
+ function extractConfig(children, parentLayout) {
105
+ const candidates = [];
106
+ const contentNodes = [];
107
+ let pastConfig = false;
108
+
109
+ for (const child of children || []) {
110
+ if (!pastConfig && child.type === "paragraph") {
111
+ // No `s` or `m` flag: a paragraph spanning several source lines is
112
+ // content, never configuration.
113
+ const match = child.text.match(/^([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.+)$/);
114
+ if (match && CONFIG_KEYS.has(match[1].toLowerCase())) {
115
+ candidates.push({ key: match[1].toLowerCase(), value: match[2].trim(), node: child });
116
+ continue;
117
+ }
118
+ }
119
+ pastConfig = true;
120
+ contentNodes.push(child);
121
+ }
122
+
123
+ // Last one wins, the same rule the assignment loop below follows, so the
124
+ // keys allowed and the layout finally set can never disagree.
125
+ const named = candidates.filter((c) => c.key === "config" || c.key === "layout").pop();
126
+ const layout = named ? named.value.toLowerCase() : "";
127
+
128
+ const allowed = new Set(COMMON_KEYS);
129
+ for (const key of LAYOUT_KEYS[layout] || []) allowed.add(key);
130
+ for (const key of CELL_KEYS[parentLayout] || []) allowed.add(key);
131
+
132
+ const config = {};
133
+ const rejected = [];
134
+ for (const candidate of candidates) {
135
+ if (!allowed.has(candidate.key)) {
136
+ rejected.push(candidate.node);
137
+ continue;
138
+ }
139
+ if (candidate.key === "config" || candidate.key === "layout") {
140
+ config.layout = candidate.value.toLowerCase();
141
+ } else {
142
+ config[candidate.key] = candidate.value;
143
+ }
144
+ }
145
+
146
+ return { config, contentNodes: rejected.concat(contentNodes) };
147
+ }
148
+
149
+ // A child scope, read as a structural cell of `parentLayout`: its own config,
150
+ // its own content.
151
+ function readCell(scope, parentLayout) {
152
+ const { config, contentNodes } = extractConfig(scope.children, parentLayout);
153
+ return {
154
+ scope,
155
+ config,
156
+ contentNodes,
157
+ title: scope.hasHeading !== false && scope.title ? scope.title : null,
158
+ };
159
+ }
160
+
161
+ // Child scopes of a structured layout, excluding comment scopes. Speaker notes
162
+ // and drilldown details are removed by the renderer before a builder sees the
163
+ // slide, but only at slide level: a `@notes` scope nested inside a cell is an
164
+ // ordinary nested scope and renders visibly, as it always has.
165
+ function cellsOf(contentNodes, parentLayout) {
166
+ return contentNodes
167
+ .filter((n) => n.type === "scope" && n.scopeType !== "comment")
168
+ .map((scope) => readCell(scope, parentLayout));
169
+ }
170
+
171
+ function nonCellsOf(contentNodes) {
172
+ return contentNodes.filter((n) => n.type !== "scope" || n.scopeType === "comment");
173
+ }
174
+
175
+ function twoDigit(n) {
176
+ return n < 10 ? "0" + n : String(n);
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Builders
181
+ //
182
+ // Each builder receives ({ config, contentNodes }, ctx) where ctx carries the
183
+ // renderer callbacks, and returns the HTML for the slide body.
184
+ // ---------------------------------------------------------------------------
185
+
186
+ function accentClass(value) {
187
+ const name = slug(value);
188
+ return name ? ` accent-${name}` : "";
189
+ }
190
+
191
+ // columns — N child scopes side by side.
192
+ // numbered: true prefix each column with 01, 02, 03…
193
+ // variant: card a bordered panel per column
194
+ // variant: panel a filled panel per column
195
+ function buildColumns({ config, contentNodes }, ctx) {
196
+ const cells = cellsOf(contentNodes, "columns");
197
+ const preamble = ctx.renderChildren(nonCellsOf(contentNodes));
198
+ const numbered = truthy(config.numbered);
199
+ const variant = variantClass(config.variant);
200
+
201
+ const columns = cells
202
+ .map((cell, i) => {
203
+ const parts = [];
204
+ if (numbered) {
205
+ parts.push(`<div class="col-index">${twoDigit(i + 1)}</div>`);
206
+ }
207
+ if (cell.config.kicker) {
208
+ parts.push(`<div class="col-kicker">${ctx.renderInline(cell.config.kicker)}</div>`);
209
+ }
210
+ if (cell.title) {
211
+ parts.push(`<h3>${ctx.renderInline(cell.title)}</h3>`);
212
+ }
213
+ parts.push(ctx.renderChildren(cell.contentNodes));
214
+ if (cell.config.caption) {
215
+ parts.push(`<div class="col-caption">${ctx.renderInline(cell.config.caption)}</div>`);
216
+ }
217
+ return `<div class="column${accentClass(cell.config.accent)}">${parts.join("\n")}</div>`;
218
+ })
219
+ .join("\n");
220
+
221
+ const body = `<div class="columns cols-${cells.length}${variant}" data-count="${cells.length}">\n${columns}\n</div>`;
222
+ return preamble ? preamble + "\n" + body : body;
223
+ }
224
+
225
+ // stats — N child scopes as figure + caption.
226
+ // The scope title is the figure; its content is the caption beneath it.
227
+ function buildStats({ config, contentNodes }, ctx) {
228
+ const cells = cellsOf(contentNodes, "stats");
229
+ const trailing = ctx.renderChildren(nonCellsOf(contentNodes));
230
+
231
+ const stats = cells
232
+ .map((cell) => {
233
+ const parts = [];
234
+ if (cell.title) {
235
+ parts.push(`<div class="stat-value">${ctx.renderInline(cell.title)}</div>`);
236
+ }
237
+ const caption = ctx.renderChildren(cell.contentNodes);
238
+ if (caption) parts.push(`<div class="stat-label">${caption}</div>`);
239
+ return `<div class="stat${accentClass(cell.config.accent)}">${parts.join("\n")}</div>`;
240
+ })
241
+ .join("\n");
242
+
243
+ const body = `<div class="stats stats-${cells.length}" data-count="${cells.length}">\n${stats}\n</div>`;
244
+ return trailing ? body + "\n" + trailing : body;
245
+ }
246
+
247
+ // pipeline — one or more chains of steps, drawn as pills joined by arrows.
248
+ // arrow: > the glyph between steps (default →)
249
+ //
250
+ // Each child scope is a row: its title is the row label, its bullet list the
251
+ // steps. A step written entirely in bold is *marked* and takes the row's
252
+ // accent; every other step is neutral. The distinction is semantic — where a
253
+ // slide sets two paths against each other, the marks are the steps a path
254
+ // cannot avoid — so authors mark meaning and the theme supplies the colour.
255
+ // A step is marked when the whole of it is one bold span. `**A** and **B**`
256
+ // also opens and closes with `**`, so the interior is checked too: stripping
257
+ // the outer pair there would leave `A** and **B`, which re-parses with its
258
+ // emphasis inverted.
259
+ function markedText(text) {
260
+ if (!/^\*\*[\s\S]+\*\*$/.test(text)) return null;
261
+ const inner = text.slice(2, -2);
262
+ return inner.includes("**") ? null : inner.trim();
263
+ }
264
+
265
+ function stepsOfRow(cell) {
266
+ const list = cell.contentNodes.find((n) => n.type === "list");
267
+ if (!list) return [];
268
+ return list.items.map((item) => {
269
+ const text = (item.title || "").trim();
270
+ const marked = markedText(text);
271
+ return { text: marked === null ? text : marked, marked: marked !== null };
272
+ });
273
+ }
274
+
275
+ function buildPipeline({ config, contentNodes }, ctx) {
276
+ const cells = cellsOf(contentNodes, "pipeline");
277
+ const preamble = ctx.renderChildren(nonCellsOf(contentNodes));
278
+ // `arrow:`, not `rule:` — `rule:` is the stack's hairline flag, and one key
279
+ // meaning a boolean in one layout and a glyph in another put the word
280
+ // "true" between every step.
281
+ const arrow = config.arrow && config.arrow.trim() ? config.arrow.trim() : "→";
282
+
283
+ const rows = cells
284
+ .map((cell) => {
285
+ const steps = stepsOfRow(cell);
286
+ const pieces = [];
287
+ steps.forEach((step, i) => {
288
+ if (i > 0) pieces.push(`<div class="pipe-arrow" aria-hidden="true">${ctx.escapeHtml(arrow)}</div>`);
289
+ pieces.push(
290
+ `<div class="pipe-step${step.marked ? " is-marked" : ""}"><span>${ctx.renderInline(step.text)}</span></div>`
291
+ );
292
+ });
293
+ const label = cell.title
294
+ ? `<div class="pipe-label">${ctx.renderInline(cell.title)}</div>`
295
+ : "";
296
+ // Content other than the step list (a note under the row, say) follows.
297
+ const rest = ctx.renderChildren(cell.contentNodes.filter((n) => n.type !== "list"));
298
+ const restHtml = rest ? `\n<div class="pipe-note">${rest}</div>` : "";
299
+ return `<div class="pipe-row${accentClass(cell.config.accent)}">${label}\n<div class="pipe-steps">${pieces.join("")}</div>${restHtml}</div>`;
300
+ })
301
+ .join("\n");
302
+
303
+ const body = `<div class="pipeline">\n${rows}\n</div>`;
304
+ return preamble ? preamble + "\n" + body : body;
305
+ }
306
+
307
+ // matrix — a comparison table whose column headers alternate accents and in
308
+ // which one row may be highlighted.
309
+ // highlight: last highlight the final row
310
+ // highlight: Our company highlight the row whose first cell contains this
311
+ function buildMatrix({ config, contentNodes }, ctx) {
312
+ const table = contentNodes.find((n) => n.type === "table");
313
+ if (!table) return ctx.renderChildren(contentNodes);
314
+ const rest = ctx.renderChildren(contentNodes.filter((n) => n !== table));
315
+
316
+ const highlight = (config.highlight || "").trim();
317
+ const highlightLast = highlight.toLowerCase() === "last";
318
+ const needle = highlightLast ? null : highlight.toLowerCase();
319
+
320
+ const thead = table.headers.length
321
+ ? `<thead><tr>${table.headers
322
+ .map((cell, i) => `<th class="col-${i % 2 === 0 ? "a" : "b"}">${ctx.renderInline(cell)}</th>`)
323
+ .join("")}</tr></thead>`
324
+ : "";
325
+
326
+ const lastIndex = table.rows.length - 1;
327
+ const tbody = table.rows
328
+ .map((row, r) => {
329
+ const first = (row[0] || "").toLowerCase();
330
+ const isHighlight =
331
+ (highlightLast && r === lastIndex) || (needle && needle.length > 0 && first.includes(needle));
332
+ const cells = row
333
+ .map((cell, i) => `<td class="col-${i % 2 === 0 ? "a" : "b"}">${ctx.renderInline(cell)}</td>`)
334
+ .join("");
335
+ return `<tr${isHighlight ? ' class="is-highlight"' : ""}>${cells}</tr>`;
336
+ })
337
+ .join("\n");
338
+
339
+ const body = `<div class="matrix"><table>${thead}\n<tbody>\n${tbody}\n</tbody></table></div>`;
340
+ return rest ? body + "\n" + rest : body;
341
+ }
342
+
343
+ // rows — a stack of label / body / value rows separated by hairlines.
344
+ // numbered: true prefix each row with 01, 02, 03…
345
+ // variant: mono the label is a monospace accent (a date, a node, a stage)
346
+ // Each child scope: title is the label, `value:` the right-hand figure, the
347
+ // remaining content the description.
348
+ function buildRows({ config, contentNodes }, ctx) {
349
+ const cells = cellsOf(contentNodes, "rows");
350
+ const preamble = ctx.renderChildren(nonCellsOf(contentNodes));
351
+ const numbered = truthy(config.numbered);
352
+ const variant = variantClass(config.variant);
353
+
354
+ const rows = cells
355
+ .map((cell, i) => {
356
+ const parts = [];
357
+ if (numbered) parts.push(`<div class="row-index">${twoDigit(i + 1)}</div>`);
358
+ if (cell.title) parts.push(`<div class="row-label">${ctx.renderInline(cell.title)}</div>`);
359
+ const body = ctx.renderChildren(cell.contentNodes);
360
+ if (body) parts.push(`<div class="row-body">${body}</div>`);
361
+ if (cell.config.value) {
362
+ parts.push(`<div class="row-value">${ctx.renderInline(cell.config.value)}</div>`);
363
+ }
364
+ return `<div class="row${accentClass(cell.config.accent)}">${parts.join("\n")}</div>`;
365
+ })
366
+ .join("\n");
367
+
368
+ const body = `<div class="rows${variant}" data-count="${cells.length}">\n${rows}\n</div>`;
369
+ return preamble ? preamble + "\n" + body : body;
370
+ }
371
+
372
+ // bars — labelled quantities drawn as proportional bars.
373
+ // Each child scope: title is the label, `value:` the figure shown at the right,
374
+ // `fill:` the bar length as a percentage, and the content a note beneath.
375
+ // With no `fill:`, bars are scaled against the largest numeric `value:`.
376
+ function parseNumber(text) {
377
+ if (!text) return null;
378
+ const match = String(text).replace(/,/g, "").match(/-?\d+(\.\d+)?/);
379
+ return match ? parseFloat(match[0]) : null;
380
+ }
381
+
382
+ function buildBars({ config, contentNodes }, ctx) {
383
+ const cells = cellsOf(contentNodes, "bars");
384
+ const preamble = ctx.renderChildren(nonCellsOf(contentNodes));
385
+
386
+ const magnitudes = cells.map((cell) => parseNumber(cell.config.value));
387
+ const peak = Math.max(...magnitudes.filter((n) => typeof n === "number" && isFinite(n)), 0);
388
+
389
+ const bars = cells
390
+ .map((cell, i) => {
391
+ let fill = parseNumber(cell.config.fill);
392
+ if (fill === null) {
393
+ const magnitude = magnitudes[i];
394
+ fill = peak > 0 && magnitude !== null ? (magnitude / peak) * 100 : 0;
395
+ }
396
+ fill = Math.max(0, Math.min(100, fill));
397
+ const head = [];
398
+ if (cell.title) head.push(`<div class="bar-label">${ctx.renderInline(cell.title)}</div>`);
399
+ if (cell.config.value) head.push(`<div class="bar-value">${ctx.renderInline(cell.config.value)}</div>`);
400
+ const note = ctx.renderChildren(cell.contentNodes);
401
+ return [
402
+ `<div class="bar${accentClass(cell.config.accent)}">`,
403
+ `<div class="bar-head">${head.join("")}</div>`,
404
+ `<div class="bar-track"><div class="bar-fill" style="width:${fill.toFixed(2)}%"></div></div>`,
405
+ note ? `<div class="bar-note">${note}</div>` : "",
406
+ `</div>`,
407
+ ]
408
+ .filter(Boolean)
409
+ .join("\n");
410
+ })
411
+ .join("\n");
412
+
413
+ const body = `<div class="bars" data-count="${cells.length}">\n${bars}\n</div>`;
414
+ return preamble ? preamble + "\n" + body : body;
415
+ }
416
+
417
+ // split — two panes side by side, each a block with its own layout.
418
+ // weights: 55 45 the ratio between them (default 50 50)
419
+ function buildSplit({ config, contentNodes }, ctx) {
420
+ const cells = cellsOf(contentNodes, "split");
421
+ const preamble = ctx.renderChildren(nonCellsOf(contentNodes));
422
+
423
+ const weights = (config.weights || "")
424
+ .split(/[\s,/]+/)
425
+ .map((w) => parseFloat(w))
426
+ .filter((w) => isFinite(w) && w > 0);
427
+
428
+ const panes = cells
429
+ .map((cell, i) => {
430
+ const weight = weights[i] || 1;
431
+ return `<div class="pane" style="flex:${weight} 1 0">${renderBlock(cell, ctx)}</div>`;
432
+ })
433
+ .join("\n");
434
+
435
+ const body = `<div class="split" data-count="${cells.length}">\n${panes}\n</div>`;
436
+ return preamble ? preamble + "\n" + body : body;
437
+ }
438
+
439
+ // stack — child scopes rendered one under another, each with its own layout.
440
+ // rule: true draw a hairline between blocks
441
+ function buildStack({ config, contentNodes }, ctx) {
442
+ const cells = cellsOf(contentNodes, "stack");
443
+ const preamble = ctx.renderChildren(nonCellsOf(contentNodes));
444
+ const ruled = truthy(config.rule) ? " is-ruled" : "";
445
+
446
+ const blocks = cells
447
+ .map((cell) => `<div class="stack-block">${renderBlock(cell, ctx)}</div>`)
448
+ .join("\n");
449
+
450
+ const body = `<div class="stack${ruled}" data-count="${cells.length}">\n${blocks}\n</div>`;
451
+ return preamble ? preamble + "\n" + body : body;
452
+ }
453
+
454
+ const BUILDERS = {
455
+ columns: buildColumns,
456
+ "two-column": buildColumns,
457
+ stats: buildStats,
458
+ pipeline: buildPipeline,
459
+ matrix: buildMatrix,
460
+ rows: buildRows,
461
+ bars: buildBars,
462
+ split: buildSplit,
463
+ stack: buildStack,
464
+ };
465
+
466
+ // Renders a block: a heading if the scope has one, then its layout's body.
467
+ // Used for split panes and stack children, which are slides-within-a-slide.
468
+ function renderBlock(cell, ctx) {
469
+ const layout = cell.config.layout;
470
+ const parts = [];
471
+
472
+ if (cell.config.kicker) {
473
+ parts.push(`<div class="kicker">${ctx.renderInline(cell.config.kicker)}</div>`);
474
+ }
475
+ if (cell.title) {
476
+ parts.push(`<h3>${ctx.renderInline(cell.title)}</h3>`);
477
+ }
478
+ if (cell.config.lede) {
479
+ parts.push(`<p class="lede">${ctx.renderInline(cell.config.lede)}</p>`);
480
+ }
481
+
482
+ parts.push(buildBody(layout, cell, ctx));
483
+
484
+ if (cell.config.footnote) {
485
+ parts.push(`<div class="footnote">${ctx.renderInline(cell.config.footnote)}</div>`);
486
+ }
487
+
488
+ const classes = ["block"];
489
+ const layoutSlug = slug(layout);
490
+ if (layoutSlug) classes.push(`layout-${layoutSlug}`);
491
+ const accent = accentClass(cell.config.accent).trim();
492
+ if (accent) classes.push(accent);
493
+
494
+ return `<div class="${classes.join(" ")}"${layout ? ` data-layout="${escapeAttrValue(layout)}"` : ""}>${parts
495
+ .filter(Boolean)
496
+ .join("\n")}</div>`;
497
+ }
498
+
499
+ // The body of a layout: a structured builder if one is named, otherwise the
500
+ // content rendered as it stands.
501
+ function buildBody(layout, cell, ctx) {
502
+ const builder = layout ? BUILDERS[layout] : null;
503
+ if (builder) return builder(cell, ctx);
504
+ return ctx.renderChildren(cell.contentNodes);
505
+ }
506
+
507
+ module.exports = {
508
+ CONFIG_KEYS,
509
+ STRUCTURED_LAYOUTS,
510
+ extractConfig,
511
+ buildBody,
512
+ accentClass,
513
+ slug,
514
+ };
package/src/slide-pdf.js CHANGED
@@ -106,9 +106,15 @@ function chromePdf(htmlPath, pdfPath, options = {}) {
106
106
  });
107
107
  }
108
108
 
109
- // Slide PDF: 16:9 landscape (13.333 x 7.5 inches)
110
- function exportSlidePdf(htmlPath, pdfPath) {
111
- return chromePdf(htmlPath, pdfPath, { paperWidth: 13.333, paperHeight: 7.5 });
109
+ // Slide PDF: landscape at the theme's page size, defaulting to the 13.333 x
110
+ // 7.5 in box that matches the default 1280 x 720 design box at 96 dpi. A theme
111
+ // declaring a different box in theme.json must print at its own size, or the
112
+ // slide overflows the page and Chrome clips it.
113
+ function exportSlidePdf(htmlPath, pdfPath, page) {
114
+ return chromePdf(htmlPath, pdfPath, {
115
+ paperWidth: (page && page.width) || 13.333,
116
+ paperHeight: (page && page.height) || 7.5,
117
+ });
112
118
  }
113
119
 
114
120
  // Document PDF: A4 portrait (8.27 x 11.69 inches)