@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.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(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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 hex = at(solid, "val") ?? "";
838
- if (hex !== "") {
839
- return { color: "#" + hex };
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 hex = at(color, "val") ?? "";
876
- if (hex === "") {
898
+ const hex2 = at(color, "val") ?? "";
899
+ if (hex2 === "") {
877
900
  continue;
878
901
  }
879
- stopStrings.push("#" + hex.toLowerCase() + " " + numToStr(pct) + "%");
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 headerCells = descendants(rows[0], "tc");
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
- headerCells.forEach((cell, i) => {
1043
- const label = this.cellText(cell);
1044
- columns.push({ key: "col" + (i + 1), label });
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
- const parsed = [];
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
- if (typeface.includes("consola") || typeface.includes("mono") || typeface.includes("courier")) {
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
- let line = "";
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
- line += "`" + text + "`";
1173
+ line2 += "`" + text + "`";
1129
1174
  anyDecoration = true;
1130
1175
  } else if (emitBold && emitItalic) {
1131
- line += "***" + text + "***";
1176
+ line2 += "***" + text + "***";
1132
1177
  anyDecoration = true;
1133
1178
  } else if (emitBold) {
1134
- line += "**" + text + "**";
1179
+ line2 += "**" + text + "**";
1135
1180
  anyDecoration = true;
1136
1181
  } else if (emitItalic) {
1137
- line += "*" + text + "*";
1182
+ line2 += "*" + text + "*";
1138
1183
  anyDecoration = true;
1139
1184
  } else {
1140
- line += text;
1185
+ line2 += text;
1141
1186
  }
1142
1187
  }
1143
1188
  if (isBullet) {
1144
- return ["- " + line, true];
1189
+ return ["- " + line2, true];
1145
1190
  }
1146
- return [line, anyDecoration];
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
- ELEMENT_TYPES: ["text", "image", "chart", "code", "table", "shape", "embed"],
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 hex = (h[0] + h[0] + h[1] + h[1] + h[2] + h[2]).toUpperCase();
1685
- return [hex, 1e5];
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 hex = m[1].slice(0, 6).toUpperCase();
1749
+ const hex2 = m[1].slice(0, 6).toUpperCase();
1692
1750
  const a = parseInt(m[1].slice(6, 8), 16);
1693
- return [hex, Math.round(a / 255 * 1e5)];
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 hex = [r, g, b].map((n) => n.toString(16).padStart(2, "0")).join("").toUpperCase();
1702
- return [hex, Math.round(a * 1e5)];
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(line) {
1777
- if (line.startsWith("- ") || line.startsWith("* ")) return [true, line.slice(2)];
1778
- return [false, line];
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(line) {
1782
- const m = /^(#{1,6})\s+(.*)$/.exec(line);
1839
+ headingPrefix(line2) {
1840
+ const m = /^(#{1,6})\s+(.*)$/.exec(line2);
1783
1841
  if (m) return [m[1].length, m[2]];
1784
- return [0, line];
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 PptxWriter = class {
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 [hex, alpha] = Color.parse(bg.color);
3223
+ const [hex2, alpha] = Color.parse(bg.color);
2547
3224
  return {
2548
- 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>',
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 [hex] = Color.parse(colorStr);
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 body = this.buildTextBody(
2719
- String(element.content ?? ""),
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
- 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>";
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 strokeWidthEmu = Emu.fromPt(toFloat(element.strokeWidth ?? 2));
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
- 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>';
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 line of lines) {
2857
- const tokens = SyntaxHighlighter.tokenize(line, language);
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="Consolas"/></a:rPr><a:t>' + Xml.text(token.text) + "</a:t></a:r>";
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 columns = Array.isArray(element.columns) ? element.columns : [];
2875
- const rows = Array.isArray(element.rows) ? element.rows : [];
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 colCount = columns.length;
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 (let i = 0; i < columns.length; i++) {
2886
- gridCols += '<a:gridCol w="' + colWidthEmu + '"/>';
2887
- }
2888
- let headerCells = "";
2889
- for (const col of columns) {
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 col of columns) {
2902
- const key = String(col.key ?? "");
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
- bodyRows += '<a:tr h="' + bodyRowH + '">' + cells + "</a:tr>";
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
- 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>";
2913
- }
2914
- buildTableCell(text, header, striped = false) {
2915
- let fill;
2916
- let textColor;
2917
- let bold;
2918
- if (header) {
2919
- fill = '<a:solidFill><a:srgbClr val="8B5CF6"/></a:solidFill>';
2920
- textColor = "FFFFFF";
2921
- bold = ' b="1"';
2922
- } else {
2923
- fill = striped ? '<a:solidFill><a:srgbClr val="F8FAFC"/></a:solidFill>' : "<a:noFill/>";
2924
- textColor = "0F172A";
2925
- bold = "";
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
- 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>";
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 line of lines) {
3857
+ for (const line2 of lines) {
3147
3858
  let headingLevel = 0;
3148
3859
  let isBullet = false;
3149
- let body = line;
3860
+ let body = line2;
3150
3861
  if (renderRuns) {
3151
- [headingLevel, body] = MarkdownInline.headingPrefix(line);
3862
+ [headingLevel, body] = MarkdownInline.headingPrefix(line2);
3152
3863
  if (headingLevel === 0) {
3153
- [isBullet, body] = MarkdownInline.bulletPrefix(line);
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
- if (isBullet) {
3178
- pPr += ' indent="-228600" marL="228600"><a:buFont typeface="Arial"/><a:buChar char="\u2022"/>';
3179
- } else {
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/>' + paragraphs + "</p:txBody>";
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="Consolas"/>';
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 line of notes.split("\n")) {
3331
- paragraphs += '<a:p><a:r><a:rPr lang="en-US" sz="1200"/><a:t>' + Xml.text(line) + "</a:t></a:r></a:p>";
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];