@peekling/runtime 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/AUTHORS +8 -0
  2. package/LICENSE +202 -0
  3. package/LICENSING.md +20 -0
  4. package/NOTICE +6 -0
  5. package/README.md +294 -0
  6. package/dist/configuration-validation.d.ts +19 -0
  7. package/dist/configuration-validation.d.ts.map +1 -0
  8. package/dist/configuration-validation.js +298 -0
  9. package/dist/content.d.ts +44 -0
  10. package/dist/content.d.ts.map +1 -0
  11. package/dist/content.js +738 -0
  12. package/dist/contracts.d.ts +10 -0
  13. package/dist/contracts.d.ts.map +1 -0
  14. package/dist/contracts.js +106 -0
  15. package/dist/defaults.d.ts +7 -0
  16. package/dist/defaults.d.ts.map +1 -0
  17. package/dist/defaults.js +6 -0
  18. package/dist/diagnostics.d.ts +25 -0
  19. package/dist/diagnostics.d.ts.map +1 -0
  20. package/dist/diagnostics.js +122 -0
  21. package/dist/direction.d.ts +3 -0
  22. package/dist/direction.d.ts.map +1 -0
  23. package/dist/direction.js +6 -0
  24. package/dist/errors.d.ts +15 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +53 -0
  27. package/dist/events.d.ts +5 -0
  28. package/dist/events.d.ts.map +1 -0
  29. package/dist/events.js +60 -0
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +3 -0
  33. package/dist/input.d.ts +31 -0
  34. package/dist/input.d.ts.map +1 -0
  35. package/dist/input.js +215 -0
  36. package/dist/json.d.ts +3 -0
  37. package/dist/json.d.ts.map +1 -0
  38. package/dist/json.js +109 -0
  39. package/dist/loader.d.ts +33 -0
  40. package/dist/loader.d.ts.map +1 -0
  41. package/dist/loader.js +477 -0
  42. package/dist/locomotion.d.ts +8 -0
  43. package/dist/locomotion.d.ts.map +1 -0
  44. package/dist/locomotion.js +29 -0
  45. package/dist/normalized.d.ts +5 -0
  46. package/dist/normalized.d.ts.map +1 -0
  47. package/dist/normalized.js +266 -0
  48. package/dist/overrides.d.ts +29 -0
  49. package/dist/overrides.d.ts.map +1 -0
  50. package/dist/overrides.js +296 -0
  51. package/dist/own-data.d.ts +18 -0
  52. package/dist/own-data.d.ts.map +1 -0
  53. package/dist/own-data.js +205 -0
  54. package/dist/pack-api.d.ts +9 -0
  55. package/dist/pack-api.d.ts.map +1 -0
  56. package/dist/pack-api.js +6 -0
  57. package/dist/pack-shared.d.ts +28 -0
  58. package/dist/pack-shared.d.ts.map +1 -0
  59. package/dist/pack-shared.js +408 -0
  60. package/dist/pack.d.ts +6 -0
  61. package/dist/pack.d.ts.map +1 -0
  62. package/dist/pack.js +316 -0
  63. package/dist/peekling.css +1 -0
  64. package/dist/peekling.css.sri +1 -0
  65. package/dist/peekling.js +7469 -0
  66. package/dist/peekling.js.sri +1 -0
  67. package/dist/peekling.min.js +2 -0
  68. package/dist/peekling.min.js.sri +1 -0
  69. package/dist/plan-compiler.d.ts +82 -0
  70. package/dist/plan-compiler.d.ts.map +1 -0
  71. package/dist/plan-compiler.js +724 -0
  72. package/dist/plan.d.ts +21 -0
  73. package/dist/plan.d.ts.map +1 -0
  74. package/dist/plan.js +287 -0
  75. package/dist/preflight-api.d.ts +3 -0
  76. package/dist/preflight-api.d.ts.map +1 -0
  77. package/dist/preflight-api.js +1 -0
  78. package/dist/preflight.d.ts +22 -0
  79. package/dist/preflight.d.ts.map +1 -0
  80. package/dist/preflight.js +158 -0
  81. package/dist/registry.d.ts +9 -0
  82. package/dist/registry.d.ts.map +1 -0
  83. package/dist/registry.js +24 -0
  84. package/dist/renderer.d.ts +16 -0
  85. package/dist/renderer.d.ts.map +1 -0
  86. package/dist/renderer.js +176 -0
  87. package/dist/resolver.d.ts +3 -0
  88. package/dist/resolver.d.ts.map +1 -0
  89. package/dist/resolver.js +30 -0
  90. package/dist/runtime-diagnostics.d.ts +3 -0
  91. package/dist/runtime-diagnostics.d.ts.map +1 -0
  92. package/dist/runtime-diagnostics.js +5 -0
  93. package/dist/runtime-validation.d.ts +6 -0
  94. package/dist/runtime-validation.d.ts.map +1 -0
  95. package/dist/runtime-validation.js +104 -0
  96. package/dist/runtime.d.ts +87 -0
  97. package/dist/runtime.d.ts.map +1 -0
  98. package/dist/runtime.js +1015 -0
  99. package/dist/sections.d.ts +15 -0
  100. package/dist/sections.d.ts.map +1 -0
  101. package/dist/sections.js +221 -0
  102. package/dist/styles.d.ts +18 -0
  103. package/dist/styles.d.ts.map +1 -0
  104. package/dist/styles.js +132 -0
  105. package/dist/tooling-preflight.d.ts +23 -0
  106. package/dist/tooling-preflight.d.ts.map +1 -0
  107. package/dist/tooling-preflight.js +207 -0
  108. package/dist/tooling-validation.d.ts +8 -0
  109. package/dist/tooling-validation.d.ts.map +1 -0
  110. package/dist/tooling-validation.js +94 -0
  111. package/dist/types.d.ts +403 -0
  112. package/dist/types.d.ts.map +1 -0
  113. package/dist/types.js +29 -0
  114. package/dist/visibility.d.ts +12 -0
  115. package/dist/visibility.d.ts.map +1 -0
  116. package/dist/visibility.js +333 -0
  117. package/dist/web-component.d.ts +31 -0
  118. package/dist/web-component.d.ts.map +1 -0
  119. package/dist/web-component.js +236 -0
  120. package/package.json +74 -0
