@agent-native/core 0.157.8 → 0.157.9
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/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +26 -8
- package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +258 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +59 -27
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
- package/corpus/templates/slides/actions/export-pptx.ts +261 -71
- package/corpus/templates/slides/actions/import-file.ts +12 -0
- package/corpus/templates/slides/actions/import-pptx.ts +9 -0
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +45 -5
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +69 -1
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +316 -64
- package/corpus/templates/slides/server/lib/source-import.ts +10 -1
- package/dist/agent/harness/migrations.d.ts +4 -0
- package/dist/agent/harness/migrations.js +39 -0
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.js +4 -0
- package/dist/agent/run-loop-with-resume.d.ts +1 -0
- package/dist/agent/run-loop-with-resume.js +45 -30
- package/dist/agent/run-migrations.d.ts +8 -0
- package/dist/agent/run-migrations.js +61 -0
- package/dist/chat-threads/migrations.d.ts +1 -0
- package/dist/chat-threads/migrations.js +1 -0
- package/dist/chat-threads/schema-migrations.d.ts +8 -0
- package/dist/chat-threads/schema-migrations.js +60 -0
- package/dist/client/AssistantChat.js +4 -0
- package/dist/client/chat/message-components.d.ts +2 -0
- package/dist/client/chat/message-components.js +11 -1
- package/dist/client/observability/ThumbsFeedback.js +35 -22
- package/dist/client/observability/useObservability.d.ts +1 -1
- package/dist/client/observability/useObservability.js +6 -3
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/feature-flags/a2a-action-route.js +4 -2
- package/dist/feature-flags/permissions.d.ts +2 -0
- package/dist/feature-flags/permissions.js +6 -1
- package/dist/ingestion/pptx.d.ts +14 -1
- package/dist/ingestion/pptx.js +222 -41
- package/dist/localization/core-messages/ar-SA.js +4 -0
- package/dist/localization/core-messages/de-DE.js +4 -0
- package/dist/localization/core-messages/en-US.d.ts +4 -0
- package/dist/localization/core-messages/en-US.js +4 -0
- package/dist/localization/core-messages/es-ES.js +4 -0
- package/dist/localization/core-messages/fr-FR.js +4 -0
- package/dist/localization/core-messages/hi-IN.js +4 -0
- package/dist/localization/core-messages/ja-JP.js +4 -0
- package/dist/localization/core-messages/ko-KR.js +4 -0
- package/dist/localization/core-messages/pt-BR.js +4 -0
- package/dist/localization/core-messages/zh-CN.js +4 -0
- package/dist/localization/core-messages/zh-TW.js +4 -0
- package/dist/localization/core-messages.d.ts +4 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/feedback.js +40 -16
- package/dist/observability/routes.d.ts +6 -6
- package/dist/observability/routes.js +15 -3
- package/dist/observability/store.js +2 -0
- package/dist/observability/traces.js +5 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +10 -10
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +7 -5
- package/dist/server/action-routes.js +9 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/better-auth-instance.js +4 -0
- package/dist/server/realtime-voice.js +2 -1
- package/dist/server/release-migrations.js +16 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/usage/migrations.d.ts +4 -0
- package/dist/usage/migrations.js +45 -0
- package/package.json +3 -3
|
@@ -5,6 +5,7 @@ import { defineAction } from "@agent-native/core";
|
|
|
5
5
|
import { ssrfSafeFetch } from "@agent-native/core/extensions/url-safety";
|
|
6
6
|
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
7
7
|
import { resolveAccess } from "@agent-native/core/sharing";
|
|
8
|
+
import type PptxGenJS from "pptxgenjs";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
|
|
10
11
|
import "../server/db/index.js"; // ensure registerShareableResource runs
|
|
@@ -19,6 +20,9 @@ import {
|
|
|
19
20
|
ASPECT_RATIO_VALUES,
|
|
20
21
|
} from "../shared/aspect-ratios.js";
|
|
21
22
|
|
|
23
|
+
type TableCell = PptxGenJS.TableCell;
|
|
24
|
+
type TableRow = PptxGenJS.TableRow;
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
27
|
* Extract inline style value for a given property from a style string.
|
|
24
28
|
*/
|
|
@@ -29,42 +33,76 @@ function getStyle(style: string, prop: string): string | null {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
/**
|
|
32
|
-
* Convert a CSS color string to a 6-char hex string (no #)
|
|
36
|
+
* Convert a CSS color string to a 6-char hex string (no #) plus an optional
|
|
37
|
+
* pptxgenjs transparency (0-100, percent transparent) carried from an
|
|
38
|
+
* alpha-bearing CSS color.
|
|
33
39
|
* Handles #hex, #shortHex, rgb(), rgba(), and named colors.
|
|
34
40
|
*/
|
|
35
|
-
function colorToHex(color: string): string {
|
|
36
|
-
if (!color) return "FFFFFF";
|
|
41
|
+
function colorToHex(color: string): { hex: string; transparency?: number } {
|
|
42
|
+
if (!color) return { hex: "FFFFFF" };
|
|
37
43
|
|
|
38
44
|
// Strip quotes / trim
|
|
39
45
|
color = color.replace(/['"]/g, "").trim();
|
|
40
46
|
|
|
41
47
|
// Already hex
|
|
42
|
-
if (/^#[0-9a-f]{
|
|
48
|
+
if (/^#[0-9a-f]{8}$/i.test(color)) {
|
|
49
|
+
const alpha = parseInt(color.slice(7, 9), 16) / 255;
|
|
50
|
+
return {
|
|
51
|
+
hex: color.slice(1, 7).toUpperCase(),
|
|
52
|
+
transparency: Math.round((1 - alpha) * 100),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (/^#[0-9a-f]{6}$/i.test(color))
|
|
56
|
+
return { hex: color.slice(1).toUpperCase() };
|
|
43
57
|
if (/^#[0-9a-f]{3}$/i.test(color)) {
|
|
44
58
|
const r = color[1],
|
|
45
59
|
g = color[2],
|
|
46
60
|
b = color[3];
|
|
47
|
-
return `${r}${r}${g}${g}${b}${b}`.toUpperCase();
|
|
61
|
+
return { hex: `${r}${r}${g}${g}${b}${b}`.toUpperCase() };
|
|
48
62
|
}
|
|
49
63
|
|
|
50
64
|
// rgb / rgba
|
|
51
65
|
const rgbMatch = color.match(
|
|
52
|
-
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*[\d.]+)?\s*\)/,
|
|
66
|
+
/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/,
|
|
53
67
|
);
|
|
54
68
|
if (rgbMatch) {
|
|
55
69
|
const hex = (n: string) => parseInt(n).toString(16).padStart(2, "0");
|
|
56
|
-
|
|
70
|
+
const result = {
|
|
71
|
+
hex: `${hex(rgbMatch[1])}${hex(rgbMatch[2])}${hex(rgbMatch[3])}`.toUpperCase(),
|
|
72
|
+
};
|
|
73
|
+
if (rgbMatch[4] === undefined) return result;
|
|
74
|
+
const alpha = parseFloat(rgbMatch[4]);
|
|
75
|
+
return { ...result, transparency: Math.round((1 - alpha) * 100) };
|
|
57
76
|
}
|
|
58
77
|
|
|
59
|
-
//
|
|
78
|
+
// Named colors used in the slide templates, plus the CSS Level 1 keyword set.
|
|
60
79
|
const named: Record<string, string> = {
|
|
61
80
|
white: "FFFFFF",
|
|
62
81
|
black: "000000",
|
|
63
82
|
transparent: "000000",
|
|
83
|
+
silver: "C0C0C0",
|
|
84
|
+
gray: "808080",
|
|
85
|
+
grey: "808080",
|
|
86
|
+
maroon: "800000",
|
|
87
|
+
red: "FF0000",
|
|
88
|
+
purple: "800080",
|
|
89
|
+
fuchsia: "FF00FF",
|
|
90
|
+
green: "008000",
|
|
91
|
+
lime: "00FF00",
|
|
92
|
+
olive: "808000",
|
|
93
|
+
yellow: "FFFF00",
|
|
94
|
+
navy: "000080",
|
|
95
|
+
blue: "0000FF",
|
|
96
|
+
teal: "008080",
|
|
97
|
+
aqua: "00FFFF",
|
|
64
98
|
};
|
|
65
|
-
|
|
99
|
+
const hex = named[color.toLowerCase()];
|
|
100
|
+
if (hex) return { hex };
|
|
66
101
|
|
|
67
|
-
|
|
102
|
+
console.warn(
|
|
103
|
+
`[export-pptx] unrecognized color "${color}", defaulting to white`,
|
|
104
|
+
);
|
|
105
|
+
return { hex: "FFFFFF" };
|
|
68
106
|
}
|
|
69
107
|
|
|
70
108
|
/**
|
|
@@ -79,11 +117,16 @@ function pxToIn(
|
|
|
79
117
|
}
|
|
80
118
|
|
|
81
119
|
/**
|
|
82
|
-
* Convert CSS font-size px to PowerPoint points
|
|
83
|
-
*
|
|
120
|
+
* Convert CSS font-size px to PowerPoint points, using this deck's actual
|
|
121
|
+
* px/inch ratio (like `pxToIn` above) instead of assuming 96 CSS px/inch —
|
|
122
|
+
* the ratio varies by aspect ratio (72 for 16:9/9:16, 108 for 1:1/4:5).
|
|
84
123
|
*/
|
|
85
|
-
function pxToPt(
|
|
86
|
-
|
|
124
|
+
function pxToPt(
|
|
125
|
+
px: number,
|
|
126
|
+
dims: { width: number; pptxInches: { w: number } },
|
|
127
|
+
): number {
|
|
128
|
+
const pxPerInch = dims.width / dims.pptxInches.w;
|
|
129
|
+
return Math.round((px / pxPerInch) * 72);
|
|
87
130
|
}
|
|
88
131
|
|
|
89
132
|
interface TextElement {
|
|
@@ -91,6 +134,7 @@ interface TextElement {
|
|
|
91
134
|
fontSize: number; // in pt
|
|
92
135
|
fontFace: string;
|
|
93
136
|
color: string; // 6-char hex
|
|
137
|
+
transparency?: number; // 0-100, percent transparent
|
|
94
138
|
bold: boolean;
|
|
95
139
|
x: number; // inches
|
|
96
140
|
y: number; // inches
|
|
@@ -109,6 +153,7 @@ interface TextRunElement {
|
|
|
109
153
|
fontSize?: number;
|
|
110
154
|
fontFace?: string;
|
|
111
155
|
color?: string;
|
|
156
|
+
transparency?: number; // 0-100, percent transparent
|
|
112
157
|
bold?: boolean;
|
|
113
158
|
italic?: boolean;
|
|
114
159
|
underline?: { style: "sng" };
|
|
@@ -130,14 +175,26 @@ interface ShapeElement {
|
|
|
130
175
|
w: number;
|
|
131
176
|
h: number;
|
|
132
177
|
fill?: string;
|
|
178
|
+
fillTransparency?: number; // 0-100, percent transparent
|
|
133
179
|
lineColor?: string;
|
|
180
|
+
lineTransparency?: number; // 0-100, percent transparent
|
|
134
181
|
lineWidth?: number;
|
|
135
182
|
rounded?: boolean;
|
|
136
183
|
order?: number;
|
|
137
184
|
}
|
|
138
185
|
|
|
186
|
+
interface TableElement {
|
|
187
|
+
rows: TableRow[];
|
|
188
|
+
x: number;
|
|
189
|
+
y: number;
|
|
190
|
+
w: number;
|
|
191
|
+
h: number;
|
|
192
|
+
order?: number;
|
|
193
|
+
}
|
|
194
|
+
|
|
139
195
|
interface GridElement {
|
|
140
196
|
color: string;
|
|
197
|
+
transparency?: number; // 0-100, percent transparent
|
|
141
198
|
stepX: number;
|
|
142
199
|
stepY: number;
|
|
143
200
|
offsetX: number;
|
|
@@ -165,6 +222,16 @@ export function assertServerPptxExportable(
|
|
|
165
222
|
throw error;
|
|
166
223
|
}
|
|
167
224
|
|
|
225
|
+
interface ParsedSlide {
|
|
226
|
+
texts: TextElement[];
|
|
227
|
+
images: ImageElement[];
|
|
228
|
+
shapes: ShapeElement[];
|
|
229
|
+
tables: TableElement[];
|
|
230
|
+
grid?: GridElement;
|
|
231
|
+
bgColor: string;
|
|
232
|
+
bgTransparency?: number; // 0-100, percent transparent
|
|
233
|
+
}
|
|
234
|
+
|
|
168
235
|
/**
|
|
169
236
|
* Parse slide HTML and extract text/image elements with positioning.
|
|
170
237
|
* We know the exact HTML structure from the slide templates.
|
|
@@ -173,13 +240,7 @@ export function parseSlideHtml(
|
|
|
173
240
|
html: string,
|
|
174
241
|
aspectRatio?: AspectRatio,
|
|
175
242
|
slideNumber = 1,
|
|
176
|
-
): {
|
|
177
|
-
texts: TextElement[];
|
|
178
|
-
images: ImageElement[];
|
|
179
|
-
shapes: ShapeElement[];
|
|
180
|
-
grid?: GridElement;
|
|
181
|
-
bgColor: string;
|
|
182
|
-
} {
|
|
243
|
+
): ParsedSlide {
|
|
183
244
|
assertServerPptxExportable(html, slideNumber);
|
|
184
245
|
const dims = getAspectRatioDims(aspectRatio);
|
|
185
246
|
if (html.includes('data-imported-pdf="true"')) {
|
|
@@ -193,6 +254,7 @@ export function parseSlideHtml(
|
|
|
193
254
|
const images: ImageElement[] = [];
|
|
194
255
|
const shapes: ShapeElement[] = [];
|
|
195
256
|
let bgColor = "000000";
|
|
257
|
+
let bgTransparency: number | undefined;
|
|
196
258
|
|
|
197
259
|
const slideW = dims.pptxInches.w;
|
|
198
260
|
const slideH = dims.pptxInches.h;
|
|
@@ -201,7 +263,11 @@ export function parseSlideHtml(
|
|
|
201
263
|
const slideStyleMatch = html.match(/class="fmd-slide"[^>]*style="([^"]*)"/);
|
|
202
264
|
if (slideStyleMatch) {
|
|
203
265
|
const bg = getStyle(slideStyleMatch[1], "background(?:-color)?");
|
|
204
|
-
if (bg)
|
|
266
|
+
if (bg) {
|
|
267
|
+
const parsedBg = colorToHex(bg);
|
|
268
|
+
bgColor = parsedBg.hex;
|
|
269
|
+
bgTransparency = parsedBg.transparency;
|
|
270
|
+
}
|
|
205
271
|
}
|
|
206
272
|
|
|
207
273
|
// Extract padding from the .fmd-slide wrapper
|
|
@@ -351,47 +417,44 @@ export function parseSlideHtml(
|
|
|
351
417
|
const mbStr = getStyle(style, "margin-bottom");
|
|
352
418
|
if (mbStr) marginBottom = parseInt(mbStr) || 0;
|
|
353
419
|
|
|
420
|
+
const parsedColor = colorToHex(color);
|
|
354
421
|
texts.push({
|
|
355
422
|
text,
|
|
356
|
-
fontSize: pxToPt(fontSize),
|
|
423
|
+
fontSize: pxToPt(fontSize, dims),
|
|
357
424
|
fontFace: "Poppins",
|
|
358
|
-
color:
|
|
425
|
+
color: parsedColor.hex,
|
|
426
|
+
transparency: parsedColor.transparency,
|
|
359
427
|
bold,
|
|
360
428
|
x: xMargin,
|
|
361
429
|
y: yPos,
|
|
362
430
|
w: contentW,
|
|
363
431
|
h: elHeight + 0.2,
|
|
364
432
|
letterSpacing: letterSpacing ? parseFloat(letterSpacing) : undefined,
|
|
365
|
-
lineSpacing: lineH
|
|
433
|
+
lineSpacing: lineH
|
|
434
|
+
? Math.round(lineH * pxToPt(fontSize, dims))
|
|
435
|
+
: undefined,
|
|
366
436
|
});
|
|
367
437
|
|
|
368
438
|
yPos += elHeight + pxToIn(marginBottom, dims) + 0.1;
|
|
369
439
|
}
|
|
370
440
|
}
|
|
371
441
|
|
|
372
|
-
return { texts, images, shapes, bgColor };
|
|
442
|
+
return { texts, images, shapes, tables: [], bgColor, bgTransparency };
|
|
373
443
|
}
|
|
374
444
|
|
|
375
445
|
type SlideDims = ReturnType<typeof getAspectRatioDims>;
|
|
376
446
|
|
|
377
|
-
function parseImportedPdfSlideHtml(
|
|
378
|
-
html: string,
|
|
379
|
-
dims: SlideDims,
|
|
380
|
-
): {
|
|
381
|
-
texts: TextElement[];
|
|
382
|
-
images: ImageElement[];
|
|
383
|
-
shapes: ShapeElement[];
|
|
384
|
-
grid?: GridElement;
|
|
385
|
-
bgColor: string;
|
|
386
|
-
} {
|
|
447
|
+
function parseImportedPdfSlideHtml(html: string, dims: SlideDims): ParsedSlide {
|
|
387
448
|
const outerStyle = html.match(
|
|
388
449
|
/class=["'][^"']*\bfmd-slide\b[^"']*["'][^>]*style=["']([^"']*)["']/i,
|
|
389
450
|
)?.[1];
|
|
390
|
-
const
|
|
451
|
+
const parsedBg = colorToHex(
|
|
391
452
|
outerStyle
|
|
392
453
|
? (getStyle(outerStyle, "background(?:-color)?") ?? "#000000") // guard:allow-raw-color - imported PDF fallback
|
|
393
454
|
: "#000000", // guard:allow-raw-color - imported PDF fallback
|
|
394
455
|
);
|
|
456
|
+
const bgColor = parsedBg.hex;
|
|
457
|
+
const bgTransparency = parsedBg.transparency;
|
|
395
458
|
const src = html.match(/<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/i)?.[1];
|
|
396
459
|
const outerAttrs = html.match(/<div\b([^>]*)>/i)?.[1] ?? "";
|
|
397
460
|
const sourceWidth = Number.parseFloat(
|
|
@@ -435,34 +498,30 @@ function parseImportedPdfSlideHtml(
|
|
|
435
498
|
]
|
|
436
499
|
: [],
|
|
437
500
|
shapes: [],
|
|
501
|
+
tables: [],
|
|
438
502
|
bgColor,
|
|
503
|
+
bgTransparency,
|
|
439
504
|
};
|
|
440
505
|
}
|
|
441
506
|
|
|
442
|
-
function parseImportedSlideHtml(
|
|
443
|
-
html: string,
|
|
444
|
-
dims: SlideDims,
|
|
445
|
-
): {
|
|
446
|
-
texts: TextElement[];
|
|
447
|
-
images: ImageElement[];
|
|
448
|
-
shapes: ShapeElement[];
|
|
449
|
-
grid?: GridElement;
|
|
450
|
-
bgColor: string;
|
|
451
|
-
} {
|
|
507
|
+
function parseImportedSlideHtml(html: string, dims: SlideDims): ParsedSlide {
|
|
452
508
|
const texts: TextElement[] = [];
|
|
453
509
|
const images: ImageElement[] = [];
|
|
454
510
|
const shapes: ShapeElement[] = [];
|
|
511
|
+
const tables: TableElement[] = [];
|
|
455
512
|
const outerStyle = html.match(
|
|
456
513
|
/class=["'][^"']*\bfmd-slide\b[^"']*["'][^>]*style=["']([^"']*)["']/i,
|
|
457
514
|
)?.[1];
|
|
458
|
-
const
|
|
515
|
+
const parsedBg = colorToHex(
|
|
459
516
|
outerStyle
|
|
460
517
|
? (getStyle(outerStyle, "background(?:-color)?") ?? "#000000") // guard:allow-raw-color - PPTX background fallback
|
|
461
518
|
: "#000000", // guard:allow-raw-color - PPTX background fallback
|
|
462
519
|
);
|
|
520
|
+
const bgColor = parsedBg.hex;
|
|
521
|
+
const bgTransparency = parsedBg.transparency;
|
|
463
522
|
const grid = outerStyle ? parseImportedGrid(outerStyle) : undefined;
|
|
464
523
|
const elementRegex =
|
|
465
|
-
/<div\b([^>]*\bdata-pptx-element-kind=["'](text|image|shape)["'][^>]*)>([\s\S]*?)<\/div>/gi;
|
|
524
|
+
/<div\b([^>]*\bdata-pptx-element-kind=["'](text|image|shape|table)["'][^>]*)>([\s\S]*?)<\/div>/gi;
|
|
466
525
|
let match: RegExpExecArray | null;
|
|
467
526
|
while ((match = elementRegex.exec(html)) !== null) {
|
|
468
527
|
const attrs = match[1];
|
|
@@ -485,14 +544,18 @@ function parseImportedSlideHtml(
|
|
|
485
544
|
const fill = getStyle(style, "background(?:-color)?");
|
|
486
545
|
const border = getStyle(style, "border");
|
|
487
546
|
const borderMatch = border?.match(/([\d.]+)px\s+solid\s+(.+)/i);
|
|
547
|
+
const parsedFill =
|
|
548
|
+
fill && !fill.includes("gradient") ? colorToHex(fill) : undefined;
|
|
549
|
+
const parsedLine = borderMatch ? colorToHex(borderMatch[2]) : undefined;
|
|
488
550
|
shapes.push({
|
|
489
551
|
...geometry,
|
|
490
|
-
...(
|
|
491
|
-
? { fill:
|
|
552
|
+
...(parsedFill
|
|
553
|
+
? { fill: parsedFill.hex, fillTransparency: parsedFill.transparency }
|
|
492
554
|
: {}),
|
|
493
|
-
...(borderMatch
|
|
555
|
+
...(borderMatch && parsedLine
|
|
494
556
|
? {
|
|
495
|
-
lineColor:
|
|
557
|
+
lineColor: parsedLine.hex,
|
|
558
|
+
lineTransparency: parsedLine.transparency,
|
|
496
559
|
lineWidth: Number(borderMatch[1]),
|
|
497
560
|
}
|
|
498
561
|
: {}),
|
|
@@ -502,7 +565,15 @@ function parseImportedSlideHtml(
|
|
|
502
565
|
continue;
|
|
503
566
|
}
|
|
504
567
|
|
|
505
|
-
|
|
568
|
+
if (kind === "table") {
|
|
569
|
+
const rows = importedTableRows(innerHtml, dims);
|
|
570
|
+
if (rows.length > 0) {
|
|
571
|
+
tables.push({ ...geometry, rows, order: match.index });
|
|
572
|
+
}
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const runs = importedTextRuns(innerHtml, dims);
|
|
506
577
|
const firstRun = runs.find((run) => run.text.trim()) ?? runs[0];
|
|
507
578
|
const firstParagraph = innerHtml.match(
|
|
508
579
|
/<p\b[^>]*style=["']([^"']*)["']/i,
|
|
@@ -517,6 +588,7 @@ function parseImportedSlideHtml(
|
|
|
517
588
|
fontSize,
|
|
518
589
|
fontFace: firstRun?.options.fontFace ?? "Poppins",
|
|
519
590
|
color: firstRun?.options.color ?? "FFFFFF",
|
|
591
|
+
transparency: firstRun?.options.transparency,
|
|
520
592
|
bold: firstRun?.options.bold ?? false,
|
|
521
593
|
align:
|
|
522
594
|
alignValue === "center" || alignValue === "right" ? alignValue : "left",
|
|
@@ -530,7 +602,7 @@ function parseImportedSlideHtml(
|
|
|
530
602
|
});
|
|
531
603
|
}
|
|
532
604
|
|
|
533
|
-
return { texts, images, shapes, grid, bgColor };
|
|
605
|
+
return { texts, images, shapes, tables, grid, bgColor, bgTransparency };
|
|
534
606
|
}
|
|
535
607
|
|
|
536
608
|
function parseImportedGrid(style: string): GridElement | undefined {
|
|
@@ -559,8 +631,10 @@ function parseImportedGrid(style: string): GridElement | undefined {
|
|
|
559
631
|
) {
|
|
560
632
|
return undefined;
|
|
561
633
|
}
|
|
634
|
+
const parsedColor = colorToHex(color);
|
|
562
635
|
return {
|
|
563
|
-
color:
|
|
636
|
+
color: parsedColor.hex,
|
|
637
|
+
transparency: parsedColor.transparency,
|
|
564
638
|
stepX: size[0],
|
|
565
639
|
stepY: size[1],
|
|
566
640
|
offsetX: position[0],
|
|
@@ -591,7 +665,7 @@ function pxToInY(px: number, dims: SlideDims): number {
|
|
|
591
665
|
return (px / dims.height) * dims.pptxInches.h;
|
|
592
666
|
}
|
|
593
667
|
|
|
594
|
-
function importedTextRuns(html: string): TextRunElement[] {
|
|
668
|
+
function importedTextRuns(html: string, dims: SlideDims): TextRunElement[] {
|
|
595
669
|
const paragraphs = [...html.matchAll(/<p\b[^>]*>([\s\S]*?)<\/p>/gi)].map(
|
|
596
670
|
(match) => match[1],
|
|
597
671
|
);
|
|
@@ -610,24 +684,105 @@ function importedTextRuns(html: string): TextRunElement[] {
|
|
|
610
684
|
const style = getAttribute(attrs, "style") ?? "";
|
|
611
685
|
const text = decodeHtmlText(stripTags(span[2]));
|
|
612
686
|
if (!text) continue;
|
|
613
|
-
runs.push({ text, options: importedRunOptions(style) });
|
|
687
|
+
runs.push({ text, options: importedRunOptions(style, dims) });
|
|
614
688
|
}
|
|
615
689
|
});
|
|
616
690
|
return runs;
|
|
617
691
|
}
|
|
618
692
|
|
|
619
|
-
function
|
|
693
|
+
function importedTableRows(html: string, dims: SlideDims): TableRow[] {
|
|
694
|
+
return [...html.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)].map(
|
|
695
|
+
(rowMatch): TableRow =>
|
|
696
|
+
[...rowMatch[1].matchAll(/<t[dh]\b([^>]*)>([\s\S]*?)<\/t[dh]>/gi)].map(
|
|
697
|
+
(cellMatch): TableCell => {
|
|
698
|
+
const attrs = cellMatch[1];
|
|
699
|
+
const cellHtml = cellMatch[2];
|
|
700
|
+
const style = getAttribute(attrs, "style") ?? "";
|
|
701
|
+
const runs = importedTextRuns(cellHtml, dims);
|
|
702
|
+
const fill = getStyle(style, "background(?:-color)?");
|
|
703
|
+
const parsedFill = fill ? colorToHex(fill) : undefined;
|
|
704
|
+
const border = getStyle(style, "border");
|
|
705
|
+
const borderMatch = border?.match(/([\d.]+)px\s+solid\s+(.+)/i);
|
|
706
|
+
const parsedBorder = borderMatch
|
|
707
|
+
? colorToHex(borderMatch[2])
|
|
708
|
+
: undefined;
|
|
709
|
+
const colSpan = Number.parseInt(
|
|
710
|
+
getAttribute(attrs, "colspan") ?? "",
|
|
711
|
+
10,
|
|
712
|
+
);
|
|
713
|
+
const rowSpan = Number.parseInt(
|
|
714
|
+
getAttribute(attrs, "rowspan") ?? "",
|
|
715
|
+
10,
|
|
716
|
+
);
|
|
717
|
+
const align = getStyle(style, "text-align");
|
|
718
|
+
const verticalAlign = getStyle(style, "vertical-align");
|
|
719
|
+
const options: NonNullable<TableCell["options"]> = {
|
|
720
|
+
...(align === "center" || align === "right" || align === "justify"
|
|
721
|
+
? { align }
|
|
722
|
+
: {}),
|
|
723
|
+
...(verticalAlign === "top" ||
|
|
724
|
+
verticalAlign === "middle" ||
|
|
725
|
+
verticalAlign === "bottom"
|
|
726
|
+
? { valign: verticalAlign }
|
|
727
|
+
: {}),
|
|
728
|
+
...(parsedFill
|
|
729
|
+
? {
|
|
730
|
+
fill: {
|
|
731
|
+
color: parsedFill.hex,
|
|
732
|
+
...(parsedFill.transparency != null
|
|
733
|
+
? { transparency: parsedFill.transparency }
|
|
734
|
+
: {}),
|
|
735
|
+
},
|
|
736
|
+
}
|
|
737
|
+
: {}),
|
|
738
|
+
...(parsedBorder && borderMatch
|
|
739
|
+
? {
|
|
740
|
+
border: {
|
|
741
|
+
type: "solid" as const,
|
|
742
|
+
color: parsedBorder.hex,
|
|
743
|
+
pt: Math.max(
|
|
744
|
+
0.5,
|
|
745
|
+
pxToPt(Number.parseFloat(borderMatch[1]), dims),
|
|
746
|
+
),
|
|
747
|
+
},
|
|
748
|
+
}
|
|
749
|
+
: {}),
|
|
750
|
+
...(Number.isFinite(colSpan) && colSpan > 1
|
|
751
|
+
? { colspan: colSpan }
|
|
752
|
+
: {}),
|
|
753
|
+
...(Number.isFinite(rowSpan) && rowSpan > 1
|
|
754
|
+
? { rowspan: rowSpan }
|
|
755
|
+
: {}),
|
|
756
|
+
};
|
|
757
|
+
const text: TableCell["text"] =
|
|
758
|
+
runs.length > 0
|
|
759
|
+
? runs.map((run) => ({ text: run.text, options: run.options }))
|
|
760
|
+
: "";
|
|
761
|
+
return { text, options };
|
|
762
|
+
},
|
|
763
|
+
),
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function importedRunOptions(
|
|
768
|
+
style: string,
|
|
769
|
+
dims: SlideDims,
|
|
770
|
+
): TextRunElement["options"] {
|
|
620
771
|
const fontSizePx = cssPx(style, "font-size");
|
|
621
772
|
const fontFamily = getStyle(style, "font-family")
|
|
622
773
|
?.replace(/["']/g, "")
|
|
623
774
|
.split(",")[0]
|
|
624
775
|
?.trim();
|
|
625
776
|
const fontWeight = getStyle(style, "font-weight");
|
|
777
|
+
const colorValue = getStyle(style, "color");
|
|
778
|
+
const parsedColor = colorValue
|
|
779
|
+
? colorToHex(colorValue) // guard:allow-raw-color - PPTX text fallback
|
|
780
|
+
: undefined;
|
|
626
781
|
return {
|
|
627
|
-
...(fontSizePx != null ? { fontSize: pxToPt(fontSizePx) } : {}),
|
|
782
|
+
...(fontSizePx != null ? { fontSize: pxToPt(fontSizePx, dims) } : {}),
|
|
628
783
|
...(fontFamily ? { fontFace: fontFamily } : {}),
|
|
629
|
-
...(
|
|
630
|
-
? { color:
|
|
784
|
+
...(parsedColor
|
|
785
|
+
? { color: parsedColor.hex, transparency: parsedColor.transparency }
|
|
631
786
|
: {}),
|
|
632
787
|
...(fontWeight
|
|
633
788
|
? {
|
|
@@ -771,13 +926,13 @@ export default defineAction({
|
|
|
771
926
|
slide && typeof slide === "object" && typeof slide.content === "string"
|
|
772
927
|
? slide.content
|
|
773
928
|
: "";
|
|
774
|
-
const { texts, images, shapes, grid, bgColor } =
|
|
775
|
-
slideContent,
|
|
776
|
-
aspectRatio,
|
|
777
|
-
slideIndex + 1,
|
|
778
|
-
);
|
|
929
|
+
const { texts, images, shapes, tables, grid, bgColor, bgTransparency } =
|
|
930
|
+
parseSlideHtml(slideContent, aspectRatio, slideIndex + 1);
|
|
779
931
|
|
|
780
|
-
pptxSlide.background = {
|
|
932
|
+
pptxSlide.background = {
|
|
933
|
+
color: bgColor,
|
|
934
|
+
...(bgTransparency != null ? { transparency: bgTransparency } : {}),
|
|
935
|
+
};
|
|
781
936
|
|
|
782
937
|
if (grid) {
|
|
783
938
|
const gridWidth = pxToIn(grid.stepX, dims);
|
|
@@ -785,6 +940,13 @@ export default defineAction({
|
|
|
785
940
|
const gridX = pxToIn(grid.offsetX, dims);
|
|
786
941
|
const gridY = pxToInY(grid.offsetY, dims);
|
|
787
942
|
const lineWidth = Math.max(0.5, grid.lineWidth * 0.75);
|
|
943
|
+
const gridLine = {
|
|
944
|
+
color: grid.color,
|
|
945
|
+
width: lineWidth,
|
|
946
|
+
...(grid.transparency != null
|
|
947
|
+
? { transparency: grid.transparency }
|
|
948
|
+
: {}),
|
|
949
|
+
};
|
|
788
950
|
|
|
789
951
|
for (let x = gridX; x < dims.pptxInches.w; x += gridWidth) {
|
|
790
952
|
pptxSlide.addShape(pptx.ShapeType.line, {
|
|
@@ -792,7 +954,7 @@ export default defineAction({
|
|
|
792
954
|
y: 0,
|
|
793
955
|
w: 0,
|
|
794
956
|
h: dims.pptxInches.h,
|
|
795
|
-
line:
|
|
957
|
+
line: gridLine,
|
|
796
958
|
});
|
|
797
959
|
}
|
|
798
960
|
for (let y = gridY; y < dims.pptxInches.h; y += gridHeight) {
|
|
@@ -801,7 +963,7 @@ export default defineAction({
|
|
|
801
963
|
y,
|
|
802
964
|
w: dims.pptxInches.w,
|
|
803
965
|
h: 0,
|
|
804
|
-
line:
|
|
966
|
+
line: gridLine,
|
|
805
967
|
});
|
|
806
968
|
}
|
|
807
969
|
}
|
|
@@ -815,6 +977,9 @@ export default defineAction({
|
|
|
815
977
|
const orderedShapes = [...shapes].sort(
|
|
816
978
|
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
817
979
|
);
|
|
980
|
+
const orderedTables = [...tables].sort(
|
|
981
|
+
(a, b) => (a.order ?? 0) - (b.order ?? 0),
|
|
982
|
+
);
|
|
818
983
|
|
|
819
984
|
// Imported elements are parsed separately because PptxGenJS needs real
|
|
820
985
|
// slide objects. Keep their source order so overlapping objects retain
|
|
@@ -823,6 +988,7 @@ export default defineAction({
|
|
|
823
988
|
...orderedTexts.map((value) => ({ kind: "text" as const, value })),
|
|
824
989
|
...orderedImages.map((value) => ({ kind: "image" as const, value })),
|
|
825
990
|
...orderedShapes.map((value) => ({ kind: "shape" as const, value })),
|
|
991
|
+
...orderedTables.map((value) => ({ kind: "table" as const, value })),
|
|
826
992
|
].sort((a, b) => (a.value.order ?? 0) - (b.value.order ?? 0));
|
|
827
993
|
|
|
828
994
|
for (const object of orderedObjects) {
|
|
@@ -840,6 +1006,7 @@ export default defineAction({
|
|
|
840
1006
|
align: t.align || "left",
|
|
841
1007
|
valign: "top" as const,
|
|
842
1008
|
wrap: true,
|
|
1009
|
+
...(t.transparency != null ? { transparency: t.transparency } : {}),
|
|
843
1010
|
...(t.letterSpacing != null
|
|
844
1011
|
? { charSpacing: t.letterSpacing }
|
|
845
1012
|
: {}),
|
|
@@ -864,6 +1031,17 @@ export default defineAction({
|
|
|
864
1031
|
h: img.h,
|
|
865
1032
|
});
|
|
866
1033
|
}
|
|
1034
|
+
} else if (object.kind === "table") {
|
|
1035
|
+
const table = object.value;
|
|
1036
|
+
pptxSlide.addTable(table.rows, {
|
|
1037
|
+
x: table.x,
|
|
1038
|
+
y: table.y,
|
|
1039
|
+
w: table.w,
|
|
1040
|
+
h: table.h,
|
|
1041
|
+
autoPage: false,
|
|
1042
|
+
border: { type: "solid", color: "FFFFFF", pt: 0.5 },
|
|
1043
|
+
margin: 0.04,
|
|
1044
|
+
});
|
|
867
1045
|
} else {
|
|
868
1046
|
const shape = object.value;
|
|
869
1047
|
pptxSlide.addShape(
|
|
@@ -873,12 +1051,24 @@ export default defineAction({
|
|
|
873
1051
|
y: shape.y,
|
|
874
1052
|
w: shape.w,
|
|
875
1053
|
h: shape.h,
|
|
876
|
-
...(shape.fill
|
|
1054
|
+
...(shape.fill
|
|
1055
|
+
? {
|
|
1056
|
+
fill: {
|
|
1057
|
+
color: shape.fill,
|
|
1058
|
+
...(shape.fillTransparency != null
|
|
1059
|
+
? { transparency: shape.fillTransparency }
|
|
1060
|
+
: {}),
|
|
1061
|
+
},
|
|
1062
|
+
}
|
|
1063
|
+
: {}),
|
|
877
1064
|
...(shape.lineColor
|
|
878
1065
|
? {
|
|
879
1066
|
line: {
|
|
880
1067
|
color: shape.lineColor,
|
|
881
1068
|
width: shape.lineWidth ?? 1,
|
|
1069
|
+
...(shape.lineTransparency != null
|
|
1070
|
+
? { transparency: shape.lineTransparency }
|
|
1071
|
+
: {}),
|
|
882
1072
|
},
|
|
883
1073
|
}
|
|
884
1074
|
: {}),
|
|
@@ -162,6 +162,10 @@ export default defineAction({
|
|
|
162
162
|
(total, r) => total + r.imageSkippedCount,
|
|
163
163
|
0,
|
|
164
164
|
);
|
|
165
|
+
const tablesDegraded = presentation.slides.reduce(
|
|
166
|
+
(total, s) => total + (s.tablesDegraded ?? 0),
|
|
167
|
+
0,
|
|
168
|
+
);
|
|
165
169
|
const sourceImport = buildSourceImportMetadata({
|
|
166
170
|
format: "pptx",
|
|
167
171
|
slides: pptxResults.map((result) => ({
|
|
@@ -172,6 +176,7 @@ export default defineAction({
|
|
|
172
176
|
editableText: true,
|
|
173
177
|
})),
|
|
174
178
|
imagesSkipped,
|
|
179
|
+
tablesDegraded,
|
|
175
180
|
});
|
|
176
181
|
if (imagesSkipped > 0) {
|
|
177
182
|
throw new Error(
|
|
@@ -202,6 +207,7 @@ export default defineAction({
|
|
|
202
207
|
deckId,
|
|
203
208
|
imported: true,
|
|
204
209
|
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
210
|
+
...(tablesDegraded > 0 ? { tablesDegraded } : {}),
|
|
205
211
|
};
|
|
206
212
|
}
|
|
207
213
|
|
|
@@ -527,9 +533,14 @@ async function importPdfPagesWithFidelity(args: {
|
|
|
527
533
|
),
|
|
528
534
|
);
|
|
529
535
|
const slides = imported.map((entry) => entry.slide);
|
|
536
|
+
const imagesSkipped = fidelityPages.reduce(
|
|
537
|
+
(total, page) => total + page.imagesSkipped,
|
|
538
|
+
0,
|
|
539
|
+
);
|
|
530
540
|
const sourceImport = buildSourceImportMetadata({
|
|
531
541
|
format: "pdf",
|
|
532
542
|
slides: imported.map((entry) => entry.snapshot),
|
|
543
|
+
imagesSkipped,
|
|
533
544
|
});
|
|
534
545
|
|
|
535
546
|
const importedTitle = await appendDeckSlides(
|
|
@@ -550,6 +561,7 @@ async function importPdfPagesWithFidelity(args: {
|
|
|
550
561
|
aspectRatio,
|
|
551
562
|
deckId,
|
|
552
563
|
imported: true,
|
|
564
|
+
...(imagesSkipped > 0 ? { imagesSkipped } : {}),
|
|
553
565
|
};
|
|
554
566
|
}
|
|
555
567
|
|