@bendyline/squisq-formats 2.3.1 → 2.3.2

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.
package/README.md CHANGED
@@ -136,6 +136,7 @@ Chapters split at H1/H2 boundaries. Providing `audio` + `audioSegments` generate
136
136
 
137
137
  ```ts
138
138
  import {
139
+ docToPptx,
139
140
  markdownDocToPptx,
140
141
  pptxToMarkdownDoc,
141
142
  pptxToContainer,
@@ -145,6 +146,10 @@ import {
145
146
  // slideBreak: 'h1' | 'h2' (default — H1 and H2 both break) | 'heading'
146
147
  const pptxBytes = await markdownDocToPptx(markdownDoc, { slideBreak: 'h2', images });
147
148
 
149
+ // Export: player-ready Doc -> 16:9 PPTX using the slideshow's canonical
150
+ // block expansion and materialized visual layers (managed cover included by default)
151
+ const visualDeck = await docToPptx(slideshowDoc, { images });
152
+
148
153
  // Import: PPTX → MarkdownDocument (add { extractImages: true } to reference slide images)
149
154
  const imported = await pptxToMarkdownDoc(pptxBuffer);
150
155
 
@@ -152,7 +157,7 @@ const imported = await pptxToMarkdownDoc(pptxBuffer);
152
157
  const container = await pptxToContainer(pptxBuffer);
153
158
  ```
154
159
 
155
- **Fidelity:** export preserves inline formatting as DrawingML runs and embeds images when provided via `options.images`. Import reads slide order from `ppt/presentation.xml` and converts each slide's title (→ H2), body text (→ bullet list), and tables. **Slide-image extraction (v1.5):** import can now extract slide-level `<p:pic>` bitmaps into `images/` — `pptxToContainer` returns a container with those files and forces `extractImages: true`, while `pptxToMarkdownDoc` leaves it off by default. Honest limit: only slide-level `<p:pic>` bitmaps are extracted — layout/master images, charts, SmartArt, and picture-fills are **not**.
160
+ **Fidelity:** `markdownDocToPptx` preserves inline formatting as DrawingML runs. `docToPptx` treats a template-backed/player-ready `Doc` as a visual deck: it uses the same renderable-block flattening, timing expansion, managed-cover policy, theme, transitions, and materialized layer stack as slideshow view, so the PPTX has the same slide count and a matching 16:9 canvas. Text, shapes, tables, paths, and supplied images become editable native PowerPoint objects; browser-only map/video/Mermaid layers degrade to a static image when supplied or a labeled text fallback. Import reads slide order from `ppt/presentation.xml` and converts each slide's title (→ H2), body text (→ bullet list), and tables. **Slide-image extraction (v1.5):** import can now extract slide-level `<p:pic>` bitmaps into `images/` — `pptxToContainer` returns a container with those files and forces `extractImages: true`, while `pptxToMarkdownDoc` leaves it off by default. Honest limit: only slide-level `<p:pic>` bitmaps are extracted — layout/master images, charts, SmartArt, and picture-fills are **not**.
156
161
 
157
162
  ### CSV
158
163
 
@@ -54,12 +54,22 @@ import {
54
54
  } from "./chunk-AONELFLA.js";
55
55
 
56
56
  // src/pptx/export.ts