@@ -0,0 +1,205 @@
1
+ import { runtimeMessage } from "./runtime-diagnostics.js";
2
+ export class OwnDataError extends TypeError {
3
+ issueCode;
4
+ constructor(path, reason, issueCode) {
5
+ super(`${path} ${reason}`);
6
+ this.name = "OwnDataError";
7
+ this.issueCode = issueCode;
8
+ }
9
+ }
10
+ const trustedConfigurations = new WeakSet();
11
+ const trustedOwnData = new WeakSet();
12
+ const trustedPackData = new WeakSet();
13
+ function plain(value) {
14
+ const prototype = Object.getPrototypeOf(value);
15
+ return prototype === null || prototype === Object.prototype;
16
+ }
17
+ function trusted(input, mode) {
18
+ if (typeof input !== "object" || input === null)
19
+ return false;
20
+ if (mode === "configuration") {
21
+ return trustedConfigurations.has(input) || trustedOwnData.has(input);
22
+ }
23
+ return mode === "pack"
24
+ ? trustedPackData.has(input)
25
+ : trustedOwnData.has(input);
26
+ }
27
+ function mark(value, ownSafe, packSafe) {
28
+ if (ownSafe)
29
+ trustedOwnData.add(value);
30
+ if (packSafe)
31
+ trustedPackData.add(value);
32
+ }
33
+ function snapshotData(input, mode, policy) {
34
+ if (!policy && trusted(input, mode))
35
+ return input;
36
+ let nodes = 0;
37
+ let values = 0;
38
+ const active = new WeakSet();
39
+ const clones = new WeakMap();
40
+ const fail = (path, issueCode, detail) => {
41
+ throw new OwnDataError(path, runtimeMessage(issueCode, detail), issueCode);
42
+ };
43
+ const visit = (value, path, depth) => {
44
+ if (policy?.maxDepth !== undefined && depth > policy.maxDepth) {
45
+ return fail(path, "data.depth", "exceeds maximum depth");
46
+ }
47
+ values += 1;
48
+ if (policy?.maxValues !== undefined && values > policy.maxValues) {
49
+ return fail(path, "data.nodes", "exceeds maximum object count");
50
+ }
51
+ if (value === null || typeof value === "boolean") {
52
+ return { value, ownSafe: true, packSafe: true };
53
+ }
54
+ if (typeof value === "string") {
55
+ if (policy?.maxStringLength !== undefined &&
56
+ value.length > policy.maxStringLength) {
57
+ return fail(path, "data.string", "exceeds maximum string length");
58
+ }
59
+ return { value, ownSafe: true, packSafe: true };
60
+ }
61
+ if (typeof value === "number") {
62
+ if (policy?.json && !Number.isFinite(value)) {
63
+ return fail(path, "data.type", "contains unsupported data");
64
+ }
65
+ return { value, ownSafe: true, packSafe: true };
66
+ }
67
+ if (typeof value === "undefined") {
68
+ if (policy?.json) {
69
+ return fail(path, "data.type", "contains unsupported data");
70
+ }
71
+ return { value, ownSafe: true, packSafe: true };
72
+ }
73
+ if (typeof value === "function") {
74
+ if (mode === "pack" || policy?.json) {
75
+ return fail(path, "data.type", "contains unsupported data");
76
+ }
77
+ return { value, ownSafe: true, packSafe: false };
78
+ }
79
+ if (typeof value !== "object") {
80
+ return fail(path, "data.type", "contains unsupported data");
81
+ }
82
+ if (mode === "configuration" &&
83
+ (path === "$.document" || path === "$.window")) {
84
+ return { value, ownSafe: false, packSafe: false };
85
+ }
86
+ if (depth > 32)
87
+ return fail(path, "data.depth", "exceeds maximum depth");
88
+ nodes += 1;
89
+ if (nodes > 20_000)
90
+ return fail(path, "data.nodes", "exceeds maximum object count");
91
+ const previous = clones.get(value);
92
+ if (previous) {
93
+ if (active.has(value)) {
94
+ return fail(path, "data.cycle", "must not contain cycles");
95
+ }
96
+ if (!policy) {
97
+ return {
98
+ value: previous.value,
99
+ ownSafe: previous.ownSafe,
100
+ packSafe: previous.packSafe,
101
+ };
102
+ }
103
+ }
104
+ let keys;
105
+ try {
106
+ keys = Reflect.ownKeys(value);
107
+ }
108
+ catch {
109
+ return fail(path, "data.reflect", "cannot be inspected safely");
110
+ }
111
+ if (keys.some((key) => typeof key !== "string")) {
112
+ return fail(path, "data.symbol", "contains symbol properties");
113
+ }
114
+ if (Array.isArray(value)) {
115
+ if (value.length > Math.min(4_096, policy?.maxArrayLength ?? 4_096))
116
+ return fail(path, "data.array", "exceeds maximum array length");
117
+ if (keys.length !== value.length + 1 ||
118
+ keys.some((key) => key !== "length" &&
119
+ (!/^\d+$/.test(key) || Number(key) >= value.length)))
120
+ return fail(path, "data.array-own", "contains custom array properties");
121
+ const output = [];
122
+ const record = { value: output };
123
+ clones.set(value, record);
124
+ active.add(value);
125
+ let ownSafe = true;
126
+ let packSafe = true;
127
+ for (let index = 0; index < value.length; index += 1) {
128
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
129
+ if (!descriptor || descriptor.get || descriptor.set) {
130
+ return fail(`${path}[${index}]`, "data.descriptor", "must be an own data property");
131
+ }
132
+ const item = visit(descriptor.value, `${path}[${index}]`, depth + 1);
133
+ output.push(item.value);
134
+ ownSafe &&= item.ownSafe;
135
+ packSafe &&= item.packSafe;
136
+ }
137
+ active.delete(value);
138
+ const frozen = Object.freeze(output);
139
+ record.ownSafe = ownSafe;
140
+ record.packSafe = packSafe;
141
+ mark(frozen, ownSafe, packSafe);
142
+ return { value: frozen, ownSafe, packSafe };
143
+ }
144
+ let isPlain;
145
+ try {
146
+ isPlain = plain(value);
147
+ }
148
+ catch {
149
+ return fail(path, "data.reflect", "cannot be inspected safely");
150
+ }
151
+ if (!isPlain)
152
+ return fail(path, "data.plain", "must use this realm's Object prototype or a null prototype");
153
+ if (keys.length > Math.min(1_024, policy?.maxObjectKeys ?? 1_024))
154
+ return fail(path, "data.keys", "exceeds maximum property count");
155
+ const output = Object.create(null);
156
+ const record = { value: output };
157
+ clones.set(value, record);
158
+ active.add(value);
159
+ let ownSafe = true;
160
+ let packSafe = true;
161
+ for (const key of keys) {
162
+ if (policy?.key && !policy.key(key)) {
163
+ fail(`${path}.${key}`, "data.key", "contains an invalid property name");
164
+ }
165
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
166
+ if (!descriptor || descriptor.get || descriptor.set) {
167
+ return fail(`${path}.${key}`, "data.descriptor", "must be an own data property");
168
+ }
169
+ const item = visit(descriptor.value, `${path}.${key}`, depth + 1);
170
+ output[key] = item.value;
171
+ ownSafe &&= item.ownSafe;
172
+ packSafe &&= item.packSafe;
173
+ }
174
+ active.delete(value);
175
+ const frozen = Object.freeze(output);
176
+ record.ownSafe = ownSafe;
177
+ record.packSafe = packSafe;
178
+ mark(frozen, ownSafe, packSafe);
179
+ return { value: frozen, ownSafe, packSafe };
180
+ };
181
+ try {
182
+ const result = visit(input, "$", 0);
183
+ if (mode === "configuration" &&
184
+ typeof result.value === "object" &&
185
+ result.value !== null) {
186
+ trustedConfigurations.add(result.value);
187
+ }
188
+ return result.value;
189
+ }
190
+ catch (cause) {
191
+ if (cause instanceof OwnDataError)
192
+ throw cause;
193
+ return fail("$", "data.reflect", "cannot be inspected safely");
194
+ }
195
+ }
196
+ export function snapshotOwnData(input, policy) {
197
+ return snapshotData(input, "own", policy);
198
+ }
199
+ /** Shared package-family boundary for data-only Pack and adapter input. */
200
+ export function snapshotPackData(input) {
201
+ return snapshotData(input, "pack");
202
+ }
203
+ export function snapshotConfiguration(input) {
204
+ return snapshotData(input, "configuration");
205
+ }
@@ -0,0 +1,9 @@
1
+ export { PackValidationError } from "./errors.js";
2
+ export { parseDataText } from "./json.js";
3
+ export { snapshotPackData } from "./own-data.js";
4
+ export { parseManifestText, validateNativePack } from "./pack.js";
5
+ export { inspectImageStructure } from "./pack-shared.js";
6
+ export { validateNormalizedPack } from "./normalized.js";
7
+ export type { ImageMimeType, ImageStructure } from "./pack-shared.js";
8
+ export type { AtlasGeometry, Direction, NativeDensity, NativeManifest, NormalizedPack, StateDefinition, } from "./types.js";
9
+ //# sourceMappingURL=pack-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack-api.d.ts","sourceRoot":"","sources":["../src/pack-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AACtE,YAAY,EACV,aAAa,EACb,SAAS,EACT,aAAa,EACb,cAAc,EACd,cAAc,EACd,eAAe,GAChB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { PackValidationError } from "./errors.js";
2
+ export { parseDataText } from "./json.js";
3
+ export { snapshotPackData } from "./own-data.js";
4
+ export { parseManifestText, validateNativePack } from "./pack.js";
5
+ export { inspectImageStructure } from "./pack-shared.js";
6
+ export { validateNormalizedPack } from "./normalized.js";
@@ -0,0 +1,28 @@
1
+ import type { NormalizedPack, StateDefinition } from "./types.js";
2
+ export declare const SHA256_PATTERN: RegExp;
3
+ type AtlasVariant = NonNullable<NormalizedPack["atlas"]["variants"]>[number];
4
+ export declare function validateSha256(value: unknown, path: string, issues: string[]): value is string;
5
+ export declare function validateDensityVariants(input: unknown, options: {
6
+ path: string;
7
+ logicalWidth?: number;
8
+ logicalHeight?: number;
9
+ native?: boolean;
10
+ }, issues: string[]): AtlasVariant[];
11
+ export declare function validateStateMap(input: unknown, cellCount: number, issues: string[]): Record<string, StateDefinition>;
12
+ export declare function validateMotionKeyframes(input: unknown, path: string, issues: string[]): Array<{
13
+ at: number;
14
+ advance: number;
15
+ lift: number;
16
+ }>;
17
+ export declare function record(value: unknown): value is Record<string, unknown>;
18
+ export declare function closed(value: Record<string, unknown>, allowed: readonly string[], path: string, issues: string[]): void;
19
+ export type ImageMimeType = "image/png" | "image/webp";
20
+ export interface ImageStructure {
21
+ readonly width: number;
22
+ readonly height: number;
23
+ readonly hasAlpha: boolean;
24
+ readonly mimeType: ImageMimeType;
25
+ }
26
+ export declare function inspectImageStructure(bytes: Uint8Array, mimeType: ImageMimeType): ImageStructure;
27
+ export {};
28
+ //# sourceMappingURL=pack-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack-shared.d.ts","sourceRoot":"","sources":["../src/pack-shared.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlE,eAAO,MAAM,cAAc,QAAmB,CAAC;AAE/C,KAAK,YAAY,GAAG,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAE7E,wBAAgB,cAAc,CAC5B,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,GACf,KAAK,IAAI,MAAM,CAQjB;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;IACP,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,EACD,MAAM,EAAE,MAAM,EAAE,GACf,YAAY,EAAE,CA8EhB;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EAAE,GACf,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CA6FjC;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,GACf,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAwDtD;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEvE;AAED,wBAAgB,MAAM,CACpB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EAAE,GACf,IAAI,CAMN;AAOD,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,YAAY,CAAC;AAEvD,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;CAClC;AAED,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,UAAU,EACjB,QAAQ,EAAE,aAAa,GACtB,cAAc,CAahB"}
@@ -0,0 +1,408 @@
1
+ import { ASSET_PATH_PATTERN, STATE_NAME_PATTERN } from "./contracts.js";
2
+ import { addPackIssue, throwPackIssue } from "./errors.js";
3
+ const MIN_STATE_FPS = 0.1;
4
+ export const SHA256_PATTERN = /^[a-f0-9]{64}$/;
5
+ export function validateSha256(value, path, issues) {
6
+ if (typeof value === "string" && SHA256_PATTERN.test(value))
7
+ return true;
8
+ addPackIssue(issues, path, `${path} must be 64 lowercase hexadecimal characters`);
9
+ return false;
10
+ }
11
+ export function validateDensityVariants(input, options, issues) {
12
+ const result = [];
13
+ if (!Array.isArray(input) || input.length < 1 || input.length > 3) {
14
+ addPackIssue(issues, `${options.path}.limit`, `${options.path} must contain 1-3 variants`);
15
+ return result;
16
+ }
17
+ const seen = new Set();
18
+ for (const [index, value] of input.entries()) {
19
+ const path = `${options.path}.${index}`;
20
+ if (!record(value)) {
21
+ addPackIssue(issues, path, `${path} must be an object`);
22
+ continue;
23
+ }
24
+ closed(value, options.native
25
+ ? ["src", "density", "sourceCellSize", "sha256"]
26
+ : ["src", "density", "cellWidth", "cellHeight", "sha256"], path, issues);
27
+ const density = value.density;
28
+ const validDensity = (density === 1 || density === 2 || density === 4) && !seen.has(density);
29
+ const src = value.src;
30
+ const validSource = typeof src === "string" &&
31
+ ASSET_PATH_PATTERN.test(src) &&
32
+ (options.native ? /\.png$/i : /\.(?:png|webp)$/i).test(src);
33
+ const sha256 = value.sha256;
34
+ const validHash = validateSha256(sha256, `${path}.sha256`, issues);
35
+ const cellWidth = options.native
36
+ ? Number(options.logicalWidth) * Number(density)
37
+ : value.cellWidth;
38
+ const cellHeight = options.native
39
+ ? Number(options.logicalHeight) * Number(density)
40
+ : value.cellHeight;
41
+ const validDimensions = options.native
42
+ ? value.sourceCellSize === cellWidth && cellWidth === cellHeight
43
+ : Number.isInteger(cellWidth) &&
44
+ cellWidth >= 1 &&
45
+ cellWidth <= 1_024 &&
46
+ Number.isInteger(cellHeight) &&
47
+ cellHeight >= 1 &&
48
+ cellHeight <= 1_024;
49
+ if (!validDimensions) {
50
+ addPackIssue(issues, `${path}.dimensions`, options.native
51
+ ? `${path}.sourceCellSize does not match density`
52
+ : `${path} has invalid cell dimensions`);
53
+ continue;
54
+ }
55
+ if (!validDensity || !validSource) {
56
+ addPackIssue(issues, `${path}.density`, `${path} has invalid or duplicate density data`);
57
+ continue;
58
+ }
59
+ if (!validHash)
60
+ continue;
61
+ seen.add(density);
62
+ result.push({
63
+ src,
64
+ density,
65
+ cellWidth: cellWidth,
66
+ cellHeight: cellHeight,
67
+ sha256,
68
+ });
69
+ }
70
+ return result.sort((left, right) => left.density - right.density);
71
+ }
72
+ export function validateStateMap(input, cellCount, issues) {
73
+ const states = Object.create(null);
74
+ if (!record(input)) {
75
+ addPackIssue(issues, "states", "states must be an object");
76
+ return states;
77
+ }
78
+ const entries = Object.entries(input);
79
+ if (entries.length === 0 || entries.length > 64) {
80
+ addPackIssue(issues, "states.limit", "states must contain 1-64 entries");
81
+ }
82
+ for (const [name, value] of entries) {
83
+ const path = `states.${name}`;
84
+ if (!STATE_NAME_PATTERN.test(name) ||
85
+ ["constructor", "prototype", "__proto__"].includes(name) ||
86
+ !record(value)) {
87
+ addPackIssue(issues, path, `${path} is invalid`);
88
+ continue;
89
+ }
90
+ closed(value, ["frames", "fps", "loop", "durations"], path, issues);
91
+ const frames = value.frames;
92
+ const durations = value.durations;
93
+ if (!Array.isArray(frames) ||
94
+ frames.length === 0 ||
95
+ frames.length > 64 ||
96
+ frames.some((frame) => !Number.isInteger(frame) || frame < 0 || frame >= cellCount)) {
97
+ addPackIssue(issues, `${path}.frames`, `${path}.frames must contain 1-64 in-bounds integers`);
98
+ continue;
99
+ }
100
+ const hasFps = value.fps !== undefined;
101
+ const hasDurations = durations !== undefined;
102
+ const fpsValid = typeof value.fps === "number" &&
103
+ Number.isFinite(value.fps) &&
104
+ value.fps >= MIN_STATE_FPS &&
105
+ value.fps <= 60;
106
+ const durationsValid = Array.isArray(durations) &&
107
+ durations.length === frames.length &&
108
+ durations.every((duration) => typeof duration === "number" &&
109
+ Number.isFinite(duration) &&
110
+ duration >= 16 &&
111
+ duration <= 10_000) &&
112
+ durations.reduce((sum, duration) => sum + duration, 0) <=
113
+ 60_000;
114
+ if (hasFps === hasDurations ||
115
+ typeof value.loop !== "boolean" ||
116
+ (hasFps && !fpsValid) ||
117
+ (hasDurations && !durationsValid)) {
118
+ addPackIssue(issues, `${path}.timing`, hasDurations && !durationsValid
119
+ ? `${path}.durations must match frames and be bounded`
120
+ : hasFps && !fpsValid
121
+ ? `${path}.fps must be >=${MIN_STATE_FPS} and <=60`
122
+ : `${path} needs boolean loop and one of fps or durations`);
123
+ continue;
124
+ }
125
+ states[name] = hasFps
126
+ ? {
127
+ frames: [...frames],
128
+ loop: value.loop,
129
+ fps: value.fps,
130
+ }
131
+ : {
132
+ frames: [...frames],
133
+ loop: value.loop,
134
+ durations: [...durations],
135
+ };
136
+ }
137
+ if (!states.idle) {
138
+ addPackIssue(issues, "states.idle", "states.idle is required");
139
+ }
140
+ return states;
141
+ }
142
+ export function validateMotionKeyframes(input, path, issues) {
143
+ const result = [];
144
+ if (!Array.isArray(input) || input.length < 2 || input.length > 8) {
145
+ addPackIssue(issues, `${path}.limit`, `${path} needs 2-8 keyframes`);
146
+ return result;
147
+ }
148
+ for (const [index, value] of input.entries()) {
149
+ const itemPath = `${path}.${index}`;
150
+ const previous = result.at(-1);
151
+ if (record(value)) {
152
+ closed(value, ["at", "advance", "lift"], itemPath, issues);
153
+ }
154
+ if (!record(value) ||
155
+ !Number.isFinite(value.at) ||
156
+ !Number.isFinite(value.advance) ||
157
+ !Number.isFinite(value.lift) ||
158
+ value.at < 0 ||
159
+ value.at > 1 ||
160
+ value.advance < 0 ||
161
+ value.advance > 1 ||
162
+ value.lift < 0 ||
163
+ value.lift > 0.5 ||
164
+ (previous !== undefined &&
165
+ (value.at <= previous.at ||
166
+ value.advance < previous.advance))) {
167
+ addPackIssue(issues, itemPath, `${itemPath} needs increasing at and bounded motion values`);
168
+ continue;
169
+ }
170
+ result.push({
171
+ at: value.at,
172
+ advance: value.advance,
173
+ lift: value.lift,
174
+ });
175
+ }
176
+ if (result.length === input.length &&
177
+ (result[0].at !== 0 ||
178
+ result[0].advance !== 0 ||
179
+ result[0].lift !== 0 ||
180
+ result.at(-1).at !== 1 ||
181
+ result.at(-1).advance !== 1 ||
182
+ result.at(-1).lift !== 0)) {
183
+ addPackIssue(issues, `${path}.endpoints`, `${path} endpoints must be 0/0/0 and 1/1/0`);
184
+ }
185
+ return result;
186
+ }
187
+ export function record(value) {
188
+ return typeof value === "object" && value !== null && !Array.isArray(value);
189
+ }
190
+ export function closed(value, allowed, path, issues) {
191
+ for (const key of Object.keys(value)) {
192
+ if (!allowed.includes(key)) {
193
+ addPackIssue(issues, `${path}.${key}`, `${path}.${key} is not allowed`);
194
+ }
195
+ }
196
+ }
197
+ const MAX_IMAGE_BYTES = 32 * 1024 * 1024;
198
+ const MAX_IMAGE_SIDE = 4_096;
199
+ const MAX_IMAGE_CHUNKS = 4_096;
200
+ const PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
201
+ export function inspectImageStructure(bytes, mimeType) {
202
+ if (bytes.byteLength > MAX_IMAGE_BYTES) {
203
+ throwPackIssue("resource.size", "Image exceeds 32 MiB");
204
+ }
205
+ const png = hasPngSignature(bytes);
206
+ const webp = hasWebpSignature(bytes);
207
+ if ((mimeType === "image/png" && webp) ||
208
+ (mimeType === "image/webp" && png)) {
209
+ throwPackIssue("atlas.mime", "Content-Type does not match atlas bytes");
210
+ }
211
+ return mimeType === "image/png" ? inspectPng(bytes) : inspectWebp(bytes);
212
+ }
213
+ function hasPngSignature(bytes) {
214
+ return PNG_SIGNATURE.every((byte, index) => bytes[index] === byte);
215
+ }
216
+ function hasWebpSignature(bytes) {
217
+ return (bytes.byteLength >= 12 &&
218
+ chunkName(bytes, 0) === "RIFF" &&
219
+ chunkName(bytes, 8) === "WEBP");
220
+ }
221
+ function chunkName(bytes, offset) {
222
+ return String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
223
+ }
224
+ function inspectPng(bytes) {
225
+ if (!hasPngSignature(bytes)) {
226
+ throwPackIssue("png.signature", "Invalid PNG signature");
227
+ }
228
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
229
+ let offset = PNG_SIGNATURE.length;
230
+ let width = 0;
231
+ let height = 0;
232
+ let colorType = -1;
233
+ let chunks = 0;
234
+ let sawHeader = false;
235
+ let sawPalette = false;
236
+ let sawTransparency = false;
237
+ let sawData = false;
238
+ while (offset < bytes.byteLength) {
239
+ if (bytes.byteLength - offset < 12) {
240
+ throwPackIssue("png.truncated", "PNG is truncated");
241
+ }
242
+ const length = view.getUint32(offset);
243
+ const dataOffset = offset + 8;
244
+ const next = dataOffset + length + 4;
245
+ if (next > bytes.byteLength || next < dataOffset) {
246
+ throwPackIssue("png.chunk", "PNG chunk is truncated");
247
+ }
248
+ const type = chunkName(bytes, offset + 4);
249
+ if (!/^[A-Za-z]{4}$/.test(type)) {
250
+ throwPackIssue("png.chunk-type", "PNG chunk type is invalid");
251
+ }
252
+ chunks += 1;
253
+ if (chunks > MAX_IMAGE_CHUNKS) {
254
+ throwPackIssue("png.chunks", "PNG has too many chunks");
255
+ }
256
+ if (!sawHeader && (type !== "IHDR" || length !== 13)) {
257
+ throwPackIssue("png.ihdr-first", "PNG must start with a 13-byte IHDR");
258
+ }
259
+ if (type === "IHDR") {
260
+ if (sawHeader || length !== 13) {
261
+ throwPackIssue("png.ihdr", "PNG has an invalid IHDR");
262
+ }
263
+ sawHeader = true;
264
+ width = view.getUint32(dataOffset);
265
+ height = view.getUint32(dataOffset + 4);
266
+ const bitDepth = bytes[dataOffset + 8];
267
+ colorType = bytes[dataOffset + 9];
268
+ const allowedDepths = {
269
+ 0: [1, 2, 4, 8, 16],
270
+ 2: [8, 16],
271
+ 3: [1, 2, 4, 8],
272
+ 4: [8, 16],
273
+ 6: [8, 16],
274
+ };
275
+ if (width < 1 ||
276
+ height < 1 ||
277
+ width > MAX_IMAGE_SIDE ||
278
+ height > MAX_IMAGE_SIDE) {
279
+ throwPackIssue("png.dimensions", `PNG dimensions must be 1-${MAX_IMAGE_SIDE} per side`);
280
+ }
281
+ if (!allowedDepths[colorType]?.includes(bitDepth) ||
282
+ bytes[dataOffset + 10] !== 0 ||
283
+ bytes[dataOffset + 11] !== 0 ||
284
+ (bytes[dataOffset + 12] !== 0 && bytes[dataOffset + 12] !== 1)) {
285
+ throwPackIssue("png.fields", "PNG IHDR fields are invalid");
286
+ }
287
+ }
288
+ else if (type === "PLTE") {
289
+ if (sawData) {
290
+ throwPackIssue("png.palette-order", "PNG palette is misplaced");
291
+ }
292
+ sawPalette = true;
293
+ }
294
+ else if (type === "tRNS") {
295
+ if (sawData) {
296
+ throwPackIssue("png.alpha-order", "PNG transparency is misplaced");
297
+ }
298
+ sawTransparency = true;
299
+ }
300
+ else if (type === "IDAT") {
301
+ if (colorType === 3 && !sawPalette) {
302
+ throwPackIssue("png.palette", "Indexed PNG requires a palette");
303
+ }
304
+ sawData = true;
305
+ }
306
+ else if (type === "IEND") {
307
+ if (length !== 0 || !sawData) {
308
+ throwPackIssue("png.iend", "PNG has an invalid IEND");
309
+ }
310
+ if (next !== bytes.byteLength) {
311
+ throwPackIssue("png.trailing", "PNG has trailing bytes");
312
+ }
313
+ return {
314
+ width,
315
+ height,
316
+ hasAlpha: colorType === 4 ||
317
+ colorType === 6 ||
318
+ (colorType === 3 && sawTransparency),
319
+ mimeType: "image/png",
320
+ };
321
+ }
322
+ else if (type.charCodeAt(0) <= 0x5a) {
323
+ throwPackIssue("png.critical", `PNG contains unsupported critical chunk ${type}`);
324
+ }
325
+ offset = next;
326
+ }
327
+ throwPackIssue("png.truncated", "PNG is truncated");
328
+ }
329
+ function inspectWebp(bytes) {
330
+ if (bytes.byteLength < 20 || !hasWebpSignature(bytes)) {
331
+ throwPackIssue("webp.header", "Invalid WebP header");
332
+ }
333
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
334
+ if (view.getUint32(4, true) + 8 !== bytes.byteLength) {
335
+ throwPackIssue("webp.size", "WebP size header is invalid");
336
+ }
337
+ let offset = 12;
338
+ let chunks = 0;
339
+ let alphaChunk = false;
340
+ let canvas;
341
+ let image;
342
+ while (offset < bytes.byteLength) {
343
+ if (bytes.byteLength - offset < 8) {
344
+ throwPackIssue("webp.chunk", "WebP chunk is truncated");
345
+ }
346
+ const type = chunkName(bytes, offset);
347
+ const length = view.getUint32(offset + 4, true);
348
+ const dataOffset = offset + 8;
349
+ const next = dataOffset + length + (length & 1);
350
+ if (next < dataOffset || next > bytes.byteLength) {
351
+ throwPackIssue("webp.chunk", "WebP chunk is truncated");
352
+ }
353
+ chunks += 1;
354
+ if (chunks > MAX_IMAGE_CHUNKS) {
355
+ throwPackIssue("webp.chunks", "WebP has too many chunks");
356
+ }
357
+ if (type === "ALPH") {
358
+ alphaChunk = true;
359
+ }
360
+ else if (type === "VP8X") {
361
+ if (length < 10) {
362
+ throwPackIssue("webp.vp8x", "WebP VP8X header is truncated");
363
+ }
364
+ canvas = webpResult(1 + uint24(bytes, dataOffset + 4), 1 + uint24(bytes, dataOffset + 7), (bytes[dataOffset] & 0x10) !== 0 || alphaChunk);
365
+ }
366
+ else if (type === "VP8L") {
367
+ if (length < 5 || bytes[dataOffset] !== 0x2f) {
368
+ throwPackIssue("webp.vp8l", "WebP VP8L header is invalid");
369
+ }
370
+ const packed = view.getUint32(dataOffset + 1, true);
371
+ const payload = webpResult(1 + (packed & 0x3fff), 1 + ((packed >>> 14) & 0x3fff), true);
372
+ image ??= canvas ?? payload;
373
+ }
374
+ else if (type === "VP8 ") {
375
+ if (length < 10 ||
376
+ bytes[dataOffset + 3] !== 0x9d ||
377
+ bytes[dataOffset + 4] !== 0x01 ||
378
+ bytes[dataOffset + 5] !== 0x2a) {
379
+ throwPackIssue("webp.vp8", "WebP VP8 header is invalid");
380
+ }
381
+ const payload = webpResult(view.getUint16(dataOffset + 6, true) & 0x3fff, view.getUint16(dataOffset + 8, true) & 0x3fff, alphaChunk);
382
+ image ??= canvas ?? payload;
383
+ }
384
+ else if (!canvas &&
385
+ !image &&
386
+ type !== "ICCP" &&
387
+ type !== "EXIF" &&
388
+ type !== "XMP ") {
389
+ throwPackIssue("webp.chunk-type", "WebP has an unsupported leading chunk");
390
+ }
391
+ offset = next;
392
+ }
393
+ if (image)
394
+ return image;
395
+ throwPackIssue("webp.image", "WebP image chunk is missing");
396
+ }
397
+ function uint24(bytes, offset) {
398
+ return (bytes[offset] | (bytes[offset + 1] << 8) | (bytes[offset + 2] << 16));
399
+ }
400
+ function webpResult(width, height, hasAlpha) {
401
+ if (width < 1 ||
402
+ height < 1 ||
403
+ width > MAX_IMAGE_SIDE ||
404
+ height > MAX_IMAGE_SIDE) {
405
+ throwPackIssue("webp.dimensions", `WebP dimensions must be 1-${MAX_IMAGE_SIDE} per side`);
406
+ }
407
+ return { width, height, hasAlpha, mimeType: "image/webp" };
408
+ }
package/dist/pack.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { type AtlasGeometry, type NormalizedPack } from "./types.js";
2
+ export declare const MAX_ATLAS_SIDE = 4096;
3
+ export declare function parseManifestText(text: string): unknown;
4
+ export declare function validateNativePack(input: unknown, geometry?: AtlasGeometry, preferredDensity?: number): NormalizedPack;
5
+ export declare function selectNativeDensity(available: readonly (1 | 2 | 4)[], required: number): 1 | 2 | 4;
6
+ //# sourceMappingURL=pack.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../src/pack.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,aAAa,EAElB,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAIpB,eAAO,MAAM,cAAc,OAAO,CAAC;AAyBnC,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,OAAO,EACd,QAAQ,CAAC,EAAE,aAAa,EACxB,gBAAgB,SAAI,GACnB,cAAc,CAqchB;AAED,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,SAAS,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,EACjC,QAAQ,EAAE,MAAM,GACf,CAAC,GAAG,CAAC,GAAG,CAAC,CAQX"}