quicklook-pptx-renderer 0.1.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +266 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +175 -0
  5. package/dist/diff/compare.d.ts +17 -0
  6. package/dist/diff/compare.js +71 -0
  7. package/dist/index.d.ts +29 -0
  8. package/dist/index.js +72 -0
  9. package/dist/lint.d.ts +27 -0
  10. package/dist/lint.js +328 -0
  11. package/dist/mapper/bleed-map.d.ts +6 -0
  12. package/dist/mapper/bleed-map.js +1 -0
  13. package/dist/mapper/constants.d.ts +2 -0
  14. package/dist/mapper/constants.js +4 -0
  15. package/dist/mapper/drawable-mapper.d.ts +16 -0
  16. package/dist/mapper/drawable-mapper.js +1464 -0
  17. package/dist/mapper/html-generator.d.ts +13 -0
  18. package/dist/mapper/html-generator.js +539 -0
  19. package/dist/mapper/image-mapper.d.ts +14 -0
  20. package/dist/mapper/image-mapper.js +70 -0
  21. package/dist/mapper/nano-malloc.d.ts +130 -0
  22. package/dist/mapper/nano-malloc.js +197 -0
  23. package/dist/mapper/ql-bleed.d.ts +35 -0
  24. package/dist/mapper/ql-bleed.js +254 -0
  25. package/dist/mapper/shape-mapper.d.ts +41 -0
  26. package/dist/mapper/shape-mapper.js +2384 -0
  27. package/dist/mapper/slide-mapper.d.ts +4 -0
  28. package/dist/mapper/slide-mapper.js +112 -0
  29. package/dist/mapper/style-builder.d.ts +12 -0
  30. package/dist/mapper/style-builder.js +30 -0
  31. package/dist/mapper/text-mapper.d.ts +14 -0
  32. package/dist/mapper/text-mapper.js +302 -0
  33. package/dist/model/enums.d.ts +25 -0
  34. package/dist/model/enums.js +2 -0
  35. package/dist/model/types.d.ts +482 -0
  36. package/dist/model/types.js +7 -0
  37. package/dist/package/content-types.d.ts +1 -0
  38. package/dist/package/content-types.js +4 -0
  39. package/dist/package/package.d.ts +10 -0
  40. package/dist/package/package.js +52 -0
  41. package/dist/package/relationships.d.ts +6 -0
  42. package/dist/package/relationships.js +25 -0
  43. package/dist/package/zip.d.ts +6 -0
  44. package/dist/package/zip.js +17 -0
  45. package/dist/reader/color.d.ts +3 -0
  46. package/dist/reader/color.js +79 -0
  47. package/dist/reader/drawing.d.ts +17 -0
  48. package/dist/reader/drawing.js +403 -0
  49. package/dist/reader/effects.d.ts +2 -0
  50. package/dist/reader/effects.js +83 -0
  51. package/dist/reader/fill.d.ts +2 -0
  52. package/dist/reader/fill.js +94 -0
  53. package/dist/reader/presentation.d.ts +5 -0
  54. package/dist/reader/presentation.js +127 -0
  55. package/dist/reader/slide-layout.d.ts +2 -0
  56. package/dist/reader/slide-layout.js +28 -0
  57. package/dist/reader/slide-master.d.ts +4 -0
  58. package/dist/reader/slide-master.js +49 -0
  59. package/dist/reader/slide.d.ts +2 -0
  60. package/dist/reader/slide.js +26 -0
  61. package/dist/reader/text-list-style.d.ts +2 -0
  62. package/dist/reader/text-list-style.js +9 -0
  63. package/dist/reader/text.d.ts +5 -0
  64. package/dist/reader/text.js +295 -0
  65. package/dist/reader/theme.d.ts +2 -0
  66. package/dist/reader/theme.js +109 -0
  67. package/dist/reader/transform.d.ts +2 -0
  68. package/dist/reader/transform.js +21 -0
  69. package/dist/render/image-renderer.d.ts +3 -0
  70. package/dist/render/image-renderer.js +33 -0
  71. package/dist/render/renderer.d.ts +9 -0
  72. package/dist/render/renderer.js +178 -0
  73. package/dist/render/shape-renderer.d.ts +3 -0
  74. package/dist/render/shape-renderer.js +175 -0
  75. package/dist/render/text-renderer.d.ts +3 -0
  76. package/dist/render/text-renderer.js +152 -0
  77. package/dist/resolve/color-resolver.d.ts +18 -0
  78. package/dist/resolve/color-resolver.js +321 -0
  79. package/dist/resolve/font-map.d.ts +2 -0
  80. package/dist/resolve/font-map.js +66 -0
  81. package/dist/resolve/inheritance.d.ts +5 -0
  82. package/dist/resolve/inheritance.js +106 -0
  83. package/package.json +74 -0
