@openfairygui/functions 0.2.0-alpha.34 → 0.2.0-alpha.36
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/README.md +3 -1
- package/dist/index.cjs +2 -2
- package/dist/index.js +2 -2
- package/dist/node.cjs +2 -2
- package/dist/node.js +2 -2
- package/dist/{publish-DCP0AYx2.cjs → publish-CykUJfVa.cjs} +144 -210
- package/dist/{publish-BJk8UzRP.js → publish-DXoaC1Nl.js} +140 -212
- package/dist/{restore-C98ugT2H.js → restore-BQp01WY3.js} +9 -1
- package/dist/{restore-BUaCK8ui.cjs → restore-CEywQUHz.cjs} +9 -1
- package/dist/web.cjs +171 -5
- package/dist/web.d.cts +3 -0
- package/dist/web.d.ts +3 -0
- package/dist/web.js +171 -5
- package/package.json +3 -2
- package/src/adapters/web/publish.ts +40 -3
- package/src/adapters/web/raster.ts +171 -1
- package/src/atlas/inputs.ts +77 -112
- package/src/atlas/jta.ts +116 -170
- package/src/atlas.ts +12 -1
- package/src/publish.ts +45 -2
- package/src/restore.ts +10 -0
package/dist/web.cjs
CHANGED
|
@@ -1,7 +1,116 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_publish = require("./publish-
|
|
2
|
+
const require_publish = require("./publish-CykUJfVa.cjs");
|
|
3
3
|
let _openfairygui_core = require("@openfairygui/core");
|
|
4
|
+
let fast_xml_parser = require("fast-xml-parser");
|
|
4
5
|
//#region src/adapters/web/raster.ts
|
|
6
|
+
const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
const MAX_SVG_DIMENSION = 16384;
|
|
8
|
+
const MAX_SVG_PIXELS = 64 * 1024 * 1024;
|
|
9
|
+
const MAX_SVG_NODES = 5e4;
|
|
10
|
+
const UNSAFE_SVG_ELEMENTS = new Set([
|
|
11
|
+
"a",
|
|
12
|
+
"animate",
|
|
13
|
+
"animatecolor",
|
|
14
|
+
"animatemotion",
|
|
15
|
+
"animatetransform",
|
|
16
|
+
"audio",
|
|
17
|
+
"canvas",
|
|
18
|
+
"discard",
|
|
19
|
+
"embed",
|
|
20
|
+
"feimage",
|
|
21
|
+
"foreignobject",
|
|
22
|
+
"iframe",
|
|
23
|
+
"image",
|
|
24
|
+
"object",
|
|
25
|
+
"script",
|
|
26
|
+
"set",
|
|
27
|
+
"style",
|
|
28
|
+
"video"
|
|
29
|
+
]);
|
|
30
|
+
function unsafeSvg(message) {
|
|
31
|
+
throw new Error(`publishBrowser: unsafe SVG input (${message}).`);
|
|
32
|
+
}
|
|
33
|
+
function svgLocalName(name) {
|
|
34
|
+
return name.split(":").at(-1).toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
function parseSvgLength(value, name) {
|
|
37
|
+
if (value === void 0) return void 0;
|
|
38
|
+
const match = String(value).match(/^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/iu);
|
|
39
|
+
if (!match) unsafeSvg(`${name} must use a finite pixel value`);
|
|
40
|
+
const parsed = Number.parseFloat(match[0]);
|
|
41
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SVG_DIMENSION) unsafeSvg(`${name} exceeds the supported dimensions`);
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
function validateSvgAttribute(name, value) {
|
|
45
|
+
const normalizedName = name.toLowerCase();
|
|
46
|
+
if (normalizedName === "xmlns" || normalizedName.startsWith("xmlns:")) return;
|
|
47
|
+
const localName = svgLocalName(name);
|
|
48
|
+
const text = String(value);
|
|
49
|
+
if (localName.startsWith("on")) unsafeSvg(`event attribute "${name}" is not allowed`);
|
|
50
|
+
if (localName === "style" || localName === "src") unsafeSvg(`attribute "${name}" is not allowed`);
|
|
51
|
+
if (localName === "href" && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) unsafeSvg(`external reference in "${name}" is not allowed`);
|
|
52
|
+
if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) unsafeSvg(`external URL in "${name}" is not allowed`);
|
|
53
|
+
for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
|
|
54
|
+
const reference = (match[1] ?? "").trim().replace(/^(['"])(.*)\1$/u, "$2");
|
|
55
|
+
if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function visitSvgEntry(entry) {
|
|
59
|
+
const pending = [entry];
|
|
60
|
+
let nodeCount = 0;
|
|
61
|
+
while (pending.length > 0) {
|
|
62
|
+
const current = pending.pop();
|
|
63
|
+
for (const [name, value] of Object.entries(current)) {
|
|
64
|
+
if (name === ":@" || name.startsWith("#") || name.startsWith("?")) continue;
|
|
65
|
+
if (++nodeCount > MAX_SVG_NODES) unsafeSvg("node count exceeds the supported limit");
|
|
66
|
+
const localName = svgLocalName(name);
|
|
67
|
+
if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
|
|
68
|
+
for (const [attributeName, attributeValue] of Object.entries(current[":@"] ?? {})) validateSvgAttribute(attributeName, attributeValue);
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
for (const child of value) if (child && typeof child === "object" && !Array.isArray(child)) pending.push(child);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function validateSvg(bytes) {
|
|
76
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg("source size is unsupported");
|
|
77
|
+
let source;
|
|
78
|
+
try {
|
|
79
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
80
|
+
} catch {
|
|
81
|
+
unsafeSvg("source is not valid UTF-8");
|
|
82
|
+
}
|
|
83
|
+
if (/<!\s*(?:doctype|entity)\b|<\?xml-stylesheet\b/iu.test(source)) unsafeSvg("DTD, entities, and stylesheets are not allowed");
|
|
84
|
+
if (fast_xml_parser.XMLValidator.validate(source, { allowBooleanAttributes: false }) !== true) unsafeSvg("source is not well-formed XML");
|
|
85
|
+
const roots = new fast_xml_parser.XMLParser({
|
|
86
|
+
preserveOrder: true,
|
|
87
|
+
ignoreAttributes: false,
|
|
88
|
+
attributeNamePrefix: "",
|
|
89
|
+
parseAttributeValue: false,
|
|
90
|
+
parseTagValue: false,
|
|
91
|
+
processEntities: false,
|
|
92
|
+
trimValues: false
|
|
93
|
+
}).parse(source).flatMap((entry) => Object.keys(entry).filter((name) => name !== ":@" && !name.startsWith("#") && !name.startsWith("?")).map((name) => ({
|
|
94
|
+
entry,
|
|
95
|
+
name
|
|
96
|
+
})));
|
|
97
|
+
if (roots.length !== 1 || svgLocalName(roots[0].name) !== "svg") unsafeSvg("a single <svg> root is required");
|
|
98
|
+
const root = roots[0].entry;
|
|
99
|
+
visitSvgEntry(root);
|
|
100
|
+
const attributes = root[":@"] ?? {};
|
|
101
|
+
const width = parseSvgLength(attributes.width, "width");
|
|
102
|
+
const height = parseSvgLength(attributes.height, "height");
|
|
103
|
+
let viewBoxWidth;
|
|
104
|
+
let viewBoxHeight;
|
|
105
|
+
if (attributes.viewBox !== void 0) {
|
|
106
|
+
const viewBox = String(attributes.viewBox).trim().split(/[\s,]+/u).map(Number);
|
|
107
|
+
if (viewBox.length !== 4 || viewBox.some((value) => !Number.isFinite(value)) || viewBox[2] <= 0 || viewBox[3] <= 0) unsafeSvg("viewBox must contain four finite values with positive dimensions");
|
|
108
|
+
viewBoxWidth = viewBox[2];
|
|
109
|
+
viewBoxHeight = viewBox[3];
|
|
110
|
+
if (viewBoxWidth > MAX_SVG_DIMENSION || viewBoxHeight > MAX_SVG_DIMENSION) unsafeSvg("viewBox exceeds the supported dimensions");
|
|
111
|
+
}
|
|
112
|
+
if ((width ?? viewBoxWidth ?? 300) * (height ?? viewBoxHeight ?? 150) > MAX_SVG_PIXELS) unsafeSvg("pixel count exceeds the supported limit");
|
|
113
|
+
}
|
|
5
114
|
function getBrowserContext(canvas) {
|
|
6
115
|
const context = canvas.getContext("2d");
|
|
7
116
|
if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
|
|
@@ -60,7 +169,15 @@ async function canvasToPng(canvas) {
|
|
|
60
169
|
async function decodeRaster(bytes, mimeType) {
|
|
61
170
|
if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
|
|
62
171
|
const copy = bytes.slice();
|
|
63
|
-
|
|
172
|
+
if (mimeType === "image/svg+xml") validateSvg(copy);
|
|
173
|
+
const blob = new Blob([copy.buffer], { type: mimeType });
|
|
174
|
+
let bitmap;
|
|
175
|
+
try {
|
|
176
|
+
bitmap = await createImageBitmap(blob);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (mimeType !== "image/svg+xml") throw error;
|
|
179
|
+
return decodeSvgWithDom(blob);
|
|
180
|
+
}
|
|
64
181
|
try {
|
|
65
182
|
const raster = createRaster(bitmap.width, bitmap.height);
|
|
66
183
|
getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
|
|
@@ -69,6 +186,26 @@ async function decodeRaster(bytes, mimeType) {
|
|
|
69
186
|
bitmap.close();
|
|
70
187
|
}
|
|
71
188
|
}
|
|
189
|
+
async function decodeSvgWithDom(blob) {
|
|
190
|
+
if (typeof globalThis.Image !== "function" || typeof globalThis.URL?.createObjectURL !== "function" || typeof globalThis.URL?.revokeObjectURL !== "function") throw new Error("publishBrowser: createImageBitmap rejected SVG and DOM image decoding is unavailable.");
|
|
191
|
+
const url = globalThis.URL.createObjectURL(blob);
|
|
192
|
+
try {
|
|
193
|
+
const image = new globalThis.Image();
|
|
194
|
+
await new Promise((resolve, reject) => {
|
|
195
|
+
image.onload = () => resolve();
|
|
196
|
+
image.onerror = () => reject(/* @__PURE__ */ new Error("publishBrowser: DOM image decoding failed for SVG."));
|
|
197
|
+
image.src = url;
|
|
198
|
+
});
|
|
199
|
+
const width = image.naturalWidth || image.width;
|
|
200
|
+
const height = image.naturalHeight || image.height;
|
|
201
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_SVG_DIMENSION || height > MAX_SVG_DIMENSION || width * height > MAX_SVG_PIXELS) unsafeSvg("decoded dimensions exceed the supported limit");
|
|
202
|
+
const raster = createRaster(width, height);
|
|
203
|
+
getBrowserContext(raster.canvas).drawImage(image, 0, 0);
|
|
204
|
+
return raster;
|
|
205
|
+
} finally {
|
|
206
|
+
globalThis.URL.revokeObjectURL(url);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
72
209
|
var BrowserImagePipeline = class {
|
|
73
210
|
rawOutput = false;
|
|
74
211
|
constructor(raster, decode, write) {
|
|
@@ -223,6 +360,15 @@ function toResult(success, files, diagnostics) {
|
|
|
223
360
|
diagnostics
|
|
224
361
|
};
|
|
225
362
|
}
|
|
363
|
+
function unsupportedSetting(setting, path, message) {
|
|
364
|
+
return {
|
|
365
|
+
level: "error",
|
|
366
|
+
code: "unsupported_publish_setting",
|
|
367
|
+
setting,
|
|
368
|
+
path,
|
|
369
|
+
message
|
|
370
|
+
};
|
|
371
|
+
}
|
|
226
372
|
/**
|
|
227
373
|
* Publish a loaded FairyGUI project to browser-provided storage.
|
|
228
374
|
*
|
|
@@ -238,14 +384,33 @@ async function publishBrowser(options) {
|
|
|
238
384
|
options.document.setLogger(createDiagnosticLogger(previousLogger, diagnostics));
|
|
239
385
|
try {
|
|
240
386
|
if (options.projectType !== "layabox") throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
|
|
241
|
-
assertBrowserImageSupport();
|
|
242
387
|
root.setProjectType(_openfairygui_core.ProjectType.LayaBox);
|
|
388
|
+
const resolved = require_publish.resolvePublishOptions(options.document, {
|
|
389
|
+
compressed: options.compressed,
|
|
390
|
+
packages: options.packages,
|
|
391
|
+
atlas: options.atlas
|
|
392
|
+
});
|
|
393
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
|
|
394
|
+
diagnostics.push(unsupportedSetting("fileExtension", "settings.publish.fileExtension", `publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`));
|
|
395
|
+
return toResult(false, files, diagnostics);
|
|
396
|
+
}
|
|
397
|
+
const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
|
|
398
|
+
const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
|
|
399
|
+
if (require_publish.resolveCodeGenerationSettings(options.document).allowGenCode) {
|
|
400
|
+
const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
|
|
401
|
+
if (packageIndex >= 0) {
|
|
402
|
+
const pkg = selectedPackages[packageIndex];
|
|
403
|
+
diagnostics.push(unsupportedSetting("codeGeneration", `packages[${root.listPackages().indexOf(pkg)}].publish.genCode`, `publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`));
|
|
404
|
+
return toResult(false, files, diagnostics);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
assertBrowserImageSupport();
|
|
243
408
|
const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
|
|
244
409
|
const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), "assets");
|
|
245
410
|
await options.document.transform(require_publish.publish({
|
|
246
411
|
output: options.output,
|
|
247
|
-
compressed:
|
|
248
|
-
fileExtension:
|
|
412
|
+
compressed: resolved.compressed,
|
|
413
|
+
fileExtension: resolved.fileExtension,
|
|
249
414
|
packages: options.packages,
|
|
250
415
|
branch: options.branch,
|
|
251
416
|
basePath: sourceAssetsPath,
|
|
@@ -262,6 +427,7 @@ async function publishBrowser(options) {
|
|
|
262
427
|
} catch (error) {
|
|
263
428
|
diagnostics.push({
|
|
264
429
|
level: "error",
|
|
430
|
+
code: "publish_failed",
|
|
265
431
|
message: error instanceof Error ? error.message : String(error)
|
|
266
432
|
});
|
|
267
433
|
return toResult(false, files, diagnostics);
|
package/dist/web.d.cts
CHANGED
|
@@ -20,6 +20,9 @@ interface BrowserPublishOptions {
|
|
|
20
20
|
interface BrowserPublishDiagnostic {
|
|
21
21
|
level: 'debug' | 'info' | 'warning' | 'error';
|
|
22
22
|
message: string;
|
|
23
|
+
code?: 'unsupported_publish_setting' | 'publish_failed';
|
|
24
|
+
setting?: string;
|
|
25
|
+
path?: string;
|
|
23
26
|
}
|
|
24
27
|
interface BrowserPublishedFile {
|
|
25
28
|
path: string;
|
package/dist/web.d.ts
CHANGED
|
@@ -20,6 +20,9 @@ interface BrowserPublishOptions {
|
|
|
20
20
|
interface BrowserPublishDiagnostic {
|
|
21
21
|
level: 'debug' | 'info' | 'warning' | 'error';
|
|
22
22
|
message: string;
|
|
23
|
+
code?: 'unsupported_publish_setting' | 'publish_failed';
|
|
24
|
+
setting?: string;
|
|
25
|
+
path?: string;
|
|
23
26
|
}
|
|
24
27
|
interface BrowserPublishedFile {
|
|
25
28
|
path: string;
|
package/dist/web.js
CHANGED
|
@@ -1,6 +1,115 @@
|
|
|
1
|
-
import { t as publish } from "./publish-
|
|
1
|
+
import { c as resolveCodeGenerationSettings, n as resolvePublishOptions, t as publish } from "./publish-DXoaC1Nl.js";
|
|
2
2
|
import { ProjectType } from "@openfairygui/core";
|
|
3
|
+
import { XMLParser, XMLValidator } from "fast-xml-parser";
|
|
3
4
|
//#region src/adapters/web/raster.ts
|
|
5
|
+
const MAX_SVG_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
6
|
+
const MAX_SVG_DIMENSION = 16384;
|
|
7
|
+
const MAX_SVG_PIXELS = 64 * 1024 * 1024;
|
|
8
|
+
const MAX_SVG_NODES = 5e4;
|
|
9
|
+
const UNSAFE_SVG_ELEMENTS = new Set([
|
|
10
|
+
"a",
|
|
11
|
+
"animate",
|
|
12
|
+
"animatecolor",
|
|
13
|
+
"animatemotion",
|
|
14
|
+
"animatetransform",
|
|
15
|
+
"audio",
|
|
16
|
+
"canvas",
|
|
17
|
+
"discard",
|
|
18
|
+
"embed",
|
|
19
|
+
"feimage",
|
|
20
|
+
"foreignobject",
|
|
21
|
+
"iframe",
|
|
22
|
+
"image",
|
|
23
|
+
"object",
|
|
24
|
+
"script",
|
|
25
|
+
"set",
|
|
26
|
+
"style",
|
|
27
|
+
"video"
|
|
28
|
+
]);
|
|
29
|
+
function unsafeSvg(message) {
|
|
30
|
+
throw new Error(`publishBrowser: unsafe SVG input (${message}).`);
|
|
31
|
+
}
|
|
32
|
+
function svgLocalName(name) {
|
|
33
|
+
return name.split(":").at(-1).toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
function parseSvgLength(value, name) {
|
|
36
|
+
if (value === void 0) return void 0;
|
|
37
|
+
const match = String(value).match(/^\s*(?:\d+(?:\.\d+)?|\.\d+)(?:px)?\s*$/iu);
|
|
38
|
+
if (!match) unsafeSvg(`${name} must use a finite pixel value`);
|
|
39
|
+
const parsed = Number.parseFloat(match[0]);
|
|
40
|
+
if (!Number.isFinite(parsed) || parsed <= 0 || parsed > MAX_SVG_DIMENSION) unsafeSvg(`${name} exceeds the supported dimensions`);
|
|
41
|
+
return parsed;
|
|
42
|
+
}
|
|
43
|
+
function validateSvgAttribute(name, value) {
|
|
44
|
+
const normalizedName = name.toLowerCase();
|
|
45
|
+
if (normalizedName === "xmlns" || normalizedName.startsWith("xmlns:")) return;
|
|
46
|
+
const localName = svgLocalName(name);
|
|
47
|
+
const text = String(value);
|
|
48
|
+
if (localName.startsWith("on")) unsafeSvg(`event attribute "${name}" is not allowed`);
|
|
49
|
+
if (localName === "style" || localName === "src") unsafeSvg(`attribute "${name}" is not allowed`);
|
|
50
|
+
if (localName === "href" && !/^#[A-Za-z_][\w:.-]*$/u.test(text)) unsafeSvg(`external reference in "${name}" is not allowed`);
|
|
51
|
+
if (/(?:^|[\s("'=])(?:https?:|file:|javascript:|data:|\/\/)/iu.test(text)) unsafeSvg(`external URL in "${name}" is not allowed`);
|
|
52
|
+
for (const match of text.matchAll(/url\s*\(([^)]*)\)/giu)) {
|
|
53
|
+
const reference = (match[1] ?? "").trim().replace(/^(['"])(.*)\1$/u, "$2");
|
|
54
|
+
if (!/^#[A-Za-z_][\w:.-]*$/u.test(reference)) unsafeSvg(`external url() in "${name}" is not allowed`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function visitSvgEntry(entry) {
|
|
58
|
+
const pending = [entry];
|
|
59
|
+
let nodeCount = 0;
|
|
60
|
+
while (pending.length > 0) {
|
|
61
|
+
const current = pending.pop();
|
|
62
|
+
for (const [name, value] of Object.entries(current)) {
|
|
63
|
+
if (name === ":@" || name.startsWith("#") || name.startsWith("?")) continue;
|
|
64
|
+
if (++nodeCount > MAX_SVG_NODES) unsafeSvg("node count exceeds the supported limit");
|
|
65
|
+
const localName = svgLocalName(name);
|
|
66
|
+
if (UNSAFE_SVG_ELEMENTS.has(localName)) unsafeSvg(`element <${name}> is not allowed`);
|
|
67
|
+
for (const [attributeName, attributeValue] of Object.entries(current[":@"] ?? {})) validateSvgAttribute(attributeName, attributeValue);
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
for (const child of value) if (child && typeof child === "object" && !Array.isArray(child)) pending.push(child);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function validateSvg(bytes) {
|
|
75
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_SVG_SOURCE_BYTES) unsafeSvg("source size is unsupported");
|
|
76
|
+
let source;
|
|
77
|
+
try {
|
|
78
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
79
|
+
} catch {
|
|
80
|
+
unsafeSvg("source is not valid UTF-8");
|
|
81
|
+
}
|
|
82
|
+
if (/<!\s*(?:doctype|entity)\b|<\?xml-stylesheet\b/iu.test(source)) unsafeSvg("DTD, entities, and stylesheets are not allowed");
|
|
83
|
+
if (XMLValidator.validate(source, { allowBooleanAttributes: false }) !== true) unsafeSvg("source is not well-formed XML");
|
|
84
|
+
const roots = new XMLParser({
|
|
85
|
+
preserveOrder: true,
|
|
86
|
+
ignoreAttributes: false,
|
|
87
|
+
attributeNamePrefix: "",
|
|
88
|
+
parseAttributeValue: false,
|
|
89
|
+
parseTagValue: false,
|
|
90
|
+
processEntities: false,
|
|
91
|
+
trimValues: false
|
|
92
|
+
}).parse(source).flatMap((entry) => Object.keys(entry).filter((name) => name !== ":@" && !name.startsWith("#") && !name.startsWith("?")).map((name) => ({
|
|
93
|
+
entry,
|
|
94
|
+
name
|
|
95
|
+
})));
|
|
96
|
+
if (roots.length !== 1 || svgLocalName(roots[0].name) !== "svg") unsafeSvg("a single <svg> root is required");
|
|
97
|
+
const root = roots[0].entry;
|
|
98
|
+
visitSvgEntry(root);
|
|
99
|
+
const attributes = root[":@"] ?? {};
|
|
100
|
+
const width = parseSvgLength(attributes.width, "width");
|
|
101
|
+
const height = parseSvgLength(attributes.height, "height");
|
|
102
|
+
let viewBoxWidth;
|
|
103
|
+
let viewBoxHeight;
|
|
104
|
+
if (attributes.viewBox !== void 0) {
|
|
105
|
+
const viewBox = String(attributes.viewBox).trim().split(/[\s,]+/u).map(Number);
|
|
106
|
+
if (viewBox.length !== 4 || viewBox.some((value) => !Number.isFinite(value)) || viewBox[2] <= 0 || viewBox[3] <= 0) unsafeSvg("viewBox must contain four finite values with positive dimensions");
|
|
107
|
+
viewBoxWidth = viewBox[2];
|
|
108
|
+
viewBoxHeight = viewBox[3];
|
|
109
|
+
if (viewBoxWidth > MAX_SVG_DIMENSION || viewBoxHeight > MAX_SVG_DIMENSION) unsafeSvg("viewBox exceeds the supported dimensions");
|
|
110
|
+
}
|
|
111
|
+
if ((width ?? viewBoxWidth ?? 300) * (height ?? viewBoxHeight ?? 150) > MAX_SVG_PIXELS) unsafeSvg("pixel count exceeds the supported limit");
|
|
112
|
+
}
|
|
4
113
|
function getBrowserContext(canvas) {
|
|
5
114
|
const context = canvas.getContext("2d");
|
|
6
115
|
if (!context) throw new Error("publishBrowser: a 2D canvas context is unavailable.");
|
|
@@ -59,7 +168,15 @@ async function canvasToPng(canvas) {
|
|
|
59
168
|
async function decodeRaster(bytes, mimeType) {
|
|
60
169
|
if (typeof createImageBitmap !== "function") throw new Error("publishBrowser: createImageBitmap is required for atlas PNG generation.");
|
|
61
170
|
const copy = bytes.slice();
|
|
62
|
-
|
|
171
|
+
if (mimeType === "image/svg+xml") validateSvg(copy);
|
|
172
|
+
const blob = new Blob([copy.buffer], { type: mimeType });
|
|
173
|
+
let bitmap;
|
|
174
|
+
try {
|
|
175
|
+
bitmap = await createImageBitmap(blob);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (mimeType !== "image/svg+xml") throw error;
|
|
178
|
+
return decodeSvgWithDom(blob);
|
|
179
|
+
}
|
|
63
180
|
try {
|
|
64
181
|
const raster = createRaster(bitmap.width, bitmap.height);
|
|
65
182
|
getBrowserContext(raster.canvas).drawImage(bitmap, 0, 0);
|
|
@@ -68,6 +185,26 @@ async function decodeRaster(bytes, mimeType) {
|
|
|
68
185
|
bitmap.close();
|
|
69
186
|
}
|
|
70
187
|
}
|
|
188
|
+
async function decodeSvgWithDom(blob) {
|
|
189
|
+
if (typeof globalThis.Image !== "function" || typeof globalThis.URL?.createObjectURL !== "function" || typeof globalThis.URL?.revokeObjectURL !== "function") throw new Error("publishBrowser: createImageBitmap rejected SVG and DOM image decoding is unavailable.");
|
|
190
|
+
const url = globalThis.URL.createObjectURL(blob);
|
|
191
|
+
try {
|
|
192
|
+
const image = new globalThis.Image();
|
|
193
|
+
await new Promise((resolve, reject) => {
|
|
194
|
+
image.onload = () => resolve();
|
|
195
|
+
image.onerror = () => reject(/* @__PURE__ */ new Error("publishBrowser: DOM image decoding failed for SVG."));
|
|
196
|
+
image.src = url;
|
|
197
|
+
});
|
|
198
|
+
const width = image.naturalWidth || image.width;
|
|
199
|
+
const height = image.naturalHeight || image.height;
|
|
200
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0 || width > MAX_SVG_DIMENSION || height > MAX_SVG_DIMENSION || width * height > MAX_SVG_PIXELS) unsafeSvg("decoded dimensions exceed the supported limit");
|
|
201
|
+
const raster = createRaster(width, height);
|
|
202
|
+
getBrowserContext(raster.canvas).drawImage(image, 0, 0);
|
|
203
|
+
return raster;
|
|
204
|
+
} finally {
|
|
205
|
+
globalThis.URL.revokeObjectURL(url);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
71
208
|
var BrowserImagePipeline = class {
|
|
72
209
|
rawOutput = false;
|
|
73
210
|
constructor(raster, decode, write) {
|
|
@@ -222,6 +359,15 @@ function toResult(success, files, diagnostics) {
|
|
|
222
359
|
diagnostics
|
|
223
360
|
};
|
|
224
361
|
}
|
|
362
|
+
function unsupportedSetting(setting, path, message) {
|
|
363
|
+
return {
|
|
364
|
+
level: "error",
|
|
365
|
+
code: "unsupported_publish_setting",
|
|
366
|
+
setting,
|
|
367
|
+
path,
|
|
368
|
+
message
|
|
369
|
+
};
|
|
370
|
+
}
|
|
225
371
|
/**
|
|
226
372
|
* Publish a loaded FairyGUI project to browser-provided storage.
|
|
227
373
|
*
|
|
@@ -237,14 +383,33 @@ async function publishBrowser(options) {
|
|
|
237
383
|
options.document.setLogger(createDiagnosticLogger(previousLogger, diagnostics));
|
|
238
384
|
try {
|
|
239
385
|
if (options.projectType !== "layabox") throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
|
|
240
|
-
assertBrowserImageSupport();
|
|
241
386
|
root.setProjectType(ProjectType.LayaBox);
|
|
387
|
+
const resolved = resolvePublishOptions(options.document, {
|
|
388
|
+
compressed: options.compressed,
|
|
389
|
+
packages: options.packages,
|
|
390
|
+
atlas: options.atlas
|
|
391
|
+
});
|
|
392
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
|
|
393
|
+
diagnostics.push(unsupportedSetting("fileExtension", "settings.publish.fileExtension", `publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`));
|
|
394
|
+
return toResult(false, files, diagnostics);
|
|
395
|
+
}
|
|
396
|
+
const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
|
|
397
|
+
const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
|
|
398
|
+
if (resolveCodeGenerationSettings(options.document).allowGenCode) {
|
|
399
|
+
const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
|
|
400
|
+
if (packageIndex >= 0) {
|
|
401
|
+
const pkg = selectedPackages[packageIndex];
|
|
402
|
+
diagnostics.push(unsupportedSetting("codeGeneration", `packages[${root.listPackages().indexOf(pkg)}].publish.genCode`, `publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`));
|
|
403
|
+
return toResult(false, files, diagnostics);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
assertBrowserImageSupport();
|
|
242
407
|
const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
|
|
243
408
|
const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), "assets");
|
|
244
409
|
await options.document.transform(publish({
|
|
245
410
|
output: options.output,
|
|
246
|
-
compressed:
|
|
247
|
-
fileExtension:
|
|
411
|
+
compressed: resolved.compressed,
|
|
412
|
+
fileExtension: resolved.fileExtension,
|
|
248
413
|
packages: options.packages,
|
|
249
414
|
branch: options.branch,
|
|
250
415
|
basePath: sourceAssetsPath,
|
|
@@ -261,6 +426,7 @@ async function publishBrowser(options) {
|
|
|
261
426
|
} catch (error) {
|
|
262
427
|
diagnostics.push({
|
|
263
428
|
level: "error",
|
|
429
|
+
code: "publish_failed",
|
|
264
430
|
message: error instanceof Error ? error.message : String(error)
|
|
265
431
|
});
|
|
266
432
|
return toResult(false, files, diagnostics);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfairygui/functions",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.36",
|
|
4
4
|
"description": "FairyGUI Headless Authoring SDK — composable transform functions.",
|
|
5
5
|
"author": "OpenFairyGUI Contributors",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,8 +73,9 @@
|
|
|
73
73
|
"publish"
|
|
74
74
|
],
|
|
75
75
|
"dependencies": {
|
|
76
|
+
"fast-xml-parser": "^5.0.0",
|
|
76
77
|
"jiti": "^2.6.1",
|
|
77
|
-
"@openfairygui/core": "0.2.0-alpha.
|
|
78
|
+
"@openfairygui/core": "0.2.0-alpha.36"
|
|
78
79
|
},
|
|
79
80
|
"optionalDependencies": {
|
|
80
81
|
"sharp": ">=0.33.0"
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { type Document, type ILogger, ProjectType } from '@openfairygui/core';
|
|
2
2
|
import type { AtlasOptions } from '../../atlas.js';
|
|
3
|
+
import { resolveCodeGenerationSettings } from '../../codegen.js';
|
|
3
4
|
import { publish } from '../../publish.js';
|
|
4
5
|
import type {
|
|
5
6
|
PublishFileSystem,
|
|
6
7
|
PublishOutputFileSystem,
|
|
7
8
|
PublishSourceFileSystem,
|
|
8
9
|
} from '../../publish/contracts.js';
|
|
10
|
+
import { resolvePublishOptions } from '../../publish/options.js';
|
|
9
11
|
import { assertBrowserImageSupport, createBrowserImageEncoder } from './raster.js';
|
|
10
12
|
|
|
11
13
|
export type BrowserPublishProjectType = 'layabox';
|
|
@@ -42,6 +44,9 @@ export interface BrowserPublishOptions {
|
|
|
42
44
|
export interface BrowserPublishDiagnostic {
|
|
43
45
|
level: 'debug' | 'info' | 'warning' | 'error';
|
|
44
46
|
message: string;
|
|
47
|
+
code?: 'unsupported_publish_setting' | 'publish_failed';
|
|
48
|
+
setting?: string;
|
|
49
|
+
path?: string;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
export interface BrowserPublishedFile {
|
|
@@ -103,6 +108,10 @@ function toResult(
|
|
|
103
108
|
};
|
|
104
109
|
}
|
|
105
110
|
|
|
111
|
+
function unsupportedSetting(setting: string, path: string, message: string): BrowserPublishDiagnostic {
|
|
112
|
+
return { level: 'error', code: 'unsupported_publish_setting', setting, path, message };
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
/**
|
|
107
116
|
* Publish a loaded FairyGUI project to browser-provided storage.
|
|
108
117
|
*
|
|
@@ -121,16 +130,43 @@ export async function publishBrowser(options: BrowserPublishOptions): Promise<Br
|
|
|
121
130
|
if (options.projectType !== 'layabox') {
|
|
122
131
|
throw new Error(`publishBrowser: unsupported project type "${String(options.projectType)}".`);
|
|
123
132
|
}
|
|
124
|
-
assertBrowserImageSupport();
|
|
125
133
|
root.setProjectType(ProjectType.LayaBox);
|
|
134
|
+
const resolved = resolvePublishOptions(options.document, {
|
|
135
|
+
compressed: options.compressed,
|
|
136
|
+
packages: options.packages,
|
|
137
|
+
atlas: options.atlas,
|
|
138
|
+
});
|
|
139
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(resolved.fileExtension)) {
|
|
140
|
+
diagnostics.push(unsupportedSetting(
|
|
141
|
+
'fileExtension',
|
|
142
|
+
'settings.publish.fileExtension',
|
|
143
|
+
`publishBrowser: unsupported fileExtension "${resolved.fileExtension}".`,
|
|
144
|
+
));
|
|
145
|
+
return toResult(false, files, diagnostics);
|
|
146
|
+
}
|
|
147
|
+
const selectedPackageNames = options.packages?.length ? new Set(options.packages) : null;
|
|
148
|
+
const selectedPackages = root.listPackages().filter((pkg) => !selectedPackageNames || selectedPackageNames.has(pkg.getName()));
|
|
149
|
+
if (resolveCodeGenerationSettings(options.document).allowGenCode) {
|
|
150
|
+
const packageIndex = selectedPackages.findIndex((pkg) => pkg.getGenCode());
|
|
151
|
+
if (packageIndex >= 0) {
|
|
152
|
+
const pkg = selectedPackages[packageIndex]!;
|
|
153
|
+
diagnostics.push(unsupportedSetting(
|
|
154
|
+
'codeGeneration',
|
|
155
|
+
`packages[${root.listPackages().indexOf(pkg)}].publish.genCode`,
|
|
156
|
+
`publishBrowser: code generation requested by package "${pkg.getName()}" is not supported.`,
|
|
157
|
+
));
|
|
158
|
+
return toResult(false, files, diagnostics);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
assertBrowserImageSupport();
|
|
126
162
|
const outputFileSystem = createTrackingFileSystem(options.outputFileSystem, files);
|
|
127
163
|
const sourceAssetsPath = options.sourceFileSystem.join(options.document.getProjectDir(), 'assets');
|
|
128
164
|
|
|
129
165
|
await options.document.transform(
|
|
130
166
|
publish({
|
|
131
167
|
output: options.output,
|
|
132
|
-
compressed:
|
|
133
|
-
fileExtension:
|
|
168
|
+
compressed: resolved.compressed,
|
|
169
|
+
fileExtension: resolved.fileExtension,
|
|
134
170
|
packages: options.packages,
|
|
135
171
|
branch: options.branch,
|
|
136
172
|
basePath: sourceAssetsPath,
|
|
@@ -149,6 +185,7 @@ export async function publishBrowser(options: BrowserPublishOptions): Promise<Br
|
|
|
149
185
|
} catch (error) {
|
|
150
186
|
diagnostics.push({
|
|
151
187
|
level: 'error',
|
|
188
|
+
code: 'publish_failed',
|
|
152
189
|
message: error instanceof Error ? error.message : String(error),
|
|
153
190
|
});
|
|
154
191
|
return toResult(false, files, diagnostics);
|