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