@@ -0,0 +1,4 @@
1
+ import type { Slide, Presentation } from "../model/types.js";
2
+ import type { StyleBuilder } from "./style-builder.js";
3
+ import { type MapperContext } from "./drawable-mapper.js";
4
+ export declare function mapSlide(slide: Slide, presentation: Presentation, styles: StyleBuilder, attachments: Map<string, Buffer>, ctx: MapperContext, slideWidthPx: number, slideHeightPx: number): string;
@@ -0,0 +1,112 @@
1
+ import { resolveColor } from "../resolve/color-resolver.js";
2
+ import { mapDrawable } from "./drawable-mapper.js";
3
+ import { nextAttachmentIndex } from "./image-mapper.js";
4
+ export function mapSlide(slide, presentation, styles, attachments, ctx, slideWidthPx, slideHeightPx) {
5
+ const parts = [];
6
+ // Background div — supports solid color and image fills
7
+ const bg = slide.background
8
+ ?? slide.slideLayout.background
9
+ ?? slide.slideLayout.slideMaster.background;
10
+ const bgCssParts = [`top:0`, `left:0`, `width:${slideWidthPx}`, `height:${slideHeightPx}`, `position:absolute`];
11
+ if (bg?.fill?.type === "image" && bg.fill.blipData) {
12
+ // Image background: render as img src (OfficeImport uses img tags, not CSS background)
13
+ const ext = detectBgImageExt(bg.fill.blipData);
14
+ const idx = nextAttachmentIndex();
15
+ const name = `Attachment${idx}.${ext}`;
16
+ attachments.set(name, bg.fill.blipData);
17
+ bgCssParts.push("background-color:#ffffff");
18
+ const bgClass = styles.addClass(bgCssParts.join("; ") + ";");
19
+ parts.push(`<div class="${bgClass}"></div>`);
20
+ parts.push(`<img src="${name}" style="position:absolute; top:0; left:0; width:${slideWidthPx}; height:${slideHeightPx};">`);
21
+ }
22
+ else {
23
+ const bgFillCss = resolveBackgroundFillCss(bg, ctx.colorMap, ctx.colorScheme);
24
+ bgCssParts.push(bgFillCss);
25
+ const bgClass = styles.addClass(bgCssParts.join("; ") + ";");
26
+ parts.push(`<div class="${bgClass}"></div>`);
27
+ }
28
+ // Drawables in z-order: master → layout → slide
29
+ for (const layer of [slide.slideLayout.slideMaster.drawables, slide.slideLayout.drawables]) {
30
+ for (const drawable of layer) {
31
+ if (drawable.placeholder)
32
+ continue;
33
+ const html = mapDrawable(drawable, styles, attachments, ctx);
34
+ if (html)
35
+ parts.push(html);
36
+ }
37
+ }
38
+ // Slide drawables — pass index for nanov2 bleed cache lookup
39
+ for (let i = 0; i < slide.drawables.length; i++) {
40
+ const html = mapDrawable(slide.drawables[i], styles, attachments, ctx, i);
41
+ if (html)
42
+ parts.push(html);
43
+ }
44
+ return parts.join("");
45
+ }
46
+ function resolveBackgroundFillCss(bg, colorMap, colorScheme) {
47
+ if (bg?.fill) {
48
+ const prop = fillToCssProp(bg.fill, colorMap, colorScheme);
49
+ if (prop)
50
+ return prop;
51
+ }
52
+ return "background-color:#ffffff";
53
+ }
54
+ function detectBgImageExt(buf) {
55
+ if (buf[0] === 0x89 && buf[1] === 0x50)
56
+ return "png";
57
+ if (buf[0] === 0xFF && buf[1] === 0xD8)
58
+ return "jpeg";
59
+ return "png";
60
+ }
61
+ function fillToCssProp(fill, colorMap, colorScheme) {
62
+ if (fill.type === "solid") {
63
+ return `background-color:${rgbaToColor(resolveColor(fill.color, colorMap, colorScheme))}`;
64
+ }
65
+ if (fill.type === "gradient" && fill.stops.length > 0) {
66
+ if (fill.stops.length === 2) {
67
+ const c1 = rgbaToColor(resolveColor(fill.stops[0].color, colorMap, colorScheme));
68
+ const c2 = rgbaToColor(resolveColor(fill.stops[1].color, colorMap, colorScheme));
69
+ const { from, to } = gradientPoints(fill.linear?.angle ?? 5400000);
70
+ return `background-image:-webkit-gradient(linear, ${from}, ${to}, from(${c1}), to(${c2}))`;
71
+ }
72
+ // 3+ stops: average to single color
73
+ let r = 0, g = 0, b = 0;
74
+ for (const stop of fill.stops) {
75
+ const c = resolveColor(stop.color, colorMap, colorScheme);
76
+ r += c.r;
77
+ g += c.g;
78
+ b += c.b;
79
+ }
80
+ const n = fill.stops.length;
81
+ return `background-color:${rgbaToColor({ r: Math.round(r / n), g: Math.round(g / n), b: Math.round(b / n), a: 1 })}`;
82
+ }
83
+ if (fill.type === "noFill")
84
+ return null;
85
+ return null;
86
+ }
87
+ function gradientPoints(angleDeg60k) {
88
+ const a = ((angleDeg60k / 60000) % 360 + 360) % 360;
89
+ if (a < 22.5 || a >= 337.5)
90
+ return { from: "left top", to: "right top" };
91
+ if (a < 67.5)
92
+ return { from: "left bottom", to: "right top" };
93
+ if (a < 112.5)
94
+ return { from: "left bottom", to: "left top" };
95
+ if (a < 157.5)
96
+ return { from: "right bottom", to: "left top" };
97
+ if (a < 202.5)
98
+ return { from: "right top", to: "left top" };
99
+ if (a < 247.5)
100
+ return { from: "right top", to: "left bottom" };
101
+ if (a < 292.5)
102
+ return { from: "left top", to: "left bottom" };
103
+ return { from: "left top", to: "right bottom" };
104
+ }
105
+ function rgbaToColor(c) {
106
+ if (c.a < 1)
107
+ return `rgba(${c.r},${c.g},${c.b},${c.a.toFixed(2)})`;
108
+ return `#${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`;
109
+ }
110
+ function hex2(n) {
111
+ return n.toString(16).padStart(2, "0");
112
+ }
@@ -0,0 +1,12 @@
1
+ /** Deduplicating CSS class builder. Mirrors CMStyle from OfficeImport. */
2
+ export declare class StyleBuilder {
3
+ private counter;
4
+ private cache;
5
+ /** Register CSS declarations and get back the class name (e.g. "s5"). */
6
+ addClass(css: string): string;
7
+ /** Emit the <style> block content with triple-specificity selectors. */
8
+ getStyleSheet(): string;
9
+ /** Reset counter and cache (called per-slide). */
10
+ reset(startIndex: number): void;
11
+ get nextIndex(): number;
12
+ }
@@ -0,0 +1,30 @@
1
+ /** Deduplicating CSS class builder. Mirrors CMStyle from OfficeImport. */
2
+ export class StyleBuilder {
3
+ counter = 0;
4
+ cache = new Map();
5
+ /** Register CSS declarations and get back the class name (e.g. "s5"). */
6
+ addClass(css) {
7
+ const existing = this.cache.get(css);
8
+ if (existing)
9
+ return existing;
10
+ const name = `s${this.counter++}`;
11
+ this.cache.set(css, name);
12
+ return name;
13
+ }
14
+ /** Emit the <style> block content with triple-specificity selectors. */
15
+ getStyleSheet() {
16
+ const lines = [];
17
+ for (const [css, name] of this.cache) {
18
+ lines.push(`.${name}.${name}.${name} {${css}}`);
19
+ }
20
+ return lines.join("\n");
21
+ }
22
+ /** Reset counter and cache (called per-slide). */
23
+ reset(startIndex) {
24
+ this.counter = startIndex;
25
+ this.cache.clear();
26
+ }
27
+ get nextIndex() {
28
+ return this.counter;
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ import type { TextBody, OrientedBounds, ColorMap, ColorScheme, FontScheme, Slide, Color } from "../model/types.js";
2
+ import type { PlaceholderType } from "../model/enums.js";
3
+ import type { StyleBuilder } from "./style-builder.js";
4
+ export interface TextMapperContext {
5
+ colorMap: ColorMap;
6
+ colorScheme: ColorScheme;
7
+ fontScheme: FontScheme;
8
+ slide?: Slide;
9
+ placeholderType?: PlaceholderType;
10
+ fontRefColor?: Color;
11
+ }
12
+ export declare function mapTextBody(textBody: TextBody, bounds: OrientedBounds, styles: StyleBuilder, ctx: TextMapperContext): string;
13
+ /** Render just the paragraphs (no margin wrapper). Used by table cells which provide their own wrapper. */
14
+ export declare function mapTextParagraphs(textBody: TextBody, styles: StyleBuilder, ctx: TextMapperContext): string;
@@ -0,0 +1,302 @@
1
+ import { resolveColor } from "../resolve/color-resolver.js";
2
+ import { resolveFontFamily } from "../resolve/font-map.js";
3
+ import { resolveParagraphProperties } from "../resolve/inheritance.js";
4
+ import { emuToPx } from "./constants.js";
5
+ // Default text body insets in EMU (OOXML spec defaults)
6
+ const DEFAULT_L_INS = 91440;
7
+ const DEFAULT_R_INS = 91440;
8
+ const DEFAULT_T_INS = 45720;
9
+ const DEFAULT_B_INS = 45720;
10
+ const DEFAULT_FONT_SIZE = 1800; // 18pt in hundredths
11
+ // Track auto-number counters per scheme within mapTextParagraphs/mapTextBody calls
12
+ let paraAutoNumCounters = new Map();
13
+ // normAutoFit font scale factor (1.0 = no scaling)
14
+ let currentFontScale = 1;
15
+ export function mapTextBody(textBody, bounds, styles, ctx) {
16
+ const bp = textBody.properties;
17
+ const lIns = emuToPx(bp?.lIns ?? DEFAULT_L_INS);
18
+ const rIns = emuToPx(bp?.rIns ?? DEFAULT_R_INS);
19
+ const tIns = emuToPx(bp?.tIns ?? DEFAULT_T_INS);
20
+ const bIns = emuToPx(bp?.bIns ?? DEFAULT_B_INS);
21
+ const outerW = emuToPx(bounds.cx);
22
+ const outerH = emuToPx(bounds.cy);
23
+ // Vertical alignment
24
+ const anchor = bp?.anchor ?? "t";
25
+ const needsTable = anchor !== "t";
26
+ const marginCss = `margin-top:${tIns}px; margin-left:${lIns}px; margin-bottom:${bIns}px; margin-right:${rIns}px;`;
27
+ paraAutoNumCounters = new Map();
28
+ // normAutoFit: OfficeImport scales font sizes by fontScale percentage to fit text in shape
29
+ const fontScale = textBody.properties?.fontScale;
30
+ if (fontScale && fontScale !== 100000) {
31
+ currentFontScale = fontScale / 100000; // 100000 = 100%
32
+ }
33
+ else {
34
+ currentFontScale = 1;
35
+ }
36
+ // QL registers wrapper/table CSS classes BEFORE paragraph CSS.
37
+ // For table case: inner (table-cell) first, then outer (table), then paragraphs.
38
+ if (needsTable) {
39
+ const vAlign = anchor === "ctr" ? "middle" : "bottom";
40
+ let outerCss;
41
+ if (anchor === "b") {
42
+ outerCss = `${marginCss} display:table; top:-10000; left:0; width:${outerW}; height:${outerH + 10000}; position:absolute;`;
43
+ }
44
+ else {
45
+ outerCss = `${marginCss} display:table; width:${outerW}; height:${outerH};`;
46
+ }
47
+ const innerCss = `display:table-cell; vertical-align:${vAlign};`;
48
+ const innerClass = styles.addClass(innerCss);
49
+ const outerClass = styles.addClass(outerCss);
50
+ const paragraphs = textBody.paragraphs.map((p, i) => mapParagraph(p, styles, ctx, i)).join("");
51
+ return `<div class="${outerClass}"><div class="${innerClass}">${paragraphs}</div></div>`;
52
+ }
53
+ const wrapperClass = styles.addClass(marginCss);
54
+ const paragraphs = textBody.paragraphs.map((p, i) => mapParagraph(p, styles, ctx, i)).join("");
55
+ return `<div class="${wrapperClass}">${paragraphs}</div>`;
56
+ }
57
+ /** Render just the paragraphs (no margin wrapper). Used by table cells which provide their own wrapper. */
58
+ export function mapTextParagraphs(textBody, styles, ctx) {
59
+ paraAutoNumCounters = new Map();
60
+ return textBody.paragraphs.map((p, i) => mapParagraph(p, styles, ctx, i)).join("");
61
+ }
62
+ function mapParagraph(para, styles, ctx, paraIndex = 0) {
63
+ const pp = para.properties;
64
+ const parts = [];
65
+ const pCss = [];
66
+ // Resolve paragraph properties through inheritance chain
67
+ const level = pp?.level ?? 0;
68
+ const resolved = ctx.slide
69
+ ? resolveParagraphProperties(pp, ctx.slide, level, ctx.placeholderType)
70
+ : pp;
71
+ // Line height: only when explicitly set, formula: 1.2 * (pctVal / 100000)
72
+ const lh = resolved?.lineSpacing;
73
+ if (lh && lh.type === "pct" && lh.val !== 100000) {
74
+ pCss.push(`line-height:${(1.2 * lh.val / 100000).toFixed(6)}`);
75
+ }
76
+ if (resolved?.align && resolved.align !== "l") {
77
+ const alignMap = { ctr: "center", r: "right", just: "justify", dist: "justify" };
78
+ const mapped = alignMap[resolved.align];
79
+ if (mapped)
80
+ pCss.push(`text-align:${mapped}`);
81
+ }
82
+ // Font-family: local paragraph → endParaRPr → inherited
83
+ const fontSource = pp?.latinFont ?? para.endParaRPr?.latinFont ?? resolved?.latinFont;
84
+ let resolvedParaFont;
85
+ if (fontSource) {
86
+ resolvedParaFont = resolveFontFamily(fontSource, ctx.fontScheme);
87
+ pCss.push(`font-family:"${resolvedParaFont}"`);
88
+ }
89
+ // Font-size: local paragraph → endParaRPr → inherited → default
90
+ // Apply normAutoFit fontScale if active
91
+ const baseFontSize = pp?.fontSize ?? para.endParaRPr?.fontSize ?? resolved?.fontSize ?? DEFAULT_FONT_SIZE;
92
+ const baseFontPx = Math.trunc((baseFontSize * currentFontScale) / 100);
93
+ pCss.push(`font-size:${baseFontPx}`);
94
+ // Space before/after paragraphs — OfficeImport uses padding, not margin (confirmed by probes)
95
+ // spcBefore is skipped on the first paragraph (QL doesn't apply it there)
96
+ const spcBefore = resolved?.spaceBefore ?? pp?.spaceBefore;
97
+ const spcAfter = resolved?.spaceAfter ?? pp?.spaceAfter;
98
+ if (spcBefore && paraIndex > 0) {
99
+ if (spcBefore.type === "pts")
100
+ pCss.push(`padding-top:${Math.trunc(spcBefore.val / 100)}px`);
101
+ else if (spcBefore.type === "pct")
102
+ pCss.push(`padding-top:${(spcBefore.val / 1000).toFixed(1)}%`);
103
+ }
104
+ if (spcAfter) {
105
+ if (spcAfter.type === "pts")
106
+ pCss.push(`padding-bottom:${Math.trunc(spcAfter.val / 100)}px`);
107
+ else if (spcAfter.type === "pct")
108
+ pCss.push(`padding-bottom:${(spcAfter.val / 1000).toFixed(1)}%`);
109
+ }
110
+ // Paragraph margins from bullet/list formatting
111
+ const marL = resolved?.marL ?? pp?.marL;
112
+ const indent = resolved?.indent ?? pp?.indent;
113
+ if (marL != null && marL !== 0)
114
+ pCss.push(`margin-left:${emuToPx(marL)}px`);
115
+ if (indent != null && indent !== 0)
116
+ pCss.push(`text-indent:${emuToPx(indent)}px`);
117
+ const pClass = styles.addClass(pCss.join("; ") + ";");
118
+ // Emit bullet span if paragraph has a bullet
119
+ const bullet = resolved?.bullet ?? pp?.bullet;
120
+ if (bullet && bullet.type !== "none") {
121
+ const marLPx = marL != null ? emuToPx(marL) : 0;
122
+ const bulletMarginLeft = -marLPx;
123
+ // Resolve bullet color from properties (bulletColor → text color → tx1)
124
+ const bulletClr = resolved?.bulletColor ?? pp?.bulletColor;
125
+ const bulletColor = bulletClr
126
+ ? rgbaToHex(resolveColor(bulletClr, ctx.colorMap, ctx.colorScheme))
127
+ : "#000000";
128
+ // Bullet font size: bulletSizePoints, bulletSizePercent, or first run's font size
129
+ const bulletSizePts = resolved?.bulletSizePoints ?? pp?.bulletSizePoints;
130
+ const bulletSizePct = resolved?.bulletSizePercent ?? pp?.bulletSizePercent;
131
+ const firstRunFontSize = para.runs[0]?.properties?.fontSize;
132
+ const bulletFontPx = bulletSizePts
133
+ ? Math.trunc(bulletSizePts / 100)
134
+ : bulletSizePct
135
+ ? Math.trunc(baseFontPx * bulletSizePct / 100000)
136
+ : firstRunFontSize
137
+ ? Math.trunc((firstRunFontSize * currentFontScale) / 100)
138
+ : baseFontPx;
139
+ // OfficeImport bullet padding scales with font size.
140
+ // Empirically: ~3x font size for char bullets, ~3.2x for autonum.
141
+ const bulletPaddingRight = bullet.type === "autoNum"
142
+ ? Math.round(bulletFontPx * 3.17)
143
+ : Math.round(bulletFontPx * 2.94);
144
+ // Bullet font family
145
+ const bulletFontFamily = resolved?.bulletFont ?? pp?.bulletFont;
146
+ let bulletFontCss = "";
147
+ if (bulletFontFamily) {
148
+ bulletFontCss = `font-family:"${bulletFontFamily}"; `;
149
+ }
150
+ let bulletText = "";
151
+ if (bullet.type === "char" && bullet.char) {
152
+ bulletText = escapeHtml(bullet.char);
153
+ }
154
+ else if (bullet.type === "autoNum") {
155
+ bulletText = escapeHtml(formatAutoNumber(bullet.autoNumScheme, bullet.startAt ?? 1, paraAutoNumCounters.get(bullet.autoNumScheme ?? "arabicPeriod") ?? 0));
156
+ const scheme = bullet.autoNumScheme ?? "arabicPeriod";
157
+ paraAutoNumCounters.set(scheme, (paraAutoNumCounters.get(scheme) ?? 0) + 1);
158
+ }
159
+ if (bulletText) {
160
+ const bulletCss = `margin-left:${bulletMarginLeft}px; padding-right:${bulletPaddingRight}px; font-size: ${bulletFontPx}; color:${bulletColor}; ${bulletFontCss}`;
161
+ const bulletClass = styles.addClass(bulletCss);
162
+ parts.push(`<span class="${bulletClass}">${bulletText}</span>`);
163
+ }
164
+ }
165
+ for (const run of para.runs) {
166
+ if (run.type === "br") {
167
+ parts.push("<br>");
168
+ continue;
169
+ }
170
+ const text = escapeHtml(String(run.text ?? ""));
171
+ if (!text)
172
+ continue;
173
+ const rp = run.properties;
174
+ const spanCss = buildRunCss(rp, baseFontSize, ctx, resolvedParaFont);
175
+ if (spanCss) {
176
+ const spanClass = styles.addClass(spanCss);
177
+ parts.push(`<span class="${spanClass}">${text}</span>`);
178
+ }
179
+ else {
180
+ parts.push(`<span>${text}</span>`);
181
+ }
182
+ }
183
+ // QL emits &nbsp; for empty paragraphs (no span wrapper) to preserve line height.
184
+ const content = parts.length > 0 ? parts.join("") : "\u00a0";
185
+ return `<p class="${pClass}">${content}</p>`;
186
+ }
187
+ function buildRunCss(rp, baseFontSize, ctx, paraFontFamily) {
188
+ if (!rp)
189
+ return "";
190
+ const parts = [];
191
+ const color = runColor(rp, ctx);
192
+ parts.push(`color:${color}`);
193
+ if (rp.bold)
194
+ parts.push("font-weight:bold");
195
+ if (rp.italic)
196
+ parts.push("font-style:italic");
197
+ const fontSize = rp.fontSize ?? baseFontSize;
198
+ const fontPx = Math.trunc((fontSize * currentFontScale) / 100);
199
+ parts.push(`font-size:${fontPx}`);
200
+ // OfficeImport always emits font-family on spans, inheriting from paragraph if not set on run
201
+ if (rp.latinFont) {
202
+ const family = resolveFontFamily(rp.latinFont, ctx.fontScheme);
203
+ parts.push(`font-family:"${family}"`);
204
+ }
205
+ else if (paraFontFamily) {
206
+ parts.push(`font-family:"${paraFontFamily}"`);
207
+ }
208
+ if (rp.underline && rp.underline !== "none")
209
+ parts.push("text-decoration:underline");
210
+ if (rp.strikethrough === "sngStrike")
211
+ parts.push("text-decoration:line-through");
212
+ if (rp.caps === "all")
213
+ parts.push("text-transform:uppercase");
214
+ if (rp.caps === "small")
215
+ parts.push("font-variant:small-caps");
216
+ // Superscript/subscript: OfficeImport just renders with reduced font size,
217
+ // does NOT use CSS vertical-align:super/sub. The baseline attribute adjusts
218
+ // the font size but the text stays on the same baseline visually.
219
+ // (Confirmed by QL output: no vertical-align in CSS for baseline text)
220
+ // Character spacing (hundredths of a point → px, 1pt = 1px at 72dpi)
221
+ if (rp.spacing != null && rp.spacing !== 0) {
222
+ const spacingPx = rp.spacing / 100;
223
+ const spacingStr = Number.isInteger(spacingPx) ? `${spacingPx}` : spacingPx.toFixed(1);
224
+ parts.push(`letter-spacing:${spacingStr}px`);
225
+ }
226
+ return parts.join("; ") + ";";
227
+ }
228
+ function runColor(rp, ctx) {
229
+ if (rp.fill && rp.fill.type === "solid") {
230
+ const rgba = resolveColor(rp.fill.color, ctx.colorMap, ctx.colorScheme);
231
+ return rgbaToHex(rgba);
232
+ }
233
+ // fontRef color from shapeStyle: fallback when run has no explicit color
234
+ if (ctx.fontRefColor) {
235
+ const rgba = resolveColor(ctx.fontRefColor, ctx.colorMap, ctx.colorScheme);
236
+ return rgbaToHex(rgba);
237
+ }
238
+ const tx1Scheme = ctx.colorMap.mappings.tx1;
239
+ if (tx1Scheme) {
240
+ const rgba = resolveColor({ type: "scheme", val: tx1Scheme }, ctx.colorMap, ctx.colorScheme);
241
+ return rgbaToHex(rgba);
242
+ }
243
+ return "#000000";
244
+ }
245
+ function rgbaToHex(c) {
246
+ if (c.a < 1) {
247
+ return `rgba(${c.r},${c.g},${c.b},${c.a.toFixed(2)})`;
248
+ }
249
+ return `#${hex2(c.r)}${hex2(c.g)}${hex2(c.b)}`;
250
+ }
251
+ function hex2(n) {
252
+ return n.toString(16).padStart(2, "0");
253
+ }
254
+ function escapeHtml(s) {
255
+ return s
256
+ .replace(/&/g, "&amp;")
257
+ .replace(/</g, "&lt;")
258
+ .replace(/>/g, "&gt;")
259
+ .replace(/"/g, "&quot;")
260
+ // OfficeImport converts second space in consecutive pairs to &nbsp;
261
+ // to prevent HTML whitespace collapse. Match this behavior.
262
+ .replace(/ {2}/g, " &nbsp;");
263
+ }
264
+ function formatAutoNumber(scheme, startAt, index) {
265
+ const num = startAt + index;
266
+ switch (scheme) {
267
+ case "arabicPlain": return `${num}`;
268
+ case "arabicPeriod": return `${num}.`;
269
+ case "arabicParenR": return `${num})`;
270
+ case "arabicParenBoth": return `(${num})`;
271
+ case "romanLcPeriod": return `${toRoman(num).toLowerCase()}.`;
272
+ case "romanUcPeriod": return `${toRoman(num)}.`;
273
+ case "alphaLcPeriod": return `${toAlpha(num).toLowerCase()}.`;
274
+ case "alphaUcPeriod": return `${toAlpha(num)}.`;
275
+ case "alphaLcParenR": return `${toAlpha(num).toLowerCase()})`;
276
+ case "alphaUcParenR": return `${toAlpha(num)})`;
277
+ case "alphaLcParenBoth": return `(${toAlpha(num).toLowerCase()})`;
278
+ case "alphaUcParenBoth": return `(${toAlpha(num)})`;
279
+ default: return `${num}.`;
280
+ }
281
+ }
282
+ function toRoman(n) {
283
+ const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
284
+ const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
285
+ let result = "";
286
+ for (let i = 0; i < vals.length; i++) {
287
+ while (n >= vals[i]) {
288
+ result += syms[i];
289
+ n -= vals[i];
290
+ }
291
+ }
292
+ return result;
293
+ }
294
+ function toAlpha(n) {
295
+ let result = "";
296
+ while (n > 0) {
297
+ n--;
298
+ result = String.fromCharCode(65 + (n % 26)) + result;
299
+ n = Math.floor(n / 26);
300
+ }
301
+ return result;
302
+ }
@@ -0,0 +1,25 @@
1
+ export type SchemeColorName = "dk1" | "lt1" | "dk2" | "lt2" | "accent1" | "accent2" | "accent3" | "accent4" | "accent5" | "accent6" | "hlink" | "folHlink" | "tx1" | "tx2" | "bg1" | "bg2" | "phClr";
2
+ export type ColorMapIndex = "bg1" | "tx1" | "bg2" | "tx2" | "accent1" | "accent2" | "accent3" | "accent4" | "accent5" | "accent6" | "hlink" | "folHlink";
3
+ export type ColorTransformType = "tint" | "shade" | "comp" | "inv" | "gray" | "alpha" | "alphaOff" | "alphaMod" | "hue" | "hueOff" | "hueMod" | "sat" | "satOff" | "satMod" | "lum" | "lumOff" | "lumMod" | "red" | "redOff" | "redMod" | "green" | "greenOff" | "greenMod" | "blue" | "blueOff" | "blueMod";
4
+ export type PresetGeometry = "rect" | "roundRect" | "ellipse" | "triangle" | "rtTriangle" | "diamond" | "parallelogram" | "trapezoid" | "pentagon" | "hexagon" | "star4" | "star5" | "star6" | "star8" | "line" | "straightConnector1" | "rightArrow" | "leftArrow" | "upArrow" | "downArrow" | "chevron" | "ribbon" | "ribbon2" | "flowChartProcess" | "flowChartDecision" | "flowChartTerminator" | "callout1" | "callout2" | "callout3" | "cloud" | "heart" | "lightningBolt" | "frame" | "bevel" | "donut" | "noSmoking" | "blockArc" | "wedgeRoundRectCallout" | "wedgeEllipseCallout" | string;
5
+ export type TextAlignment = "l" | "ctr" | "r" | "just" | "dist";
6
+ export type TextAnchorType = "t" | "ctr" | "b";
7
+ export type WrapType = "square" | "none";
8
+ export type FlowType = "horz" | "vert" | "vert270" | "wordArtVert" | "wordArtVertRtl" | "eaVert";
9
+ export type TextAutoFitType = "noAutoFit" | "normAutoFit" | "spAutoFit";
10
+ export type UnderlineType = "none" | "sng" | "dbl" | "heavy" | "dotted" | "dottedHeavy" | "dash" | "dashHeavy" | "dashLong" | "dashLongHeavy" | "dotDash" | "dotDashHeavy" | "dotDotDash" | "dotDotDashHeavy" | "wavy" | "wavyHeavy" | "wavyDbl" | string;
11
+ export type StrikeThroughType = "noStrike" | "sngStrike" | "dblStrike";
12
+ export type CapsType = "none" | "small" | "all";
13
+ export type DashType = "solid" | "dot" | "dash" | "lgDash" | "dashDot" | "lgDashDot" | "lgDashDotDot" | "sysDash" | "sysDot" | "sysDashDot" | "sysDashDotDot";
14
+ export type LineCapType = "flat" | "sq" | "rnd";
15
+ export type LineJoinType = "round" | "bevel" | "miter";
16
+ export type CompoundType = "sng" | "dbl" | "thickThin" | "thinThick" | "tri";
17
+ export type LineEndType = "none" | "triangle" | "stealth" | "diamond" | "oval" | "arrow";
18
+ export type LineEndSize = "sm" | "med" | "lg";
19
+ export type FontReferenceIndex = "major" | "minor" | "none";
20
+ export type PlaceholderType = "title" | "body" | "ctrTitle" | "subTitle" | "dt" | "ftr" | "sldNum" | "sldImg" | "chart" | "tbl" | "clipArt" | "dgm" | "media" | "obj" | "pic" | string;
21
+ export type SlideLayoutType = "blank" | "title" | "obj" | "secHead" | "twoObj" | "titleOnly" | "cust" | "twoTxTwoObj" | "twoObjAndTx" | "twoObjOverTx" | "objTx" | "txObj" | "objOnly" | "picTx" | "txAndChart" | "txAndClipArt" | "txAndMedia" | "dgm" | "chart" | "txAndObj" | "clipArtAndTx" | "mediaAndTx" | "objAndTx" | "objOverTx" | "vertTitleAndTx" | "vertTx" | string;
22
+ export type BulletType = "char" | "autoNum" | "blip" | "none";
23
+ export type AutoNumberScheme = "arabicPlain" | "arabicPeriod" | "arabicParenR" | "arabicParenBoth" | "romanLcPeriod" | "romanUcPeriod" | "alphaLcPeriod" | "alphaUcPeriod" | "alphaLcParenR" | "alphaUcParenR" | "alphaLcParenBoth" | "alphaUcParenBoth" | string;
24
+ export type PatternType = "pct5" | "pct10" | "pct20" | "pct25" | "pct30" | "pct40" | "pct50" | "pct60" | "pct70" | "pct75" | "pct80" | "pct90" | "horz" | "vert" | "ltHorz" | "ltVert" | "dkHorz" | "dkVert" | "narHorz" | "narVert" | "wdHorz" | "wdVert" | "dashHorz" | "dashVert" | "cross" | "dnDiag" | "upDiag" | "ltDnDiag" | "ltUpDiag" | "dkDnDiag" | "dkUpDiag" | "wdDnDiag" | "wdUpDiag" | "dashDnDiag" | "dashUpDiag" | "diagCross" | "smCheck" | "lgCheck" | "smGrid" | "lgGrid" | "dotGrid" | "smConfetti" | "lgConfetti" | "horzBrick" | "diagBrick" | "solidDmnd" | "openDmnd" | "dotDmnd" | "plaid" | "sphere" | "weave" | "divot" | "shingle" | "wave" | "trellis" | "zigZag" | string;
25
+ export type ShadowAlignment = "tl" | "t" | "tr" | "l" | "ctr" | "r" | "bl" | "b" | "br";
@@ -0,0 +1,2 @@
1
+ // Mirrors OAD* enum types from OfficeImport headers
2
+ export {};