@bendyline/squisq-formats 2.8.15 → 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.
- package/README.md +1 -1
- package/THIRD_PARTY_LICENSES.txt +369 -0
- package/dist/{chunk-MF4ZQQJ7.js → chunk-BGQDPDCD.js} +12 -3
- package/dist/{chunk-BBB7FCK7.js → chunk-JB3ZVN7D.js} +1 -1
- package/dist/{chunk-NQNIFIDP.js → chunk-NNNHOTVM.js} +174 -53
- package/dist/{chunk-S5VXWSRR.js → chunk-PIEDORQL.js} +375 -55
- package/dist/{chunk-ZJERBWTB.js → chunk-WPLCWAN3.js} +10 -4
- package/dist/cjkFontData.generated-LJKMYRHP.js +16 -0
- package/dist/docx/index.js +1 -1
- package/dist/emojiFontData.generated-EPQOUGVZ.js +16 -0
- package/dist/epub/index.js +1 -1
- package/dist/hangulFontData.generated-CVLPZH2N.js +16 -0
- package/dist/index.js +6 -6
- package/dist/outside-in/index.js +2 -2
- package/dist/pdf/index.d.ts +37 -17
- package/dist/pdf/index.js +1 -1
- package/dist/pptx/index.js +1 -1
- package/dist/registry/index.js +1 -1
- package/package.json +6 -3
- /package/dist/{chunk-KHRX22VW.js → chunk-2IG4BEAX.js} +0 -0
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
|
|
71
71
|
import {
|
|
72
72
|
buildPreviewDoc,
|
|
73
|
+
coverRepeatsLeadingBlock,
|
|
73
74
|
createTemplateContext,
|
|
74
75
|
docToMarkdown,
|
|
75
76
|
expandCoverBlock,
|
|
@@ -78,7 +79,8 @@ import {
|
|
|
78
79
|
isGeometryBoundTemplate,
|
|
79
80
|
isTemplateBlock,
|
|
80
81
|
resolvePersistentLayers,
|
|
81
|
-
resolveThemeForDoc
|
|
82
|
+
resolveThemeForDoc,
|
|
83
|
+
shapePath
|
|
82
84
|
} from "@bendyline/squisq/doc";
|
|
83
85
|
import { splitIconMarkers as splitIconMarkers3 } from "@bendyline/squisq/icon-marker";
|
|
84
86
|
import {
|
|
@@ -89,11 +91,24 @@ import {
|
|
|
89
91
|
|
|
90
92
|
// src/pptx/pictureLayers.ts
|
|
91
93
|
import { svgSourceFromBytes } from "@bendyline/squisq/markdown";
|
|
92
|
-
function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight, hyperlinkRelId, svgRelId) {
|
|
94
|
+
function buildImageShape(img, shapeId, left, top, maxWidth, maxHeight, hyperlinkRelId, svgRelId, srcRect = "") {
|
|
93
95
|
const name = escapeXml(img.alt || "Picture");
|
|
94
96
|
const descr = img.alt ? ` descr="${escapeXml(img.alt)}"` : "";
|
|
95
97
|
const link = hyperlinkRelId ? `<a:hlinkClick r:id="${hyperlinkRelId}"/>` : "";
|
|
96
|
-
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
|
+
};
|
|
97
112
|
}
|
|
98
113
|
function fitCentered(area, width, height) {
|
|
99
114
|
const scale = Math.min(area.width / width, area.height / height);
|
|
@@ -448,9 +463,30 @@ function cssGradientToString(gradient) {
|
|
|
448
463
|
// src/pptx/drawingXml.ts
|
|
449
464
|
function officeTypeface(family, fallback) {
|
|
450
465
|
const resolved = resolveFontFamily(family, fallback);
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
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
|
+
]);
|
|
454
490
|
function solidColorXml(color) {
|
|
455
491
|
const alpha = color.alpha < 1e5 ? `<a:alpha val="${color.alpha}"/>` : "";
|
|
456
492
|
return `<a:solidFill><a:srgbClr val="${color.hex}">${alpha}</a:srgbClr></a:solidFill>`;
|
|
@@ -506,13 +542,17 @@ function patternFillXml(pattern, ground) {
|
|
|
506
542
|
function buildLayerShape(layer, shapeId) {
|
|
507
543
|
const rect = toEmuRect(resolveLayerRect(layer.position, 100, 100));
|
|
508
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/>";
|
|
509
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);
|
|
510
550
|
const stroke = lineXml(
|
|
511
551
|
layer.content.stroke ?? (layer.content.shape === "line" ? "#ffffff" : void 0),
|
|
512
552
|
layer.content.strokeWidth ?? (layer.content.shape === "line" ? 2 : void 0),
|
|
513
553
|
layer.content.borderStyle
|
|
514
554
|
);
|
|
515
|
-
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}"
|
|
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>`;
|
|
516
556
|
}
|
|
517
557
|
|
|
518
558
|
// src/pptx/backdrop.ts
|
|
@@ -751,6 +791,235 @@ function planDeckBackdrops(blocks) {
|
|
|
751
791
|
return { master: { base: masterBase, overlays: masterOverlays }, layouts, slides };
|
|
752
792
|
}
|
|
753
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
|
+
|
|
754
1023
|
// src/pptx/layerText.ts
|
|
755
1024
|
import { splitIconMarkers as splitIconMarkers2 } from "@bendyline/squisq/icon-marker";
|
|
756
1025
|
|
|
@@ -1130,7 +1399,8 @@ function buildLayerTextShape(layer, shapeId, maxBottom, listIndentPx, textScale
|
|
|
1130
1399
|
const rect = toEmuRect(virtualRect);
|
|
1131
1400
|
const align = style.textAlign === "center" ? "ctr" : style.textAlign === "right" ? "r" : "l";
|
|
1132
1401
|
const inset = Math.max(0, Math.round(padding * EMU_PER_CANVAS_PX));
|
|
1133
|
-
const
|
|
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>`;
|
|
1134
1404
|
const paragraphs = layer.content.paragraphs || blockGaps ? buildStructuredParagraphs(paragraphSource, renderedStyle, align, {
|
|
1135
1405
|
// A list with no marker column of its own hangs its bullets by the
|
|
1136
1406
|
// player's list indent.
|
|
@@ -1366,13 +1636,18 @@ async function docToPptx(doc, options = {}) {
|
|
|
1366
1636
|
Math.max(1, expanded.length + 1),
|
|
1367
1637
|
VIEWPORT_PRESETS.landscape
|
|
1368
1638
|
);
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1639
|
+
const coverLayers = expandCoverBlock(doc.startBlock, coverContext);
|
|
1640
|
+
if (coverRepeatsLeadingBlock(doc) && expanded[0] && expanded[0].id === doc.blocks[0]?.id) {
|
|
1641
|
+
expanded[0] = { ...expanded[0], layers: coverLayers };
|
|
1642
|
+
} else {
|
|
1643
|
+
slideBlocks.push({
|
|
1644
|
+
id: "cover-block",
|
|
1645
|
+
startTime: -1,
|
|
1646
|
+
duration: 0,
|
|
1647
|
+
audioSegment: -1,
|
|
1648
|
+
layers: coverLayers
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1376
1651
|
}
|
|
1377
1652
|
slideBlocks.push(...expanded);
|
|
1378
1653
|
if (slideBlocks.length === 0) {
|
|
@@ -1652,7 +1927,7 @@ function buildLayerSlideXml(block, ctx, plan) {
|
|
|
1652
1927
|
}
|
|
1653
1928
|
const textScale = isGeometryBoundTemplate(block.template) ? 1 : estimateSlideTextScale(layers);
|
|
1654
1929
|
const units = mergeTextFlows(layers, textScale);
|
|
1655
|
-
const
|
|
1930
|
+
const textBoxes = units.filter(({ layer }) => layer.type === "text").map(({ layer }) => resolveLayerRect(layer.position, 1920, 1));
|
|
1656
1931
|
for (const layer of layers) {
|
|
1657
1932
|
if (layer.type !== "text") continue;
|
|
1658
1933
|
for (const run of splitIconMarkers3(layer.content.text ?? "")) {
|
|
@@ -1662,8 +1937,11 @@ function buildLayerSlideXml(block, ctx, plan) {
|
|
|
1662
1937
|
for (let index = 0; index < units.length; index++) {
|
|
1663
1938
|
if ((index & 31) === 0) ctx.signal?.throwIfAborted();
|
|
1664
1939
|
const { layer, listIndentPx } = units[index];
|
|
1665
|
-
const
|
|
1666
|
-
const
|
|
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];
|
|
1667
1945
|
const textBottom = nextTextY === void 0 || currentY === void 0 ? void 0 : Math.max(currentY + 1, nextTextY - 8);
|
|
1668
1946
|
shapes.push(
|
|
1669
1947
|
...layer.type === "text" ? [buildLayerTextShape(layer, ctx.allocShapeId(), textBottom, listIndentPx, textScale)] : convertLayerToShapes(layer, ctx, textBottom)
|
|
@@ -1688,10 +1966,24 @@ function convertLayerToShapes(layer, ctx, textBottom) {
|
|
|
1688
1966
|
const data = svg ? void 0 : ctx.images?.get(layer.content.src);
|
|
1689
1967
|
if (data) {
|
|
1690
1968
|
const image = ctx.addImage(layer.content.src, data, layer.content.alt);
|
|
1691
|
-
const rect =
|
|
1969
|
+
const { rect, srcRect } = fitPicture(
|
|
1970
|
+
toEmuRect(resolveLayerRect(layer.position, 1920, 1080)),
|
|
1971
|
+
readImageDimensions(data),
|
|
1972
|
+
layer.content.fit
|
|
1973
|
+
);
|
|
1692
1974
|
const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === image);
|
|
1693
1975
|
return [
|
|
1694
|
-
buildImageShape(
|
|
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
|
+
)
|
|
1695
1987
|
];
|
|
1696
1988
|
}
|
|
1697
1989
|
return [
|
|
@@ -1891,46 +2183,74 @@ function treeItemsToText(items, depth = 0) {
|
|
|
1891
2183
|
}
|
|
1892
2184
|
return lines.filter(Boolean).join("\n");
|
|
1893
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;
|
|
1894
2198
|
function buildPathLayerShapes(layer, ctx) {
|
|
2199
|
+
let d = layer.content.d;
|
|
1895
2200
|
if (layer.content.shapeKind) {
|
|
1896
|
-
const
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
const
|
|
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
|
+
}
|
|
1918
2246
|
const rect = toEmuRect({
|
|
1919
|
-
x:
|
|
1920
|
-
y:
|
|
1921
|
-
width: Math.max(1,
|
|
1922
|
-
height: Math.max(1,
|
|
2247
|
+
x: bounds.x,
|
|
2248
|
+
y: bounds.y,
|
|
2249
|
+
width: Math.max(1, bounds.width),
|
|
2250
|
+
height: Math.max(1, bounds.height)
|
|
1923
2251
|
});
|
|
1924
|
-
const flipH = x2 < x1 ? ' flipH="1"' : "";
|
|
1925
|
-
const flipV = y2 < y1 ? ' flipV="1"' : "";
|
|
1926
|
-
const stroke = parseCssColor(layer.content.stroke, "FFFFFF");
|
|
1927
|
-
const width = Math.max(1, Math.round((layer.content.strokeWidth ?? 2) * EMU_PER_CANVAS_PX));
|
|
1928
|
-
const head = markerXml("headEnd", layer.content.startMarker);
|
|
1929
|
-
const tail = markerXml("tailEnd", layer.content.endMarker);
|
|
1930
|
-
const dash = dashXml(layer.content.borderStyle, layer.content.dasharray);
|
|
1931
|
-
const shapeId = ctx.allocShapeId();
|
|
1932
2252
|
return [
|
|
1933
|
-
`<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>`
|
|
1934
2254
|
];
|
|
1935
2255
|
}
|
|
1936
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: ${
|
|
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
|
}
|