@bendyline/squisq-formats 2.8.16 → 2.8.17

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.
@@ -570,12 +570,13 @@ function convertCodeBlock(node) {
570
570
  return parts.join("");
571
571
  }
572
572
  function convertTable(node, ctx) {
573
+ const columnCount = Math.max(1, ...node.children.map((row) => row.children.length));
573
574
  const rows = [];
574
575
  for (let ri = 0; ri < node.children.length; ri++) {
575
576
  const row = node.children[ri];
576
577
  rows.push(convertTableRow(row, ctx, ri === 0, node.align));
577
578
  }
578
- return `<w:tbl><w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr><w:tblGrid/>` + rows.join("") + `</w:tbl>`;
579
+ return `<w:tbl><w:tblPr><w:tblStyle w:val="TableGrid"/><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/></w:tblBorders></w:tblPr><w:tblGrid>${"<w:gridCol/>".repeat(columnCount)}</w:tblGrid>` + rows.join("") + `</w:tbl>`;
579
580
  }
580
581
  function convertTableRow(row, ctx, isHeader, align) {
581
582
  const cells = [];
@@ -588,11 +589,19 @@ function convertTableRow(row, ctx, isHeader, align) {
588
589
  return `<w:tr>${trPr}${cells.join("")}</w:tr>`;
589
590
  }
590
591
  function convertTableCell(cell, ctx, isHeader, align) {
591
- const runs = convertInlines(cell.children, ctx, bodyFormat(ctx));
592
+ const format = isHeader ? { ...bodyFormat(ctx), bold: true } : bodyFormat(ctx);
593
+ const runs = convertInlines(cell.children, ctx, format);
592
594
  const jcMap = { left: "left", center: "center", right: "right" };
593
595
  const jc = align ? `<w:jc w:val="${jcMap[align]}"/>` : "";
594
596
  const pPr = `<w:pPr><w:spacing w:after="0"/>${jc}</w:pPr>`;
595
- return `<w:tc><w:p>${pPr}${runs}</w:p></w:tc>`;
597
+ const tcPr = isHeader ? `<w:tcPr><w:shd w:val="clear" w:color="auto" w:fill="${tableHeaderFill(ctx)}"/></w:tcPr>` : "";
598
+ return `<w:tc>${tcPr}<w:p>${pPr}${runs}</w:p></w:tc>`;
599
+ }
600
+ function tableHeaderFill(ctx) {
601
+ const channels = (hex, fallback) => (/^[0-9a-f]{6}$/i.test(hex ?? "") ? hex : fallback).match(/../g).map((pair) => parseInt(pair, 16));
602
+ const page = channels(ctx.backgroundColor, "FFFFFF");
603
+ const ink = channels(ctx.bodyColor, "000000");
604
+ return page.map((channel, index) => Math.round(channel + (ink[index] - channel) * 0.1)).map((channel) => channel.toString(16).padStart(2, "0")).join("").toUpperCase();
596
605
  }
597
606
  function convertThematicBreak() {
598
607
  return `<w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="1" w:color="auto"/></w:pBdr></w:pPr></w:p>`;
@@ -6,7 +6,7 @@ import {
6
6
  import {
7
7
  convert,
8
8
  defaultRegistry
9
- } from "./chunk-LSUMEIQ7.js";
9
+ } from "./chunk-2IG4BEAX.js";
10
10
  import {
11
11
  ConversionError
12
12
  } from "./chunk-KXOZMWBS.js";
@@ -79,7 +79,8 @@ import {
79
79
  isGeometryBoundTemplate,
80
80
  isTemplateBlock,
81
81
  resolvePersistentLayers,
82
- resolveThemeForDoc
82
+ resolveThemeForDoc,
83
+ shapePath
83
84
  } from "@bendyline/squisq/doc";
84
85
  import { splitIconMarkers as splitIconMarkers3 } from "@bendyline/squisq/icon-marker";
85
86
  import {
@@ -90,11 +91,24 @@ import {
90
91
 
91
92
  // src/pptx/pictureLayers.ts
92
93
  import { svgSourceFromBytes } from "@bendyline/squisq/markdown";
93
- function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight, hyperlinkRelId, svgRelId) {
94
+ function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight, hyperlinkRelId, svgRelId, srcRect = "") {
94
95
  const name = escapeXml(img.alt || "Picture");
95
96
  const descr = img.alt ? ` descr="${escapeXml(img.alt)}"` : "";
96
97
  const link = hyperlinkRelId ? `<a:hlinkClick r:id="${hyperlinkRelId}"/>` : "";
97
- return `<p:pic><p:nvPicPr>` + (link ? `<p:cNvPr id="${shapeId}" name="${name}"${descr}>${link}</p:cNvPr>` : `<p:cNvPr id="${shapeId}" name="${name}"${descr}/>`) + `<p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill>` + blipXml(img.relId, svgRelId) + `<a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${maxWidth}" cy="${maxHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
98
+ return `<p:pic><p:nvPicPr>` + (link ? `<p:cNvPr id="${shapeId}" name="${name}"${descr}>${link}</p:cNvPr>` : `<p:cNvPr id="${shapeId}" name="${name}"${descr}/>`) + `<p:cNvPicPr><a:picLocks noChangeAspect="1"/></p:cNvPicPr><p:nvPr/></p:nvPicPr><p:blipFill>` + blipXml(img.relId, svgRelId) + srcRect + `<a:stretch><a:fillRect/></a:stretch></p:blipFill><p:spPr><a:xfrm><a:off x="${left}" y="${top}"/><a:ext cx="${maxWidth}" cy="${maxHeight}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr></p:pic>`;
99
+ }
100
+ function fitPicture(box, size, fit) {
101
+ if (!size || size.width <= 0 || size.height <= 0 || fit === "fill" || box.height <= 0)
102
+ return { rect: box, srcRect: "" };
103
+ if (fit !== "cover") return { rect: fitCentered(box, size.width, size.height), srcRect: "" };
104
+ const image = size.width / size.height;
105
+ const frame = box.width / box.height;
106
+ if (Math.abs(image - frame) < 1e-3) return { rect: box, srcRect: "" };
107
+ const edge = Math.round((image > frame ? 1 - frame / image : 1 - image / frame) / 2 * 1e5);
108
+ return {
109
+ rect: box,
110
+ srcRect: image > frame ? `<a:srcRect l="${edge}" r="${edge}"/>` : `<a:srcRect t="${edge}" b="${edge}"/>`
111
+ };
98
112
  }
99
113
  function fitCentered(area, width, height) {
100
114
  const scale = Math.min(area.width / width, area.height / height);
@@ -449,9 +463,30 @@ function cssGradientToString(gradient) {
449
463
  // src/pptx/drawingXml.ts
450
464
  function officeTypeface(family, fallback) {
451
465
  const resolved = resolveFontFamily(family, fallback);
452
- const first = resolved.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
453
- return first || fallback;
454
- }
466
+ const names = resolved.split(",").map((name) => name.trim().replace(/^['"]|['"]$/g, "")).filter(Boolean);
467
+ const named = names.find((name) => !GENERIC_FONT_FAMILIES.has(name.toLowerCase()));
468
+ if (named) return named;
469
+ const kind = names.map((name) => name.toLowerCase());
470
+ if (kind.includes("monospace") || kind.includes("ui-monospace")) return "Consolas";
471
+ if (kind.includes("serif") || kind.includes("ui-serif")) return "Georgia";
472
+ return names.length ? "Segoe UI" : fallback;
473
+ }
474
+ var GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set([
475
+ "system-ui",
476
+ "-apple-system",
477
+ "blinkmacsystemfont",
478
+ "ui-sans-serif",
479
+ "ui-serif",
480
+ "ui-monospace",
481
+ "ui-rounded",
482
+ "sans-serif",
483
+ "serif",
484
+ "monospace",
485
+ "cursive",
486
+ "fantasy",
487
+ "emoji",
488
+ "math"
489
+ ]);
455
490
  function solidColorXml(color) {
456
491
  const alpha = color.alpha < 1e5 ? `<a:alpha val="${color.alpha}"/>` : "";
457
492
  return `<a:solidFill><a:srgbClr val="${color.hex}">${alpha}</a:srgbClr></a:solidFill>`;
@@ -507,13 +542,17 @@ function patternFillXml(pattern, ground) {
507
542
  function buildLayerShape(layer, shapeId) {
508
543
  const rect = toEmuRect(resolveLayerRect(layer.position, 100, 100));
509
544
  const geometry = layer.content.shape === "circle" ? "ellipse" : layer.content.shape === "line" ? "line" : layer.content.borderRadius ? "roundRect" : "rect";
545
+ const box = resolveLayerRect(layer.position, 100, 100);
546
+ const shorter = Math.min(box.width, box.height);
547
+ const radius = Math.min(shorter / 2, Math.max(0, layer.content.borderRadius ?? 0));
548
+ const adjust = geometry === "roundRect" && shorter > 0 ? `<a:avLst><a:gd name="adj" fmla="val ${Math.round(radius / shorter * 1e5)}"/></a:avLst>` : "<a:avLst/>";
510
549
  const fill = layer.content.shape === "line" ? "<a:noFill/>" : layer.content.pattern ? patternFillXml(layer.content.pattern) : fillXml(layer.content.fill, layer.content.fillOpacity, layer.content.gradient);
511
550
  const stroke = lineXml(
512
551
  layer.content.stroke ?? (layer.content.shape === "line" ? "#ffffff" : void 0),
513
552
  layer.content.strokeWidth ?? (layer.content.shape === "line" ? 2 : void 0),
514
553
  layer.content.borderStyle
515
554
  );
516
- 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>`;
555
+ 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}">${adjust}</a:prstGeom>` + fill + stroke + `</p:spPr></p:sp>`;
517
556
  }
518
557
 
519
558
  // src/pptx/backdrop.ts
@@ -752,6 +791,235 @@ function planDeckBackdrops(blocks) {
752
791
  return { master: { base: masterBase, overlays: masterOverlays }, layouts, slides };
753
792
  }
754
793
 
794
+ // src/pptx/pathGeometry.ts
795
+ var ARITY = {
796
+ M: 2,
797
+ L: 2,
798
+ H: 1,
799
+ V: 1,
800
+ C: 6,
801
+ S: 4,
802
+ Q: 4,
803
+ T: 2,
804
+ A: 7,
805
+ Z: 0
806
+ };
807
+ function arcToCubics(x0, y0, rx, ry, rotation, largeArc, sweep, x, y) {
808
+ if (x0 === x && y0 === y || !Number.isFinite(rx + ry)) return [];
809
+ rx = Math.abs(rx);
810
+ ry = Math.abs(ry);
811
+ if (rx === 0 || ry === 0) return [{ op: "L", x, y }];
812
+ const phi = rotation * Math.PI / 180;
813
+ const [cos, sin] = [Math.cos(phi), Math.sin(phi)];
814
+ const dx = (x0 - x) / 2;
815
+ const dy = (y0 - y) / 2;
816
+ const x1 = cos * dx + sin * dy;
817
+ const y1 = -sin * dx + cos * dy;
818
+ const lambda = x1 * x1 / (rx * rx) + y1 * y1 / (ry * ry);
819
+ if (lambda > 1) {
820
+ rx *= Math.sqrt(lambda);
821
+ ry *= Math.sqrt(lambda);
822
+ }
823
+ const numerator = rx * rx * ry * ry - rx * rx * y1 * y1 - ry * ry * x1 * x1;
824
+ const denominator = rx * rx * y1 * y1 + ry * ry * x1 * x1;
825
+ const factor = (largeArc === sweep ? -1 : 1) * Math.sqrt(Math.max(0, numerator / (denominator || 1)));
826
+ const cxp = factor * rx * y1 / ry;
827
+ const cyp = -factor * ry * x1 / rx;
828
+ const cx = cos * cxp - sin * cyp + (x0 + x) / 2;
829
+ const cy = sin * cxp + cos * cyp + (y0 + y) / 2;
830
+ const angle = (ux, uy, vx, vy) => {
831
+ const turn = Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
832
+ return turn;
833
+ };
834
+ const start = angle(1, 0, (x1 - cxp) / rx, (y1 - cyp) / ry);
835
+ let delta = angle((x1 - cxp) / rx, (y1 - cyp) / ry, (-x1 - cxp) / rx, (-y1 - cyp) / ry);
836
+ if (!sweep && delta > 0) delta -= 2 * Math.PI;
837
+ if (sweep && delta < 0) delta += 2 * Math.PI;
838
+ const pieces = Math.max(1, Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9));
839
+ const step = delta / pieces;
840
+ const handle = 4 / 3 * Math.tan(step / 4);
841
+ const point = (theta) => [
842
+ cx + rx * Math.cos(theta) * cos - ry * Math.sin(theta) * sin,
843
+ cy + rx * Math.cos(theta) * sin + ry * Math.sin(theta) * cos
844
+ ];
845
+ const tangent = (theta) => [
846
+ -rx * Math.sin(theta) * cos - ry * Math.cos(theta) * sin,
847
+ -rx * Math.sin(theta) * sin + ry * Math.cos(theta) * cos
848
+ ];
849
+ const segments = [];
850
+ for (let i = 0; i < pieces; i++) {
851
+ const from = start + step * i;
852
+ const to = from + step;
853
+ const [px, py] = point(from);
854
+ const [qx, qy] = point(to);
855
+ const [tx0, ty0] = tangent(from);
856
+ const [tx1, ty1] = tangent(to);
857
+ segments.push({
858
+ op: "C",
859
+ x1: px + handle * tx0,
860
+ y1: py + handle * ty0,
861
+ x2: qx - handle * tx1,
862
+ y2: qy - handle * ty1,
863
+ // The last piece ends exactly on the authored endpoint.
864
+ x: i === pieces - 1 ? x : qx,
865
+ y: i === pieces - 1 ? y : qy
866
+ });
867
+ }
868
+ return segments;
869
+ }
870
+ function parseSvgPath(d) {
871
+ const tokens = d.match(/[a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/gu);
872
+ if (!tokens || !/^[Mm]$/u.test(tokens[0])) return null;
873
+ const segments = [];
874
+ let [cx, cy] = [0, 0];
875
+ let [startX, startY] = [0, 0];
876
+ let control;
877
+ let index = 0;
878
+ while (index < tokens.length) {
879
+ const command = tokens[index++];
880
+ const upper = command.toUpperCase();
881
+ const arity = ARITY[upper];
882
+ if (arity === void 0 || !/^[a-zA-Z]$/u.test(command)) return null;
883
+ const relative = command !== upper;
884
+ if (upper === "Z") {
885
+ segments.push({ op: "Z" });
886
+ [cx, cy] = [startX, startY];
887
+ control = void 0;
888
+ continue;
889
+ }
890
+ let first = true;
891
+ while (first || index < tokens.length && !/^[a-zA-Z]$/u.test(tokens[index])) {
892
+ const numbers = tokens.slice(index, index + arity).map(Number);
893
+ if (numbers.length < arity || numbers.some((value) => !Number.isFinite(value))) return null;
894
+ index += arity;
895
+ const [ox, oy] = relative ? [cx, cy] : [0, 0];
896
+ let next;
897
+ switch (upper) {
898
+ case "M": {
899
+ [cx, cy] = [ox + numbers[0], oy + numbers[1]];
900
+ if (first) {
901
+ segments.push({ op: "M", x: cx, y: cy });
902
+ [startX, startY] = [cx, cy];
903
+ } else segments.push({ op: "L", x: cx, y: cy });
904
+ break;
905
+ }
906
+ case "L":
907
+ [cx, cy] = [ox + numbers[0], oy + numbers[1]];
908
+ segments.push({ op: "L", x: cx, y: cy });
909
+ break;
910
+ case "H":
911
+ cx = ox + numbers[0];
912
+ segments.push({ op: "L", x: cx, y: cy });
913
+ break;
914
+ case "V":
915
+ cy = oy + numbers[0];
916
+ segments.push({ op: "L", x: cx, y: cy });
917
+ break;
918
+ case "C":
919
+ case "S": {
920
+ const smooth = upper === "S";
921
+ const x1 = smooth ? control?.kind === "C" ? 2 * cx - control.x : cx : ox + numbers[0];
922
+ const y1 = smooth ? control?.kind === "C" ? 2 * cy - control.y : cy : oy + numbers[1];
923
+ const rest = numbers.slice(smooth ? 0 : 2);
924
+ const [x2, y2, x, y] = [ox + rest[0], oy + rest[1], ox + rest[2], oy + rest[3]];
925
+ segments.push({ op: "C", x1, y1, x2, y2, x, y });
926
+ next = { kind: "C", x: x2, y: y2 };
927
+ [cx, cy] = [x, y];
928
+ break;
929
+ }
930
+ case "Q":
931
+ case "T": {
932
+ const smooth = upper === "T";
933
+ const x1 = smooth ? control?.kind === "Q" ? 2 * cx - control.x : cx : ox + numbers[0];
934
+ const y1 = smooth ? control?.kind === "Q" ? 2 * cy - control.y : cy : oy + numbers[1];
935
+ const rest = numbers.slice(smooth ? 0 : 2);
936
+ const [x, y] = [ox + rest[0], oy + rest[1]];
937
+ segments.push({ op: "Q", x1, y1, x, y });
938
+ next = { kind: "Q", x: x1, y: y1 };
939
+ [cx, cy] = [x, y];
940
+ break;
941
+ }
942
+ case "A": {
943
+ const [x, y] = [ox + numbers[5], oy + numbers[6]];
944
+ segments.push(
945
+ ...arcToCubics(
946
+ cx,
947
+ cy,
948
+ numbers[0],
949
+ numbers[1],
950
+ numbers[2],
951
+ numbers[3] !== 0,
952
+ numbers[4] !== 0,
953
+ x,
954
+ y
955
+ )
956
+ );
957
+ [cx, cy] = [x, y];
958
+ break;
959
+ }
960
+ }
961
+ control = next;
962
+ first = false;
963
+ }
964
+ }
965
+ return segments;
966
+ }
967
+ function pathBounds(segments) {
968
+ const xs = [];
969
+ const ys = [];
970
+ for (const segment of segments) {
971
+ if (segment.op === "Z") continue;
972
+ xs.push(segment.x);
973
+ ys.push(segment.y);
974
+ if (segment.op === "C" || segment.op === "Q") {
975
+ xs.push(segment.x1);
976
+ ys.push(segment.y1);
977
+ }
978
+ if (segment.op === "C") {
979
+ xs.push(segment.x2);
980
+ ys.push(segment.y2);
981
+ }
982
+ }
983
+ if (!xs.length) return null;
984
+ const [x, y] = [Math.min(...xs), Math.min(...ys)];
985
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
986
+ }
987
+ function isStraightLine(segments) {
988
+ const points = segments.filter((segment) => segment.op !== "Z");
989
+ if (points.length < 2 || points.length !== segments.length) return false;
990
+ if (points.some((segment) => segment.op === "C" || segment.op === "Q")) return false;
991
+ const first = points[0];
992
+ const last = points[points.length - 1];
993
+ const [dx, dy] = [last.x - first.x, last.y - first.y];
994
+ const length = Math.hypot(dx, dy);
995
+ if (length === 0) return false;
996
+ return points.every((segment) => {
997
+ const point = segment;
998
+ return Math.abs((point.x - first.x) * dy - (point.y - first.y) * dx) / length < 0.5;
999
+ });
1000
+ }
1001
+ function customGeometryXml(segments, bounds, scale, filled) {
1002
+ const unit = (value, origin) => Math.round((value - origin) * scale);
1003
+ const pt = (x, y) => `<a:pt x="${unit(x, bounds.x)}" y="${unit(y, bounds.y)}"/>`;
1004
+ const body = segments.map((segment) => {
1005
+ switch (segment.op) {
1006
+ case "M":
1007
+ return `<a:moveTo>${pt(segment.x, segment.y)}</a:moveTo>`;
1008
+ case "L":
1009
+ return `<a:lnTo>${pt(segment.x, segment.y)}</a:lnTo>`;
1010
+ case "C":
1011
+ return `<a:cubicBezTo>${pt(segment.x1, segment.y1)}${pt(segment.x2, segment.y2)}${pt(segment.x, segment.y)}</a:cubicBezTo>`;
1012
+ case "Q":
1013
+ return `<a:quadBezTo>${pt(segment.x1, segment.y1)}${pt(segment.x, segment.y)}</a:quadBezTo>`;
1014
+ case "Z":
1015
+ return "<a:close/>";
1016
+ }
1017
+ }).join("");
1018
+ const width = Math.max(1, Math.round(bounds.width * scale));
1019
+ const height = Math.max(1, Math.round(bounds.height * scale));
1020
+ return `<a:custGeom><a:avLst/><a:gdLst/><a:ahLst/><a:cxnLst/><a:rect l="0" t="0" r="r" b="b"/><a:pathLst><a:path w="${width}" h="${height}"${filled ? "" : ' fill="none"'}>${body}</a:path></a:pathLst></a:custGeom>`;
1021
+ }
1022
+
755
1023
  // src/pptx/layerText.ts
756
1024
  import { splitIconMarkers as splitIconMarkers2 } from "@bendyline/squisq/icon-marker";
757
1025
 
@@ -1131,7 +1399,8 @@ function buildLayerTextShape(layer, shapeId, maxBottom, listIndentPx, textScale
1131
1399
  const rect = toEmuRect(virtualRect);
1132
1400
  const align = style.textAlign === "center" ? "ctr" : style.textAlign === "right" ? "r" : "l";
1133
1401
  const inset = Math.max(0, Math.round(padding * EMU_PER_CANVAS_PX));
1134
- const bodyPr = `<a:bodyPr wrap="square" anchor="${vertical}" vertOverflow="clip" horzOverflow="clip" lIns="${inset}" rIns="${inset}" tIns="${inset}" bIns="${inset}"><a:noAutofit/></a:bodyPr>`;
1402
+ const unbreakable = !visibleBox && sourceText.trim() !== "" && sourceText.split("\n").every((line) => !/\s/u.test(line.trim()));
1403
+ const bodyPr = `<a:bodyPr wrap="${unbreakable ? "none" : "square"}" anchor="${vertical}" vertOverflow="clip" horzOverflow="${unbreakable ? "overflow" : "clip"}" lIns="${inset}" rIns="${inset}" tIns="${inset}" bIns="${inset}"><a:noAutofit/></a:bodyPr>`;
1135
1404
  const paragraphs = layer.content.paragraphs || blockGaps ? buildStructuredParagraphs(paragraphSource, renderedStyle, align, {
1136
1405
  // A list with no marker column of its own hangs its bullets by the
1137
1406
  // player's list indent.
@@ -1658,7 +1927,7 @@ function buildLayerSlideXml(block, ctx, plan) {
1658
1927
  }
1659
1928
  const textScale = isGeometryBoundTemplate(block.template) ? 1 : estimateSlideTextScale(layers);
1660
1929
  const units = mergeTextFlows(layers, textScale);
1661
- const textLayerY = units.filter(({ layer }) => layer.type === "text").map(({ layer }) => resolvePositionValue(layer.position.y, 1080));
1930
+ const textBoxes = units.filter(({ layer }) => layer.type === "text").map(({ layer }) => resolveLayerRect(layer.position, 1920, 1));
1662
1931
  for (const layer of layers) {
1663
1932
  if (layer.type !== "text") continue;
1664
1933
  for (const run of splitIconMarkers3(layer.content.text ?? "")) {
@@ -1668,8 +1937,11 @@ function buildLayerSlideXml(block, ctx, plan) {
1668
1937
  for (let index = 0; index < units.length; index++) {
1669
1938
  if ((index & 31) === 0) ctx.signal?.throwIfAborted();
1670
1939
  const { layer, listIndentPx } = units[index];
1671
- const currentY = layer.type === "text" ? resolvePositionValue(layer.position.y, 1080) : void 0;
1672
- const nextTextY = currentY === void 0 ? void 0 : textLayerY.filter((candidate) => candidate > currentY + 1).sort((a, b) => a - b)[0];
1940
+ const current = layer.type === "text" ? resolveLayerRect(layer.position, 1920, 1) : void 0;
1941
+ const currentY = current?.y;
1942
+ const nextTextY = current === void 0 ? void 0 : textBoxes.filter(
1943
+ (candidate) => candidate.y > current.y + 1 && candidate.x < current.x + current.width - TEXT_COLUMN_OVERLAP_PX && candidate.x + candidate.width > current.x + TEXT_COLUMN_OVERLAP_PX
1944
+ ).map((candidate) => candidate.y).sort((a, b) => a - b)[0];
1673
1945
  const textBottom = nextTextY === void 0 || currentY === void 0 ? void 0 : Math.max(currentY + 1, nextTextY - 8);
1674
1946
  shapes.push(
1675
1947
  ...layer.type === "text" ? [buildLayerTextShape(layer, ctx.allocShapeId(), textBottom, listIndentPx, textScale)] : convertLayerToShapes(layer, ctx, textBottom)
@@ -1694,10 +1966,24 @@ function convertLayerToShapes(layer, ctx, textBottom) {
1694
1966
  const data = svg ? void 0 : ctx.images?.get(layer.content.src);
1695
1967
  if (data) {
1696
1968
  const image = ctx.addImage(layer.content.src, data, layer.content.alt);
1697
- const rect = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
1969
+ const { rect, srcRect } = fitPicture(
1970
+ toEmuRect(resolveLayerRect(layer.position, 1920, 1080)),
1971
+ readImageDimensions(data),
1972
+ layer.content.fit
1973
+ );
1698
1974
  const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === image);
1699
1975
  return [
1700
- buildImageShape(embedded, ctx.allocShapeId(), rect.x, rect.y, rect.width, rect.height)
1976
+ buildImageShape(
1977
+ embedded,
1978
+ ctx.allocShapeId(),
1979
+ rect.x,
1980
+ rect.y,
1981
+ rect.width,
1982
+ rect.height,
1983
+ null,
1984
+ void 0,
1985
+ srcRect
1986
+ )
1701
1987
  ];
1702
1988
  }
1703
1989
  return [
@@ -1897,46 +2183,74 @@ function treeItemsToText(items, depth = 0) {
1897
2183
  }
1898
2184
  return lines.filter(Boolean).join("\n");
1899
2185
  }
2186
+ function pathLineXml(layer) {
2187
+ const { stroke, strokeWidth } = layer.content;
2188
+ if (stroke === "none" || stroke === "transparent" || strokeWidth === 0)
2189
+ return "<a:ln><a:noFill/></a:ln>";
2190
+ const color = parseCssColor(stroke, "FFFFFF");
2191
+ const width = Math.max(1, Math.round((strokeWidth ?? 2) * EMU_PER_CANVAS_PX));
2192
+ const head = markerXml("headEnd", layer.content.startMarker);
2193
+ const tail = markerXml("tailEnd", layer.content.endMarker);
2194
+ const dash = dashXml(layer.content.borderStyle, layer.content.dasharray);
2195
+ return `<a:ln w="${width}">${solidColorXml(color)}${dash}${head}${tail}</a:ln>`;
2196
+ }
2197
+ var TEXT_COLUMN_OVERLAP_PX = 8;
1900
2198
  function buildPathLayerShapes(layer, ctx) {
2199
+ let d = layer.content.d;
1901
2200
  if (layer.content.shapeKind) {
1902
- const shape = {
1903
- id: layer.id,
1904
- type: "shape",
1905
- position: layer.position,
1906
- content: {
1907
- shape: layer.content.shapeKind === "circle" ? "circle" : "rect",
1908
- fill: layer.content.fill,
1909
- fillOpacity: layer.content.fillOpacity,
1910
- gradient: layer.content.gradient,
1911
- stroke: layer.content.stroke,
1912
- strokeWidth: layer.content.strokeWidth,
1913
- borderStyle: layer.content.borderStyle
1914
- }
1915
- };
1916
- return [buildLayerShape(shape, ctx.allocShapeId())];
1917
- }
1918
- const numbers = layer.content.d.match(/-?\d+(?:\.\d+)?/g)?.map(Number);
1919
- if (!numbers || numbers.length < 4) return [];
1920
- const x1 = numbers[0];
1921
- const y1 = numbers[1];
1922
- const x2 = numbers[numbers.length - 2];
1923
- const y2 = numbers[numbers.length - 1];
2201
+ const box = resolveLayerRect(layer.position, 100, 100);
2202
+ const named = shapePath(layer.content.shapeKind, box.x, box.y, box.width, box.height);
2203
+ if (!named) {
2204
+ const shape = {
2205
+ id: layer.id,
2206
+ type: "shape",
2207
+ position: layer.position,
2208
+ content: {
2209
+ shape: layer.content.shapeKind === "circle" ? "circle" : "rect",
2210
+ fill: layer.content.fill,
2211
+ fillOpacity: layer.content.fillOpacity,
2212
+ gradient: layer.content.gradient,
2213
+ stroke: layer.content.stroke,
2214
+ strokeWidth: layer.content.strokeWidth,
2215
+ borderStyle: layer.content.borderStyle
2216
+ }
2217
+ };
2218
+ return [buildLayerShape(shape, ctx.allocShapeId())];
2219
+ }
2220
+ d = named;
2221
+ }
2222
+ const segments = parseSvgPath(d);
2223
+ const bounds = segments ? pathBounds(segments) : null;
2224
+ if (!segments || !bounds) return [];
2225
+ const { fill, gradient, fillOpacity } = layer.content;
2226
+ const filled = !!gradient || !!fill && fill !== "none" && fill !== "transparent";
2227
+ const name = `<p:nvSpPr><p:cNvPr id="${ctx.allocShapeId()}" name="${escapeXml(layer.id)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr>`;
2228
+ if (!filled && isStraightLine(segments)) {
2229
+ const points = segments.filter(
2230
+ (segment) => segment.op === "M" || segment.op === "L"
2231
+ );
2232
+ const from = points[0];
2233
+ const to = points[points.length - 1];
2234
+ const rect2 = toEmuRect({
2235
+ x: Math.min(from.x, to.x),
2236
+ y: Math.min(from.y, to.y),
2237
+ width: Math.max(1, Math.abs(to.x - from.x)),
2238
+ height: Math.max(1, Math.abs(to.y - from.y))
2239
+ });
2240
+ const flipH = to.x < from.x ? ' flipH="1"' : "";
2241
+ const flipV = to.y < from.y ? ' flipV="1"' : "";
2242
+ return [
2243
+ `<p:sp>${name}<p:spPr><a:xfrm${flipH}${flipV}><a:off x="${rect2.x}" y="${rect2.y}"/><a:ext cx="${rect2.width}" cy="${rect2.height}"/></a:xfrm><a:prstGeom prst="line"><a:avLst/></a:prstGeom><a:noFill/>` + pathLineXml(layer) + `</p:spPr></p:sp>`
2244
+ ];
2245
+ }
1924
2246
  const rect = toEmuRect({
1925
- x: Math.min(x1, x2),
1926
- y: Math.min(y1, y2),
1927
- width: Math.max(1, Math.abs(x2 - x1)),
1928
- height: Math.max(1, Math.abs(y2 - y1))
2247
+ x: bounds.x,
2248
+ y: bounds.y,
2249
+ width: Math.max(1, bounds.width),
2250
+ height: Math.max(1, bounds.height)
1929
2251
  });
1930
- const flipH = x2 < x1 ? ' flipH="1"' : "";
1931
- const flipV = y2 < y1 ? ' flipV="1"' : "";
1932
- const stroke = parseCssColor(layer.content.stroke, "FFFFFF");
1933
- const width = Math.max(1, Math.round((layer.content.strokeWidth ?? 2) * EMU_PER_CANVAS_PX));
1934
- const head = markerXml("headEnd", layer.content.startMarker);
1935
- const tail = markerXml("tailEnd", layer.content.endMarker);
1936
- const dash = dashXml(layer.content.borderStyle, layer.content.dasharray);
1937
- const shapeId = ctx.allocShapeId();
1938
2252
  return [
1939
- `<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>`
2253
+ `<p:sp>${name}<p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm>` + customGeometryXml(segments, bounds, EMU_PER_CANVAS_PX, filled) + (filled ? fillXml(fill, fillOpacity, gradient) : "<a:noFill/>") + pathLineXml(layer) + `</p:spPr></p:sp>`
1940
2254
  ];
1941
2255
  }
1942
2256
  function textLayerForPlaceholder(position, text, ctx) {
@@ -28,7 +28,7 @@ import {
28
28
 
29
29
  // src/epub/export.ts
30
30
  import JSZip from "jszip";
31
- import { resolveFontFamily } from "@bendyline/squisq/schemas";
31
+ import { contrastRatio, isHex, resolveFontFamily } from "@bendyline/squisq/schemas";
32
32
  import { resolveThemeForDoc } from "@bendyline/squisq/doc";
33
33
  import { isHtmlLineBreak, readFrontmatterThemeId, sanitizeUrl } from "@bendyline/squisq/markdown";
34
34
  async function markdownDocToEpub(authoredDoc, options = {}) {
@@ -669,14 +669,20 @@ ${navItems}
669
669
  </body>
670
670
  </html>`;
671
671
  }
672
+ function readsOnPaper(color) {
673
+ return !!color && isHex(color) && contrastRatio(color, "#ffffff") >= 4.5;
674
+ }
672
675
  function generateStylesheet(themeId, doc, registry, iconFonts = []) {
673
676
  let themeVars = "";
674
677
  if (themeId) {
675
678
  const theme = resolveThemeForDoc(doc, themeId, registry);
679
+ const ink = [theme.colors.primary, theme.colors.secondary, theme.colors.highlight].find(
680
+ (color) => readsOnPaper(color)
681
+ );
676
682
  themeVars = `
677
- --epub-bg: ${theme.colors.background};
678
- --epub-text: ${theme.colors.text};
679
- --epub-primary: ${theme.colors.primary};
683
+ --epub-bg: ${theme.colors.background};${readsOnPaper(theme.colors.text) ? `
684
+ --epub-text: ${theme.colors.text};` : ""}${ink ? `
685
+ --epub-primary: ${ink};` : ""}
680
686
  --epub-heading-font: ${resolveFontFamily(theme.typography.titleFont, "serif")};
681
687
  --epub-body-font: ${resolveFontFamily(theme.typography.bodyFont, "sans-serif")};`;
682
688
  }
@@ -4,7 +4,7 @@ import {
4
4
  docxToDoc,
5
5
  docxToMarkdownDoc,
6
6
  markdownDocToDocx
7
- } from "../chunk-MF4ZQQJ7.js";
7
+ } from "../chunk-BGQDPDCD.js";
8
8
  import "../chunk-DHAK4PII.js";
9
9
  import "../chunk-VCZUXQUL.js";
10
10
  import "../chunk-BZ5SHMOY.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  docToEpub,
3
3
  markdownDocToEpub
4
- } from "../chunk-ZJERBWTB.js";
4
+ } from "../chunk-WPLCWAN3.js";
5
5
  import "../chunk-WMIDXNGR.js";
6
6
  import "../chunk-VCZUXQUL.js";
7
7
  import "../chunk-BZ5SHMOY.js";
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  docToEpub,
11
11
  markdownDocToEpub
12
- } from "./chunk-ZJERBWTB.js";
12
+ } from "./chunk-WPLCWAN3.js";
13
13
  import {
14
14
  BUILTIN_FORMAT_IDS
15
15
  } from "./chunk-2P5HJAQL.js";
@@ -29,7 +29,7 @@ import {
29
29
  renderOutsideInDocument,
30
30
  withOutsideInMarkdownEditing,
31
31
  withOutsideInMetadata
32
- } from "./chunk-KZUP6TIV.js";
32
+ } from "./chunk-JB3ZVN7D.js";
33
33
  import {
34
34
  OUTSIDE_IN_FORMAT_IDS,
35
35
  OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
@@ -45,7 +45,7 @@ import {
45
45
  defaultRegistry,
46
46
  prepareConversion,
47
47
  resolveConversionLimits
48
- } from "./chunk-LSUMEIQ7.js";
48
+ } from "./chunk-2IG4BEAX.js";
49
49
  import {
50
50
  ConversionError
51
51
  } from "./chunk-KXOZMWBS.js";
@@ -54,13 +54,13 @@ import {
54
54
  docxToDoc,
55
55
  docxToMarkdownDoc,
56
56
  markdownDocToDocx
57
- } from "./chunk-MF4ZQQJ7.js";
57
+ } from "./chunk-BGQDPDCD.js";
58
58
  import {
59
59
  docToPptx,
60
60
  markdownDocToPptx,
61
61
  pptxToDoc,
62
62
  pptxToMarkdownDoc
63
- } from "./chunk-SWLDHEZI.js";
63
+ } from "./chunk-PIEDORQL.js";
64
64
  import "./chunk-6N2J7C2B.js";
65
65
  import "./chunk-WMIDXNGR.js";
66
66
  import "./chunk-DHAK4PII.js";
@@ -7,7 +7,7 @@ import {
7
7
  renderOutsideInDocument,
8
8
  withOutsideInMarkdownEditing,
9
9
  withOutsideInMetadata
10
- } from "../chunk-KZUP6TIV.js";
10
+ } from "../chunk-JB3ZVN7D.js";
11
11
  import {
12
12
  OUTSIDE_IN_FORMAT_IDS,
13
13
  OUTSIDE_IN_OUTPUT_TEMPLATE_KEY,
@@ -15,7 +15,7 @@ import {
15
15
  isOutsideInTargetPath,
16
16
  resolveOutsideInLayout
17
17
  } from "../chunk-WJTC5OD6.js";
18
- import "../chunk-LSUMEIQ7.js";
18
+ import "../chunk-2IG4BEAX.js";
19
19
  import "../chunk-KXOZMWBS.js";
20
20
  import "../chunk-7AWFHP5U.js";
21
21
  export {
@@ -4,7 +4,7 @@ import {
4
4
  pptxToContainer,
5
5
  pptxToDoc,
6
6
  pptxToMarkdownDoc
7
- } from "../chunk-SWLDHEZI.js";
7
+ } from "../chunk-PIEDORQL.js";
8
8
  import {
9
9
  analyzePptxLayouts,
10
10
  inspectPptxLayouts
@@ -9,7 +9,7 @@ import {
9
9
  defaultRegistry,
10
10
  prepareConversion,
11
11
  resolveConversionLimits
12
- } from "../chunk-LSUMEIQ7.js";
12
+ } from "../chunk-2IG4BEAX.js";
13
13
  import {
14
14
  ConversionError
15
15
  } from "../chunk-KXOZMWBS.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bendyline/squisq-formats",
3
- "version": "2.8.16",
3
+ "version": "2.8.17",
4
4
  "description": "Document format converters — DOCX, PDF, OOXML import/export",
5
5
  "license": "MIT",
6
6
  "author": "Bendyline",
@@ -135,7 +135,7 @@
135
135
  "typecheck": "tsc --noEmit"
136
136
  },
137
137
  "dependencies": {
138
- "@bendyline/squisq": "2.13.14",
138
+ "@bendyline/squisq": "2.13.15",
139
139
  "@pdf-lib/fontkit": "1.1.1",
140
140
  "@pdf-lib/upng": "1.0.1",
141
141
  "@xmldom/xmldom": "0.9.12",
File without changes