@particle-academy/dark-slide 0.6.1 → 0.7.0
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/dist/index.cjs +886 -123
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +83 -2
- package/dist/index.d.ts +83 -2
- package/dist/index.js +886 -123
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -652,6 +652,13 @@ function deepText(node) {
|
|
|
652
652
|
var PptxReader = class {
|
|
653
653
|
constructor() {
|
|
654
654
|
this.currentSlideRels = {};
|
|
655
|
+
/**
|
|
656
|
+
* The mono typeface this deck was written with, read back from the theme's
|
|
657
|
+
* `<a:extLst>`. Empty when the package does not record one — anything not
|
|
658
|
+
* written by a current DarkSlide — in which case the name sniff below is the
|
|
659
|
+
* only signal available.
|
|
660
|
+
*/
|
|
661
|
+
this.monoTypeface = "";
|
|
655
662
|
this.parts = {};
|
|
656
663
|
}
|
|
657
664
|
/** Read a PPTX file's bytes into a Deck schema object. */
|
|
@@ -662,6 +669,21 @@ var PptxReader = class {
|
|
|
662
669
|
this.parts = unzipSync(bytes);
|
|
663
670
|
return this.extract();
|
|
664
671
|
}
|
|
672
|
+
/**
|
|
673
|
+
* The mono typeface recorded in `theme1.xml`'s `<a:extLst>`, or `""`.
|
|
674
|
+
*
|
|
675
|
+
* Deliberately a regex rather than a parse: this runs before the deck is
|
|
676
|
+
* built, the element is one attribute deep, and a theme part that does not
|
|
677
|
+
* carry the extension is the common case rather than an error.
|
|
678
|
+
*/
|
|
679
|
+
readMonoTypeface() {
|
|
680
|
+
const xml = this.getPart("ppt/theme/theme1.xml");
|
|
681
|
+
if (xml === false) {
|
|
682
|
+
return "";
|
|
683
|
+
}
|
|
684
|
+
const m = /<ds:monoFont[^>]*typeface="([^"]*)"/.exec(xml);
|
|
685
|
+
return m ? m[1].replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&") : "";
|
|
686
|
+
}
|
|
665
687
|
getPart(name) {
|
|
666
688
|
const p = this.parts[name];
|
|
667
689
|
return p === void 0 ? false : DECODER.decode(p);
|
|
@@ -673,6 +695,7 @@ var PptxReader = class {
|
|
|
673
695
|
theme: { name: "imported" },
|
|
674
696
|
slides: []
|
|
675
697
|
};
|
|
698
|
+
this.monoTypeface = this.readMonoTypeface();
|
|
676
699
|
const presentationRels = this.getPart("ppt/_rels/presentation.xml.rels");
|
|
677
700
|
if (presentationRels === false) {
|
|
678
701
|
return deck;
|
|
@@ -836,9 +859,9 @@ var PptxReader = class {
|
|
|
836
859
|
const solidFill = descendant(bgPr, "solidFill");
|
|
837
860
|
const solid = solidFill ? descendant(solidFill, "srgbClr") : void 0;
|
|
838
861
|
if (solid) {
|
|
839
|
-
const
|
|
840
|
-
if (
|
|
841
|
-
return { color: "#" +
|
|
862
|
+
const hex2 = at(solid, "val") ?? "";
|
|
863
|
+
if (hex2 !== "") {
|
|
864
|
+
return { color: "#" + hex2 };
|
|
842
865
|
}
|
|
843
866
|
}
|
|
844
867
|
const grad = descendant(bgPr, "gradFill");
|
|
@@ -874,11 +897,11 @@ var PptxReader = class {
|
|
|
874
897
|
if (!color) {
|
|
875
898
|
continue;
|
|
876
899
|
}
|
|
877
|
-
const
|
|
878
|
-
if (
|
|
900
|
+
const hex2 = at(color, "val") ?? "";
|
|
901
|
+
if (hex2 === "") {
|
|
879
902
|
continue;
|
|
880
903
|
}
|
|
881
|
-
stopStrings.push("#" +
|
|
904
|
+
stopStrings.push("#" + hex2.toLowerCase() + " " + numToStr(pct) + "%");
|
|
882
905
|
}
|
|
883
906
|
if (stopStrings.length === 0) {
|
|
884
907
|
return null;
|
|
@@ -1039,14 +1062,25 @@ var PptxReader = class {
|
|
|
1039
1062
|
if (rows.length === 0) {
|
|
1040
1063
|
return null;
|
|
1041
1064
|
}
|
|
1042
|
-
const
|
|
1065
|
+
const tblPr = descendant(tbl, "tblPr");
|
|
1066
|
+
const hasHeader = tblPr !== void 0 && (at(tblPr, "firstRow") ?? "0") === "1";
|
|
1067
|
+
const gridWidths = descendants(tbl, "gridCol").map((c) => parseInt(at(c, "w") ?? "0", 10) || 0);
|
|
1068
|
+
const gridTotal = gridWidths.reduce((a, b) => a + b, 0);
|
|
1069
|
+
const firstCells = descendants(rows[0], "tc");
|
|
1070
|
+
const columnCount = Math.max(firstCells.length, gridWidths.length);
|
|
1043
1071
|
const columns = [];
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1072
|
+
for (let i = 0; i < columnCount; i++) {
|
|
1073
|
+
const column = {
|
|
1074
|
+
key: "col" + (i + 1),
|
|
1075
|
+
label: hasHeader && firstCells[i] !== void 0 ? this.cellText(firstCells[i]) : ""
|
|
1076
|
+
};
|
|
1077
|
+
if (gridTotal > 0 && gridWidths[i] !== void 0) {
|
|
1078
|
+
column.width = gridWidths[i] / gridTotal;
|
|
1079
|
+
}
|
|
1080
|
+
columns.push(column);
|
|
1081
|
+
}
|
|
1048
1082
|
const bodyRows = [];
|
|
1049
|
-
for (let r = 1; r < rows.length; r++) {
|
|
1083
|
+
for (let r = hasHeader ? 1 : 0; r < rows.length; r++) {
|
|
1050
1084
|
const rowCells = descendants(rows[r], "tc");
|
|
1051
1085
|
const rowData = {};
|
|
1052
1086
|
columns.forEach((col, i) => {
|
|
@@ -1084,7 +1118,7 @@ var PptxReader = class {
|
|
|
1084
1118
|
if (runs.length === 0) {
|
|
1085
1119
|
return [isBullet ? "- " : "", isBullet];
|
|
1086
1120
|
}
|
|
1087
|
-
|
|
1121
|
+
let parsed = [];
|
|
1088
1122
|
let allBold = true;
|
|
1089
1123
|
let allItalic = true;
|
|
1090
1124
|
let anyNonEmpty = false;
|
|
@@ -1101,7 +1135,8 @@ var PptxReader = class {
|
|
|
1101
1135
|
const latin = el(rPr, "latin");
|
|
1102
1136
|
if (latin) {
|
|
1103
1137
|
const typeface = (at(latin, "typeface") ?? "").toLowerCase();
|
|
1104
|
-
|
|
1138
|
+
const recorded = this.monoTypeface.toLowerCase();
|
|
1139
|
+
if (recorded !== "" && typeface === recorded || typeface.includes("consola") || typeface.includes("mono") || typeface.includes("courier")) {
|
|
1105
1140
|
code = true;
|
|
1106
1141
|
}
|
|
1107
1142
|
}
|
|
@@ -1120,32 +1155,42 @@ var PptxReader = class {
|
|
|
1120
1155
|
if (!anyNonEmpty) {
|
|
1121
1156
|
return [isBullet ? "- " : "", isBullet];
|
|
1122
1157
|
}
|
|
1123
|
-
|
|
1158
|
+
const merged = [];
|
|
1159
|
+
for (const run of parsed) {
|
|
1160
|
+
const last = merged[merged.length - 1];
|
|
1161
|
+
if (last && last.b === run.b && last.i === run.i && last.code === run.code) {
|
|
1162
|
+
last.text += run.text;
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
merged.push({ ...run });
|
|
1166
|
+
}
|
|
1167
|
+
parsed = merged;
|
|
1168
|
+
let line2 = "";
|
|
1124
1169
|
let anyDecoration = false;
|
|
1125
1170
|
for (const run of parsed) {
|
|
1126
1171
|
const text = run.text;
|
|
1127
1172
|
const emitBold = run.b && !allBold;
|
|
1128
1173
|
const emitItalic = run.i && !allItalic;
|
|
1129
1174
|
if (run.code) {
|
|
1130
|
-
|
|
1175
|
+
line2 += "`" + text + "`";
|
|
1131
1176
|
anyDecoration = true;
|
|
1132
1177
|
} else if (emitBold && emitItalic) {
|
|
1133
|
-
|
|
1178
|
+
line2 += "***" + text + "***";
|
|
1134
1179
|
anyDecoration = true;
|
|
1135
1180
|
} else if (emitBold) {
|
|
1136
|
-
|
|
1181
|
+
line2 += "**" + text + "**";
|
|
1137
1182
|
anyDecoration = true;
|
|
1138
1183
|
} else if (emitItalic) {
|
|
1139
|
-
|
|
1184
|
+
line2 += "*" + text + "*";
|
|
1140
1185
|
anyDecoration = true;
|
|
1141
1186
|
} else {
|
|
1142
|
-
|
|
1187
|
+
line2 += text;
|
|
1143
1188
|
}
|
|
1144
1189
|
}
|
|
1145
1190
|
if (isBullet) {
|
|
1146
|
-
return ["- " +
|
|
1191
|
+
return ["- " + line2, true];
|
|
1147
1192
|
}
|
|
1148
|
-
return [
|
|
1193
|
+
return [line2, anyDecoration];
|
|
1149
1194
|
}
|
|
1150
1195
|
};
|
|
1151
1196
|
function randInt(min, max) {
|
|
@@ -1190,7 +1235,14 @@ function clone(v) {
|
|
|
1190
1235
|
// src/schema/schema.ts
|
|
1191
1236
|
var Schema = {
|
|
1192
1237
|
VERSION: "0.1.0",
|
|
1193
|
-
|
|
1238
|
+
/**
|
|
1239
|
+
* `kpiBand` and `metadataGrid` are COMPOSITES: they expand into a `table`
|
|
1240
|
+
* before the writer serialises anything, so they add no OOXML surface and
|
|
1241
|
+
* read back as the table they became. See table/composites.
|
|
1242
|
+
*/
|
|
1243
|
+
ELEMENT_TYPES: ["text", "image", "chart", "code", "table", "shape", "embed", "kpiBand", "metadataGrid"],
|
|
1244
|
+
/** The subset of ELEMENT_TYPES that is sugar over a `table`. */
|
|
1245
|
+
COMPOSITE_ELEMENT_TYPES: ["kpiBand", "metadataGrid"],
|
|
1194
1246
|
SLIDE_LAYOUTS: [
|
|
1195
1247
|
"blank",
|
|
1196
1248
|
"title",
|
|
@@ -1541,6 +1593,12 @@ var Validator = class {
|
|
|
1541
1593
|
errors.push(err(`${path}/code`, "string", gettype(element.code ?? null), element.code ?? null, "Code element must have a `code` string."));
|
|
1542
1594
|
}
|
|
1543
1595
|
break;
|
|
1596
|
+
case "kpiBand":
|
|
1597
|
+
case "metadataGrid":
|
|
1598
|
+
if (!Array.isArray(element.items) || element.items.length === 0) {
|
|
1599
|
+
errors.push(err(`${path}/items`, "non-empty array", gettype(element.items ?? null), element.items ?? null, `A \`${element.type}\` must have a non-empty \`items\` array.`));
|
|
1600
|
+
}
|
|
1601
|
+
break;
|
|
1544
1602
|
}
|
|
1545
1603
|
}
|
|
1546
1604
|
return errors;
|
|
@@ -1683,16 +1741,16 @@ var Color = {
|
|
|
1683
1741
|
let m = /^#([0-9a-fA-F]{3})$/.exec(c);
|
|
1684
1742
|
if (m) {
|
|
1685
1743
|
const h = m[1];
|
|
1686
|
-
const
|
|
1687
|
-
return [
|
|
1744
|
+
const hex2 = (h[0] + h[0] + h[1] + h[1] + h[2] + h[2]).toUpperCase();
|
|
1745
|
+
return [hex2, 1e5];
|
|
1688
1746
|
}
|
|
1689
1747
|
m = /^#([0-9a-fA-F]{6})$/.exec(c);
|
|
1690
1748
|
if (m) return [m[1].toUpperCase(), 1e5];
|
|
1691
1749
|
m = /^#([0-9a-fA-F]{8})$/.exec(c);
|
|
1692
1750
|
if (m) {
|
|
1693
|
-
const
|
|
1751
|
+
const hex2 = m[1].slice(0, 6).toUpperCase();
|
|
1694
1752
|
const a = parseInt(m[1].slice(6, 8), 16);
|
|
1695
|
-
return [
|
|
1753
|
+
return [hex2, Math.round(a / 255 * 1e5)];
|
|
1696
1754
|
}
|
|
1697
1755
|
m = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([0-9.]+)\s*)?\)$/i.exec(c);
|
|
1698
1756
|
if (m) {
|
|
@@ -1700,8 +1758,8 @@ var Color = {
|
|
|
1700
1758
|
const g = parseInt(m[2], 10);
|
|
1701
1759
|
const b = parseInt(m[3], 10);
|
|
1702
1760
|
const a = m[4] !== void 0 ? parseFloat(m[4]) : 1;
|
|
1703
|
-
const
|
|
1704
|
-
return [
|
|
1761
|
+
const hex2 = [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
|
|
1762
|
+
return [hex2, Math.round(a * 1e5)];
|
|
1705
1763
|
}
|
|
1706
1764
|
const named = NAMED[c.toLowerCase()];
|
|
1707
1765
|
if (named !== void 0) return [named, 1e5];
|
|
@@ -1775,15 +1833,15 @@ var MarkdownInline = {
|
|
|
1775
1833
|
return runs;
|
|
1776
1834
|
},
|
|
1777
1835
|
/** [isBullet, contentWithoutMarker]. */
|
|
1778
|
-
bulletPrefix(
|
|
1779
|
-
if (
|
|
1780
|
-
return [false,
|
|
1836
|
+
bulletPrefix(line2) {
|
|
1837
|
+
if (line2.startsWith("- ") || line2.startsWith("* ")) return [true, line2.slice(2)];
|
|
1838
|
+
return [false, line2];
|
|
1781
1839
|
},
|
|
1782
1840
|
/** [level (1..6, 0=none), contentWithoutMarker]. */
|
|
1783
|
-
headingPrefix(
|
|
1784
|
-
const m = /^(#{1,6})\s+(.*)$/.exec(
|
|
1841
|
+
headingPrefix(line2) {
|
|
1842
|
+
const m = /^(#{1,6})\s+(.*)$/.exec(line2);
|
|
1785
1843
|
if (m) return [m[1].length, m[2]];
|
|
1786
|
-
return [0,
|
|
1844
|
+
return [0, line2];
|
|
1787
1845
|
}
|
|
1788
1846
|
};
|
|
1789
1847
|
|
|
@@ -1995,8 +2053,610 @@ var Xml = {
|
|
|
1995
2053
|
}
|
|
1996
2054
|
};
|
|
1997
2055
|
|
|
2056
|
+
// src/table/composites.ts
|
|
2057
|
+
var COMPOSITE_TYPES = ["kpiBand", "metadataGrid"];
|
|
2058
|
+
var Composites = {
|
|
2059
|
+
TYPES: COMPOSITE_TYPES,
|
|
2060
|
+
isComposite(type) {
|
|
2061
|
+
return typeof type === "string" && COMPOSITE_TYPES.includes(type);
|
|
2062
|
+
},
|
|
2063
|
+
expand(element, theme = {}) {
|
|
2064
|
+
switch (element?.type) {
|
|
2065
|
+
case "kpiBand":
|
|
2066
|
+
return kpiBand(element, theme);
|
|
2067
|
+
case "metadataGrid":
|
|
2068
|
+
return metadataGrid(element, theme);
|
|
2069
|
+
default:
|
|
2070
|
+
return element;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
};
|
|
2074
|
+
function kpiBand(element, theme) {
|
|
2075
|
+
const items = itemsOf(element);
|
|
2076
|
+
const style = isPlainObject(element.style) ? element.style : {};
|
|
2077
|
+
const accent = themeColor(theme, "accent", "#8B5CF6");
|
|
2078
|
+
const fill2 = style.fill ?? null;
|
|
2079
|
+
const valueColor = style.valueColor ?? style.color ?? accent;
|
|
2080
|
+
const captionColor = style.captionColor ?? themeColor(theme, "muted", "#64748B");
|
|
2081
|
+
const valueSize = style.valueFontSize ?? 40;
|
|
2082
|
+
const captionSize = style.captionFontSize ?? 20;
|
|
2083
|
+
const align = style.align ?? "center";
|
|
2084
|
+
const columns = [];
|
|
2085
|
+
const values = {};
|
|
2086
|
+
const captions = {};
|
|
2087
|
+
items.forEach((item, i) => {
|
|
2088
|
+
const key = `k${i}`;
|
|
2089
|
+
columns.push({ key, label: "" });
|
|
2090
|
+
values[key] = String(item.value ?? "");
|
|
2091
|
+
captions[key] = String(item.caption ?? "");
|
|
2092
|
+
});
|
|
2093
|
+
const borders = style.borders ?? {
|
|
2094
|
+
inner: { width: 0.75, color: themeColor(theme, "muted", "#D9DEE4") },
|
|
2095
|
+
outer: { width: 0.75, color: themeColor(theme, "muted", "#D9DEE4") }
|
|
2096
|
+
};
|
|
2097
|
+
return {
|
|
2098
|
+
id: element.id ?? "kpi-band",
|
|
2099
|
+
type: "table",
|
|
2100
|
+
x: element.x ?? 0.06,
|
|
2101
|
+
y: element.y ?? 0.5,
|
|
2102
|
+
w: element.w ?? 0.88,
|
|
2103
|
+
h: element.h ?? 0.2,
|
|
2104
|
+
z: element.z ?? null,
|
|
2105
|
+
hidden: element.hidden ?? null,
|
|
2106
|
+
animation: element.animation ?? null,
|
|
2107
|
+
columns,
|
|
2108
|
+
rows: [
|
|
2109
|
+
{
|
|
2110
|
+
cells: values,
|
|
2111
|
+
height: style.valueHeight ?? 44,
|
|
2112
|
+
fontSize: valueSize,
|
|
2113
|
+
color: valueColor,
|
|
2114
|
+
bold: true,
|
|
2115
|
+
align,
|
|
2116
|
+
anchor: "bottom",
|
|
2117
|
+
borders: withoutSide(borders, "bottom")
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
cells: captions,
|
|
2121
|
+
height: style.captionHeight ?? 30,
|
|
2122
|
+
fontSize: captionSize,
|
|
2123
|
+
color: captionColor,
|
|
2124
|
+
align,
|
|
2125
|
+
anchor: "top",
|
|
2126
|
+
borders: withoutSide(borders, "top")
|
|
2127
|
+
}
|
|
2128
|
+
],
|
|
2129
|
+
style: {
|
|
2130
|
+
header: false,
|
|
2131
|
+
stripe: false,
|
|
2132
|
+
fill: fill2,
|
|
2133
|
+
padding: style.padding ?? { left: 8, right: 8, top: 2, bottom: 2 }
|
|
2134
|
+
}
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function metadataGrid(element, theme) {
|
|
2138
|
+
const items = itemsOf(element);
|
|
2139
|
+
const style = isPlainObject(element.style) ? element.style : {};
|
|
2140
|
+
const across = isNumeric(element.columns) ? Math.max(1, Math.trunc(Number(element.columns))) : 3;
|
|
2141
|
+
const labelColor = style.labelColor ?? themeColor(theme, "muted", "#64748B");
|
|
2142
|
+
const valueColor = style.valueColor ?? themeColor(theme, "text", "#0F172A");
|
|
2143
|
+
const fill2 = style.fill ?? null;
|
|
2144
|
+
const columns = [];
|
|
2145
|
+
for (let i = 0; i < across; i++) columns.push({ key: `c${i}`, label: "" });
|
|
2146
|
+
const rows = [];
|
|
2147
|
+
for (let start = 0; start < items.length; start += across) {
|
|
2148
|
+
const chunk = items.slice(start, start + across);
|
|
2149
|
+
const labels = {};
|
|
2150
|
+
const values = {};
|
|
2151
|
+
for (let i = 0; i < across; i++) {
|
|
2152
|
+
const item = isPlainObject(chunk[i]) ? chunk[i] : {};
|
|
2153
|
+
labels[`c${i}`] = String(item.label ?? "");
|
|
2154
|
+
values[`c${i}`] = String(item.value ?? "");
|
|
2155
|
+
}
|
|
2156
|
+
rows.push({
|
|
2157
|
+
cells: labels,
|
|
2158
|
+
height: style.labelHeight ?? 18,
|
|
2159
|
+
fontSize: style.labelFontSize ?? 18,
|
|
2160
|
+
color: labelColor,
|
|
2161
|
+
letterSpacing: style.labelLetterSpacing ?? 1.2,
|
|
2162
|
+
caps: "small",
|
|
2163
|
+
bold: true,
|
|
2164
|
+
anchor: "bottom"
|
|
2165
|
+
});
|
|
2166
|
+
rows.push({
|
|
2167
|
+
cells: values,
|
|
2168
|
+
height: style.valueHeight ?? 26,
|
|
2169
|
+
fontSize: style.valueFontSize ?? 28,
|
|
2170
|
+
color: valueColor,
|
|
2171
|
+
bold: true,
|
|
2172
|
+
anchor: "top"
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
return {
|
|
2176
|
+
id: element.id ?? "metadata-grid",
|
|
2177
|
+
type: "table",
|
|
2178
|
+
x: element.x ?? 0.06,
|
|
2179
|
+
y: element.y ?? 0.5,
|
|
2180
|
+
w: element.w ?? 0.88,
|
|
2181
|
+
h: element.h ?? 0.22,
|
|
2182
|
+
z: element.z ?? null,
|
|
2183
|
+
hidden: element.hidden ?? null,
|
|
2184
|
+
animation: element.animation ?? null,
|
|
2185
|
+
columns,
|
|
2186
|
+
rows,
|
|
2187
|
+
style: {
|
|
2188
|
+
header: false,
|
|
2189
|
+
stripe: false,
|
|
2190
|
+
borders: style.borders ?? false,
|
|
2191
|
+
fill: fill2,
|
|
2192
|
+
padding: style.padding ?? { left: 10, right: 10, top: 2, bottom: 2 }
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
function withoutSide(borders, side) {
|
|
2197
|
+
if (!isPlainObject(borders)) return borders;
|
|
2198
|
+
return { ...borders, [side]: false };
|
|
2199
|
+
}
|
|
2200
|
+
function itemsOf(element) {
|
|
2201
|
+
const items = Array.isArray(element?.items) ? element.items : [];
|
|
2202
|
+
return items.filter((i) => isPlainObject(i));
|
|
2203
|
+
}
|
|
2204
|
+
function themeColor(theme, key, fallback) {
|
|
2205
|
+
const colors = isPlainObject(theme?.colors) ? theme.colors : {};
|
|
2206
|
+
const value = colors[key];
|
|
2207
|
+
return typeof value === "string" && value !== "" ? value : fallback;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/table/table-resolver.ts
|
|
2211
|
+
var DEFAULT_FONT_SIZE = 28;
|
|
2212
|
+
var DEFAULT_BODY_COLOR = "#0F172A";
|
|
2213
|
+
var DEFAULT_HEADER_COLOR = "#FFFFFF";
|
|
2214
|
+
var DEFAULT_ACCENT = "#8B5CF6";
|
|
2215
|
+
var DEFAULT_STRIPE_FILL = "#F8FAFC";
|
|
2216
|
+
var DEFAULT_BORDER_COLOR = "#D9DEE4";
|
|
2217
|
+
var DEFAULT_BORDER_WIDTH = 0.75;
|
|
2218
|
+
var DEFAULT_PADDING_X = 7.2;
|
|
2219
|
+
var DEFAULT_PADDING_Y = 3.6;
|
|
2220
|
+
var DEFAULT_HEADER_HEIGHT = 40;
|
|
2221
|
+
var DEFAULT_BODY_HEIGHT = 30;
|
|
2222
|
+
var STYLE_KEYS = [
|
|
2223
|
+
"fill",
|
|
2224
|
+
"color",
|
|
2225
|
+
"bold",
|
|
2226
|
+
"italic",
|
|
2227
|
+
"underline",
|
|
2228
|
+
"align",
|
|
2229
|
+
"anchor",
|
|
2230
|
+
"fontSize",
|
|
2231
|
+
"letterSpacing",
|
|
2232
|
+
"caps",
|
|
2233
|
+
"fontFamily",
|
|
2234
|
+
"padding",
|
|
2235
|
+
"borders"
|
|
2236
|
+
];
|
|
2237
|
+
var CELL_SPEC_KEYS = [
|
|
2238
|
+
"text",
|
|
2239
|
+
"colSpan",
|
|
2240
|
+
"rowSpan",
|
|
2241
|
+
"fill",
|
|
2242
|
+
"color",
|
|
2243
|
+
"bold",
|
|
2244
|
+
"italic",
|
|
2245
|
+
"underline",
|
|
2246
|
+
"align",
|
|
2247
|
+
"anchor",
|
|
2248
|
+
"fontSize",
|
|
2249
|
+
"letterSpacing",
|
|
2250
|
+
"caps",
|
|
2251
|
+
"fontFamily",
|
|
2252
|
+
"padding",
|
|
2253
|
+
"borders"
|
|
2254
|
+
];
|
|
2255
|
+
var TableResolver = {
|
|
2256
|
+
DEFAULT_FONT_SIZE,
|
|
2257
|
+
DEFAULT_BORDER_WIDTH,
|
|
2258
|
+
DEFAULT_BORDER_COLOR,
|
|
2259
|
+
resolve(element, theme = {}) {
|
|
2260
|
+
const columns = normalizeColumns(Array.isArray(element?.columns) ? element.columns : []);
|
|
2261
|
+
const rawRows = Array.isArray(element?.rows) ? element.rows : [];
|
|
2262
|
+
const style = isPlainObject(element?.style) ? element.style : {};
|
|
2263
|
+
const accent = themeColor2(theme, "accent", DEFAULT_ACCENT);
|
|
2264
|
+
const headerSpec = style.header ?? [];
|
|
2265
|
+
const hasHeader = headerSpec !== false;
|
|
2266
|
+
const headerStyle = isPlainObject(headerSpec) ? headerSpec : {};
|
|
2267
|
+
const bodyStyle = isPlainObject(style.body) ? style.body : {};
|
|
2268
|
+
const stripeSpec = style.stripe ?? [];
|
|
2269
|
+
const stripeOn = stripeSpec !== false;
|
|
2270
|
+
const stripeStyle = isPlainObject(stripeSpec) ? stripeSpec : {};
|
|
2271
|
+
const tableDefaults = {
|
|
2272
|
+
color: DEFAULT_BODY_COLOR,
|
|
2273
|
+
align: "left",
|
|
2274
|
+
anchor: "middle",
|
|
2275
|
+
fontSize: DEFAULT_FONT_SIZE
|
|
2276
|
+
};
|
|
2277
|
+
const tableStyle = styleKeys(style);
|
|
2278
|
+
const grid = buildGrid(columns, rawRows, hasHeader);
|
|
2279
|
+
const rows = [];
|
|
2280
|
+
const rowCount = grid.length;
|
|
2281
|
+
grid.forEach((gridRow, r) => {
|
|
2282
|
+
const isHeader = hasHeader && r === 0;
|
|
2283
|
+
const rowSource = gridRow.source;
|
|
2284
|
+
const rowStyle = styleKeys(rowSource);
|
|
2285
|
+
let bandStyle = isHeader ? { fill: accent, color: DEFAULT_HEADER_COLOR, bold: true, ...headerStyle } : bodyStyle;
|
|
2286
|
+
if (!isHeader && stripeOn) {
|
|
2287
|
+
const bodyIndex = hasHeader ? r - 1 : r;
|
|
2288
|
+
if (bodyIndex % 2 === 1) {
|
|
2289
|
+
bandStyle = { ...bandStyle, fill: DEFAULT_STRIPE_FILL, ...stripeStyle };
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const cells = gridRow.cells.map(
|
|
2293
|
+
(slot, c) => resolveCell(
|
|
2294
|
+
slot,
|
|
2295
|
+
[tableDefaults, tableStyle, bandStyle, styleKeys(columns[c]), rowStyle],
|
|
2296
|
+
{
|
|
2297
|
+
firstRow: r === 0,
|
|
2298
|
+
lastRow: r === rowCount - 1,
|
|
2299
|
+
firstCol: c === 0,
|
|
2300
|
+
lastCol: c === columns.length - 1
|
|
2301
|
+
}
|
|
2302
|
+
)
|
|
2303
|
+
);
|
|
2304
|
+
rows.push({
|
|
2305
|
+
header: isHeader,
|
|
2306
|
+
height: rowHeight(rowSource, bandStyle, tableStyle, isHeader),
|
|
2307
|
+
cells
|
|
2308
|
+
});
|
|
2309
|
+
});
|
|
2310
|
+
return { columns, rows, hasHeader };
|
|
2311
|
+
},
|
|
2312
|
+
normalizeColumns,
|
|
2313
|
+
columnWidthsEmu
|
|
2314
|
+
};
|
|
2315
|
+
function normalizeColumns(raw) {
|
|
2316
|
+
const columns = raw.map((col, i) => {
|
|
2317
|
+
const c = isPlainObject(col) ? col : { key: String(col) };
|
|
2318
|
+
return {
|
|
2319
|
+
key: String(c.key ?? `col${i}`),
|
|
2320
|
+
label: String(c.label ?? c.key ?? ""),
|
|
2321
|
+
width: isNumeric(c.width) && Number(c.width) > 0 ? Number(c.width) : null,
|
|
2322
|
+
align: c.align ?? null,
|
|
2323
|
+
anchor: c.anchor ?? null,
|
|
2324
|
+
widthFrac: 0
|
|
2325
|
+
};
|
|
2326
|
+
});
|
|
2327
|
+
const n = columns.length;
|
|
2328
|
+
if (n === 0) return [];
|
|
2329
|
+
const declared = columns.map((c) => c.width).filter((w) => w !== null);
|
|
2330
|
+
if (declared.length === 0) {
|
|
2331
|
+
columns.forEach((c) => c.widthFrac = 1 / n);
|
|
2332
|
+
return columns;
|
|
2333
|
+
}
|
|
2334
|
+
const asFractions = Math.max(...declared) <= 1;
|
|
2335
|
+
const undeclared = n - declared.length;
|
|
2336
|
+
let weights;
|
|
2337
|
+
if (asFractions) {
|
|
2338
|
+
const remaining = Math.max(0, 1 - declared.reduce((a, b) => a + b, 0));
|
|
2339
|
+
const share = undeclared > 0 ? remaining / undeclared : 0;
|
|
2340
|
+
weights = columns.map((c) => c.width ?? share);
|
|
2341
|
+
} else {
|
|
2342
|
+
weights = columns.map((c) => c.width ?? 1);
|
|
2343
|
+
}
|
|
2344
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
2345
|
+
columns.forEach((c, i) => c.widthFrac = total > 0 ? weights[i] / total : 1 / n);
|
|
2346
|
+
return columns;
|
|
2347
|
+
}
|
|
2348
|
+
function columnWidthsEmu(columns, totalEmu) {
|
|
2349
|
+
const out = [];
|
|
2350
|
+
let cum = 0;
|
|
2351
|
+
let prev = 0;
|
|
2352
|
+
for (const col of columns) {
|
|
2353
|
+
cum += col.widthFrac;
|
|
2354
|
+
const edge = Math.round(cum * totalEmu);
|
|
2355
|
+
out.push(edge - prev);
|
|
2356
|
+
prev = edge;
|
|
2357
|
+
}
|
|
2358
|
+
return out;
|
|
2359
|
+
}
|
|
2360
|
+
function buildGrid(columns, rawRows, hasHeader) {
|
|
2361
|
+
const n = columns.length;
|
|
2362
|
+
const grid = [];
|
|
2363
|
+
if (hasHeader) {
|
|
2364
|
+
grid.push({
|
|
2365
|
+
source: {},
|
|
2366
|
+
cells: columns.map((col) => ({ spec: { text: col.label }, merged: "none", colSpan: 1, rowSpan: 1 }))
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
for (const row of rawRows) {
|
|
2370
|
+
if (!isPlainObject(row)) continue;
|
|
2371
|
+
const cellMap = isPlainObject(row.cells) ? row.cells : row;
|
|
2372
|
+
grid.push({
|
|
2373
|
+
source: row,
|
|
2374
|
+
cells: columns.map((col) => ({
|
|
2375
|
+
spec: cellSpec(cellMap[col.key]),
|
|
2376
|
+
merged: "none",
|
|
2377
|
+
colSpan: 1,
|
|
2378
|
+
rowSpan: 1
|
|
2379
|
+
}))
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
for (let r = 0; r < grid.length; r++) {
|
|
2383
|
+
for (let c = 0; c < n; c++) {
|
|
2384
|
+
if (grid[r].cells[c].merged !== "none") continue;
|
|
2385
|
+
const spec = grid[r].cells[c].spec;
|
|
2386
|
+
const colSpan = clampSpan(spec.colSpan, n - c);
|
|
2387
|
+
const rowSpan = clampSpan(spec.rowSpan, grid.length - r);
|
|
2388
|
+
grid[r].cells[c].colSpan = colSpan;
|
|
2389
|
+
grid[r].cells[c].rowSpan = rowSpan;
|
|
2390
|
+
for (let dr = 0; dr < rowSpan; dr++) {
|
|
2391
|
+
for (let dc = 0; dc < colSpan; dc++) {
|
|
2392
|
+
if (dr === 0 && dc === 0) continue;
|
|
2393
|
+
const covered = dc > 0 && dr > 0 ? "both" : dc > 0 ? "horizontal" : "vertical";
|
|
2394
|
+
grid[r + dr].cells[c + dc].merged = covered;
|
|
2395
|
+
grid[r + dr].cells[c + dc].spec = { text: "" };
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
return grid;
|
|
2401
|
+
}
|
|
2402
|
+
function clampSpan(span, available) {
|
|
2403
|
+
const s = isNumeric(span) ? Math.trunc(Number(span)) : 1;
|
|
2404
|
+
return Math.max(1, Math.min(s, Math.max(1, available)));
|
|
2405
|
+
}
|
|
2406
|
+
function cellSpec(value) {
|
|
2407
|
+
if (isPlainObject(value)) {
|
|
2408
|
+
if (CELL_SPEC_KEYS.some((k) => k in value)) {
|
|
2409
|
+
return { ...value, text: scalarText(value.text ?? "") };
|
|
2410
|
+
}
|
|
2411
|
+
return { text: JSON.stringify(value) };
|
|
2412
|
+
}
|
|
2413
|
+
if (Array.isArray(value)) return { text: JSON.stringify(value) };
|
|
2414
|
+
return { text: scalarText(value) };
|
|
2415
|
+
}
|
|
2416
|
+
function scalarText(value) {
|
|
2417
|
+
if (value === null || value === void 0) return "";
|
|
2418
|
+
if (typeof value === "boolean") return value ? "1" : "";
|
|
2419
|
+
if (typeof value === "number" || typeof value === "string") return String(value);
|
|
2420
|
+
return JSON.stringify(value);
|
|
2421
|
+
}
|
|
2422
|
+
function resolveCell(slot, chain, edges) {
|
|
2423
|
+
const spec = slot.spec;
|
|
2424
|
+
const layers = [...chain, styleKeys(spec)];
|
|
2425
|
+
const resolved = {};
|
|
2426
|
+
for (const layer of layers) {
|
|
2427
|
+
for (const k of Object.keys(layer)) resolved[k] = layer[k];
|
|
2428
|
+
}
|
|
2429
|
+
const fill2 = resolved.fill ?? null;
|
|
2430
|
+
return {
|
|
2431
|
+
text: String(spec.text ?? ""),
|
|
2432
|
+
bold: Boolean(resolved.bold ?? false),
|
|
2433
|
+
italic: Boolean(resolved.italic ?? false),
|
|
2434
|
+
underline: Boolean(resolved.underline ?? false),
|
|
2435
|
+
color: hex(resolved.color ?? DEFAULT_BODY_COLOR, "0F172A"),
|
|
2436
|
+
fill: fill2 === false || fill2 === null || fill2 === "none" ? null : hex(fill2, "FFFFFF"),
|
|
2437
|
+
align: alignOf(resolved.align ?? "left"),
|
|
2438
|
+
anchor: anchorOf(resolved.anchor ?? "middle"),
|
|
2439
|
+
fontSize: Math.max(1, Number(resolved.fontSize ?? DEFAULT_FONT_SIZE) / 2),
|
|
2440
|
+
letterSpacing: Number(resolved.letterSpacing ?? 0),
|
|
2441
|
+
caps: capsOf(resolved.caps ?? "none"),
|
|
2442
|
+
fontFamily: resolved.fontFamily !== void 0 && resolved.fontFamily !== null ? String(resolved.fontFamily) : null,
|
|
2443
|
+
padding: resolvePadding(resolved.padding ?? null),
|
|
2444
|
+
borders: resolveBorders(layers, edges),
|
|
2445
|
+
colSpan: slot.colSpan,
|
|
2446
|
+
rowSpan: slot.rowSpan,
|
|
2447
|
+
merged: slot.merged
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
function resolveBorders(layers, edges) {
|
|
2451
|
+
const sides = [
|
|
2452
|
+
["left", "firstCol"],
|
|
2453
|
+
["right", "lastCol"],
|
|
2454
|
+
["top", "firstRow"],
|
|
2455
|
+
["bottom", "lastRow"]
|
|
2456
|
+
];
|
|
2457
|
+
const out = {};
|
|
2458
|
+
for (const [side, edgeKey] of sides) {
|
|
2459
|
+
const isOuter = edges[edgeKey];
|
|
2460
|
+
let value = { width: DEFAULT_BORDER_WIDTH, color: DEFAULT_BORDER_COLOR };
|
|
2461
|
+
for (const layer of layers) {
|
|
2462
|
+
if (!("borders" in layer)) continue;
|
|
2463
|
+
const spec = layer.borders;
|
|
2464
|
+
if (spec === false || spec === null || spec === "none") {
|
|
2465
|
+
value = null;
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
if (!isPlainObject(spec)) continue;
|
|
2469
|
+
if (spec.none) {
|
|
2470
|
+
value = null;
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2473
|
+
if (spec.width !== void 0 || spec.color !== void 0 || spec.style !== void 0) {
|
|
2474
|
+
value = spec;
|
|
2475
|
+
}
|
|
2476
|
+
if ("all" in spec) value = spec.all;
|
|
2477
|
+
const band = isOuter ? "outer" : "inner";
|
|
2478
|
+
if (band in spec) value = spec[band];
|
|
2479
|
+
if (side in spec) value = spec[side];
|
|
2480
|
+
}
|
|
2481
|
+
out[side] = borderSide(value);
|
|
2482
|
+
}
|
|
2483
|
+
return out;
|
|
2484
|
+
}
|
|
2485
|
+
function borderSide(value) {
|
|
2486
|
+
if (value === false || value === null || value === void 0 || value === "none") return null;
|
|
2487
|
+
if (!isPlainObject(value)) return null;
|
|
2488
|
+
const width = isNumeric(value.width) ? Number(value.width) : DEFAULT_BORDER_WIDTH;
|
|
2489
|
+
if (width <= 0) return null;
|
|
2490
|
+
const style = String(value.style ?? "solid");
|
|
2491
|
+
return {
|
|
2492
|
+
width,
|
|
2493
|
+
color: hex(value.color ?? DEFAULT_BORDER_COLOR, "D9DEE4"),
|
|
2494
|
+
style: ["solid", "dash", "dot"].includes(style) ? style : "solid"
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
function resolvePadding(padding) {
|
|
2498
|
+
const out = {
|
|
2499
|
+
left: DEFAULT_PADDING_X,
|
|
2500
|
+
right: DEFAULT_PADDING_X,
|
|
2501
|
+
top: DEFAULT_PADDING_Y,
|
|
2502
|
+
bottom: DEFAULT_PADDING_Y
|
|
2503
|
+
};
|
|
2504
|
+
if (isNumeric(padding)) {
|
|
2505
|
+
const v = Number(padding);
|
|
2506
|
+
return { left: v, right: v, top: v, bottom: v };
|
|
2507
|
+
}
|
|
2508
|
+
if (isPlainObject(padding)) {
|
|
2509
|
+
for (const side of ["left", "right", "top", "bottom"]) {
|
|
2510
|
+
if (isNumeric(padding[side])) out[side] = Number(padding[side]);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return out;
|
|
2514
|
+
}
|
|
2515
|
+
function styleKeys(source) {
|
|
2516
|
+
const out = {};
|
|
2517
|
+
if (!isPlainObject(source)) return out;
|
|
2518
|
+
for (const k of STYLE_KEYS) {
|
|
2519
|
+
if (k in source && source[k] !== null && source[k] !== void 0) out[k] = source[k];
|
|
2520
|
+
}
|
|
2521
|
+
return out;
|
|
2522
|
+
}
|
|
2523
|
+
function rowHeight(rowSource, bandStyle, tableStyle, isHeader) {
|
|
2524
|
+
for (const candidate of [rowSource?.height, bandStyle?.height, tableStyle?.rowHeight]) {
|
|
2525
|
+
if (isNumeric(candidate)) return Number(candidate);
|
|
2526
|
+
}
|
|
2527
|
+
return isHeader ? DEFAULT_HEADER_HEIGHT : DEFAULT_BODY_HEIGHT;
|
|
2528
|
+
}
|
|
2529
|
+
function themeColor2(theme, key, fallback) {
|
|
2530
|
+
const colors = isPlainObject(theme?.colors) ? theme.colors : {};
|
|
2531
|
+
const value = colors[key];
|
|
2532
|
+
return typeof value === "string" && value !== "" ? value : fallback;
|
|
2533
|
+
}
|
|
2534
|
+
function hex(value, fallback) {
|
|
2535
|
+
return Color.parse(typeof value === "string" ? value : null, fallback)[0];
|
|
2536
|
+
}
|
|
2537
|
+
function alignOf(value) {
|
|
2538
|
+
switch (String(value)) {
|
|
2539
|
+
case "center":
|
|
2540
|
+
case "centre":
|
|
2541
|
+
return "center";
|
|
2542
|
+
case "right":
|
|
2543
|
+
return "right";
|
|
2544
|
+
case "justify":
|
|
2545
|
+
return "justify";
|
|
2546
|
+
default:
|
|
2547
|
+
return "left";
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
function anchorOf(value) {
|
|
2551
|
+
switch (String(value)) {
|
|
2552
|
+
case "top":
|
|
2553
|
+
return "top";
|
|
2554
|
+
case "bottom":
|
|
2555
|
+
return "bottom";
|
|
2556
|
+
default:
|
|
2557
|
+
return "middle";
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function capsOf(value) {
|
|
2561
|
+
switch (String(value)) {
|
|
2562
|
+
case "small":
|
|
2563
|
+
return "small";
|
|
2564
|
+
case "all":
|
|
2565
|
+
case "upper":
|
|
2566
|
+
return "all";
|
|
2567
|
+
default:
|
|
2568
|
+
return "none";
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// src/text/box-decoration.ts
|
|
2573
|
+
var ACCENT_GUTTER_PT = 8;
|
|
2574
|
+
var BoxDecoration = {
|
|
2575
|
+
ACCENT_GUTTER_PT,
|
|
2576
|
+
/** The `<p:spPr>` interior: geometry, fill and line, in schema order. */
|
|
2577
|
+
spPr(style, widthEmu, heightEmu) {
|
|
2578
|
+
return geometry(style, widthEmu, heightEmu) + fill(style, widthEmu) + line(style);
|
|
2579
|
+
},
|
|
2580
|
+
hasDecoration(style) {
|
|
2581
|
+
return style?.fill !== void 0 || style?.accentBar !== void 0 || style?.border !== void 0 || style?.radius !== void 0;
|
|
2582
|
+
},
|
|
2583
|
+
/**
|
|
2584
|
+
* `lIns`/`tIns`/`rIns`/`bIns` for the text body, or an empty string when the
|
|
2585
|
+
* element says nothing — decks that predate this keep their bytes.
|
|
2586
|
+
*
|
|
2587
|
+
* An accent bar with no explicit padding gets a left inset wide enough to
|
|
2588
|
+
* clear it, because text printed on top of the bar is the obvious way for
|
|
2589
|
+
* this feature to look broken.
|
|
2590
|
+
*/
|
|
2591
|
+
bodyInsets(style) {
|
|
2592
|
+
const padding = style?.padding ?? null;
|
|
2593
|
+
const bar = isPlainObject(style?.accentBar) ? style.accentBar : null;
|
|
2594
|
+
if ((padding === null || padding === void 0) && bar === null) return "";
|
|
2595
|
+
const sides = { left: 7.2, right: 7.2, top: 3.6, bottom: 3.6 };
|
|
2596
|
+
if (bar !== null && (bar.side ?? "left") !== "right") {
|
|
2597
|
+
sides.left = Number(bar.width ?? 4) + ACCENT_GUTTER_PT;
|
|
2598
|
+
}
|
|
2599
|
+
if (bar !== null && (bar.side ?? "left") === "right") {
|
|
2600
|
+
sides.right = Number(bar.width ?? 4) + ACCENT_GUTTER_PT;
|
|
2601
|
+
}
|
|
2602
|
+
if (isNumeric(padding)) {
|
|
2603
|
+
for (const k of Object.keys(sides)) sides[k] = Number(padding);
|
|
2604
|
+
} else if (isPlainObject(padding)) {
|
|
2605
|
+
for (const k of Object.keys(sides)) {
|
|
2606
|
+
if (isNumeric(padding[k])) sides[k] = Number(padding[k]);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
return ` lIns="${Emu.fromPt(sides.left)}" tIns="${Emu.fromPt(sides.top)}" rIns="${Emu.fromPt(sides.right)}" bIns="${Emu.fromPt(sides.bottom)}"`;
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
function geometry(style, widthEmu, heightEmu) {
|
|
2613
|
+
const radius = style?.radius ?? null;
|
|
2614
|
+
if (!isNumeric(radius) || Number(radius) <= 0) {
|
|
2615
|
+
return '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>';
|
|
2616
|
+
}
|
|
2617
|
+
const shorter = Math.max(1, Math.min(widthEmu, heightEmu));
|
|
2618
|
+
let adj = Math.round(Emu.fromPt(Number(radius)) / (shorter / 2) * 1e5);
|
|
2619
|
+
adj = Math.max(0, Math.min(5e4, adj));
|
|
2620
|
+
return `<a:prstGeom prst="roundRect"><a:avLst><a:gd name="adj" fmla="val ${adj}"/></a:avLst></a:prstGeom>`;
|
|
2621
|
+
}
|
|
2622
|
+
function fill(style, widthEmu) {
|
|
2623
|
+
const bar = isPlainObject(style?.accentBar) ? style.accentBar : null;
|
|
2624
|
+
const hasFill = style?.fill !== void 0 && style.fill !== false && style.fill !== "none";
|
|
2625
|
+
if (bar === null) {
|
|
2626
|
+
if (!hasFill) return "<a:noFill/>";
|
|
2627
|
+
const [hex2] = Color.parse(String(style.fill), "FFFFFF");
|
|
2628
|
+
return `<a:solidFill><a:srgbClr val="${hex2}"/></a:solidFill>`;
|
|
2629
|
+
}
|
|
2630
|
+
const [barHex] = Color.parse(String(bar.color ?? "#8B5CF6"), "8B5CF6");
|
|
2631
|
+
const [restHex] = Color.parse(hasFill ? String(style.fill) : "#FFFFFF", "FFFFFF");
|
|
2632
|
+
const barEmu = Emu.fromPt(Number(bar.width ?? 4));
|
|
2633
|
+
let pos = widthEmu > 0 ? Math.round(barEmu / widthEmu * 1e5) : 1e3;
|
|
2634
|
+
pos = Math.max(1, Math.min(99998, pos));
|
|
2635
|
+
const right = (bar.side ?? "left") === "right";
|
|
2636
|
+
let stops;
|
|
2637
|
+
if (right) {
|
|
2638
|
+
const edge = 1e5 - pos;
|
|
2639
|
+
stops = `<a:gs pos="0"><a:srgbClr val="${restHex}"/></a:gs><a:gs pos="${edge - 1}"><a:srgbClr val="${restHex}"/></a:gs><a:gs pos="${edge}"><a:srgbClr val="${barHex}"/></a:gs><a:gs pos="100000"><a:srgbClr val="${barHex}"/></a:gs>`;
|
|
2640
|
+
} else {
|
|
2641
|
+
stops = `<a:gs pos="0"><a:srgbClr val="${barHex}"/></a:gs><a:gs pos="${pos}"><a:srgbClr val="${barHex}"/></a:gs><a:gs pos="${pos + 1}"><a:srgbClr val="${restHex}"/></a:gs><a:gs pos="100000"><a:srgbClr val="${restHex}"/></a:gs>`;
|
|
2642
|
+
}
|
|
2643
|
+
return `<a:gradFill flip="none" rotWithShape="0"><a:gsLst>${stops}</a:gsLst><a:lin ang="0" scaled="0"/></a:gradFill>`;
|
|
2644
|
+
}
|
|
2645
|
+
function line(style) {
|
|
2646
|
+
const border = style?.border ?? null;
|
|
2647
|
+
if (border === null || border === void 0 || border === false || border === "none") return "";
|
|
2648
|
+
if (!isPlainObject(border)) return "";
|
|
2649
|
+
const width = isNumeric(border.width) ? Number(border.width) : 1;
|
|
2650
|
+
if (width <= 0) return "";
|
|
2651
|
+
const [hex2] = Color.parse(String(border.color ?? "#CBD5E1"), "CBD5E1");
|
|
2652
|
+
const dash = (border.style ?? "solid") !== "solid" ? `<a:prstDash val="${Xml.attr(String(border.style))}"/>` : "";
|
|
2653
|
+
return `<a:ln w="${Emu.fromPt(width)}"><a:solidFill><a:srgbClr val="${hex2}"/></a:solidFill>${dash}</a:ln>`;
|
|
2654
|
+
}
|
|
2655
|
+
|
|
1998
2656
|
// src/writer/pptx-writer.ts
|
|
1999
2657
|
var NS_CHART = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
2658
|
+
var ANCHOR_ATTR = { top: "t", middle: "ctr", bottom: "b" };
|
|
2659
|
+
var ALIGN_ATTR = { left: "l", center: "ctr", right: "r", justify: "just" };
|
|
2000
2660
|
var LAYOUT_ORDER = [
|
|
2001
2661
|
"blank",
|
|
2002
2662
|
"title",
|
|
@@ -2024,7 +2684,7 @@ function toInt(v) {
|
|
|
2024
2684
|
if (!Number.isFinite(n)) return 0;
|
|
2025
2685
|
return Math.trunc(n);
|
|
2026
2686
|
}
|
|
2027
|
-
var
|
|
2687
|
+
var _PptxWriter = class _PptxWriter {
|
|
2028
2688
|
constructor(tempDir = null, allowHttpImages = false) {
|
|
2029
2689
|
this.tempDir = tempDir;
|
|
2030
2690
|
this.allowHttpImages = allowHttpImages;
|
|
@@ -2035,6 +2695,19 @@ var PptxWriter = class {
|
|
|
2035
2695
|
/** Ordered list of chart part XML queued for the archive. */
|
|
2036
2696
|
this.chartFiles = [];
|
|
2037
2697
|
this.themeAccent = "8B5CF6";
|
|
2698
|
+
/** The deck's theme, kept whole so the table resolver can read its colours. */
|
|
2699
|
+
this.deckTheme = {};
|
|
2700
|
+
/**
|
|
2701
|
+
* Monospace typeface for code runs, from `theme.fonts.mono`.
|
|
2702
|
+
*
|
|
2703
|
+
* There is no third slot in OOXML's `<a:fontScheme>` — a theme carries a
|
|
2704
|
+
* major and a minor font and nothing else — so unlike heading and body this
|
|
2705
|
+
* cannot ride along in theme1.xml and has to be written onto each code run.
|
|
2706
|
+
* That is why it was missed in all three engines: accepted by every
|
|
2707
|
+
* validator, published in the JSON Schema handed to an LLM as the tool
|
|
2708
|
+
* definition, named in the writers' own docblocks, and applied nowhere.
|
|
2709
|
+
*/
|
|
2710
|
+
this.themeMono = "Consolas";
|
|
2038
2711
|
this.tnId = 0;
|
|
2039
2712
|
this.pendingSlideRels = {};
|
|
2040
2713
|
}
|
|
@@ -2048,6 +2721,9 @@ var PptxWriter = class {
|
|
|
2048
2721
|
this.chartFiles = [];
|
|
2049
2722
|
this.pendingSlideRels = {};
|
|
2050
2723
|
[this.themeAccent] = Color.parse(deck?.theme?.colors?.accent ?? "#8B5CF6", "8B5CF6");
|
|
2724
|
+
this.deckTheme = isPlainObject(deck?.theme) ? deck.theme : {};
|
|
2725
|
+
const mono = deck?.theme?.fonts?.mono;
|
|
2726
|
+
this.themeMono = typeof mono === "string" && mono.trim() !== "" ? mono.trim() : "Consolas";
|
|
2051
2727
|
const slides = deck?.slides ?? [];
|
|
2052
2728
|
const slideCount = slides.length;
|
|
2053
2729
|
const files = [];
|
|
@@ -2159,9 +2835,10 @@ var PptxWriter = class {
|
|
|
2159
2835
|
const [surface] = Color.parse(colors.surface ?? "#E7E6E6", "E7E6E6");
|
|
2160
2836
|
const heading = Xml.attr(String(deck?.theme?.fonts?.heading ?? "Calibri"));
|
|
2161
2837
|
const body = Xml.attr(String(deck?.theme?.fonts?.body ?? "Calibri"));
|
|
2838
|
+
const mono = Xml.attr(this.themeMono);
|
|
2162
2839
|
const palette = [...CHART_PALETTE];
|
|
2163
2840
|
palette[0] = accent;
|
|
2164
|
-
return Xml.declaration() + '<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="DarkSlide"><a:themeElements><a:clrScheme name="DarkSlide"><a:dk1><a:srgbClr val="' + text + '"/></a:dk1><a:lt1><a:srgbClr val="' + bg + '"/></a:lt1><a:dk2><a:srgbClr val="' + muted + '"/></a:dk2><a:lt2><a:srgbClr val="' + surface + '"/></a:lt2><a:accent1><a:srgbClr val="' + palette[0] + '"/></a:accent1><a:accent2><a:srgbClr val="' + palette[1] + '"/></a:accent2><a:accent3><a:srgbClr val="' + palette[2] + '"/></a:accent3><a:accent4><a:srgbClr val="' + palette[3] + '"/></a:accent4><a:accent5><a:srgbClr val="' + palette[4] + '"/></a:accent5><a:accent6><a:srgbClr val="' + palette[5] + '"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="DarkSlide"><a:majorFont><a:latin typeface="' + heading + '"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="' + body + '"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="DarkSlide"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>';
|
|
2841
|
+
return Xml.declaration() + '<a:theme xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" name="DarkSlide"><a:themeElements><a:clrScheme name="DarkSlide"><a:dk1><a:srgbClr val="' + text + '"/></a:dk1><a:lt1><a:srgbClr val="' + bg + '"/></a:lt1><a:dk2><a:srgbClr val="' + muted + '"/></a:dk2><a:lt2><a:srgbClr val="' + surface + '"/></a:lt2><a:accent1><a:srgbClr val="' + palette[0] + '"/></a:accent1><a:accent2><a:srgbClr val="' + palette[1] + '"/></a:accent2><a:accent3><a:srgbClr val="' + palette[2] + '"/></a:accent3><a:accent4><a:srgbClr val="' + palette[3] + '"/></a:accent4><a:accent5><a:srgbClr val="' + palette[4] + '"/></a:accent5><a:accent6><a:srgbClr val="' + palette[5] + '"/></a:accent6><a:hlink><a:srgbClr val="0563C1"/></a:hlink><a:folHlink><a:srgbClr val="954F72"/></a:folHlink></a:clrScheme><a:fontScheme name="DarkSlide"><a:majorFont><a:latin typeface="' + heading + '"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="' + body + '"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="DarkSlide"><a:fillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:fillStyleLst><a:lnStyleLst><a:ln w="6350"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="12700"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln><a:ln w="19050"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></a:bgFillStyleLst></a:fmtScheme></a:themeElements><a:extLst><a:ext uri="' + _PptxWriter.MONO_FONT_EXT_URI + '"><ds:monoFont xmlns:ds="' + _PptxWriter.NS_DARK_SLIDE + '" typeface="' + mono + '"/></a:ext></a:extLst></a:theme>';
|
|
2165
2842
|
}
|
|
2166
2843
|
buildSlideMaster() {
|
|
2167
2844
|
return Xml.declaration() + '<p:sldMaster xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><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><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></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>' + this.buildSlideLayoutIdLst() + '</p:sldLayoutIdLst><p:txStyles><p:titleStyle><a:lvl1pPr algn="ctr"><a:defRPr sz="4400"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:defRPr></a:lvl1pPr></p:titleStyle><p:bodyStyle><a:lvl1pPr><a:defRPr sz="2400"><a:solidFill><a:schemeClr val="tx1"/></a:solidFill></a:defRPr></a:lvl1pPr></p:bodyStyle><p:otherStyle/></p:txStyles></p:sldMaster>';
|
|
@@ -2545,9 +3222,9 @@ var PptxWriter = class {
|
|
|
2545
3222
|
}
|
|
2546
3223
|
}
|
|
2547
3224
|
if (typeof bg.color === "string") {
|
|
2548
|
-
const [
|
|
3225
|
+
const [hex2, alpha] = Color.parse(bg.color);
|
|
2549
3226
|
return {
|
|
2550
|
-
xml: '<p:bg><p:bgPr><a:solidFill><a:srgbClr val="' +
|
|
3227
|
+
xml: '<p:bg><p:bgPr><a:solidFill><a:srgbClr val="' + hex2 + '"><a:alpha val="' + alpha + '"/></a:srgbClr></a:solidFill><a:effectLst/></p:bgPr></p:bg>',
|
|
2551
3228
|
rels
|
|
2552
3229
|
};
|
|
2553
3230
|
}
|
|
@@ -2590,8 +3267,8 @@ var PptxWriter = class {
|
|
|
2590
3267
|
colorStr = part;
|
|
2591
3268
|
pos = count <= 1 ? 0 : i / (count - 1);
|
|
2592
3269
|
}
|
|
2593
|
-
const [
|
|
2594
|
-
stops.push({ hex, pos: Math.max(0, Math.min(1, pos)) });
|
|
3270
|
+
const [hex2] = Color.parse(colorStr);
|
|
3271
|
+
stops.push({ hex: hex2, pos: Math.max(0, Math.min(1, pos)) });
|
|
2595
3272
|
});
|
|
2596
3273
|
let gsList = "";
|
|
2597
3274
|
for (const stop of stops) {
|
|
@@ -2664,6 +3341,9 @@ var PptxWriter = class {
|
|
|
2664
3341
|
// ─── Element dispatch ─────────────────────────────────────────────────
|
|
2665
3342
|
buildElementXml(element, shapeId, slideNumber) {
|
|
2666
3343
|
const rels = [];
|
|
3344
|
+
if (Composites.isComposite(element.type)) {
|
|
3345
|
+
element = Composites.expand(element, this.deckTheme);
|
|
3346
|
+
}
|
|
2667
3347
|
let xml;
|
|
2668
3348
|
switch (element.type) {
|
|
2669
3349
|
case "text":
|
|
@@ -2717,13 +3397,12 @@ var PptxWriter = class {
|
|
|
2717
3397
|
// ─── Element renderers ────────────────────────────────────────────────
|
|
2718
3398
|
buildTextShape(element, shapeId) {
|
|
2719
3399
|
const xfrm = this.xfrmFromFractions(element);
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
element.style ?? {},
|
|
2723
|
-
String(element.format ?? "plain")
|
|
2724
|
-
);
|
|
3400
|
+
const style = isPlainObject(element.style) ? element.style : {};
|
|
3401
|
+
const body = this.buildTextBody(String(element.content ?? ""), style, String(element.format ?? "plain"));
|
|
2725
3402
|
const id = element.id ?? `text-${shapeId}`;
|
|
2726
|
-
|
|
3403
|
+
const widthEmu = Emu.fromFracX(toFloat(element.w ?? 0.8));
|
|
3404
|
+
const heightEmu = Emu.fromFracY(toFloat(element.h ?? 0.2));
|
|
3405
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr>' + xfrm + BoxDecoration.spPr(style, widthEmu, heightEmu) + "</p:spPr>" + body + "</p:sp>";
|
|
2727
3406
|
}
|
|
2728
3407
|
buildImageShape(element, shapeId, slideNumber, rels) {
|
|
2729
3408
|
const src = String(element.src ?? "");
|
|
@@ -2837,11 +3516,19 @@ var PptxWriter = class {
|
|
|
2837
3516
|
prst = "rect";
|
|
2838
3517
|
}
|
|
2839
3518
|
const [fillHex, fillAlpha] = Color.parse(element.fill ?? "rgba(139,92,246,0.15)", "8B5CF6");
|
|
2840
|
-
const [strokeHex] = Color.parse(element.stroke ?? "#8B5CF6", "8B5CF6");
|
|
2841
|
-
const
|
|
3519
|
+
const [strokeHex, strokeAlpha] = Color.parse(element.stroke ?? "#8B5CF6", "8B5CF6");
|
|
3520
|
+
const strokeWidth = toFloat(element.strokeWidth ?? 2);
|
|
3521
|
+
const strokeWidthEmu = Emu.fromPt(strokeWidth);
|
|
2842
3522
|
const dashStr = element.dashed ? '<a:prstDash val="dash"/>' : "";
|
|
2843
3523
|
const fillXml = fillAlpha === 0 ? "<a:noFill/>" : '<a:solidFill><a:srgbClr val="' + fillHex + '"><a:alpha val="' + fillAlpha + '"/></a:srgbClr></a:solidFill>';
|
|
2844
|
-
|
|
3524
|
+
const lnXml = strokeWidth <= 0 || strokeAlpha === 0 ? "<a:ln><a:noFill/></a:ln>" : '<a:ln w="' + strokeWidthEmu + '"><a:solidFill><a:srgbClr val="' + strokeHex + '"/></a:solidFill>' + dashStr + "</a:ln>";
|
|
3525
|
+
const content = String(element.content ?? "");
|
|
3526
|
+
const txBody = content === "" ? '<p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody>' : this.buildTextBody(
|
|
3527
|
+
content,
|
|
3528
|
+
{ align: "center", verticalAlign: "middle", ...isPlainObject(element.style) ? element.style : {} },
|
|
3529
|
+
String(element.format ?? "plain")
|
|
3530
|
+
);
|
|
3531
|
+
return '<p:sp><p:nvSpPr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>' + xfrm + '<a:prstGeom prst="' + prst + '"><a:avLst/></a:prstGeom>' + fillXml + lnXml + "</p:spPr>" + txBody + "</p:sp>";
|
|
2845
3532
|
}
|
|
2846
3533
|
buildCodeShape(element, shapeId) {
|
|
2847
3534
|
const xfrm = this.xfrmFromFractions(element);
|
|
@@ -2855,15 +3542,15 @@ var PptxWriter = class {
|
|
|
2855
3542
|
const sz = Emu.hundredthsOfPoint(12);
|
|
2856
3543
|
let paragraphs = "";
|
|
2857
3544
|
const lines = code.split("\n");
|
|
2858
|
-
for (const
|
|
2859
|
-
const tokens = SyntaxHighlighter.tokenize(
|
|
3545
|
+
for (const line2 of lines) {
|
|
3546
|
+
const tokens = SyntaxHighlighter.tokenize(line2, language);
|
|
2860
3547
|
let runs = "";
|
|
2861
3548
|
for (const token of tokens) {
|
|
2862
3549
|
if (token.text === "") {
|
|
2863
3550
|
continue;
|
|
2864
3551
|
}
|
|
2865
3552
|
const color = SyntaxHighlighter.colorFor(token.kind);
|
|
2866
|
-
runs += '<a:r><a:rPr lang="en-US" sz="' + sz + '"><a:solidFill><a:srgbClr val="' + color + '"/></a:solidFill><a:latin typeface="
|
|
3553
|
+
runs += '<a:r><a:rPr lang="en-US" sz="' + sz + '"><a:solidFill><a:srgbClr val="' + color + '"/></a:solidFill><a:latin typeface="' + Xml.attr(this.themeMono) + '"/></a:rPr><a:t>' + Xml.text(token.text) + "</a:t></a:r>";
|
|
2867
3554
|
}
|
|
2868
3555
|
if (runs === "") {
|
|
2869
3556
|
runs = '<a:endParaRPr lang="en-US" sz="' + sz + '"/>';
|
|
@@ -2873,60 +3560,81 @@ var PptxWriter = class {
|
|
|
2873
3560
|
return '<p:txBody><a:bodyPr wrap="square" anchor="t" rtlCol="0" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/><a:lstStyle/>' + paragraphs + "</p:txBody>";
|
|
2874
3561
|
}
|
|
2875
3562
|
buildTable(element, shapeId) {
|
|
2876
|
-
const
|
|
2877
|
-
|
|
2878
|
-
if (columns.length === 0) {
|
|
3563
|
+
const columnsRaw = Array.isArray(element.columns) ? element.columns : [];
|
|
3564
|
+
if (columnsRaw.length === 0) {
|
|
2879
3565
|
return this.buildPlaceholder("[table: no columns]", element, shapeId);
|
|
2880
3566
|
}
|
|
3567
|
+
const table = TableResolver.resolve(element, this.deckTheme);
|
|
2881
3568
|
const totalWidthEmu = Emu.fromFracX(toFloat(element.w ?? 0.5));
|
|
2882
|
-
const
|
|
2883
|
-
const colWidthEmu = Math.round(totalWidthEmu / Math.max(1, colCount));
|
|
2884
|
-
const headerRowH = Emu.fromPt(40);
|
|
2885
|
-
const bodyRowH = Emu.fromPt(30);
|
|
3569
|
+
const widths = TableResolver.columnWidthsEmu(table.columns, totalWidthEmu);
|
|
2886
3570
|
let gridCols = "";
|
|
2887
|
-
for (
|
|
2888
|
-
gridCols += '<a:gridCol w="' +
|
|
2889
|
-
}
|
|
2890
|
-
let
|
|
2891
|
-
for (const
|
|
2892
|
-
const label = String(col.label ?? col.key ?? "");
|
|
2893
|
-
headerCells += this.buildTableCell(label, true);
|
|
2894
|
-
}
|
|
2895
|
-
const headerRow = '<a:tr h="' + headerRowH + '">' + headerCells + "</a:tr>";
|
|
2896
|
-
let bodyRows = "";
|
|
2897
|
-
let rowIndex = 0;
|
|
2898
|
-
for (const row of rows) {
|
|
2899
|
-
if (!isPlainObject(row)) {
|
|
2900
|
-
continue;
|
|
2901
|
-
}
|
|
3571
|
+
for (const w of widths) {
|
|
3572
|
+
gridCols += '<a:gridCol w="' + w + '"/>';
|
|
3573
|
+
}
|
|
3574
|
+
let rowsXml = "";
|
|
3575
|
+
for (const row of table.rows) {
|
|
2902
3576
|
let cells = "";
|
|
2903
|
-
for (const
|
|
2904
|
-
|
|
2905
|
-
const value = row[key] ?? "";
|
|
2906
|
-
const text = isScalar2(value) ? scalarToString(value) : JSON.stringify(value);
|
|
2907
|
-
cells += this.buildTableCell(String(text), false, rowIndex % 2 === 1);
|
|
3577
|
+
for (const cell of row.cells) {
|
|
3578
|
+
cells += this.buildTableCell(cell);
|
|
2908
3579
|
}
|
|
2909
|
-
|
|
2910
|
-
rowIndex++;
|
|
3580
|
+
rowsXml += '<a:tr h="' + Emu.fromPt(row.height) + '">' + cells + "</a:tr>";
|
|
2911
3581
|
}
|
|
2912
3582
|
const xfrm = this.xfrmFromFractions(element);
|
|
2913
3583
|
const id = element.id ?? `table-${shapeId}`;
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3584
|
+
const tblPr = "<a:tblPr" + (table.hasHeader ? ' firstRow="1"' : "") + "><a:tableStyleId>{2D5ABB26-0587-4C30-8999-92F81FD0307C}</a:tableStyleId></a:tblPr>";
|
|
3585
|
+
return '<p:graphicFrame><p:nvGraphicFramePr><p:cNvPr id="' + shapeId + '" name="' + Xml.attr(String(id)) + '"/><p:cNvGraphicFramePr><a:graphicFrameLocks noGrp="1"/></p:cNvGraphicFramePr><p:nvPr/></p:nvGraphicFramePr><p:xfrm>' + innerXfrm(xfrm) + '</p:xfrm><a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"><a:tbl>' + tblPr + "<a:tblGrid>" + gridCols + "</a:tblGrid>" + rowsXml + "</a:tbl></a:graphicData></a:graphic></p:graphicFrame>";
|
|
3586
|
+
}
|
|
3587
|
+
/**
|
|
3588
|
+
* Serialise one resolved cell. Makes no styling decisions — every value here
|
|
3589
|
+
* was decided by the resolver.
|
|
3590
|
+
*
|
|
3591
|
+
* Three things in here are load-bearing and easy to get wrong:
|
|
3592
|
+
*
|
|
3593
|
+
* - `gridSpan` / `rowSpan` / `hMerge` / `vMerge` are attributes of
|
|
3594
|
+
* `<a:tc>`, NOT of `<a:tcPr>`. On `tcPr` they parse fine and are silently
|
|
3595
|
+
* ignored, so the table renders unmerged with no error anywhere.
|
|
3596
|
+
* - `<a:tcPr>` has a FIXED child order: lnL, lnR, lnT, lnB, then the fill.
|
|
3597
|
+
* Emitting the fill first produces a file whose fill a reader drops.
|
|
3598
|
+
* - "No border" is STATED. An absent `<a:lnL>` is an UNSPECIFIED rule, not
|
|
3599
|
+
* an absent one, and a reader supplies its own.
|
|
3600
|
+
*/
|
|
3601
|
+
buildTableCell(cell) {
|
|
3602
|
+
let attrs = "";
|
|
3603
|
+
if (cell.colSpan > 1) attrs += ' gridSpan="' + cell.colSpan + '"';
|
|
3604
|
+
if (cell.rowSpan > 1) attrs += ' rowSpan="' + cell.rowSpan + '"';
|
|
3605
|
+
if (cell.merged === "horizontal" || cell.merged === "both") attrs += ' hMerge="1"';
|
|
3606
|
+
if (cell.merged === "vertical" || cell.merged === "both") attrs += ' vMerge="1"';
|
|
3607
|
+
const pad = cell.padding;
|
|
3608
|
+
const tcPrAttrs = ' marL="' + Emu.fromPt(pad.left) + '" marR="' + Emu.fromPt(pad.right) + '" marT="' + Emu.fromPt(pad.top) + '" marB="' + Emu.fromPt(pad.bottom) + '" anchor="' + ANCHOR_ATTR[cell.anchor] + '"';
|
|
3609
|
+
let borders = "";
|
|
3610
|
+
for (const [side, suffix] of [["left", "L"], ["right", "R"], ["top", "T"], ["bottom", "B"]]) {
|
|
3611
|
+
const spec = cell.borders[side];
|
|
3612
|
+
if (spec === null) {
|
|
3613
|
+
borders += "<a:ln" + suffix + "><a:noFill/></a:ln" + suffix + ">";
|
|
3614
|
+
continue;
|
|
3615
|
+
}
|
|
3616
|
+
borders += "<a:ln" + suffix + ' w="' + Emu.fromPt(spec.width) + '" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:srgbClr val="' + spec.color + '"/></a:solidFill><a:prstDash val="' + spec.style + '"/></a:ln' + suffix + ">";
|
|
3617
|
+
}
|
|
3618
|
+
const fill2 = cell.fill === null ? "<a:noFill/>" : '<a:solidFill><a:srgbClr val="' + cell.fill + '"/></a:solidFill>';
|
|
3619
|
+
return "<a:tc" + attrs + '><a:txBody><a:bodyPr/><a:lstStyle/><a:p><a:pPr algn="' + ALIGN_ATTR[cell.align] + '"/>' + this.buildCellRun(cell) + "</a:p></a:txBody><a:tcPr" + tcPrAttrs + ">" + borders + fill2 + "</a:tcPr></a:tc>";
|
|
3620
|
+
}
|
|
3621
|
+
buildCellRun(cell) {
|
|
3622
|
+
let rPr = '<a:rPr lang="en-US" sz="' + Emu.hundredthsOfPoint(cell.fontSize) + '"';
|
|
3623
|
+
if (cell.bold) rPr += ' b="1"';
|
|
3624
|
+
if (cell.italic) rPr += ' i="1"';
|
|
3625
|
+
if (cell.underline) rPr += ' u="sng"';
|
|
3626
|
+
if (cell.letterSpacing !== 0) rPr += ' spc="' + Emu.hundredthsOfPoint(cell.letterSpacing) + '"';
|
|
3627
|
+
if (cell.caps !== "none") rPr += ' cap="' + cell.caps + '"';
|
|
3628
|
+
rPr += ">";
|
|
3629
|
+
rPr += '<a:solidFill><a:srgbClr val="' + cell.color + '"/></a:solidFill>';
|
|
3630
|
+
if (cell.fontFamily !== null) {
|
|
3631
|
+
rPr += '<a:latin typeface="' + Xml.attr(cell.fontFamily) + '"/>';
|
|
2928
3632
|
}
|
|
2929
|
-
|
|
3633
|
+
rPr += "</a:rPr>";
|
|
3634
|
+
if (cell.text === "") {
|
|
3635
|
+
return '<a:endParaRPr lang="en-US" sz="' + Emu.hundredthsOfPoint(cell.fontSize) + '"/>';
|
|
3636
|
+
}
|
|
3637
|
+
return "<a:r>" + rPr + "<a:t>" + Xml.text(cell.text) + "</a:t></a:r>";
|
|
2930
3638
|
}
|
|
2931
3639
|
// ─── Charts ───────────────────────────────────────────────────────────
|
|
2932
3640
|
buildChart(element, shapeId, slideNumber, rels) {
|
|
@@ -3143,16 +3851,19 @@ var PptxWriter = class {
|
|
|
3143
3851
|
anchor = 't="t"';
|
|
3144
3852
|
}
|
|
3145
3853
|
const renderRuns = format === "markdown";
|
|
3854
|
+
const spacing = this.paragraphSpacing(style);
|
|
3855
|
+
const bullet = this.bulletMarkup(style.bullet ?? null);
|
|
3856
|
+
const runExtra = this.runExtraAttrs(style);
|
|
3146
3857
|
let paragraphs = "";
|
|
3147
3858
|
const lines = content.split("\n");
|
|
3148
|
-
for (const
|
|
3859
|
+
for (const line2 of lines) {
|
|
3149
3860
|
let headingLevel = 0;
|
|
3150
3861
|
let isBullet = false;
|
|
3151
|
-
let body =
|
|
3862
|
+
let body = line2;
|
|
3152
3863
|
if (renderRuns) {
|
|
3153
|
-
[headingLevel, body] = MarkdownInline.headingPrefix(
|
|
3864
|
+
[headingLevel, body] = MarkdownInline.headingPrefix(line2);
|
|
3154
3865
|
if (headingLevel === 0) {
|
|
3155
|
-
[isBullet, body] = MarkdownInline.bulletPrefix(
|
|
3866
|
+
[isBullet, body] = MarkdownInline.bulletPrefix(line2);
|
|
3156
3867
|
}
|
|
3157
3868
|
}
|
|
3158
3869
|
let paragraphSz = sz;
|
|
@@ -3176,11 +3887,9 @@ var PptxWriter = class {
|
|
|
3176
3887
|
paragraphBold = ' b="1"';
|
|
3177
3888
|
}
|
|
3178
3889
|
let pPr = '<a:pPr algn="' + align + '"';
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
pPr += "><a:buNone/>";
|
|
3183
|
-
}
|
|
3890
|
+
pPr += isBullet ? ' indent="-228600" marL="228600">' : ">";
|
|
3891
|
+
pPr += spacing;
|
|
3892
|
+
pPr += isBullet ? bullet : "<a:buNone/>";
|
|
3184
3893
|
pPr += "</a:pPr>";
|
|
3185
3894
|
let runs = "";
|
|
3186
3895
|
if (renderRuns) {
|
|
@@ -3196,17 +3905,18 @@ var PptxWriter = class {
|
|
|
3196
3905
|
fontFamily,
|
|
3197
3906
|
token.b,
|
|
3198
3907
|
token.i,
|
|
3199
|
-
token.code
|
|
3908
|
+
token.code,
|
|
3909
|
+
runExtra
|
|
3200
3910
|
);
|
|
3201
3911
|
}
|
|
3202
3912
|
} else {
|
|
3203
|
-
runs = this.buildRun(body, paragraphSz, paragraphBold, baseItalic, baseUnderline, colorHex, fontFamily, false, false, false);
|
|
3913
|
+
runs = this.buildRun(body, paragraphSz, paragraphBold, baseItalic, baseUnderline, colorHex, fontFamily, false, false, false, runExtra);
|
|
3204
3914
|
}
|
|
3205
3915
|
paragraphs += "<a:p>" + pPr + runs + "</a:p>";
|
|
3206
3916
|
}
|
|
3207
|
-
return '<p:txBody><a:bodyPr wrap="square" anchor="' + anchor.slice(3, -1) + '" rtlCol="0"/><a:lstStyle/>
|
|
3917
|
+
return '<p:txBody><a:bodyPr wrap="square" anchor="' + anchor.slice(3, -1) + '" rtlCol="0"' + BoxDecoration.bodyInsets(style) + "/><a:lstStyle/>" + paragraphs + "</p:txBody>";
|
|
3208
3918
|
}
|
|
3209
|
-
buildRun(text, sz, baseBold, baseItalic, baseUnderline, colorHex, fontFamily, bold, italic, code) {
|
|
3919
|
+
buildRun(text, sz, baseBold, baseItalic, baseUnderline, colorHex, fontFamily, bold, italic, code, extra = "") {
|
|
3210
3920
|
const b = bold ? ' b="1"' : baseBold;
|
|
3211
3921
|
const i = (italic ? ' i="1"' : "") || baseItalic;
|
|
3212
3922
|
const u = baseUnderline;
|
|
@@ -3214,11 +3924,68 @@ var PptxWriter = class {
|
|
|
3214
3924
|
let family = fontFamily;
|
|
3215
3925
|
if (code) {
|
|
3216
3926
|
color = "8B5CF6";
|
|
3217
|
-
family = '<a:latin typeface="
|
|
3927
|
+
family = '<a:latin typeface="' + Xml.attr(this.themeMono) + '"/>';
|
|
3218
3928
|
}
|
|
3219
|
-
const rPr = '<a:rPr lang="en-US" sz="' + sz + '"' + b + i + u + '><a:solidFill><a:srgbClr val="' + color + '"/></a:solidFill>' + family + "</a:rPr>";
|
|
3929
|
+
const rPr = '<a:rPr lang="en-US" sz="' + sz + '"' + b + i + u + extra + '><a:solidFill><a:srgbClr val="' + color + '"/></a:solidFill>' + family + "</a:rPr>";
|
|
3220
3930
|
return "<a:r>" + rPr + "<a:t>" + Xml.text(text) + "</a:t></a:r>";
|
|
3221
3931
|
}
|
|
3932
|
+
/**
|
|
3933
|
+
* `<a:lnSpc>` / `<a:spcBef>` / `<a:spcAft>` for a paragraph.
|
|
3934
|
+
*
|
|
3935
|
+
* `lineHeight` is a MULTIPLE (1.4 = 140%), matching CSS and the fancy-slides
|
|
3936
|
+
* editor; `spaceBefore` / `spaceAfter` are points. Empty when the style is
|
|
3937
|
+
* silent, so decks that predate this keep their bytes.
|
|
3938
|
+
*/
|
|
3939
|
+
paragraphSpacing(style) {
|
|
3940
|
+
let out = "";
|
|
3941
|
+
if (isNumeric(style?.lineHeight)) {
|
|
3942
|
+
out += '<a:lnSpc><a:spcPct val="' + Math.round(toFloat(style.lineHeight) * 1e5) + '"/></a:lnSpc>';
|
|
3943
|
+
}
|
|
3944
|
+
if (isNumeric(style?.spaceBefore)) {
|
|
3945
|
+
out += '<a:spcBef><a:spcPts val="' + Emu.hundredthsOfPoint(toFloat(style.spaceBefore)) + '"/></a:spcBef>';
|
|
3946
|
+
}
|
|
3947
|
+
if (isNumeric(style?.spaceAfter)) {
|
|
3948
|
+
out += '<a:spcAft><a:spcPts val="' + Emu.hundredthsOfPoint(toFloat(style.spaceAfter)) + '"/></a:spcAft>';
|
|
3949
|
+
}
|
|
3950
|
+
return out;
|
|
3951
|
+
}
|
|
3952
|
+
/**
|
|
3953
|
+
* The bullet markup for a list paragraph.
|
|
3954
|
+
*
|
|
3955
|
+
* `none` suppresses it, `number` makes an auto-numbered list, and anything
|
|
3956
|
+
* else is taken as the literal character — which is all a check-mark list is.
|
|
3957
|
+
* The default stays the round bullet the writer has always emitted.
|
|
3958
|
+
*/
|
|
3959
|
+
bulletMarkup(bullet) {
|
|
3960
|
+
if (bullet === null || bullet === void 0 || bullet === "") {
|
|
3961
|
+
return '<a:buFont typeface="Arial"/><a:buChar char="\u2022"/>';
|
|
3962
|
+
}
|
|
3963
|
+
if (bullet === "none" || bullet === false) {
|
|
3964
|
+
return "<a:buNone/>";
|
|
3965
|
+
}
|
|
3966
|
+
if (bullet === "number") {
|
|
3967
|
+
return '<a:buAutoNum type="arabicPeriod"/>';
|
|
3968
|
+
}
|
|
3969
|
+
return '<a:buFont typeface="Arial"/><a:buChar char="' + Xml.attr(String(bullet)) + '"/>';
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* Run attributes that apply to every run in the body: letter spacing and
|
|
3973
|
+
* capitalisation. Both are `<a:rPr>` attributes, so they have to be built as
|
|
3974
|
+
* a string and appended rather than nested.
|
|
3975
|
+
*/
|
|
3976
|
+
runExtraAttrs(style) {
|
|
3977
|
+
let out = "";
|
|
3978
|
+
if (isNumeric(style?.letterSpacing)) {
|
|
3979
|
+
out += ' spc="' + Emu.hundredthsOfPoint(toFloat(style.letterSpacing)) + '"';
|
|
3980
|
+
}
|
|
3981
|
+
const caps = style?.caps ?? null;
|
|
3982
|
+
if (caps === "small") {
|
|
3983
|
+
out += ' cap="small"';
|
|
3984
|
+
} else if (caps === "all" || caps === "upper") {
|
|
3985
|
+
out += ' cap="all"';
|
|
3986
|
+
}
|
|
3987
|
+
return out;
|
|
3988
|
+
}
|
|
3222
3989
|
weightToBold(weight) {
|
|
3223
3990
|
if (isNumeric(weight) && toInt(weight) >= 600) {
|
|
3224
3991
|
return ' b="1"';
|
|
@@ -3329,8 +4096,8 @@ var PptxWriter = class {
|
|
|
3329
4096
|
buildNotesSlideXml(slide, _slideNumber) {
|
|
3330
4097
|
const notes = String(slide.notes ?? "");
|
|
3331
4098
|
let paragraphs = "";
|
|
3332
|
-
for (const
|
|
3333
|
-
paragraphs += '<a:p><a:r><a:rPr lang="en-US" sz="1200"/><a:t>' + Xml.text(
|
|
4099
|
+
for (const line2 of notes.split("\n")) {
|
|
4100
|
+
paragraphs += '<a:p><a:r><a:rPr lang="en-US" sz="1200"/><a:t>' + Xml.text(line2) + "</a:t></a:r></a:p>";
|
|
3334
4101
|
}
|
|
3335
4102
|
return Xml.declaration() + '<p:notes xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"><p:cSld><p:spTree><p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="0" cy="0"/><a:chOff x="0" y="0"/><a:chExt cx="0" cy="0"/></a:xfrm></p:grpSpPr><p:sp><p:nvSpPr><p:cNvPr id="2" name="Notes Placeholder"/><p:cNvSpPr><a:spLocks noGrp="1"/></p:cNvSpPr><p:nvPr><p:ph type="body"/></p:nvPr></p:nvSpPr><p:spPr><a:xfrm><a:off x="685800" y="1700213"/><a:ext cx="5772150" cy="3679371"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/>' + paragraphs + "</p:txBody></p:sp></p:spTree></p:cSld></p:notes>";
|
|
3336
4103
|
}
|
|
@@ -3338,6 +4105,11 @@ var PptxWriter = class {
|
|
|
3338
4105
|
return Xml.declaration() + '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="../slides/slide' + slideNumber + '.xml"/></Relationships>';
|
|
3339
4106
|
}
|
|
3340
4107
|
};
|
|
4108
|
+
/** Extension uri under which the deck's mono typeface is recorded in theme1.xml. */
|
|
4109
|
+
_PptxWriter.MONO_FONT_EXT_URI = "urn:particle-academy:dark-slide:mono-font";
|
|
4110
|
+
/** Namespace for DarkSlide's own elements inside an `<a:ext>`. */
|
|
4111
|
+
_PptxWriter.NS_DARK_SLIDE = "urn:particle-academy:dark-slide";
|
|
4112
|
+
var PptxWriter = _PptxWriter;
|
|
3341
4113
|
function toFloat(v) {
|
|
3342
4114
|
if (typeof v === "number") return v;
|
|
3343
4115
|
const n = parseFloat(String(v));
|
|
@@ -3349,15 +4121,6 @@ function ucwords(s) {
|
|
|
3349
4121
|
function innerXfrm(xfrm) {
|
|
3350
4122
|
return xfrm.slice("<a:xfrm>".length, -"</a:xfrm>".length);
|
|
3351
4123
|
}
|
|
3352
|
-
function isScalar2(v) {
|
|
3353
|
-
return typeof v === "string" || typeof v === "number" || typeof v === "boolean" || v === null || typeof v === "bigint";
|
|
3354
|
-
}
|
|
3355
|
-
function scalarToString(v) {
|
|
3356
|
-
if (v === true) return "1";
|
|
3357
|
-
if (v === false) return "";
|
|
3358
|
-
if (v === null) return "";
|
|
3359
|
-
return String(v);
|
|
3360
|
-
}
|
|
3361
4124
|
function getImageSize(bytes) {
|
|
3362
4125
|
if (bytes.length >= 24 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
3363
4126
|
const w = bytes[16] << 24 | bytes[17] << 16 | bytes[18] << 8 | bytes[19];
|