@1agh/maude 1.0.5 → 1.0.7

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.
@@ -84,10 +84,13 @@ try {
84
84
  // failure, so the success path and desktop output are unchanged.
85
85
  const diag = [];
86
86
  page.on('console', (m) => {
87
- if (m.type() === 'error' || m.type() === 'warning') diag.push(`console.${m.type()}: ${m.text()}`);
87
+ if (m.type() === 'error' || m.type() === 'warning')
88
+ diag.push(`console.${m.type()}: ${m.text()}`);
88
89
  });
89
90
  page.on('pageerror', (e) => diag.push(`pageerror: ${e.message}`));
90
- page.on('requestfailed', (r) => diag.push(`requestfailed: ${r.url()} — ${r.failure()?.errorText ?? '?'}`));
91
+ page.on('requestfailed', (r) =>
92
+ diag.push(`requestfailed: ${r.url()} — ${r.failure()?.errorText ?? '?'}`)
93
+ );
91
94
  try {
92
95
  await page.goto(url, { waitUntil: 'load', timeout: timeoutMs });
93
96
  } catch (e) {
@@ -33,14 +33,16 @@ const {
33
33
  out,
34
34
  'out-dir': outDir,
35
35
  'bundle-path': bundlePath,
36
+ 'core-path': corePath,
36
37
  'widen-to-artboard': widenFlag,
37
38
  multi: multiFlag,
38
39
  timeout = '12',
39
40
  } = args;
40
41
 
41
- if (!url || !bundlePath) {
42
+ if (!url || !bundlePath || !corePath) {
42
43
  console.error(
43
- 'usage: _svg-playwright.mjs --url <url> --selector <css> --out <path> --bundle-path <iife.js>'
44
+ 'usage: _svg-playwright.mjs --url <url> --selector <css> --out <path> ' +
45
+ '--bundle-path <iife.js> --core-path <capture-core.js>'
44
46
  );
45
47
  process.exit(2);
46
48
  }
@@ -71,9 +73,15 @@ try {
71
73
  world.style.transform = 'none';
72
74
  }
73
75
  });
74
- // Inject dom-to-svg into the page. Bundle attaches its exports under
75
- // `window.domToSvg`.
76
+ // Inject dom-to-svg (IIFE — attaches `window.domToSvg`) and the SHARED
77
+ // capture spine (module — attaches `window.__maudeCaptureCore`). The core is
78
+ // the SAME source the canvas runtime's export-capture bridge imports
79
+ // (exporters/capture-core.ts, DDR-231 single-spine): the serializer logic
80
+ // must never be re-inlined here — it forked once and the browser lane would
81
+ // silently drift from this shim.
76
82
  await page.addScriptTag({ path: bundlePath });
83
+ await page.addScriptTag({ path: corePath, type: 'module' });
84
+ await page.waitForFunction(() => !!window.__maudeCaptureCore, { timeout: timeoutMs });
77
85
 
78
86
  const written = [];
79
87
 
@@ -82,155 +90,28 @@ try {
82
90
  // multi-target loop must not leave an already-captured artboard sitting at
83
91
  // the origin, overlapping the next target's geometry (the scatter bug).
84
92
  const pinArtboard = async (handle) => {
85
- const saved = await handle.evaluate((el) => {
86
- const ab = el.closest('[data-dc-screen]') ?? el;
87
- const prev = { left: ab.style.left, top: ab.style.top };
88
- ab.style.left = '0px';
89
- ab.style.top = '0px';
90
- return prev;
91
- });
93
+ const saved = await handle.evaluate((el) => window.__maudeCaptureCore.pinArtboard(el));
92
94
  return async () => {
93
- await handle.evaluate((el, prev) => {
94
- const ab = el.closest('[data-dc-screen]') ?? el;
95
- ab.style.left = prev.left;
96
- ab.style.top = prev.top;
97
- }, saved);
95
+ await handle.evaluate(
96
+ (el, prev) => window.__maudeCaptureCore.restoreArtboard(el, prev),
97
+ saved
98
+ );
98
99
  };
99
100
  };
100
101
 
101
102
  // Single in-page serializer used by BOTH branches (single + multi) so they
102
103
  // can't drift again (the `formatXML` 500 bug came from a divergent copy).
103
- // Note: dom-to-svg exports only elementToSVG / inlineResources /
104
- // documentToSVG — there is NO `formatXML` pretty-printer; serialize straight
105
- // to a string. dom-to-svg also does NOT paint the captured root's background
106
- // fill, so the artboard's background was dropped (item 4) — we prepend a
107
- // backdrop <rect> from the artboard's effective (visible) background color.
104
+ // The ENTIRE capture logic (backdrop rect from the effective background,
105
+ // oklch→sRGB normalization for vector editors, inlineResources) lives in the
106
+ // shared capture spine (exporters/capture-core.ts) injected above — the same
107
+ // code the canvas runtime's export-capture bridge runs in the member's
108
+ // browser (DDR-231). Nothing capture-shaped may be inlined here again.
108
109
  const serializeOne = async (handle, widenToArtboard) => {
109
110
  return await handle.evaluate(
110
- async (el, opts) => {
111
- const target = opts.widenToArtboard ? (el.closest('[data-dc-screen]') ?? el) : el;
112
- // window.domToSvg is the IIFE-injected entry.
113
- const { elementToSVG, inlineResources } = /** @type any */ (window).domToSvg;
114
-
115
- // Convert ANY computed color (modern Chromium serializes `oklch(...)`
116
- // verbatim from getComputedStyle) to sRGB rgb()/rgba(). Affinity
117
- // Designer / older Illustrator / Inkscape don't parse CSS Color 4
118
- // (`oklch`, `color()`) and silently drop the fill — the artboard then
119
- // shows as transparent (the exact symptom: bg vanished in Affinity).
120
- // A 1×1 canvas normalizes any CSS color string to sRGB bytes. Returns
121
- // null if the value can't be painted (keep the rect off rather than
122
- // emit a wrong colour).
123
- const _srgbCache = new Map();
124
- const _srgbCanvas = document.createElement('canvas');
125
- _srgbCanvas.width = 1;
126
- _srgbCanvas.height = 1;
127
- const _srgbCtx = _srgbCanvas.getContext('2d');
128
- const toSrgb = (color) => {
129
- if (_srgbCache.has(color)) return _srgbCache.get(color);
130
- let result = null;
131
- try {
132
- if (_srgbCtx) {
133
- _srgbCtx.clearRect(0, 0, 1, 1);
134
- _srgbCtx.fillStyle = '#000';
135
- _srgbCtx.fillStyle = color; // ignored (stays #000) if syntax unsupported
136
- _srgbCtx.fillRect(0, 0, 1, 1);
137
- const [r, g, b, a] = _srgbCtx.getImageData(0, 0, 1, 1).data;
138
- if (a !== 0) {
139
- result =
140
- a === 255
141
- ? `rgb(${r}, ${g}, ${b})`
142
- : `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
143
- }
144
- }
145
- } catch {
146
- result = null;
147
- }
148
- _srgbCache.set(color, result);
149
- return result;
150
- };
151
-
152
- const isOpaque = (bg) =>
153
- !!bg &&
154
- bg !== 'transparent' &&
155
- !/^rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)$/.test(bg) &&
156
- !/^rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0(\.0+)?\s*\)$/.test(bg);
157
- // Effective background of the artboard region: the DS theme bg usually
158
- // sits on an inner full-bleed wrapper (`.app` / `.mdcc`), occasionally
159
- // on the artboard element itself, sometimes on body. Descend first to
160
- // find a near-full-bleed opaque bg, then fall back to ancestors. An
161
- // intentionally transparent artboard returns null → stays transparent.
162
- const effectiveBg = (node) => {
163
- const abRect = node.getBoundingClientRect();
164
- const abArea = Math.max(1, abRect.width * abRect.height);
165
- const queue = [node];
166
- let guard = 0;
167
- while (queue.length && guard < 80) {
168
- const e = queue.shift();
169
- guard += 1;
170
- const bg = getComputedStyle(e).backgroundColor;
171
- if (isOpaque(bg)) {
172
- const r = e.getBoundingClientRect();
173
- if (r.width * r.height >= abArea * 0.6) return bg;
174
- }
175
- for (const c of e.children) queue.push(c);
176
- }
177
- let cur = node.parentElement;
178
- let up = 0;
179
- while (cur && up < 12) {
180
- const bg = getComputedStyle(cur).backgroundColor;
181
- if (isOpaque(bg)) return bg;
182
- cur = cur.parentElement;
183
- up += 1;
184
- }
185
- return null;
186
- };
187
-
188
- const bg = effectiveBg(target);
189
- // Normalize to sRGB so the fill survives in vector editors (item 4 /
190
- // oklch follow-up). If conversion fails, skip the backdrop rather than
191
- // emit an unparseable fill.
192
- const fillColor = bg ? toSrgb(bg) : null;
193
- const svgDoc = elementToSVG(target);
194
- const root = svgDoc.documentElement;
195
- if (fillColor) {
196
- const NS = 'http://www.w3.org/2000/svg';
197
- const rect = svgDoc.createElementNS(NS, 'rect');
198
- const vb = (root.getAttribute('viewBox') || '').split(/\s+/).map(Number);
199
- if (vb.length === 4 && vb.every((n) => !Number.isNaN(n))) {
200
- rect.setAttribute('x', String(vb[0]));
201
- rect.setAttribute('y', String(vb[1]));
202
- rect.setAttribute('width', String(vb[2]));
203
- rect.setAttribute('height', String(vb[3]));
204
- } else {
205
- rect.setAttribute('x', '0');
206
- rect.setAttribute('y', '0');
207
- rect.setAttribute('width', '100%');
208
- rect.setAttribute('height', '100%');
209
- }
210
- rect.setAttribute('fill', fillColor);
211
- root.insertBefore(rect, root.firstChild);
212
- }
213
- // base64-embeds fonts + images so the SVG is portable outside the
214
- // dev-server origin. Some external fetches fail silently — Affinity
215
- // tolerates missing resources better than missing primitives.
216
- try {
217
- await inlineResources(root);
218
- } catch {
219
- /* best-effort */
220
- }
221
- let out = new XMLSerializer().serializeToString(svgDoc);
222
- // dom-to-svg copies computed colours through VERBATIM, and modern
223
- // Chromium serializes DS tokens as `oklch(...)` — Affinity Designer /
224
- // older Illustrator / Inkscape can't parse CSS Color 4 and drop every
225
- // such fill/stroke (the artboard bg + many element colours vanish).
226
- // Rewrite every CSS Color 4 colour function (oklch/oklab/lch/lab/color)
227
- // anywhere in the serialized SVG to its sRGB rgb()/rgba() equivalent.
228
- // Each token is a standalone colour (no nested parens), so a single
229
- // `\([^)]*\)` match is safe; toSrgb returns null on an unconvertible
230
- // value and we keep the original rather than corrupt it.
231
- out = out.replace(/(?:oklch|oklab|lch|lab|color)\([^)]*\)/gi, (m) => toSrgb(m) || m);
232
- return out;
233
- },
111
+ (el, opts) =>
112
+ window.__maudeCaptureCore.svgForElement(el, window.domToSvg, {
113
+ widenToArtboard: opts.widenToArtboard,
114
+ }),
234
115
  { widenToArtboard }
235
116
  );
236
117
  };
@@ -124,6 +124,15 @@ import {
124
124
  diffLayoutPositions,
125
125
  } from './commands/move-artboards-command.ts';
126
126
  import { scopedCdSelector, selectorIndex, shortText } from './dom-selection.ts';
127
+ import {
128
+ type DomToSvgApi,
129
+ pinArtboard,
130
+ rasterizeSvg,
131
+ resetWorldPlane,
132
+ restoreArtboard,
133
+ restoreWorldPlane,
134
+ svgForElement,
135
+ } from './exporters/capture-core.ts';
127
136
  // Photo editor (feature-photo-editor) — schema.ts is DEPENDENCY-FREE (no pixi),
128
137
  // so this static import is safe and adds zero bundle cost. The WebGL compositor
129
138
  // (photo/pipeline.ts, which imports pixi.js) is loaded via a DYNAMIC import
@@ -1787,8 +1796,128 @@ export function DesignCanvas(props: DesignCanvasProps) {
1787
1796
  }
1788
1797
  DesignCanvas.displayName = 'DesignCanvas';
1789
1798
 
1799
+ // DDR-231 (hybrid export lanes) — the workspace `browser` export lane.
1800
+ //
1801
+ // In workspace mode the studio shell cannot capture the canvas itself: the
1802
+ // iframe is a separate (untrusted, DDR-054) origin, unreachable by DOM from
1803
+ // the shell. So the SHELL ASKS and the CANVAS CAPTURES — this bridge listens
1804
+ // for an `export-capture` postMessage from the embedding parent, renders the
1805
+ // requested artboards through the shared capture spine (capture-core.ts — the
1806
+ // same code the playwright SVG shim injects, DDR-231's single-spine rule) and
1807
+ // replies with per-artboard Blobs (structured-clonable across the origin
1808
+ // split). dom-to-svg loads LAZILY through the importmap (`dom-to-svg` runtime
1809
+ // bundle — the pixi.js pattern), so a canvas that never exports pays zero
1810
+ // bundle cost.
1811
+ //
1812
+ // Capture mutates live DOM (world zoom reset + artboard pinned to origin) and
1813
+ // restores everything afterwards — the member is looking at this plane. The
1814
+ // shell overlays a veil during capture to cover the brief reflow.
1815
+ function useExportCaptureBridge() {
1816
+ useEffect(() => {
1817
+ if (typeof window === 'undefined' || window.parent === window) return;
1818
+ let busy = false;
1819
+ const onMsg = (e: MessageEvent) => {
1820
+ // Only the embedding shell may drive capture — a message from any other
1821
+ // window (a nested iframe, an opened popup) is ignored outright.
1822
+ if (e.source !== window.parent) return;
1823
+ const m = e.data as {
1824
+ dgn?: string;
1825
+ id?: unknown;
1826
+ format?: unknown;
1827
+ artboardIds?: unknown;
1828
+ scale?: unknown;
1829
+ } | null;
1830
+ if (!m || m.dgn !== 'export-capture' || typeof m.id !== 'string') return;
1831
+ const requestId = m.id;
1832
+ // DDR-231 security (L1): reply to the ASKING origin, never `'*'`. The
1833
+ // done-message carries the rendered artboard blobs; a wildcard target
1834
+ // would leak them to whatever page happens to be the parent if the
1835
+ // shell's `frame-ancestors` CSP were ever absent (tests / misconfig).
1836
+ // `e.origin` here is the embedding shell (we already gated
1837
+ // `e.source === window.parent`); fall back to `'*'` only for an opaque
1838
+ // `"null"` origin where a targeted post is impossible.
1839
+ const replyOrigin = e.origin && e.origin !== 'null' ? e.origin : '*';
1840
+ const reply = (msg: Record<string, unknown>) => {
1841
+ try {
1842
+ window.parent.postMessage(msg, replyOrigin);
1843
+ } catch {
1844
+ /* parent gone — nothing to deliver to */
1845
+ }
1846
+ };
1847
+ if (busy) {
1848
+ reply({
1849
+ dgn: 'export-capture-error',
1850
+ id: requestId,
1851
+ message: 'a capture is already running on this canvas',
1852
+ });
1853
+ return;
1854
+ }
1855
+ busy = true;
1856
+ const format = m.format === 'svg' ? 'svg' : 'png';
1857
+ const scale = Math.max(1, Math.min(8, Number(m.scale) || 1));
1858
+ const wanted = Array.isArray(m.artboardIds)
1859
+ ? m.artboardIds.filter((s): s is string => typeof s === 'string')
1860
+ : null;
1861
+ void (async () => {
1862
+ const savedWorld = resetWorldPlane();
1863
+ try {
1864
+ const all = Array.from(document.querySelectorAll('[data-dc-screen]'));
1865
+ const targets = wanted?.length
1866
+ ? all.filter((el) => wanted.includes(el.getAttribute('data-dc-screen') ?? ''))
1867
+ : all;
1868
+ if (!targets.length) throw new Error('no artboards matched the capture request');
1869
+ const api = (await import('dom-to-svg')) as unknown as DomToSvgApi;
1870
+ const items: { name: string; type: string; blob: Blob }[] = [];
1871
+ for (let i = 0; i < targets.length; i += 1) {
1872
+ const el = targets[i] as Element;
1873
+ const abId = el.getAttribute('data-dc-screen') || `artboard-${i + 1}`;
1874
+ const prevPos = pinArtboard(el);
1875
+ try {
1876
+ const svg = await svgForElement(el, api);
1877
+ if (format === 'svg') {
1878
+ items.push({
1879
+ name: `${abId}.svg`,
1880
+ type: 'image/svg+xml',
1881
+ blob: new Blob([svg], { type: 'image/svg+xml' }),
1882
+ });
1883
+ } else {
1884
+ items.push({
1885
+ name: `${abId}.png`,
1886
+ type: 'image/png',
1887
+ blob: await rasterizeSvg(svg, scale),
1888
+ });
1889
+ }
1890
+ } finally {
1891
+ restoreArtboard(el, prevPos);
1892
+ }
1893
+ reply({
1894
+ dgn: 'export-capture-progress',
1895
+ id: requestId,
1896
+ current: i + 1,
1897
+ total: targets.length,
1898
+ });
1899
+ }
1900
+ reply({ dgn: 'export-capture-done', id: requestId, items });
1901
+ } catch (err) {
1902
+ reply({
1903
+ dgn: 'export-capture-error',
1904
+ id: requestId,
1905
+ message: err instanceof Error ? err.message : String(err),
1906
+ });
1907
+ } finally {
1908
+ restoreWorldPlane(savedWorld);
1909
+ busy = false;
1910
+ }
1911
+ })();
1912
+ };
1913
+ window.addEventListener('message', onMsg);
1914
+ return () => window.removeEventListener('message', onMsg);
1915
+ }, []);
1916
+ }
1917
+
1790
1918
  function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
1791
1919
  ensureEngineStyles();
1920
+ useExportCaptureBridge();
1792
1921
 
1793
1922
  const hostRef = useRef<HTMLDivElement | null>(null);
1794
1923
  const worldRef = useRef<HTMLDivElement | null>(null);