@flighthq/shape-formats 0.3.0-next.906.07cea63 → 0.3.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/dist/shapeJson.d.ts.map +1 -1
- package/dist/shapeJson.js +109 -18
- package/dist/shapeJson.js.map +1 -1
- package/package.json +6 -4
- package/src/shapeJson.test.ts +145 -25
package/dist/shapeJson.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shapeJson.d.ts","sourceRoot":"","sources":["../src/shapeJson.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"shapeJson.d.ts","sourceRoot":"","sources":["../src/shapeJson.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAQrG,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,MAAM,CA+B1G;AASD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,KAAK,GAAG,IAAI,CAsCpG"}
|
package/dist/shapeJson.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
|
-
import {
|
|
2
|
+
import { appendShapeBeginTextureFill, appendShapeBeginFill, appendShapeBeginGradientFill, appendShapeCircle, appendShapeCubicCurveTo, appendShapeCurveTo, appendShapeDrawTriangles, appendShapeEllipse, appendShapeEndFill, appendShapeLineTextureStyle, appendShapeLineGradientStyle, appendShapeLineStyle, appendShapeLineTo, appendShapeMoveTo, appendShapePath, appendShapeRectangle, appendShapeRoundRectangle, createShape, } from '@flighthq/shape/contract';
|
|
3
3
|
// Serializes a shape's full drawing-command stream to a native JSON string that `parseShapeJson`
|
|
4
|
-
// restores
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// restores. Every non-texture command round-trips exactly, with one exception the format cannot
|
|
5
|
+
// represent: JSON has no NaN or Infinity literal, so a non-finite coordinate is written as `null` and
|
|
6
|
+
// the document will not parse back. That is deliberate — the alternative is restoring a shape whose
|
|
7
|
+
// geometry silently differs from the one serialized. `beginTextureFill`/`lineTextureStyle` textures
|
|
8
|
+
// serialize as an ordinal `ShapeTextureReference` (see the type) rather than the live `Texture`.
|
|
7
9
|
export function formatShapeJson(shape, options) {
|
|
8
10
|
const commands = shape.data.commands;
|
|
9
11
|
const entries = [];
|
|
10
|
-
let
|
|
12
|
+
let textureOrdinal = 0;
|
|
11
13
|
let i = 0;
|
|
12
14
|
while (i < commands.length) {
|
|
13
15
|
const key = commands[i];
|
|
@@ -29,9 +31,9 @@ export function formatShapeJson(shape, options) {
|
|
|
29
31
|
args.push(value);
|
|
30
32
|
}
|
|
31
33
|
else {
|
|
32
|
-
// The only remaining object arg in the command registry is a live
|
|
34
|
+
// The only remaining object arg in the command registry is a live Texture;
|
|
33
35
|
// it has no serializable id, so it becomes an ordinal reference resolved on parse.
|
|
34
|
-
args.push({
|
|
36
|
+
args.push({ texture: { index: textureOrdinal++ } });
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
entries.push({ key, args });
|
|
@@ -42,7 +44,10 @@ export function formatShapeJson(shape, options) {
|
|
|
42
44
|
// Restores a `Shape` from a `formatShapeJson` string by replaying each command through the matching
|
|
43
45
|
// `appendShape*` builder. Returns `null` for malformed JSON, a missing/mismatched `shapeFormat`
|
|
44
46
|
// version tag, a non-array `commands` field, a malformed command entry, an unknown command key, or a
|
|
45
|
-
// malformed argument
|
|
47
|
+
// malformed argument — where malformed covers the argument *list* as well as each value: a count
|
|
48
|
+
// outside the command's arity, a value of the wrong positional type, or a non-finite number.
|
|
49
|
+
// Texture-bearing commands whose reference cannot be resolved are dropped rather than rejected, since
|
|
50
|
+
// an unresolved reference is a missing asset rather than a malformed document.
|
|
46
51
|
export function parseShapeJson(text, options) {
|
|
47
52
|
let root;
|
|
48
53
|
try {
|
|
@@ -58,7 +63,7 @@ export function parseShapeJson(text, options) {
|
|
|
58
63
|
const rawCommands = root.commands;
|
|
59
64
|
if (!Array.isArray(rawCommands))
|
|
60
65
|
return null;
|
|
61
|
-
const
|
|
66
|
+
const resolveTexture = options?.resolveTexture;
|
|
62
67
|
const shape = createShape();
|
|
63
68
|
for (const entry of rawCommands) {
|
|
64
69
|
if (!isPlainObject(entry))
|
|
@@ -73,7 +78,7 @@ export function parseShapeJson(text, options) {
|
|
|
73
78
|
const args = [];
|
|
74
79
|
let drop = false;
|
|
75
80
|
for (const raw of rawArgs) {
|
|
76
|
-
const reconstructed = reconstructShapeCommandArg(raw,
|
|
81
|
+
const reconstructed = reconstructShapeCommandArg(raw, resolveTexture);
|
|
77
82
|
if (reconstructed === MALFORMED_ARG)
|
|
78
83
|
return null;
|
|
79
84
|
if (reconstructed === DROP_COMMAND) {
|
|
@@ -84,13 +89,15 @@ export function parseShapeJson(text, options) {
|
|
|
84
89
|
}
|
|
85
90
|
if (drop)
|
|
86
91
|
continue;
|
|
92
|
+
if (!isValidShapeCommandArgs(key, args))
|
|
93
|
+
return null;
|
|
87
94
|
appender(shape, ...args);
|
|
88
95
|
}
|
|
89
96
|
return shape;
|
|
90
97
|
}
|
|
91
98
|
// Rebuilds a single command argument from its serialized form. Returns `MALFORMED_ARG` for an
|
|
92
|
-
// unrecognized object shape and `DROP_COMMAND` when a
|
|
93
|
-
function reconstructShapeCommandArg(value,
|
|
99
|
+
// unrecognized object shape and `DROP_COMMAND` when a texture reference cannot be resolved.
|
|
100
|
+
function reconstructShapeCommandArg(value, resolveTexture) {
|
|
94
101
|
if (value === null)
|
|
95
102
|
return null;
|
|
96
103
|
const type = typeof value;
|
|
@@ -100,8 +107,8 @@ function reconstructShapeCommandArg(value, resolveBitmap) {
|
|
|
100
107
|
return value;
|
|
101
108
|
if (!isPlainObject(value))
|
|
102
109
|
return MALFORMED_ARG;
|
|
103
|
-
if (isPlainObject(value.
|
|
104
|
-
const resolved =
|
|
110
|
+
if (isPlainObject(value.texture) && typeof value.texture.index === 'number') {
|
|
111
|
+
const resolved = resolveTexture?.({ index: value.texture.index }) ?? null;
|
|
105
112
|
return resolved === null ? DROP_COMMAND : resolved;
|
|
106
113
|
}
|
|
107
114
|
if (typeof value.a === 'number' &&
|
|
@@ -114,6 +121,60 @@ function reconstructShapeCommandArg(value, resolveBitmap) {
|
|
|
114
121
|
}
|
|
115
122
|
return MALFORMED_ARG;
|
|
116
123
|
}
|
|
124
|
+
// Checks a reconstructed argument list against the appender's declared shape. Without this the parser
|
|
125
|
+
// validated only that an entry *looked* like a command and then spread whatever it found: too few args
|
|
126
|
+
// left required parameters undefined, wrong-typed args wrote strings into numeric slots, and extra args
|
|
127
|
+
// were dropped without complaint — each producing a corrupt Shape rather than the documented null.
|
|
128
|
+
//
|
|
129
|
+
// Runs after reconstruction, so matrices are already Matrix values and textures already resolved, and
|
|
130
|
+
// after the drop check, so an unresolved texture is still a dropped command rather than a parse failure.
|
|
131
|
+
function isValidShapeCommandArgs(key, args) {
|
|
132
|
+
const spec = SHAPE_COMMAND_ARG_SPECS[key];
|
|
133
|
+
// Treating a missing spec as invalid is what makes the two tables self-checking: a key that gained
|
|
134
|
+
// an appender without a spec would make its own command unparseable, so the full-vocabulary
|
|
135
|
+
// round-trip test fails the moment they drift apart.
|
|
136
|
+
if (spec === undefined)
|
|
137
|
+
return false;
|
|
138
|
+
if (args.length < spec.required || args.length > spec.types.length)
|
|
139
|
+
return false;
|
|
140
|
+
for (let i = 0; i < args.length; i++) {
|
|
141
|
+
if (!isValidShapeCommandArg(args[i], spec.types[i]))
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
function isValidShapeCommandArg(value, type) {
|
|
147
|
+
switch (type) {
|
|
148
|
+
// Non-finite values are rejected here rather than passed through: JSON has no NaN or Infinity
|
|
149
|
+
// literal, so a NaN in a shape serializes as `null` and an out-of-range literal like 1e999 parses
|
|
150
|
+
// back as Infinity. Either one silently changes the geometry it describes.
|
|
151
|
+
case 'number':
|
|
152
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
153
|
+
case 'numbers':
|
|
154
|
+
return isFiniteNumberArray(value);
|
|
155
|
+
case 'numbersOrNull':
|
|
156
|
+
return value === null || isFiniteNumberArray(value);
|
|
157
|
+
case 'string':
|
|
158
|
+
return typeof value === 'string';
|
|
159
|
+
case 'boolean':
|
|
160
|
+
return typeof value === 'boolean';
|
|
161
|
+
case 'matrixOrNull':
|
|
162
|
+
return value === null || isMatrixValue(value);
|
|
163
|
+
// Already resolved by resolveTexture, which returns either a Texture or null; a null resolution
|
|
164
|
+
// dropped the command before reaching here, so anything left is the caller's own object.
|
|
165
|
+
case 'texture':
|
|
166
|
+
return typeof value === 'object' && value !== null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function isFiniteNumberArray(value) {
|
|
170
|
+
if (!Array.isArray(value))
|
|
171
|
+
return false;
|
|
172
|
+
for (const entry of value) {
|
|
173
|
+
if (typeof entry !== 'number' || !Number.isFinite(entry))
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
117
178
|
function isMatrixValue(value) {
|
|
118
179
|
return (isPlainObject(value) &&
|
|
119
180
|
typeof value.a === 'number' &&
|
|
@@ -132,11 +193,41 @@ function isSerializableScalarOrArray(value) {
|
|
|
132
193
|
}
|
|
133
194
|
// Sentinel returned when a command argument is structurally invalid; parse aborts to `null`.
|
|
134
195
|
const MALFORMED_ARG = Symbol('shapeFormats.malformedArg');
|
|
135
|
-
// Sentinel returned when a
|
|
196
|
+
// Sentinel returned when a texture reference cannot be resolved; the owning command is dropped.
|
|
136
197
|
const DROP_COMMAND = Symbol('shapeFormats.dropCommand');
|
|
137
|
-
const SHAPE_JSON_FORMAT =
|
|
198
|
+
const SHAPE_JSON_FORMAT = 2;
|
|
199
|
+
const GRADIENT_ARG_SPEC = {
|
|
200
|
+
required: 4,
|
|
201
|
+
types: ['string', 'numbers', 'numbers', 'numbers', 'matrixOrNull', 'string', 'string', 'number'],
|
|
202
|
+
};
|
|
203
|
+
const TEXTURE_ARG_SPEC = { required: 1, types: ['texture', 'matrixOrNull'] };
|
|
204
|
+
const SHAPE_COMMAND_ARG_SPECS = {
|
|
205
|
+
beginTextureFill: TEXTURE_ARG_SPEC,
|
|
206
|
+
beginFill: { required: 0, types: ['number', 'number'] },
|
|
207
|
+
beginGradientFill: GRADIENT_ARG_SPEC,
|
|
208
|
+
cubicCurveTo: { required: 6, types: ['number', 'number', 'number', 'number', 'number', 'number'] },
|
|
209
|
+
curveTo: { required: 4, types: ['number', 'number', 'number', 'number'] },
|
|
210
|
+
drawCircle: { required: 3, types: ['number', 'number', 'number'] },
|
|
211
|
+
drawEllipse: { required: 4, types: ['number', 'number', 'number', 'number'] },
|
|
212
|
+
drawPath: { required: 2, types: ['numbers', 'numbers', 'string'] },
|
|
213
|
+
drawRectangle: { required: 4, types: ['number', 'number', 'number', 'number'] },
|
|
214
|
+
drawRoundRectangle: {
|
|
215
|
+
required: 6,
|
|
216
|
+
types: ['number', 'number', 'number', 'number', 'number', 'number'],
|
|
217
|
+
},
|
|
218
|
+
drawTriangles: { required: 1, types: ['numbers', 'numbersOrNull', 'numbersOrNull', 'string'] },
|
|
219
|
+
endFill: { required: 0, types: [] },
|
|
220
|
+
lineTextureStyle: TEXTURE_ARG_SPEC,
|
|
221
|
+
lineGradientStyle: GRADIENT_ARG_SPEC,
|
|
222
|
+
lineStyle: {
|
|
223
|
+
required: 0,
|
|
224
|
+
types: ['number', 'number', 'number', 'boolean', 'string', 'string', 'string', 'number'],
|
|
225
|
+
},
|
|
226
|
+
lineTo: { required: 2, types: ['number', 'number'] },
|
|
227
|
+
moveTo: { required: 2, types: ['number', 'number'] },
|
|
228
|
+
};
|
|
138
229
|
const SHAPE_COMMAND_APPENDERS = {
|
|
139
|
-
|
|
230
|
+
beginTextureFill: appendShapeBeginTextureFill,
|
|
140
231
|
beginFill: appendShapeBeginFill,
|
|
141
232
|
beginGradientFill: appendShapeBeginGradientFill,
|
|
142
233
|
cubicCurveTo: appendShapeCubicCurveTo,
|
|
@@ -148,7 +239,7 @@ const SHAPE_COMMAND_APPENDERS = {
|
|
|
148
239
|
drawRoundRectangle: appendShapeRoundRectangle,
|
|
149
240
|
drawTriangles: appendShapeDrawTriangles,
|
|
150
241
|
endFill: appendShapeEndFill,
|
|
151
|
-
|
|
242
|
+
lineTextureStyle: appendShapeLineTextureStyle,
|
|
152
243
|
lineGradientStyle: appendShapeLineGradientStyle,
|
|
153
244
|
lineStyle: appendShapeLineStyle,
|
|
154
245
|
lineTo: appendShapeLineTo,
|
package/dist/shapeJson.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shapeJson.js","sourceRoot":"","sources":["../src/shapeJson.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"shapeJson.js","sourceRoot":"","sources":["../src/shapeJson.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/shape-formats",
|
|
3
|
-
"version": "0.3.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
4
6
|
"repository": {
|
|
5
7
|
"type": "git",
|
|
6
8
|
"url": "https://github.com/flighthq/flight.git",
|
|
@@ -36,9 +38,9 @@
|
|
|
36
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
|
-
"@flighthq/geometry": "0.3.0
|
|
40
|
-
"@flighthq/shape": "0.3.0
|
|
41
|
-
"@flighthq/types": "0.3.0
|
|
41
|
+
"@flighthq/geometry": "0.3.0",
|
|
42
|
+
"@flighthq/shape": "0.3.0",
|
|
43
|
+
"@flighthq/types": "0.3.0"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"typescript": "^5.3.0"
|
package/src/shapeJson.test.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import { createMatrix } from '@flighthq/geometry/contract';
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
appendShapeBeginTextureFill,
|
|
4
4
|
appendShapeBeginFill,
|
|
5
5
|
appendShapeBeginGradientFill,
|
|
6
|
+
appendShapeCircle,
|
|
6
7
|
appendShapeCubicCurveTo,
|
|
7
8
|
appendShapeCurveTo,
|
|
9
|
+
appendShapeDrawTriangles,
|
|
10
|
+
appendShapeEllipse,
|
|
8
11
|
appendShapeEndFill,
|
|
12
|
+
appendShapeLineGradientStyle,
|
|
9
13
|
appendShapeLineStyle,
|
|
14
|
+
appendShapeLineTextureStyle,
|
|
15
|
+
appendShapeRectangle,
|
|
16
|
+
appendShapeRoundRectangle,
|
|
10
17
|
appendShapeLineTo,
|
|
11
18
|
appendShapeMoveTo,
|
|
12
19
|
appendShapePath,
|
|
13
20
|
createShape,
|
|
14
21
|
getShapeCommandCount,
|
|
15
22
|
} from '@flighthq/shape/contract';
|
|
16
|
-
import type {
|
|
23
|
+
import type { Texture } from '@flighthq/types/contract';
|
|
17
24
|
|
|
18
25
|
import { formatShapeJson, parseShapeJson } from './shapeJson';
|
|
19
26
|
|
|
@@ -37,14 +44,31 @@ function createEveryNonBitmapCommandShape() {
|
|
|
37
44
|
appendShapeCurveTo(shape, 150, 50, 100, 100);
|
|
38
45
|
appendShapeCubicCurveTo(shape, 10, 10, 20, 20, 30, 30);
|
|
39
46
|
appendShapePath(shape, [1, 2], [0, 0, 50, 50], 'evenOdd');
|
|
47
|
+
appendShapeCircle(shape, 5, 6, 7);
|
|
48
|
+
appendShapeEllipse(shape, 1, 2, 3, 4);
|
|
49
|
+
appendShapeRectangle(shape, 10, 11, 12, 13);
|
|
50
|
+
appendShapeRoundRectangle(shape, 1, 2, 3, 4, 5, 6);
|
|
51
|
+
appendShapeDrawTriangles(shape, [0, 0, 1, 0, 0, 1], [0, 1, 2], [0, 0, 1, 0, 0, 1], 'positive');
|
|
52
|
+
appendShapeDrawTriangles(shape, [0, 0, 1, 0, 0, 1], null, null, 'none');
|
|
53
|
+
appendShapeLineGradientStyle(
|
|
54
|
+
shape,
|
|
55
|
+
'radial',
|
|
56
|
+
[0x112233ff, 0x445566ff],
|
|
57
|
+
[1, 0],
|
|
58
|
+
[0, 255],
|
|
59
|
+
null,
|
|
60
|
+
'reflect',
|
|
61
|
+
'linearRGB',
|
|
62
|
+
0.5,
|
|
63
|
+
);
|
|
40
64
|
appendShapeEndFill(shape);
|
|
41
65
|
return shape;
|
|
42
66
|
}
|
|
43
67
|
|
|
44
|
-
// A minimal stand-in for a live
|
|
68
|
+
// A minimal stand-in for a live Texture; the codec never inspects texture internals, only
|
|
45
69
|
// swaps the object for an ordinal reference and back.
|
|
46
|
-
function
|
|
47
|
-
return {
|
|
70
|
+
function createFakeTexture(): Texture {
|
|
71
|
+
return {} as Texture;
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
describe('formatShapeJson', () => {
|
|
@@ -52,7 +76,7 @@ describe('formatShapeJson', () => {
|
|
|
52
76
|
const shape = createShape();
|
|
53
77
|
appendShapeBeginFill(shape, 0x112233ff, 1);
|
|
54
78
|
const parsed = JSON.parse(formatShapeJson(shape));
|
|
55
|
-
expect(parsed.shapeFormat).toBe(
|
|
79
|
+
expect(parsed.shapeFormat).toBe(2);
|
|
56
80
|
expect(Array.isArray(parsed.commands)).toBe(true);
|
|
57
81
|
expect(parsed.commands[0]).toEqual({ key: 'beginFill', args: [0x112233ff, 1] });
|
|
58
82
|
});
|
|
@@ -64,14 +88,12 @@ describe('formatShapeJson', () => {
|
|
|
64
88
|
expect(parsed.commands[0].args[4]).toEqual({ a: 2, b: 0, c: 0, d: 3, tx: 5, ty: 7 });
|
|
65
89
|
});
|
|
66
90
|
|
|
67
|
-
it('serializes a
|
|
91
|
+
it('serializes a texture as an ordinal reference, never the texture', () => {
|
|
68
92
|
const shape = createShape();
|
|
69
|
-
|
|
93
|
+
appendShapeBeginTextureFill(shape, createFakeTexture(), null);
|
|
70
94
|
const parsed = JSON.parse(formatShapeJson(shape));
|
|
71
|
-
expect(parsed.commands[0].args[0]).toEqual({
|
|
95
|
+
expect(parsed.commands[0].args[0]).toEqual({ texture: { index: 0 } });
|
|
72
96
|
expect(parsed.commands[0].args[1]).toBeNull();
|
|
73
|
-
expect(parsed.commands[0].args[2]).toBe(true);
|
|
74
|
-
expect(parsed.commands[0].args[3]).toBe(false);
|
|
75
97
|
});
|
|
76
98
|
|
|
77
99
|
it('honors the space option for pretty-printing', () => {
|
|
@@ -82,7 +104,7 @@ describe('formatShapeJson', () => {
|
|
|
82
104
|
});
|
|
83
105
|
|
|
84
106
|
describe('parseShapeJson', () => {
|
|
85
|
-
it('round-trips every non-
|
|
107
|
+
it('round-trips every non-texture command losslessly', () => {
|
|
86
108
|
const shape = createEveryNonBitmapCommandShape();
|
|
87
109
|
const json = formatShapeJson(shape);
|
|
88
110
|
const restored = parseShapeJson(json);
|
|
@@ -100,7 +122,7 @@ describe('parseShapeJson', () => {
|
|
|
100
122
|
});
|
|
101
123
|
|
|
102
124
|
it('returns null for a mismatched version tag', () => {
|
|
103
|
-
expect(parseShapeJson(JSON.stringify({ shapeFormat:
|
|
125
|
+
expect(parseShapeJson(JSON.stringify({ shapeFormat: 3, commands: [] }))).toBeNull();
|
|
104
126
|
});
|
|
105
127
|
|
|
106
128
|
it('returns null when the top level is not an object', () => {
|
|
@@ -108,40 +130,40 @@ describe('parseShapeJson', () => {
|
|
|
108
130
|
});
|
|
109
131
|
|
|
110
132
|
it('returns null for an unknown command key', () => {
|
|
111
|
-
const json = JSON.stringify({ shapeFormat:
|
|
133
|
+
const json = JSON.stringify({ shapeFormat: 2, commands: [{ key: 'notACommand', args: [] }] });
|
|
112
134
|
expect(parseShapeJson(json)).toBeNull();
|
|
113
135
|
});
|
|
114
136
|
|
|
115
137
|
it('returns null for a malformed argument object', () => {
|
|
116
|
-
const json = JSON.stringify({ shapeFormat:
|
|
138
|
+
const json = JSON.stringify({ shapeFormat: 2, commands: [{ key: 'beginFill', args: [{ nonsense: true }, 1] }] });
|
|
117
139
|
expect(parseShapeJson(json)).toBeNull();
|
|
118
140
|
});
|
|
119
141
|
|
|
120
|
-
it('round-trips a bitmap fill through the resolver', () => {
|
|
121
|
-
const
|
|
142
|
+
it('round-trips a bitmap fill through the texture resolver', () => {
|
|
143
|
+
const texture = createFakeTexture();
|
|
122
144
|
const shape = createShape();
|
|
123
|
-
|
|
145
|
+
appendShapeBeginTextureFill(shape, texture, createMatrix(1, 0, 0, 1, 3, 4));
|
|
124
146
|
appendShapeMoveTo(shape, 5, 6);
|
|
125
147
|
|
|
126
148
|
const seen: number[] = [];
|
|
127
149
|
const restored = parseShapeJson(formatShapeJson(shape), {
|
|
128
|
-
|
|
150
|
+
resolveTexture: (reference) => {
|
|
129
151
|
seen.push(reference.index);
|
|
130
|
-
return
|
|
152
|
+
return texture;
|
|
131
153
|
},
|
|
132
154
|
});
|
|
133
155
|
|
|
134
156
|
expect(restored).not.toBeNull();
|
|
135
157
|
expect(seen).toEqual([0]);
|
|
136
158
|
expect(getShapeCommandCount(restored!)).toBe(2);
|
|
137
|
-
expect(restored!.data.commands[0]).toBe('
|
|
138
|
-
expect(restored!.data.commands[2]).toBe(
|
|
159
|
+
expect(restored!.data.commands[0]).toBe('beginTextureFill');
|
|
160
|
+
expect(restored!.data.commands[2]).toBe(texture);
|
|
139
161
|
});
|
|
140
162
|
|
|
141
163
|
it('drops a bitmap fill when no resolver is supplied and keeps the rest intact', () => {
|
|
142
164
|
const shape = createShape();
|
|
143
165
|
appendShapeBeginFill(shape, 0xffffffff, 1);
|
|
144
|
-
|
|
166
|
+
appendShapeBeginTextureFill(shape, createFakeTexture(), null);
|
|
145
167
|
appendShapeLineTo(shape, 9, 9);
|
|
146
168
|
|
|
147
169
|
const restored = parseShapeJson(formatShapeJson(shape));
|
|
@@ -153,12 +175,110 @@ describe('parseShapeJson', () => {
|
|
|
153
175
|
|
|
154
176
|
it('drops a bitmap fill when the resolver returns null', () => {
|
|
155
177
|
const shape = createShape();
|
|
156
|
-
|
|
178
|
+
appendShapeBeginTextureFill(shape, createFakeTexture(), null);
|
|
157
179
|
appendShapeEndFill(shape);
|
|
158
180
|
|
|
159
|
-
const restored = parseShapeJson(formatShapeJson(shape), {
|
|
181
|
+
const restored = parseShapeJson(formatShapeJson(shape), { resolveTexture: () => null });
|
|
160
182
|
expect(restored).not.toBeNull();
|
|
161
183
|
expect(getShapeCommandCount(restored!)).toBe(1);
|
|
162
184
|
expect(restored!.data.commands[0]).toBe('endFill');
|
|
163
185
|
});
|
|
164
186
|
});
|
|
187
|
+
|
|
188
|
+
describe('parseShapeJson argument validation', () => {
|
|
189
|
+
function document(commands: readonly unknown[]): string {
|
|
190
|
+
return JSON.stringify({ shapeFormat: 2, commands });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Every rejection below used to build a Shape instead. The parser checked that an entry looked like
|
|
194
|
+
// a command and then spread whatever it found straight into the appender, so a bad document produced
|
|
195
|
+
// corrupt geometry rather than the null its own contract documents.
|
|
196
|
+
it('rejects a command with too few arguments', () => {
|
|
197
|
+
expect(parseShapeJson(document([{ key: 'moveTo', args: [1] }]))).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('rejects a command with too many arguments', () => {
|
|
201
|
+
expect(parseShapeJson(document([{ key: 'moveTo', args: [1, 2, 3] }]))).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('rejects a string where a number is expected', () => {
|
|
205
|
+
expect(parseShapeJson(document([{ key: 'moveTo', args: ['1', '2'] }]))).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('rejects a number where a string is expected', () => {
|
|
209
|
+
expect(parseShapeJson(document([{ key: 'drawPath', args: [[1], [0, 0], 7] }]))).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('rejects a number where an array is expected', () => {
|
|
213
|
+
expect(parseShapeJson(document([{ key: 'drawPath', args: [1, [0, 0], 'evenOdd'] }]))).toBeNull();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('rejects a non-numeric entry inside a numeric array', () => {
|
|
217
|
+
expect(parseShapeJson(document([{ key: 'drawPath', args: [[1], [0, 'x'], 'evenOdd'] }]))).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// JSON has no NaN or Infinity literal: a NaN coordinate serializes as `null`, and an out-of-range
|
|
221
|
+
// literal parses back as Infinity. Both silently change the geometry, so both are refused.
|
|
222
|
+
it('rejects a null where a number is expected, which is how NaN survives JSON', () => {
|
|
223
|
+
expect(parseShapeJson(document([{ key: 'moveTo', args: [null, 5] }]))).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('rejects an out-of-range literal that parses as Infinity', () => {
|
|
227
|
+
expect(parseShapeJson('{"shapeFormat":2,"commands":[{"key":"moveTo","args":[1e999,2]}]}')).toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('rejects a non-finite entry inside a numeric array', () => {
|
|
231
|
+
expect(
|
|
232
|
+
parseShapeJson('{"shapeFormat":2,"commands":[{"key":"drawPath","args":[[1],[1e999,0],"evenOdd"]}]}'),
|
|
233
|
+
).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('accepts a command that omits its optional trailing arguments', () => {
|
|
237
|
+
const restored = parseShapeJson(
|
|
238
|
+
document([
|
|
239
|
+
{ key: 'lineStyle', args: [] },
|
|
240
|
+
{ key: 'endFill', args: [] },
|
|
241
|
+
]),
|
|
242
|
+
);
|
|
243
|
+
expect(restored).not.toBeNull();
|
|
244
|
+
expect(getShapeCommandCount(restored!)).toBe(2);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('accepts a null matrix in an optional matrix position', () => {
|
|
248
|
+
const restored = parseShapeJson(
|
|
249
|
+
document([{ key: 'beginGradientFill', args: ['linear', [1], [1], [0], null, 'pad', 'rgb', 0] }]),
|
|
250
|
+
);
|
|
251
|
+
expect(restored).not.toBeNull();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('rejects a non-matrix object in a matrix position', () => {
|
|
255
|
+
expect(
|
|
256
|
+
parseShapeJson(
|
|
257
|
+
document([{ key: 'beginGradientFill', args: ['linear', [1], [1], [0], { a: 1 }, 'pad', 'rgb', 0] }]),
|
|
258
|
+
),
|
|
259
|
+
).toBeNull();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// A texture command still drops rather than failing the parse: an unresolved reference is a missing
|
|
263
|
+
// asset, not a malformed document, and that distinction predates this validation.
|
|
264
|
+
it('drops rather than rejects a texture command whose reference does not resolve', () => {
|
|
265
|
+
const shape = createShape();
|
|
266
|
+
appendShapeBeginTextureFill(shape, createFakeTexture(), null);
|
|
267
|
+
appendShapeEndFill(shape);
|
|
268
|
+
const restored = parseShapeJson(formatShapeJson(shape), { resolveTexture: () => null });
|
|
269
|
+
expect(restored).not.toBeNull();
|
|
270
|
+
expect(getShapeCommandCount(restored!)).toBe(1);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// The two tables are indexed by the same key set and are read together on every command, so a key
|
|
274
|
+
// in the appender table with no arg spec would make its command unparseable — isValidShapeCommandArgs
|
|
275
|
+
// treats a missing spec as invalid. Round-tripping a shape that exercises the whole non-texture
|
|
276
|
+
// vocabulary therefore *is* the consistency check: a forgotten spec turns this null.
|
|
277
|
+
it('parses every command the serializer can emit, proving each has an argument spec', () => {
|
|
278
|
+
const shape = createEveryNonBitmapCommandShape();
|
|
279
|
+
const expected = getShapeCommandCount(shape);
|
|
280
|
+
const restored = parseShapeJson(formatShapeJson(shape));
|
|
281
|
+
expect(restored).not.toBeNull();
|
|
282
|
+
expect(getShapeCommandCount(restored!)).toBe(expected);
|
|
283
|
+
});
|
|
284
|
+
});
|