@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 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(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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 hex = at(solid, "val") ?? "";
840
- if (hex !== "") {
841
- return { color: "#" + hex };
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 hex = at(color, "val") ?? "";
878
- if (hex === "") {
900
+ const hex2 = at(color, "val") ?? "";
901
+ if (hex2 === "") {
879
902
  continue;
880
903
  }
881
- stopStrings.push("#" + hex.toLowerCase() + " " + numToStr(pct) + "%");
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 headerCells = descendants(rows[0], "tc");
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
- headerCells.forEach((cell, i) => {
1045
- const label = this.cellText(cell);
1046
- columns.push({ key: "col" + (i + 1), label });
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
- const parsed = [];
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
- if (typeface.includes("consola") || typeface.includes("mono") || typeface.includes("courier")) {
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
- let line = "";
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
- line += "`" + text + "`";
1175
+ line2 += "`" + text + "`";
1131
1176
  anyDecoration = true;
1132
1177
  } else if (emitBold && emitItalic) {
1133
- line += "***" + text + "***";
1178
+ line2 += "***" + text + "***";
1134
1179
  anyDecoration = true;
1135
1180
  } else if (emitBold) {
1136
- line += "**" + text + "**";
1181
+ line2 += "**" + text + "**";
1137
1182
  anyDecoration = true;
1138
1183
  } else if (emitItalic) {
1139
- line += "*" + text + "*";
1184
+ line2 += "*" + text + "*";
1140
1185
  anyDecoration = true;
1141
1186
  } else {
1142
- line += text;
1187
+ line2 += text;
1143
1188
  }
1144
1189
  }
1145
1190
  if (isBullet) {
1146
- return ["- " + line, true];
1191
+ return ["- " + line2, true];
1147
1192
  }
1148
- return [line, anyDecoration];
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
- ELEMENT_TYPES: ["text", "image", "chart", "code", "table", "shape", "embed"],
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 hex = (h[0] + h[0] + h[1] + h[1] + h[2] + h[2]).toUpperCase();
1687
- return [hex, 1e5];
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 hex = m[1].slice(0, 6).toUpperCase();
1751
+ const hex2 = m[1].slice(0, 6).toUpperCase();
1694
1752
  const a = parseInt(m[1].slice(6, 8), 16);
1695
- return [hex, Math.round(a / 255 * 1e5)];
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 hex = [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
1704
- return [hex, Math.round(a * 1e5)];
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(line) {
1779
- if (line.startsWith("- ") || line.startsWith("* ")) return [true, line.slice(2)];
1780
- return [false, line];
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(line) {
1784
- const m = /^(#{1,6})\s+(.*)$/.exec(line);
1841
+ headingPrefix(line2) {
1842
+ const m = /^(#{1,6})\s+(.*)$/.exec(line2);
1785
1843
  if (m) return [m[1].length, m[2]];
1786
- return [0, line];
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 PptxWriter = class {
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 [hex, alpha] = Color.parse(bg.color);
3225
+ const [hex2, alpha] = Color.parse(bg.color);
2549
3226
  return {
2550
- xml: '<p:bg><p:bgPr><a:solidFill><a:srgbClr val="' + hex + '"><a:alpha val="' + alpha + '"/></a:srgbClr></a:solidFill><a:effectLst/></p:bgPr></p:bg>',
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 [hex] = Color.parse(colorStr);
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 body = this.buildTextBody(
2721
- String(element.content ?? ""),
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
- 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 + '<a:prstGeom prst="rect"><a:avLst/></a:prstGeom><a:noFill/></p:spPr>' + body + "</p:sp>";
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 strokeWidthEmu = Emu.fromPt(toFloat(element.strokeWidth ?? 2));
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
- 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 + '<a:ln w="' + strokeWidthEmu + '"><a:solidFill><a:srgbClr val="' + strokeHex + '"/></a:solidFill>' + dashStr + '</a:ln></p:spPr><p:txBody><a:bodyPr/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></p:txBody></p:sp>';
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 line of lines) {
2859
- const tokens = SyntaxHighlighter.tokenize(line, language);
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="Consolas"/></a:rPr><a:t>' + Xml.text(token.text) + "</a:t></a:r>";
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 columns = Array.isArray(element.columns) ? element.columns : [];
2877
- const rows = Array.isArray(element.rows) ? element.rows : [];
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 colCount = columns.length;
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 (let i = 0; i < columns.length; i++) {
2888
- gridCols += '<a:gridCol w="' + colWidthEmu + '"/>';
2889
- }
2890
- let headerCells = "";
2891
- for (const col of columns) {
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 col of columns) {
2904
- const key = String(col.key ?? "");
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
- bodyRows += '<a:tr h="' + bodyRowH + '">' + cells + "</a:tr>";
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
- 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><a:tblPr firstRow="1" bandRow="1"><a:tableStyleId>{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}</a:tableStyleId></a:tblPr><a:tblGrid>' + gridCols + "</a:tblGrid>" + headerRow + bodyRows + "</a:tbl></a:graphicData></a:graphic></p:graphicFrame>";
2915
- }
2916
- buildTableCell(text, header, striped = false) {
2917
- let fill;
2918
- let textColor;
2919
- let bold;
2920
- if (header) {
2921
- fill = '<a:solidFill><a:srgbClr val="8B5CF6"/></a:solidFill>';
2922
- textColor = "FFFFFF";
2923
- bold = ' b="1"';
2924
- } else {
2925
- fill = striped ? '<a:solidFill><a:srgbClr val="F8FAFC"/></a:solidFill>' : "<a:noFill/>";
2926
- textColor = "0F172A";
2927
- bold = "";
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
- return '<a:tc><a:txBody><a:bodyPr wrap="square" anchor="ctr" lIns="91440" tIns="45720" rIns="91440" bIns="45720"/><a:lstStyle/><a:p><a:pPr algn="l"/><a:r><a:rPr lang="en-US" sz="1400"' + bold + '><a:solidFill><a:srgbClr val="' + textColor + '"/></a:solidFill></a:rPr><a:t>' + Xml.text(text) + "</a:t></a:r></a:p></a:txBody><a:tcPr>" + fill + "</a:tcPr></a:tc>";
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 line of lines) {
3859
+ for (const line2 of lines) {
3149
3860
  let headingLevel = 0;
3150
3861
  let isBullet = false;
3151
- let body = line;
3862
+ let body = line2;
3152
3863
  if (renderRuns) {
3153
- [headingLevel, body] = MarkdownInline.headingPrefix(line);
3864
+ [headingLevel, body] = MarkdownInline.headingPrefix(line2);
3154
3865
  if (headingLevel === 0) {
3155
- [isBullet, body] = MarkdownInline.bulletPrefix(line);
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
- if (isBullet) {
3180
- pPr += ' indent="-228600" marL="228600"><a:buFont typeface="Arial"/><a:buChar char="\u2022"/>';
3181
- } else {
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/>' + paragraphs + "</p:txBody>";
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="Consolas"/>';
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 line of notes.split("\n")) {
3333
- paragraphs += '<a:p><a:r><a:rPr lang="en-US" sz="1200"/><a:t>' + Xml.text(line) + "</a:t></a:r></a:p>";
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];