@1agh/maude 0.24.0 → 0.26.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/cli/commands/design.mjs +5 -0
- package/cli/lib/design-link.mjs +13 -6
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/gitignore-block.test.mjs +1 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +481 -78
- package/plugins/design/dev-server/annotations-layer.tsx +817 -170
- package/plugins/design/dev-server/api.ts +15 -1
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-optimize.mjs +35 -0
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/draw-build.sh +48 -0
- package/plugins/design/dev-server/bin/draw-proof.sh +129 -0
- package/plugins/design/dev-server/bin/smoke.sh +114 -12
- package/plugins/design/dev-server/bin/svg-optimize.sh +24 -0
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +130 -82
- package/plugins/design/dev-server/canvas-icons.tsx +169 -0
- package/plugins/design/dev-server/canvas-lib.tsx +110 -0
- package/plugins/design/dev-server/canvas-shell.tsx +113 -89
- package/plugins/design/dev-server/client/app.jsx +1084 -417
- package/plugins/design/dev-server/config.schema.json +10 -0
- package/plugins/design/dev-server/context.ts +9 -0
- package/plugins/design/dev-server/dist/client.bundle.js +242 -20
- package/plugins/design/dev-server/dist/comment-mount.js +40 -62
- package/plugins/design/dev-server/draw/brush.ts +639 -0
- package/plugins/design/dev-server/draw/composition.ts +229 -0
- package/plugins/design/dev-server/draw/geometry.ts +578 -0
- package/plugins/design/dev-server/draw/index.ts +28 -0
- package/plugins/design/dev-server/draw/layout.ts +260 -0
- package/plugins/design/dev-server/draw/optimize.ts +65 -0
- package/plugins/design/dev-server/draw/palette.ts +417 -0
- package/plugins/design/dev-server/draw/primitives.ts +643 -0
- package/plugins/design/dev-server/draw/serialize.ts +458 -0
- package/plugins/design/dev-server/draw/test/brush.test.ts +213 -0
- package/plugins/design/dev-server/draw/test/composition.test.ts +141 -0
- package/plugins/design/dev-server/draw/test/geometry.test.ts +199 -0
- package/plugins/design/dev-server/draw/test/gradient.test.ts +167 -0
- package/plugins/design/dev-server/draw/test/layout.test.ts +120 -0
- package/plugins/design/dev-server/draw/test/optimize.test.ts +40 -0
- package/plugins/design/dev-server/draw/test/palette.test.ts +123 -0
- package/plugins/design/dev-server/draw/test/primitives.test.ts +129 -0
- package/plugins/design/dev-server/draw/test/serialize.test.ts +105 -0
- package/plugins/design/dev-server/export-dialog.tsx +189 -1
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/http.ts +40 -9
- package/plugins/design/dev-server/input-router.tsx +9 -2
- package/plugins/design/dev-server/sync/index.ts +73 -17
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +83 -3
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +243 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +95 -6
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +11 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +32 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +52 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +9 -11
- package/plugins/design/dev-server/tool-palette.tsx +140 -11
- package/plugins/design/dev-server/tsconfig.json +8 -1
- package/plugins/design/dev-server/use-annotation-resize.tsx +208 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +55 -9
- package/plugins/design/templates/_shell.html +36 -9
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +15 -0
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -0
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
//
|
|
1
|
+
// PPTX adapter — native editable slides via svg2pptx (export-pipeline-fixes
|
|
2
|
+
// item 6, final architecture; see DDR-069).
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// the
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Pipeline per artboard:
|
|
5
|
+
// 1. Render the artboard to SVG (the project's vector pipeline: exact glyph
|
|
6
|
+
// positions, oklch→sRGB, fonts inlined).
|
|
7
|
+
// 2. Pre-process the SVG (`preprocessSvg`): lift each `<tspan>` x/y onto its
|
|
8
|
+
// `<text>` parent (dom-to-svg positions on the tspan; svg2pptx reads the
|
|
9
|
+
// text element → without this every run collapses to 0,0 and overlaps),
|
|
10
|
+
// and collapse the CSS `font-family` stack to its first concrete name (a
|
|
11
|
+
// PPTX `typeface` is a SINGLE font, not a fallback list).
|
|
12
|
+
// 3. svg2pptx converts the SVG to NATIVE PowerPoint shapes + text boxes —
|
|
13
|
+
// editable in PowerPoint / Keynote / Canva, opens everywhere (no `svgBlip`
|
|
14
|
+
// that Canva rejects, no text reflow that broke dom-to-pptx).
|
|
15
|
+
// 4. Merge the per-artboard single-slide decks into one (`mergeDecks`).
|
|
16
|
+
//
|
|
17
|
+
// svg2pptx is a Python tool (the only OSS path that yields faithful + editable
|
|
18
|
+
// native objects — see DDR-069 for the alternatives ruled out: dom-to-pptx
|
|
19
|
+
// reflows, SVG-image `svgBlip` breaks Canva, LibreOffice emits empty). When
|
|
20
|
+
// python3 + svg2pptx are absent we fall back to a PNG-per-slide deck (faithful,
|
|
21
|
+
// universal, Canva-safe, NOT editable) and log a one-line install hint.
|
|
10
22
|
|
|
11
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
23
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
24
|
import { tmpdir } from 'node:os';
|
|
13
25
|
import path from 'node:path';
|
|
14
26
|
|
|
15
|
-
import
|
|
27
|
+
import PptxGenJS from 'pptxgenjs';
|
|
16
28
|
|
|
29
|
+
import { getBrowserBundle } from './_browser-bundles.ts';
|
|
17
30
|
import {
|
|
18
31
|
type ExportContext,
|
|
19
32
|
type ExportOptions,
|
|
@@ -22,44 +35,24 @@ import {
|
|
|
22
35
|
} from './index.ts';
|
|
23
36
|
import type { Target } from './scope.ts';
|
|
24
37
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
return path.join(path.dirname(pkgJson), 'dist', 'dom-to-pptx.bundle.js');
|
|
38
|
+
const SVG_PLAYWRIGHT = path.join(import.meta.dir, '..', 'bin', '_svg-playwright.mjs');
|
|
39
|
+
const PNG_PLAYWRIGHT = path.join(import.meta.dir, '..', 'bin', '_png-playwright.mjs');
|
|
40
|
+
const PPTX_MIME = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
41
|
+
const SLIDE_REL = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide';
|
|
42
|
+
const SLIDE_CT = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml';
|
|
43
|
+
const SCREEN_DPI = 96; // canvas px → inches (PNG fallback slide sizing)
|
|
44
|
+
const FALLBACK_SCALE = 3; // raster fallback rendered at 3× for crisp non-vector viewers
|
|
45
|
+
|
|
46
|
+
type ElementTarget = Extract<Target, { kind: 'element' }>;
|
|
47
|
+
|
|
48
|
+
function isElementTarget(t: Target): t is ElementTarget {
|
|
49
|
+
return t.kind === 'element';
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
ctx: ExportContext,
|
|
43
|
-
outFile: string,
|
|
44
|
-
timeoutSec: number,
|
|
45
|
-
bundlePath: string,
|
|
46
|
-
selector?: string
|
|
47
|
-
): Promise<void> {
|
|
48
|
-
const args = [
|
|
49
|
-
PPTX_PLAYWRIGHT,
|
|
50
|
-
'--url',
|
|
51
|
-
canvasShellUrl(ctx, target.file),
|
|
52
|
-
'--selector',
|
|
53
|
-
selector ?? target.cssPath,
|
|
54
|
-
'--out',
|
|
55
|
-
outFile,
|
|
56
|
-
'--bundle-path',
|
|
57
|
-
bundlePath,
|
|
58
|
-
'--timeout',
|
|
59
|
-
String(timeoutSec),
|
|
60
|
-
];
|
|
52
|
+
/** Spawn a playwright shim via `node`, returning the written file paths (stdout). */
|
|
53
|
+
async function spawnShim(args: string[]): Promise<string[]> {
|
|
61
54
|
const proc = Bun.spawn(['node', ...args], {
|
|
62
|
-
cwd: path.dirname(
|
|
55
|
+
cwd: path.dirname(args[0]),
|
|
63
56
|
stdout: 'pipe',
|
|
64
57
|
stderr: 'pipe',
|
|
65
58
|
});
|
|
@@ -69,195 +62,331 @@ async function captureOne(
|
|
|
69
62
|
]);
|
|
70
63
|
const code = await proc.exited;
|
|
71
64
|
if (code !== 0) {
|
|
72
|
-
throw new Error(
|
|
65
|
+
throw new Error(`${path.basename(args[0])} exited ${code}: ${stderr.trim() || stdout.trim()}`);
|
|
73
66
|
}
|
|
67
|
+
return stdout
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
.filter(Boolean);
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
|
|
77
|
-
targets: Target[],
|
|
78
|
-
options: ExportOptions,
|
|
79
|
-
ctx: ExportContext
|
|
80
|
-
): Promise<ExportResult> {
|
|
81
|
-
if (!targets.length) {
|
|
82
|
-
return {
|
|
83
|
-
filename: 'export.pptx',
|
|
84
|
-
contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
85
|
-
body: new Uint8Array(0),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
const elementTargets = targets.filter(
|
|
89
|
-
(t): t is Extract<Target, { kind: 'element' }> => t.kind === 'element'
|
|
90
|
-
);
|
|
91
|
-
if (!elementTargets.length) {
|
|
92
|
-
throw new Error('pptx adapter requires element targets (got file-tree)');
|
|
93
|
-
}
|
|
94
|
-
const timeoutSec = (options.timeoutSec as number | undefined) ?? 20;
|
|
95
|
-
const bundlePath = pptxBundlePath();
|
|
96
|
-
const tmp = mkdtempSync(path.join(tmpdir(), 'maude-pptx-'));
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
// Resolve the artboard set we need to render. For `multi: true` we walk
|
|
100
|
-
// `[data-dc-screen]` and render each separately, then merge.
|
|
101
|
-
const target = elementTargets[0];
|
|
102
|
-
if (!target) throw new Error('pptx adapter: no element target');
|
|
103
|
-
const baseSlug = target.canvasSlug ?? 'export';
|
|
73
|
+
// ─── SVG pre-processing (the fix that makes svg2pptx faithful) ────────────────
|
|
104
74
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Make a dom-to-svg SVG consumable by svg2pptx without text collapsing to the
|
|
77
|
+
* origin or rendering in a fallback font. Two transforms:
|
|
78
|
+
* - lift the first `<tspan>` x/y onto the enclosing `<text>` (svg2pptx reads
|
|
79
|
+
* `text@x/y`; dom-to-svg leaves those off and positions the tspan);
|
|
80
|
+
* - collapse `font-family="<stack>"` to its first concrete name (a PPTX
|
|
81
|
+
* typeface is one font name, not a CSS fallback list).
|
|
82
|
+
* Pure string transform — exported for unit coverage.
|
|
83
|
+
*/
|
|
84
|
+
export function preprocessSvg(svg: string): string {
|
|
85
|
+
let out = svg.replace(
|
|
86
|
+
/<text\b([^>]*)>(\s*)<tspan\b([^>]*)>/g,
|
|
87
|
+
(full, textAttrs, ws, tspanAttrs) => {
|
|
88
|
+
if (/\bx=/.test(textAttrs)) return full; // already positioned
|
|
89
|
+
const x = /\bx="([^"]*)"/.exec(tspanAttrs)?.[1];
|
|
90
|
+
const y = /\by="([^"]*)"/.exec(tspanAttrs)?.[1];
|
|
91
|
+
if (x == null || y == null) return full;
|
|
92
|
+
return `<text x="${x}" y="${y}"${textAttrs}>${ws}<tspan${tspanAttrs}>`;
|
|
122
93
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
133
|
-
} finally {
|
|
134
|
-
rmSync(tmp, { recursive: true, force: true });
|
|
135
|
-
}
|
|
94
|
+
);
|
|
95
|
+
out = out.replace(/font-family="([^"]*)"/g, (m, val: string) => {
|
|
96
|
+
const first = val
|
|
97
|
+
.split(',')[0]
|
|
98
|
+
.replace(/"|["']/g, '')
|
|
99
|
+
.trim();
|
|
100
|
+
return first ? `font-family="${first}"` : m;
|
|
101
|
+
});
|
|
102
|
+
return out;
|
|
136
103
|
}
|
|
137
104
|
|
|
138
|
-
//
|
|
139
|
-
void path_dirname;
|
|
105
|
+
// ─── svg2pptx (Python) detection + invocation ─────────────────────────────────
|
|
140
106
|
|
|
141
107
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* its browser warm across requests, but still one extra navigation per
|
|
145
|
-
* canvas-as-separate export. Worth caching long-term.
|
|
108
|
+
* argv prefix for svg2pptx. `MAUDE_SVG2PPTX` overrides (space-separated, e.g. a
|
|
109
|
+
* venv path or `python3 -m svg2pptx`); defaults to the `svg2pptx` CLI on PATH.
|
|
146
110
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
[
|
|
158
|
-
|
|
159
|
-
|
|
111
|
+
function svg2pptxArgv(): string[] {
|
|
112
|
+
const env = process.env.MAUDE_SVG2PPTX?.trim();
|
|
113
|
+
return env ? env.split(/\s+/) : ['svg2pptx'];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let _svg2pptxAvailable: boolean | null = null;
|
|
117
|
+
async function svg2pptxAvailable(): Promise<boolean> {
|
|
118
|
+
if (_svg2pptxAvailable !== null) return _svg2pptxAvailable;
|
|
119
|
+
try {
|
|
120
|
+
const [bin, ...rest] = svg2pptxArgv();
|
|
121
|
+
const proc = Bun.spawn([bin, ...rest, '--version'], { stdout: 'pipe', stderr: 'pipe' });
|
|
122
|
+
_svg2pptxAvailable = (await proc.exited) === 0;
|
|
123
|
+
} catch {
|
|
124
|
+
_svg2pptxAvailable = false;
|
|
125
|
+
}
|
|
126
|
+
return _svg2pptxAvailable;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function runSvg2pptx(inSvg: string, outPptx: string): Promise<void> {
|
|
130
|
+
const [bin, ...rest] = svg2pptxArgv();
|
|
131
|
+
const proc = Bun.spawn([bin, ...rest, inSvg, outPptx], { stdout: 'pipe', stderr: 'pipe' });
|
|
160
132
|
const [stdout, stderr] = await Promise.all([
|
|
161
133
|
new Response(proc.stdout).text(),
|
|
162
134
|
new Response(proc.stderr).text(),
|
|
163
135
|
]);
|
|
164
136
|
const code = await proc.exited;
|
|
165
137
|
if (code !== 0) {
|
|
166
|
-
throw new Error(
|
|
167
|
-
`_enumerate-artboards-playwright exited ${code}: ${stderr.trim() || stdout.trim()}`
|
|
168
|
-
);
|
|
138
|
+
throw new Error(`svg2pptx exited ${code}: ${stderr.trim() || stdout.trim()}`);
|
|
169
139
|
}
|
|
170
|
-
return stdout
|
|
171
|
-
.split('\n')
|
|
172
|
-
.map((s) => s.trim())
|
|
173
|
-
.filter(Boolean);
|
|
174
140
|
}
|
|
175
141
|
|
|
142
|
+
// ─── deck merge (single-slide svg2pptx decks → one) ───────────────────────────
|
|
143
|
+
|
|
176
144
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
145
|
+
* Merge N single-slide svg2pptx decks into one. svg2pptx output carries no
|
|
146
|
+
* media (pure native shapes) so there's no media-collision to namespace — we
|
|
147
|
+
* keep the first deck's master/layout/theme skeleton, append each slide
|
|
148
|
+
* renumbered, and reconcile presentation.xml + its rels + Content-Types.
|
|
149
|
+
* Pure (in→out buffers); exported for unit coverage.
|
|
181
150
|
*/
|
|
182
|
-
async function
|
|
183
|
-
if (
|
|
184
|
-
if (
|
|
151
|
+
export async function mergeDecks(decks: Uint8Array[]): Promise<Uint8Array> {
|
|
152
|
+
if (decks.length === 0) return new Uint8Array(0);
|
|
153
|
+
if (decks.length === 1) return decks[0];
|
|
185
154
|
const JSZip = (await import('jszip')).default;
|
|
186
|
-
|
|
187
|
-
// skeleton, replace its slide collection with the union from all inputs.
|
|
188
|
-
const base = await JSZip.loadAsync(new Uint8Array(readFileSync(files[0])));
|
|
189
|
-
const slides: Array<{ name: string; xml: string; rels: string }> = [];
|
|
190
|
-
|
|
191
|
-
for (const file of files) {
|
|
192
|
-
const zip = await JSZip.loadAsync(new Uint8Array(readFileSync(file)));
|
|
193
|
-
const slideNames = Object.keys(zip.files).filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n));
|
|
194
|
-
for (const sn of slideNames) {
|
|
195
|
-
const slideEntry = zip.file(sn);
|
|
196
|
-
if (!slideEntry) continue;
|
|
197
|
-
const xml = await slideEntry.async('string');
|
|
198
|
-
const relsName = sn.replace('slides/', 'slides/_rels/').replace('.xml', '.xml.rels');
|
|
199
|
-
const relsEntry = zip.file(relsName);
|
|
200
|
-
const rels = relsEntry ? await relsEntry.async('string') : '';
|
|
201
|
-
slides.push({ name: sn, xml, rels });
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Rewrite slide filenames so they're contiguous.
|
|
155
|
+
const base = await JSZip.loadAsync(decks[0]);
|
|
206
156
|
const next = new JSZip();
|
|
157
|
+
|
|
158
|
+
// Skeleton (master / layouts / theme / presProps / viewProps / docProps …),
|
|
159
|
+
// everything except the slides (re-emitted renumbered).
|
|
207
160
|
for (const name of Object.keys(base.files)) {
|
|
208
|
-
// `Object.keys(base.files)` includes directory entries — `base.file(name)`
|
|
209
|
-
// returns null for those. Bare path-based filter first, then the entry
|
|
210
|
-
// lookup with a defensive null-check.
|
|
211
161
|
if (/\/$/.test(name)) continue;
|
|
212
|
-
if (/^ppt\/slides
|
|
162
|
+
if (/^ppt\/slides\//.test(name)) continue;
|
|
213
163
|
const entry = base.file(name);
|
|
214
164
|
if (!entry || entry.dir) continue;
|
|
215
165
|
next.file(name, await entry.async('uint8array'));
|
|
216
166
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
167
|
+
|
|
168
|
+
// Append each deck's slide(s), contiguous.
|
|
169
|
+
let count = 0;
|
|
170
|
+
for (const buf of decks) {
|
|
171
|
+
const zip = await JSZip.loadAsync(buf);
|
|
172
|
+
const slideNames = Object.keys(zip.files)
|
|
173
|
+
.filter((n) => /^ppt\/slides\/slide\d+\.xml$/.test(n))
|
|
174
|
+
.sort();
|
|
175
|
+
for (const sn of slideNames) {
|
|
176
|
+
const slideEntry = zip.file(sn);
|
|
177
|
+
if (!slideEntry) continue;
|
|
178
|
+
count += 1;
|
|
179
|
+
next.file(`ppt/slides/slide${count}.xml`, await slideEntry.async('string'));
|
|
180
|
+
const relsName = `${sn.replace('ppt/slides/', 'ppt/slides/_rels/')}.rels`;
|
|
181
|
+
const relsEntry = zip.file(relsName);
|
|
182
|
+
if (relsEntry) {
|
|
183
|
+
next.file(`ppt/slides/_rels/slide${count}.xml.rels`, await relsEntry.async('string'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
236
186
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
187
|
+
|
|
188
|
+
// presentation.xml — grow sldIdLst to N.
|
|
189
|
+
const presEntry = base.file('ppt/presentation.xml');
|
|
190
|
+
const presRelsEntry = base.file('ppt/_rels/presentation.xml.rels');
|
|
191
|
+
if (!presEntry || !presRelsEntry) return decks[0];
|
|
192
|
+
let pres = await presEntry.async('string');
|
|
193
|
+
const idEntries = Array.from(
|
|
194
|
+
{ length: count },
|
|
195
|
+
(_, i) => `<p:sldId id="${256 + i}" r:id="rId${100 + i}"/>`
|
|
196
|
+
).join('');
|
|
197
|
+
pres = pres.replace(
|
|
244
198
|
/<p:sldIdLst>[\s\S]*?<\/p:sldIdLst>/,
|
|
245
199
|
`<p:sldIdLst>${idEntries}</p:sldIdLst>`
|
|
246
200
|
);
|
|
247
|
-
next.file(
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
next.file(
|
|
201
|
+
next.file('ppt/presentation.xml', pres);
|
|
202
|
+
|
|
203
|
+
// presentation.xml.rels — STRIP slide rels (Type ending `/relationships/slide`,
|
|
204
|
+
// `[^>]*` so the `//` in the Type URL doesn't truncate the match), KEEP
|
|
205
|
+
// everything else (slideMaster/theme/presProps), append N slide rels.
|
|
206
|
+
let prels = await presRelsEntry.async('string');
|
|
207
|
+
prels = prels.replace(/<Relationship\b[^>]*Type="[^"]*\/relationships\/slide"[^>]*\/>/g, '');
|
|
208
|
+
const slideRels = Array.from(
|
|
209
|
+
{ length: count },
|
|
210
|
+
(_, i) =>
|
|
211
|
+
`<Relationship Id="rId${100 + i}" Type="${SLIDE_REL}" Target="slides/slide${i + 1}.xml"/>`
|
|
212
|
+
).join('');
|
|
213
|
+
prels = prels.replace(/<\/Relationships>\s*$/, `${slideRels}</Relationships>`);
|
|
214
|
+
next.file('ppt/_rels/presentation.xml.rels', prels);
|
|
215
|
+
|
|
216
|
+
// Content-Types — contiguous slide Overrides, then drop any Override pointing
|
|
217
|
+
// at a non-existent part (svg2pptx/pptxgenjs over-declare slideMaster<N> etc.).
|
|
218
|
+
const ctEntry = base.file('[Content_Types].xml');
|
|
219
|
+
if (ctEntry) {
|
|
220
|
+
let ct = await ctEntry.async('string');
|
|
221
|
+
ct = ct.replace(/<Override\s+PartName="\/ppt\/slides\/slide\d+\.xml"[^>]*\/>/g, '');
|
|
222
|
+
const slideOverrides = Array.from(
|
|
223
|
+
{ length: count },
|
|
224
|
+
(_, i) => `<Override PartName="/ppt/slides/slide${i + 1}.xml" ContentType="${SLIDE_CT}"/>`
|
|
225
|
+
).join('');
|
|
226
|
+
ct = ct.replace(/<\/Types>\s*$/, `${slideOverrides}</Types>`);
|
|
227
|
+
const parts = new Set(Object.keys(next.files).filter((n) => !n.endsWith('/')));
|
|
228
|
+
ct = ct.replace(/<Override\s+PartName="([^"]+)"[^>]*\/>/g, (full, p: string) =>
|
|
229
|
+
parts.has(p.replace(/^\//, '')) ? full : ''
|
|
230
|
+
);
|
|
231
|
+
next.file('[Content_Types].xml', ct);
|
|
232
|
+
}
|
|
261
233
|
|
|
262
234
|
return next.generateAsync({ type: 'uint8array' });
|
|
263
235
|
}
|
|
236
|
+
|
|
237
|
+
// ─── render helpers ───────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
function svgArgsFor(target: ElementTarget, url: string, outDir: string, bundle: string): string[] {
|
|
240
|
+
const args = [
|
|
241
|
+
SVG_PLAYWRIGHT,
|
|
242
|
+
'--url',
|
|
243
|
+
url,
|
|
244
|
+
'--selector',
|
|
245
|
+
target.cssPath,
|
|
246
|
+
'--bundle-path',
|
|
247
|
+
bundle,
|
|
248
|
+
'--timeout',
|
|
249
|
+
'20',
|
|
250
|
+
];
|
|
251
|
+
if (target.multi) args.push('--multi', '1', '--out-dir', outDir);
|
|
252
|
+
else {
|
|
253
|
+
if (target.widen) args.push('--widen-to-artboard', '1');
|
|
254
|
+
args.push('--out', path.join(outDir, `${target.canvasSlug}.svg`));
|
|
255
|
+
}
|
|
256
|
+
return args;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function pngArgsFor(target: ElementTarget, url: string, outDir: string): string[] {
|
|
260
|
+
const args = [
|
|
261
|
+
PNG_PLAYWRIGHT,
|
|
262
|
+
'--url',
|
|
263
|
+
url,
|
|
264
|
+
'--selector',
|
|
265
|
+
target.cssPath,
|
|
266
|
+
'--scale',
|
|
267
|
+
String(FALLBACK_SCALE),
|
|
268
|
+
'--timeout',
|
|
269
|
+
'20',
|
|
270
|
+
];
|
|
271
|
+
if (target.multi) args.push('--multi', '1', '--out-dir', outDir);
|
|
272
|
+
else {
|
|
273
|
+
if (target.widen) args.push('--widen-to-artboard', '1');
|
|
274
|
+
args.push('--out', path.join(outDir, `${target.canvasSlug}.png`));
|
|
275
|
+
}
|
|
276
|
+
return args;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ─── PNG fallback deck ────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
async function buildPngDeck(pngPaths: string[]): Promise<Uint8Array> {
|
|
282
|
+
const JSZip = (await import('jszip')).default;
|
|
283
|
+
// Slide size from the first PNG (px → inches); contain-fit the rest.
|
|
284
|
+
const dims = pngPaths.map((p) => {
|
|
285
|
+
// PNG IHDR: width @ byte 16, height @ 20 (big-endian).
|
|
286
|
+
const b = readFileSync(p);
|
|
287
|
+
return { w: b.readUInt32BE(16), h: b.readUInt32BE(20), p };
|
|
288
|
+
});
|
|
289
|
+
const maxW = Math.max(...dims.map((d) => d.w));
|
|
290
|
+
const maxH = Math.max(...dims.map((d) => d.h));
|
|
291
|
+
// The PNGs are FALLBACK_SCALE× the artboard; normalise to artboard inches.
|
|
292
|
+
const slideW = maxW / SCREEN_DPI / FALLBACK_SCALE;
|
|
293
|
+
const slideH = maxH / SCREEN_DPI / FALLBACK_SCALE;
|
|
294
|
+
const pptx = new PptxGenJS();
|
|
295
|
+
pptx.defineLayout({ name: 'MAUDE_ARTBOARD', width: slideW, height: slideH });
|
|
296
|
+
pptx.layout = 'MAUDE_ARTBOARD';
|
|
297
|
+
for (const d of dims) {
|
|
298
|
+
const slide = pptx.addSlide();
|
|
299
|
+
const nativeW = d.w / SCREEN_DPI / FALLBACK_SCALE;
|
|
300
|
+
const nativeH = d.h / SCREEN_DPI / FALLBACK_SCALE;
|
|
301
|
+
const scale = Math.min(slideW / nativeW, slideH / nativeH);
|
|
302
|
+
const w = nativeW * scale;
|
|
303
|
+
const h = nativeH * scale;
|
|
304
|
+
const dataUri = `data:image/png;base64,${readFileSync(d.p).toString('base64')}`;
|
|
305
|
+
slide.addImage({ data: dataUri, x: (slideW - w) / 2, y: (slideH - h) / 2, w, h });
|
|
306
|
+
}
|
|
307
|
+
const buf = new Uint8Array((await pptx.write({ outputType: 'nodebuffer' })) as Uint8Array);
|
|
308
|
+
// Strip pptxgenjs's phantom slideMaster<N> Content-Types Overrides.
|
|
309
|
+
const zip = await JSZip.loadAsync(buf);
|
|
310
|
+
const ctEntry = zip.file('[Content_Types].xml');
|
|
311
|
+
if (ctEntry) {
|
|
312
|
+
const parts = new Set(Object.keys(zip.files).filter((n) => !n.endsWith('/')));
|
|
313
|
+
let ct = await ctEntry.async('string');
|
|
314
|
+
ct = ct.replace(/<Override\s+PartName="([^"]+)"[^>]*\/>/g, (full, p: string) =>
|
|
315
|
+
parts.has(p.replace(/^\//, '')) ? full : ''
|
|
316
|
+
);
|
|
317
|
+
zip.file('[Content_Types].xml', ct);
|
|
318
|
+
}
|
|
319
|
+
return zip.generateAsync({ type: 'uint8array' });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function run(
|
|
323
|
+
targets: Target[],
|
|
324
|
+
options: ExportOptions,
|
|
325
|
+
ctx: ExportContext
|
|
326
|
+
): Promise<ExportResult> {
|
|
327
|
+
const empty: ExportResult = {
|
|
328
|
+
filename: 'export.pptx',
|
|
329
|
+
contentType: PPTX_MIME,
|
|
330
|
+
body: new Uint8Array(0),
|
|
331
|
+
};
|
|
332
|
+
if (!targets.length) return empty;
|
|
333
|
+
const elementTargets = targets.filter(isElementTarget);
|
|
334
|
+
if (!elementTargets.length) {
|
|
335
|
+
throw new Error('pptx adapter requires element targets (got file-tree)');
|
|
336
|
+
}
|
|
337
|
+
// `options.raster` forces the PNG (non-editable) path even when svg2pptx exists.
|
|
338
|
+
const forceRaster = options.raster === true || options.mode === 'raster';
|
|
339
|
+
const baseSlug = elementTargets[0]?.canvasSlug ?? 'export';
|
|
340
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'maude-pptx-'));
|
|
341
|
+
const svgDir = path.join(tmp, 'svg');
|
|
342
|
+
const pngDir = path.join(tmp, 'png');
|
|
343
|
+
mkdirSync(svgDir, { recursive: true });
|
|
344
|
+
mkdirSync(pngDir, { recursive: true });
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const useNative = !forceRaster && (await svg2pptxAvailable());
|
|
348
|
+
|
|
349
|
+
if (useNative) {
|
|
350
|
+
try {
|
|
351
|
+
const bundle = await getBrowserBundle('dom-to-svg', 'domToSvg');
|
|
352
|
+
const svgFiles: string[] = [];
|
|
353
|
+
for (const t of elementTargets) {
|
|
354
|
+
svgFiles.push(
|
|
355
|
+
...(await spawnShim(svgArgsFor(t, canvasShellUrl(ctx, t.file), svgDir, bundle)))
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
if (!svgFiles.length) return empty;
|
|
359
|
+
const deckBuffers: Uint8Array[] = [];
|
|
360
|
+
for (let i = 0; i < svgFiles.length; i += 1) {
|
|
361
|
+
const pre = path.join(svgDir, `slide-${i + 1}.pre.svg`);
|
|
362
|
+
writeFileSync(pre, preprocessSvg(readFileSync(svgFiles[i], 'utf8')));
|
|
363
|
+
const outPptx = path.join(svgDir, `slide-${i + 1}.pptx`);
|
|
364
|
+
await runSvg2pptx(pre, outPptx);
|
|
365
|
+
deckBuffers.push(new Uint8Array(readFileSync(outPptx)));
|
|
366
|
+
}
|
|
367
|
+
const body = await mergeDecks(deckBuffers);
|
|
368
|
+
return { filename: `${baseSlug}.pptx`, contentType: PPTX_MIME, body };
|
|
369
|
+
} catch (err) {
|
|
370
|
+
// svg2pptx present but failed on this canvas — fall through to PNG so
|
|
371
|
+
// the export still succeeds, with a diagnostic.
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
console.error(` warn: svg2pptx native PPTX failed, falling back to PNG: ${msg}`);
|
|
374
|
+
}
|
|
375
|
+
} else if (!forceRaster) {
|
|
376
|
+
console.error(
|
|
377
|
+
' note: svg2pptx not found — exporting a (non-editable) PNG deck. For editable native PowerPoint objects: `pip install svg2pptx` (needs python3).'
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// PNG fallback (faithful, universal, Canva-safe, not editable).
|
|
382
|
+
const pngFiles: string[] = [];
|
|
383
|
+
for (const t of elementTargets) {
|
|
384
|
+
pngFiles.push(...(await spawnShim(pngArgsFor(t, canvasShellUrl(ctx, t.file), pngDir))));
|
|
385
|
+
}
|
|
386
|
+
if (!pngFiles.length) return empty;
|
|
387
|
+
const body = await buildPngDeck(pngFiles);
|
|
388
|
+
return { filename: `${baseSlug}.pptx`, contentType: PPTX_MIME, body };
|
|
389
|
+
} finally {
|
|
390
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
391
|
+
}
|
|
392
|
+
}
|