57
- import { resolveFontFamily } from "@bendyline/squisq/schemas";
58
- import { docToMarkdown, resolveThemeForDoc } from "@bendyline/squisq/doc";
57
+ import { resolveFontFamily, VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
58
+ import {
59
+ createTemplateContext,
60
+ docToMarkdown,
61
+ expandCoverBlock,
62
+ expandDocBlocks,
63
+ flattenRenderableBlocks,
64
+ isTemplateBlock,
65
+ resolvePersistentLayers,
66
+ resolveThemeForDoc
67
+ } from "@bendyline/squisq/doc";
68
+ import { stripIconMarkers } from "@bendyline/squisq/icon-marker";
59
69
  import { readFrontmatterThemeId } from "@bendyline/squisq/markdown";
60
70
 
61
71
  // src/pptx/styles.ts
62
- var SLIDE_WIDTH = 9144e3;
72
+ var SLIDE_WIDTH = 12192e3;
63
73
  var SLIDE_HEIGHT = 6858e3;
64
74
  var TITLE_LEFT = 457200;
65
75
  var TITLE_TOP = 274638;
@@ -82,7 +92,7 @@ function buildPresentationXml(slideCount, slideRelIds, slideMasterRelId, _themeR
82
92
  for (let i = 0; i < slideCount; i++) {
83
93
  sldIdList.push(`<p:sldId id="${256 + i}" r:id="${slideRelIds[i]}"/>`);
84
94
  }
85
- return xmlDeclaration() + `<p:presentation xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}" saveSubsetFonts="1"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="${slideMasterRelId}"/></p:sldMasterIdLst><p:sldIdLst>${sldIdList.join("")}</p:sldIdLst><p:sldSz cx="${SLIDE_WIDTH}" cy="${SLIDE_HEIGHT}" type="screen4x3"/><p:notesSz cx="${SLIDE_HEIGHT}" cy="${SLIDE_WIDTH}"/></p:presentation>`;
95
+ return xmlDeclaration() + `<p:presentation xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}" saveSubsetFonts="1"><p:sldMasterIdLst><p:sldMasterId id="2147483648" r:id="${slideMasterRelId}"/></p:sldMasterIdLst><p:sldIdLst>${sldIdList.join("")}</p:sldIdLst><p:sldSz cx="${SLIDE_WIDTH}" cy="${SLIDE_HEIGHT}" type="screen16x9"/><p:notesSz cx="${SLIDE_HEIGHT}" cy="${SLIDE_WIDTH}"/></p:presentation>`;
86
96
  }
87
97
  function buildSlideMasterXml(layoutRelId) {
88
98
  return xmlDeclaration() + `<p:sldMaster xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}"><p:cSld><p:bg><p:bgRef idx="1001"><a:schemeClr val="bg1"/></p:bgRef></p:bg><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/></p:spTree></p:cSld><p:clrMap bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2" accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/><p:sldLayoutIdLst><p:sldLayoutId id="2147483649" r:id="${layoutRelId}"/></p:sldLayoutIdLst></p:sldMaster>`;
@@ -121,11 +131,69 @@ async function markdownDocToPptx(doc, options = {}) {
121
131
  return buildPptxPackage(slideXmls, slideContexts, options);
122
132
  }
123
133
  async function docToPptx(doc, options = {}) {
124
- const markdownDoc = docToMarkdown(doc);
125
- if (!options.themeId && doc.themeId) {
126
- options = { ...options, themeId: doc.themeId };
134
+ options.signal?.throwIfAborted();
135
+ const effectiveOptions = !options.themeId && doc.themeId ? { ...options, themeId: doc.themeId } : options;
136
+ const flatBlocks = flattenRenderableBlocks(doc.blocks);
137
+ if (!flatBlocks.some(isTemplateBlock)) {
138
+ return markdownDocToPptx(docToMarkdown(doc), effectiveOptions);
139
+ }
140
+ const theme = resolveThemeForDoc(doc, effectiveOptions.themeId, effectiveOptions.themeRegistry);
141
+ const style = slideStyleFromTheme(theme, effectiveOptions);
142
+ const audioSegments = doc.audio?.segments?.map((segment) => ({
143
+ startTime: segment.startTime,
144
+ duration: segment.duration
145
+ }));
146
+ const persistentLayers = resolvePersistentLayers(
147
+ { persistentLayers: doc.persistentLayers },
148
+ theme
149
+ );
150
+ const expanded = expandDocBlocks(flatBlocks, {
151
+ audioSegments,
152
+ viewport: VIEWPORT_PRESETS.landscape,
153
+ persistentLayers,
154
+ theme,
155
+ customTemplates: doc.customTemplates
156
+ });
157
+ const slideBlocks = [];
158
+ if (shouldIncludeCoverSlide(doc, effectiveOptions) && doc.startBlock) {
159
+ const coverContext = createTemplateContext(
160
+ theme,
161
+ 0,
162
+ Math.max(1, expanded.length + 1),
163
+ VIEWPORT_PRESETS.landscape
164
+ );
165
+ slideBlocks.push({
166
+ id: "cover-block",
167
+ startTime: -1,
168
+ duration: 0,
169
+ audioSegment: -1,
170
+ layers: expandCoverBlock(doc.startBlock, coverContext)
171
+ });
172
+ }
173
+ slideBlocks.push(...expanded);
174
+ if (slideBlocks.length === 0) {
175
+ slideBlocks.push({ id: "empty-slide", startTime: 0, duration: 0, audioSegment: 0, layers: [] });
127
176
  }
128
- return markdownDocToPptx(markdownDoc, options);
177
+ const slideXmls = [];
178
+ const slideContexts = [];
179
+ for (let index = 0; index < slideBlocks.length; index++) {
180
+ if ((index & 31) === 0) effectiveOptions.signal?.throwIfAborted();
181
+ const ctx = new SlideContext(
182
+ style,
183
+ effectiveOptions.images,
184
+ index,
185
+ effectiveOptions.allowRelativeHyperlinks ?? false,
186
+ effectiveOptions.signal
187
+ );
188
+ slideXmls.push(buildLayerSlideXml(slideBlocks[index], ctx));
189
+ slideContexts.push(ctx);
190
+ }
191
+ return buildPptxPackage(slideXmls, slideContexts, effectiveOptions);
192
+ }
193
+ function officeTypeface(family, fallback) {
194
+ const resolved = resolveFontFamily(family, fallback);
195
+ const first = resolved.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
196
+ return first || fallback;
129
197
  }
130
198
  function resolveSlideStyle(themeId, options, doc) {
131
199
  if (!themeId) {
@@ -142,19 +210,37 @@ function resolveSlideStyle(themeId, options, doc) {
142
210
  };
143
211
  }
144
212
  const theme = resolveThemeForDoc(doc, themeId, options.themeRegistry);
213
+ return slideStyleFromTheme(theme, options);
214
+ }
215
+ function slideStyleFromTheme(theme, options) {
145
216
  const c = theme.colors;
146
217
  return {
147
218
  background: toOoxmlHex(c.background, "FFFFFF"),
148
219
  text: toOoxmlHex(c.text, "333333"),
149
220
  titleColor: toOoxmlHex(c.highlight || c.secondary || c.text, "333333"),
150
221
  mutedColor: toOoxmlHex(c.textMuted || c.text, "666666"),
151
- titleFont: resolveFontFamily(theme.typography?.titleFont, DEFAULT_TITLE_FONT),
152
- bodyFont: resolveFontFamily(theme.typography?.bodyFont, options.defaultFont || DEFAULT_FONT),
153
- codeFont: resolveFontFamily(theme.typography?.monoFont, DEFAULT_CODE_FONT),
222
+ titleFont: officeTypeface(theme.typography?.titleFont, DEFAULT_TITLE_FONT),
223
+ bodyFont: officeTypeface(theme.typography?.bodyFont, options.defaultFont || DEFAULT_FONT),
224
+ codeFont: officeTypeface(theme.typography?.monoFont, DEFAULT_CODE_FONT),
154
225
  codeColor: toOoxmlHex(c.textMuted || c.text, "333333"),
155
226
  hasTheme: true
156
227
  };
157
228
  }
229
+ function shouldIncludeCoverSlide(doc, options) {
230
+ if (options.includeCoverSlide !== void 0) return options.includeCoverSlide;
231
+ const frontmatterValue = doc.frontmatter?.["squisq-cover-slide"] ?? doc.frontmatter?.["cover-slide"];
232
+ if (typeof frontmatterValue === "boolean") return frontmatterValue;
233
+ if (typeof frontmatterValue === "string") {
234
+ const normalized = frontmatterValue.trim().toLowerCase();
235
+ if (normalized === "false" || normalized === "no" || normalized === "off" || normalized === "0") {
236
+ return false;
237
+ }
238
+ if (normalized === "true" || normalized === "yes" || normalized === "on" || normalized === "1") {
239
+ return true;
240
+ }
241
+ }
242
+ return true;
243
+ }
158
244
  function segmentIntoSlides(children, slideBreak) {
159
245
  const maxDepth = slideBreak === "h1" ? 1 : slideBreak === "h2" ? 2 : 6;
160
246
  const slides = [];
@@ -301,6 +387,437 @@ function buildSlideXml(slide, ctx) {
301
387
  const extensionAttrs = transitionXml.includes("p14:") ? ` xmlns:mc="${NS_MC}" xmlns:p14="${NS_PML_2010}" mc:Ignorable="p14"` : "";
302
388
  return xmlDeclaration() + `<p:sld xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}"${extensionAttrs}><p:cSld>` + bgXml + `<p:spTree>` + shapes.join("") + `</p:spTree></p:cSld>` + transitionXml + `</p:sld>`;
303
389
  }
390
+ function buildLayerSlideXml(block, ctx) {
391
+ const shapes = [
392
+ `<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>`
393
+ ];
394
+ const textLayerY = (block.layers ?? []).filter((layer) => layer.type === "text").map((layer) => resolvePositionValue(layer.position.y, 1080));
395
+ for (let index = 0; index < (block.layers?.length ?? 0); index++) {
396
+ if ((index & 31) === 0) ctx.signal?.throwIfAborted();
397
+ const layer = block.layers[index];
398
+ const currentY = layer.type === "text" ? resolvePositionValue(layer.position.y, 1080) : void 0;
399
+ const nextTextY = currentY === void 0 ? void 0 : textLayerY.filter((candidate) => candidate > currentY + 1).sort((a, b) => a - b)[0];
400
+ shapes.push(
401
+ ...convertLayerToShapes(
402
+ layer,
403
+ ctx,
404
+ nextTextY === void 0 || currentY === void 0 ? void 0 : Math.max(currentY + 1, nextTextY - 8)
405
+ )
406
+ );
407
+ }
408
+ const bgXml = ctx.style.hasTheme ? `<p:bg><p:bgPr><a:solidFill><a:srgbClr val="${ctx.style.background}"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>` : "";
409
+ const transitionXml = buildTransitionXml(block.transition);
410
+ const extensionAttrs = transitionXml.includes("p14:") ? ` xmlns:mc="${NS_MC}" xmlns:p14="${NS_PML_2010}" mc:Ignorable="p14"` : "";
411
+ return xmlDeclaration() + `<p:sld xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}"${extensionAttrs}><p:cSld>${bgXml}<p:spTree>${shapes.join("")}</p:spTree></p:cSld>` + transitionXml + `</p:sld>`;
412
+ }
413
+ function convertLayerToShapes(layer, ctx, textBottom) {
414
+ switch (layer.type) {
415
+ case "text":
416
+ return [buildLayerTextShape(layer, ctx.allocShapeId(), textBottom)];
417
+ case "shape":
418
+ return [buildLayerShape(layer, ctx.allocShapeId())];
419
+ case "image": {
420
+ const data = ctx.images?.get(layer.content.src);
421
+ if (data) {
422
+ const image = ctx.addImage(layer.content.src, data, layer.content.alt);
423
+ const rect = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
424
+ const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === image);
425
+ return [
426
+ buildImageShape(embedded, ctx.allocShapeId(), rect.x, rect.y, rect.width, rect.height)
427
+ ];
428
+ }
429
+ return [
430
+ buildLayerTextShape(
431
+ textLayerForPlaceholder(
432
+ layer.position,
433
+ `[Image: ${layer.content.alt || layer.content.src}]`,
434
+ ctx
435
+ ),
436
+ ctx.allocShapeId()
437
+ )
438
+ ];
439
+ }
440
+ case "table":
441
+ return buildTableLayerShapes(layer, ctx);
442
+ case "tree":
443
+ return [
444
+ buildLayerTextShape(
445
+ {
446
+ ...layer,
447
+ type: "text",
448
+ content: {
449
+ text: treeItemsToText(layer.content.items),
450
+ style: {
451
+ fontSize: layer.content.style.fontSize,
452
+ fontFamily: layer.content.style.fontFamily,
453
+ color: layer.content.style.rowColor,
454
+ lineHeight: 1.35
455
+ }
456
+ }
457
+ },
458
+ ctx.allocShapeId()
459
+ )
460
+ ];
461
+ case "path":
462
+ return buildPathLayerShapes(layer, ctx);
463
+ case "map": {
464
+ const staticSrc = layer.content.staticSrc;
465
+ const data = staticSrc ? ctx.images?.get(staticSrc) : void 0;
466
+ if (staticSrc && data) {
467
+ const relId = ctx.addImage(staticSrc, data, "Map");
468
+ const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
469
+ const rect = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
470
+ return [
471
+ buildImageShape(embedded, ctx.allocShapeId(), rect.x, rect.y, rect.width, rect.height)
472
+ ];
473
+ }
474
+ return [
475
+ buildLayerTextShape(
476
+ textLayerForPlaceholder(
477
+ layer.position,
478
+ `Map: ${layer.content.center.lat.toFixed(4)}, ${layer.content.center.lng.toFixed(4)}`,
479
+ ctx
480
+ ),
481
+ ctx.allocShapeId()
482
+ )
483
+ ];
484
+ }
485
+ case "video": {
486
+ const poster = layer.content.posterSrc;
487
+ const data = poster ? ctx.images?.get(poster) : void 0;
488
+ if (poster && data) {
489
+ const relId = ctx.addImage(poster, data, layer.content.alt);
490
+ const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
491
+ const rect = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
492
+ return [
493
+ buildImageShape(embedded, ctx.allocShapeId(), rect.x, rect.y, rect.width, rect.height)
494
+ ];
495
+ }
496
+ return [
497
+ buildLayerTextShape(
498
+ textLayerForPlaceholder(
499
+ layer.position,
500
+ `[Video: ${layer.content.alt || layer.content.src}]`,
501
+ ctx
502
+ ),
503
+ ctx.allocShapeId()
504
+ )
505
+ ];
506
+ }
507
+ case "mermaid":
508
+ return [
509
+ buildLayerTextShape(
510
+ textLayerForPlaceholder(layer.position, layer.content.source, ctx),
511
+ ctx.allocShapeId()
512
+ )
513
+ ];
514
+ }
515
+ }
516
+ function resolvePositionValue(value, dimension) {
517
+ if (typeof value === "number") return value;
518
+ if (typeof value === "string") {
519
+ const trimmed = value.trim();
520
+ if (trimmed.endsWith("%")) {
521
+ const percent = Number.parseFloat(trimmed);
522
+ if (Number.isFinite(percent)) return percent / 100 * dimension;
523
+ }
524
+ const numeric = Number.parseFloat(trimmed);
525
+ if (Number.isFinite(numeric)) return numeric;
526
+ }
527
+ return 0;
528
+ }
529
+ function resolveLayerRect(position, defaultWidth, defaultHeight) {
530
+ const width = Math.max(
531
+ 1,
532
+ position.width != null ? resolvePositionValue(position.width, 1920) : defaultWidth
533
+ );
534
+ const height = Math.max(
535
+ 1,
536
+ position.height != null ? resolvePositionValue(position.height, 1080) : defaultHeight
537
+ );
538
+ let x = resolvePositionValue(position.x, 1920);
539
+ let y = resolvePositionValue(position.y, 1080);
540
+ const anchor = position.anchor ?? "top-left";
541
+ if (anchor === "center") {
542
+ x -= width / 2;
543
+ y -= height / 2;
544
+ } else {
545
+ if (anchor.endsWith("right")) x -= width;
546
+ if (anchor.startsWith("bottom")) y -= height;
547
+ }
548
+ const clippedX = Math.min(1919, Math.max(0, x));
549
+ const clippedY = Math.min(1079, Math.max(0, y));
550
+ const clippedWidth = Math.max(1, Math.min(width - (clippedX - x), 1920 - clippedX));
551
+ const clippedHeight = Math.max(1, Math.min(height - (clippedY - y), 1080 - clippedY));
552
+ return { x: clippedX, y: clippedY, width: clippedWidth, height: clippedHeight };
553
+ }
554
+ function toEmuRect(rect) {
555
+ return {
556
+ x: Math.round(rect.x / 1920 * SLIDE_WIDTH),
557
+ y: Math.round(rect.y / 1080 * SLIDE_HEIGHT),
558
+ width: Math.max(1, Math.round(rect.width / 1920 * SLIDE_WIDTH)),
559
+ height: Math.max(1, Math.round(rect.height / 1080 * SLIDE_HEIGHT))
560
+ };
561
+ }
562
+ function buildLayerTextShape(layer, shapeId, maxBottom) {
563
+ const text = stripIconMarkers(layer.content.text ?? "");
564
+ const style = layer.content.style;
565
+ const padding = style.padding ?? 0;
566
+ const rawLines = text.split("\n");
567
+ const estimatedWidth = Math.min(
568
+ 1920,
569
+ Math.max(1, Math.max(...rawLines.map((line) => line.length), 1) * style.fontSize * 0.55)
570
+ );
571
+ const boxWidth = layer.position.width != null ? resolvePositionValue(layer.position.width, 1920) : estimatedWidth;
572
+ const fittedFontSize = rawLines.length === 1 && style.fontSize >= 80 && text.length > 0 ? Math.min(style.fontSize, boxWidth / (text.length * 0.78)) : style.fontSize;
573
+ const charsPerLine = Math.max(1, Math.floor(boxWidth / Math.max(1, fittedFontSize * 0.58)));
574
+ const wrappedLineCount = Math.max(
575
+ 1,
576
+ rawLines.reduce((count, line) => count + Math.max(1, Math.ceil(line.length / charsPerLine)), 0)
577
+ );
578
+ const estimatedHeight = Math.max(
579
+ fittedFontSize * (style.lineHeight ?? 1.4) * wrappedLineCount + padding * 2,
580
+ fittedFontSize
581
+ );
582
+ const resolvedRect = resolveLayerRect(layer.position, boxWidth, estimatedHeight);
583
+ const safeBottom = Math.min(1060, maxBottom ?? 1060);
584
+ const virtualRect = {
585
+ ...resolvedRect,
586
+ y: Math.min(resolvedRect.y, safeBottom - 1),
587
+ height: Math.max(1, Math.min(resolvedRect.height, safeBottom - resolvedRect.y))
588
+ };
589
+ const heightScale = Math.min(1, virtualRect.height / estimatedHeight);
590
+ const renderedFontSize = Math.max(1, fittedFontSize * heightScale * (heightScale < 1 ? 0.9 : 1));
591
+ const renderedStyle = renderedFontSize === style.fontSize ? style : { ...style, fontSize: renderedFontSize };
592
+ const rect = toEmuRect(virtualRect);
593
+ const align = style.textAlign === "center" ? "ctr" : style.textAlign === "right" ? "r" : "l";
594
+ const vertical = style.verticalAlign === "bottom" ? "b" : style.verticalAlign === "middle" ? "ctr" : "t";
595
+ const inset = Math.max(0, Math.round(padding / 1920 * SLIDE_WIDTH));
596
+ const bodyPr = `<a:bodyPr wrap="square" anchor="${vertical}" vertOverflow="clip" horzOverflow="clip" lIns="${inset}" rIns="${inset}" tIns="${inset}" bIns="${inset}"><a:noAutofit/></a:bodyPr>`;
597
+ const paragraphs = text.split("\n").map((line) => buildLayerTextParagraph(line, renderedStyle, align)).join("");
598
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` + fillXml(style.background, style.backgroundOpacity, style.backgroundGradient) + lineXml(style.borderColor, style.borderWidth, style.borderStyle) + `</p:spPr><p:txBody>${bodyPr}<a:lstStyle/>${paragraphs}</p:txBody></p:sp>`;
599
+ }
600
+ function buildLayerTextParagraph(text, style, align) {
601
+ const size = Math.max(100, Math.round(style.fontSize * 75));
602
+ const color = parseCssColor(style.color, "000000");
603
+ const typeface = officeTypeface(style.fontFamily, DEFAULT_FONT);
604
+ const runProperties = [
605
+ `lang="en-US"`,
606
+ `dirty="0"`,
607
+ `sz="${size}"`,
608
+ ...style.fontWeight === "bold" ? ['b="1"'] : [],
609
+ ...style.fontStyle === "italic" ? ['i="1"'] : []
610
+ ].join(" ");
611
+ const lineSpacing = Math.max(1e4, Math.round((style.lineHeight ?? 1.4) * 1e5));
612
+ return `<a:p><a:pPr algn="${align}"><a:lnSpc><a:spcPct val="${lineSpacing}"/></a:lnSpc></a:pPr><a:r><a:rPr ${runProperties}>${solidColorXml(color)}<a:latin typeface="${escapeXml(typeface)}"/></a:rPr><a:t>${escapeXml(text || " ")}</a:t></a:r></a:p>`;
613
+ }
614
+ function buildLayerShape(layer, shapeId) {
615
+ const rect = toEmuRect(resolveLayerRect(layer.position, 100, 100));
616
+ const geometry = layer.content.shape === "circle" ? "ellipse" : layer.content.shape === "line" ? "line" : layer.content.borderRadius ? "roundRect" : "rect";
617
+ const fill = layer.content.shape === "line" ? "<a:noFill/>" : fillXml(layer.content.fill, layer.content.fillOpacity, layer.content.gradient);
618
+ const stroke = lineXml(
619
+ layer.content.stroke ?? (layer.content.shape === "line" ? "#ffffff" : void 0),
620
+ layer.content.strokeWidth ?? (layer.content.shape === "line" ? 2 : void 0),
621
+ layer.content.borderStyle
622
+ );
623
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="${geometry}"><a:avLst/></a:prstGeom>` + fill + stroke + `</p:spPr></p:sp>`;
624
+ }
625
+ function buildTableLayerShapes(layer, ctx) {
626
+ const rows = [layer.content.headers, ...layer.content.rows];
627
+ const columnCount = Math.max(1, ...rows.map((row) => row.length));
628
+ const rowCount = Math.max(1, rows.length);
629
+ const rect = resolveLayerRect(layer.position, 1920, 1080);
630
+ const cellWidth = rect.width / columnCount;
631
+ const cellHeight = rect.height / rowCount;
632
+ const shapes = [];
633
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
634
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
635
+ const isHeader = rowIndex === 0;
636
+ const text = rows[rowIndex]?.[columnIndex] ?? "";
637
+ shapes.push(
638
+ buildLayerTextShape(
639
+ {
640
+ id: `${layer.id}-${rowIndex}-${columnIndex}`,
641
+ type: "text",
642
+ position: {
643
+ x: rect.x + columnIndex * cellWidth,
644
+ y: rect.y + rowIndex * cellHeight,
645
+ width: cellWidth,
646
+ height: cellHeight
647
+ },
648
+ content: {
649
+ text,
650
+ style: {
651
+ fontSize: layer.content.style.fontSize,
652
+ fontFamily: isHeader ? layer.content.style.headerFontFamily ?? layer.content.style.fontFamily : layer.content.style.fontFamily,
653
+ fontWeight: isHeader ? "bold" : "normal",
654
+ color: isHeader ? layer.content.style.headerColor : layer.content.style.cellColor,
655
+ background: isHeader ? layer.content.style.headerBackground : layer.content.style.cellBackground,
656
+ borderColor: layer.content.style.borderColor,
657
+ borderWidth: 1,
658
+ padding: Math.max(4, layer.content.style.fontSize * 0.25),
659
+ verticalAlign: "middle",
660
+ textAlign: layer.content.align?.[columnIndex] ?? "left",
661
+ lineHeight: 1.2
662
+ }
663
+ }
664
+ },
665
+ ctx.allocShapeId()
666
+ )
667
+ );
668
+ }
669
+ }
670
+ return shapes;
671
+ }
672
+ function treeItemsToText(items, depth = 0) {
673
+ const lines = [];
674
+ for (const item of items) {
675
+ lines.push(
676
+ `${" ".repeat(depth)}${item.isDir ? "\u25BE " : "\u2022 "}${item.label}${item.comment ? ` \u2014 ${item.comment}` : ""}`
677
+ );
678
+ if (item.children.length > 0) lines.push(treeItemsToText(item.children, depth + 1));
679
+ }
680
+ return lines.filter(Boolean).join("\n");
681
+ }
682
+ function buildPathLayerShapes(layer, ctx) {
683
+ if (layer.content.shapeKind) {
684
+ const shape = {
685
+ id: layer.id,
686
+ type: "shape",
687
+ position: layer.position,
688
+ content: {
689
+ shape: layer.content.shapeKind === "circle" ? "circle" : "rect",
690
+ fill: layer.content.fill,
691
+ fillOpacity: layer.content.fillOpacity,
692
+ gradient: layer.content.gradient,
693
+ stroke: layer.content.stroke,
694
+ strokeWidth: layer.content.strokeWidth,
695
+ borderStyle: layer.content.borderStyle
696
+ }
697
+ };
698
+ return [buildLayerShape(shape, ctx.allocShapeId())];
699
+ }
700
+ const numbers = layer.content.d.match(/-?\d+(?:\.\d+)?/g)?.map(Number);
701
+ if (!numbers || numbers.length < 4) return [];
702
+ const x1 = numbers[0];
703
+ const y1 = numbers[1];
704
+ const x2 = numbers[numbers.length - 2];
705
+ const y2 = numbers[numbers.length - 1];
706
+ const rect = toEmuRect({
707
+ x: Math.min(x1, x2),
708
+ y: Math.min(y1, y2),
709
+ width: Math.max(1, Math.abs(x2 - x1)),
710
+ height: Math.max(1, Math.abs(y2 - y1))
711
+ });
712
+ const flipH = x2 < x1 ? ' flipH="1"' : "";
713
+ const flipV = y2 < y1 ? ' flipV="1"' : "";
714
+ const stroke = parseCssColor(layer.content.stroke, "FFFFFF");
715
+ const width = Math.max(1, Math.round((layer.content.strokeWidth ?? 2) * 9525));
716
+ const head = markerXml("headEnd", layer.content.startMarker);
717
+ const tail = markerXml("tailEnd", layer.content.endMarker);
718
+ const dash = dashXml(layer.content.borderStyle, layer.content.dasharray);
719
+ const shapeId = ctx.allocShapeId();
720
+ return [
721
+ `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm${flipH}${flipV}><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:noFill/><a:ln w="${width}">${solidColorXml(stroke)}${dash}${head}${tail}</a:ln></p:spPr></p:sp>`
722
+ ];
723
+ }
724
+ function textLayerForPlaceholder(position, text, ctx) {
725
+ return {
726
+ id: `placeholder-${ctx.allocShapeId()}`,
727
+ type: "text",
728
+ position,
729
+ content: {
730
+ text,
731
+ style: {
732
+ fontSize: 28,
733
+ fontFamily: ctx.style.bodyFont,
734
+ fontStyle: "italic",
735
+ color: `#${ctx.style.mutedColor}`,
736
+ textAlign: "center",
737
+ verticalAlign: "middle",
738
+ padding: 12
739
+ }
740
+ }
741
+ };
742
+ }
743
+ function parseCssColor(color, fallback) {
744
+ if (!color) return { hex: toOoxmlHex(fallback), alpha: 1e5 };
745
+ const trimmed = color.trim();
746
+ if (trimmed === "transparent" || trimmed === "none") return { hex: "000000", alpha: 0 };
747
+ const hexMatch = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(trimmed);
748
+ if (hexMatch) {
749
+ const raw = hexMatch[1];
750
+ const expanded = raw.length === 3 ? raw.split("").map((char) => char + char).join("") : raw;
751
+ const alpha = expanded.length === 8 ? Math.round(Number.parseInt(expanded.slice(6, 8), 16) / 255 * 1e5) : 1e5;
752
+ return { hex: expanded.slice(0, 6).toUpperCase(), alpha };
753
+ }
754
+ const rgbMatch = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(trimmed);
755
+ if (rgbMatch) {
756
+ const channel = (value) => Math.max(0, Math.min(255, Math.round(Number(value)))).toString(16).padStart(2, "0");
757
+ const alpha = rgbMatch[4] ? Math.round(Math.max(0, Math.min(1, Number(rgbMatch[4]))) * 1e5) : 1e5;
758
+ return {
759
+ hex: `${channel(rgbMatch[1])}${channel(rgbMatch[2])}${channel(rgbMatch[3])}`.toUpperCase(),
760
+ alpha
761
+ };
762
+ }
763
+ const named = {
764
+ black: "000000",
765
+ white: "FFFFFF",
766
+ red: "FF0000",
767
+ blue: "0000FF",
768
+ green: "008000",
769
+ gray: "808080",
770
+ grey: "808080"
771
+ };
772
+ return { hex: named[trimmed.toLowerCase()] ?? toOoxmlHex(fallback), alpha: 1e5 };
773
+ }
774
+ function solidColorXml(color) {
775
+ const alpha = color.alpha < 1e5 ? `<a:alpha val="${color.alpha}"/>` : "";
776
+ return `<a:solidFill><a:srgbClr val="${color.hex}">${alpha}</a:srgbClr></a:solidFill>`;
777
+ }
778
+ function fillXml(fill, opacity, gradient) {
779
+ if (gradient) return gradientFillXml(gradient.from, gradient.to, gradient.angle ?? 0, opacity);
780
+ if (!fill || fill === "none" || fill === "transparent") return "<a:noFill/>";
781
+ if (fill.includes("gradient(")) {
782
+ const colors = fill.match(/#[0-9a-f]{3,8}|rgba?\([^)]*\)/gi) ?? [];
783
+ if (colors.length >= 2) {
784
+ const angle = Number.parseFloat(
785
+ /(?:linear-gradient\()\s*([\d.-]+)deg/i.exec(fill)?.[1] ?? "0"
786
+ );
787
+ return gradientFillXml(colors[0], colors[colors.length - 1], angle, opacity);
788
+ }
789
+ }
790
+ const color = parseCssColor(fill, "000000");
791
+ if (opacity !== void 0)
792
+ color.alpha = Math.round(color.alpha * Math.max(0, Math.min(1, opacity)));
793
+ return solidColorXml(color);
794
+ }
795
+ function gradientFillXml(from, to, angle, opacity) {
796
+ const first = parseCssColor(from, "000000");
797
+ const second = parseCssColor(to, first.hex);
798
+ if (opacity !== void 0) {
799
+ const factor = Math.max(0, Math.min(1, opacity));
800
+ first.alpha = Math.round(first.alpha * factor);
801
+ second.alpha = Math.round(second.alpha * factor);
802
+ }
803
+ const ooxmlAngle = Math.round(((90 - angle) % 360 + 360) % 360 * 6e4);
804
+ return `<a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:srgbClr val="${first.hex}">${first.alpha < 1e5 ? `<a:alpha val="${first.alpha}"/>` : ""}</a:srgbClr></a:gs><a:gs pos="100000"><a:srgbClr val="${second.hex}">${second.alpha < 1e5 ? `<a:alpha val="${second.alpha}"/>` : ""}</a:srgbClr></a:gs></a:gsLst><a:lin ang="${ooxmlAngle}" scaled="1"/></a:gradFill>`;
805
+ }
806
+ function lineXml(color, widthPx, borderStyle) {
807
+ if (!color || !widthPx || widthPx <= 0) return "<a:ln><a:noFill/></a:ln>";
808
+ const width = Math.max(1, Math.round(widthPx * 9525));
809
+ return `<a:ln w="${width}">${solidColorXml(parseCssColor(color, "000000"))}${dashXml(borderStyle)}</a:ln>`;
810
+ }
811
+ function dashXml(style, dasharray) {
812
+ if (style === "dashed" || dasharray && dasharray !== "none") return '<a:prstDash val="dash"/>';
813
+ if (style === "dotted") return '<a:prstDash val="dot"/>';
814
+ return "";
815
+ }
816
+ function markerXml(tag, marker) {
817
+ if (!marker || marker === "none") return "";
818
+ const type = marker === "arrow" ? "triangle" : marker === "open" ? "arrow" : marker === "diamond" ? "diamond" : marker === "circle" ? "oval" : "square";
819
+ return `<a:${tag} type="${type}" w="med" len="med"/>`;
820
+ }
304
821
  function buildTransitionXml(transition) {
305
822
  if (!transition || transition.type === "cut") return "";
306
823
  const child = buildTransitionChildXml(transition);
@@ -71,6 +71,12 @@ interface PptxExportOptions {
71
71
  images?: Map<string, ArrayBuffer>;
72
72
  /** Permit carefully validated relative hyperlink targets. Default: false. */
73
73
  allowRelativeHyperlinks?: boolean;
74
+ /**
75
+ * Include the Doc's managed cover (`startBlock`) as slide 1. Defaults to
76
+ * the document's `squisq-cover-slide` / `cover-slide` setting, then true.
77
+ * MarkdownDocument export is unaffected because it has no managed cover.
78
+ */
79
+ includeCoverSlide?: boolean;
74
80
  }
75
81
  /**
76
82
  * Convert a MarkdownDocument to a .pptx ArrayBuffer.
@@ -79,7 +85,10 @@ declare function markdownDocToPptx(doc: MarkdownDocument, options?: PptxExportOp
79
85
  /**
80
86
  * Convert a squisq Doc to a .pptx ArrayBuffer.
81
87
  *
82
- * Convenience wrapper: Doc -> MarkdownDocument -> PPTX.
88
+ * Template-backed Docs are exported through the same canonical slideshow
89
+ * expansion as the player, then their materialized visual layers are mapped
90
+ * to native DrawingML shapes. Plain, non-template Docs retain the semantic
91
+ * Markdown export path for backward compatibility.
83
92
  */
84
93
  declare function docToPptx(doc: Doc, options?: PptxExportOptions): Promise<ArrayBuffer>;
85
94
 
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { ExtractedFileTheme, InferSourceFormat, InferThemeOptions, InferredFileT
9
9
  export { BUILTIN_FORMAT_IDS, BuiltinFormatOptions, ConversionError, ConversionErrorCode, ConversionErrorOptions, ConversionLimits, ConversionResult, ConvertOptions, ConvertSource, DEFAULT_CONVERSION_LIMITS, DbkFormatOptions, FormatDefinition, FormatId, FormatRegistry, MarkdownFormatOptions, NormalizedInput, PreparedConversion, PreparedExportOptions, convert, createRegistry, defaultFormats, defaultRegistry, prepareConversion, resolveConversionLimits } from './registry/index.js';
10
10
  export { Z as ZipSafetyError, a as ZipSafetyErrorCode, b as ZipSafetyErrorOptions, c as ZipSafetyLimits } from './zipLimits-BOKCB7qk.js';
11
11
  export { H as HtmlExportOptions, a as HtmlImportOptions, c as collectImagePaths, h as htmlToMarkdown, b as htmlToMarkdownDoc, d as htmlToMarkdownDocSync } from './import-C8whCC7_.js';
12
- export { P as PptxExportOptions, a as PptxImportOptions, d as docToPptx, m as markdownDocToPptx, p as pptxToMarkdownDoc } from './import-C3htUTss.js';
12
+ export { P as PptxExportOptions, a as PptxImportOptions, d as docToPptx, m as markdownDocToPptx, p as pptxToMarkdownDoc } from './import-BhBGQqnz.js';
13
13
  export { X as XlsxExportOptions, a as XlsxImportOptions, d as docToXlsx, m as markdownDocToXlsx, x as xlsxToMarkdownDoc } from './export-D9msROJS.js';
14
14
  import '@bendyline/squisq/schemas';
15
15
  import '@bendyline/squisq/markdown';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  defaultRegistry,
12
12
  prepareConversion,
13
13
  resolveConversionLimits
14
- } from "./chunk-LBHZSY7N.js";
14
+ } from "./chunk-PBDY5C5R.js";
15
15
  import {
16
16
  inferThemeFromFile
17
17
  } from "./chunk-6S6GU3ZG.js";
@@ -33,7 +33,7 @@ import {
33
33
  markdownDocToPptx,
34
34
  pptxToDoc,
35
35
  pptxToMarkdownDoc
36
- } from "./chunk-IPN56VLW.js";
36
+ } from "./chunk-FJKN7C6B.js";
37
37
  import "./chunk-TAAENIRB.js";
38
38
  import "./chunk-GX7RAUME.js";
39
39
  import {
@@ -1,5 +1,5 @@
1
- import { a as PptxImportOptions } from '../import-C3htUTss.js';
2
- export { P as PptxExportOptions, d as docToPptx, m as markdownDocToPptx, b as pptxToContainer, p as pptxToMarkdownDoc } from '../import-C3htUTss.js';
1
+ import { a as PptxImportOptions } from '../import-BhBGQqnz.js';
2
+ export { P as PptxExportOptions, d as docToPptx, m as markdownDocToPptx, b as pptxToContainer, p as pptxToMarkdownDoc } from '../import-BhBGQqnz.js';
3
3
  import { Doc } from '@bendyline/squisq/schemas';
4
4
  export { A as AnalyzePptxLayoutsOptions, a as AnalyzedLayout, E as ExtractedPlaceholder, b as ExtractedSlideLayout, I as InspectPptxLayoutsOptions, L as LayoutVerdict, P as PptxColorHints, c as PptxLayoutInference, d as PptxLayoutSummary, e as analyzePptxLayouts, i as inspectPptxLayouts } from '../layouts-CTdPlB-u.js';
5
5
  import '@bendyline/squisq/markdown';
@@ -4,7 +4,7 @@ import {
4
4
  pptxToContainer,
5
5
  pptxToDoc,
6
6
  pptxToMarkdownDoc
7
- } from "../chunk-IPN56VLW.js";
7
+ } from "../chunk-FJKN7C6B.js";
8
8
  import {
9
9
  analyzePptxLayouts,
10
10
  inspectPptxLayouts
@@ -3,7 +3,7 @@ import { TransformStyleInput, TransformStyleRegistry } from '@bendyline/squisq/t
3
3
  import { ParseOptions, StringifyOptions, MarkdownDocument } from '@bendyline/squisq/markdown';
4
4
  import { ContentContainer } from '@bendyline/squisq/storage';
5
5
  import { DocxImportOptions, DocxExportOptions } from '../docx/index.js';
6
- import { a as PptxImportOptions, P as PptxExportOptions } from '../import-C3htUTss.js';
6
+ import { a as PptxImportOptions, P as PptxExportOptions } from '../import-BhBGQqnz.js';
7
7
  import { a as XlsxImportOptions, X as XlsxExportOptions } from '../export-D9msROJS.js';
8
8
  import { CsvImportOptions, CsvExportOptions } from '../csv/index.js';
9
9
  import { PdfImportOptions, PdfExportOptions } from '../pdf/index.js';
@@ -7,7 +7,7 @@ import {
7
7
  defaultRegistry,
8
8
  prepareConversion,
9
9
  resolveConversionLimits
10
- } from "../chunk-LBHZSY7N.js";
10
+ } from "../chunk-PBDY5C5R.js";
11
11
  import {
12
12
  ConversionError
13
13
  } from "../chunk-KXOZMWBS.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -107,7 +107,7 @@
107
107
  "dependencies": {
108
108
  "@pdf-lib/upng": "1.0.1",
109
109
  "@xmldom/xmldom": "0.9.10",
110
- "@bendyline/squisq": "2.3.1",
110
+ "@bendyline/squisq": "2.3.2",
111
111
  "jszip": "3.10.1",
112
112
  "pdf-lib": "1.17.1",
113
113
  "pdfjs-dist": "4.10.38"
File without changes