parse-tmx 4.2.0 → 4.3.3

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 CHANGED
@@ -24,7 +24,7 @@ pnpm i parse-tmx
24
24
 
25
25
  ## <a name="documentation">📖 Documentation</a>
26
26
 
27
- We highly recommend you take a look at the [documentation](https://esposter.com/docs/api/modules/parse-tmx.html) to level up.
27
+ We highly recommend you take a look at the [documentation](https://github.com/Esposter/Esposter/tree/main/openspec/specs/parse-tmx) to level up.
28
28
 
29
29
  ### Usage
30
30
 
@@ -0,0 +1,156 @@
1
+ import { Result, ResultAsync } from "neverthrow";
2
+ export declare enum Shape {
3
+ Ellipse = "ellipse",
4
+ Point = "point",
5
+ Polygon = "polygon",
6
+ Polyline = "polyline",
7
+ Rectangle = "rectangle"
8
+ }
9
+ interface TMXAnimationFrameParsed {
10
+ duration: number;
11
+ tileid: number;
12
+ }
13
+ interface TMXAnimationParsed {
14
+ frames: TMXAnimationFrameParsed[];
15
+ }
16
+ interface TMXFlipsParsed {
17
+ Diagonal: boolean;
18
+ Horizontal: boolean;
19
+ Vertical: boolean;
20
+ }
21
+ interface TMXChunkParsed {
22
+ data?: number[];
23
+ flips?: TMXFlipsParsed[];
24
+ height: number;
25
+ width: number;
26
+ x: number;
27
+ y: number;
28
+ }
29
+ interface TMXExportParsed {
30
+ format: string;
31
+ target: string;
32
+ }
33
+ interface TMXEditorSettingsParsed {
34
+ export?: TMXExportParsed;
35
+ }
36
+ interface TMXImageParsed {
37
+ format?: string;
38
+ height: number;
39
+ source: string;
40
+ trans?: string;
41
+ width: number;
42
+ }
43
+ interface TMXPropertyParsed {
44
+ name: string;
45
+ properties?: TMXPropertiesParsed;
46
+ value: boolean | number | string;
47
+ }
48
+ type TMXPropertiesParsed = TMXPropertyParsed[];
49
+ interface TMXObjectParsed {
50
+ flips?: TMXFlipsParsed;
51
+ gid?: number;
52
+ height: number;
53
+ id: number;
54
+ name: string;
55
+ points?: [number, number][];
56
+ properties?: TMXPropertiesParsed;
57
+ rotation?: number;
58
+ shape: Shape;
59
+ template?: string;
60
+ text?: string;
61
+ type: string;
62
+ visible?: number;
63
+ width: number;
64
+ x: number;
65
+ y: number;
66
+ }
67
+ interface TMXTileParsed {
68
+ animation?: TMXAnimationParsed;
69
+ id: number;
70
+ image?: TMXImageParsed;
71
+ objects?: TMXObjectParsed[];
72
+ probability?: number;
73
+ properties?: TMXPropertiesParsed;
74
+ terrain?: string;
75
+ type?: string;
76
+ }
77
+ interface TMXEmbeddedTilesetParsed {
78
+ columns: number;
79
+ firstgid: number;
80
+ image?: TMXImageParsed;
81
+ margin?: number;
82
+ name: string;
83
+ spacing?: number;
84
+ tilecount: number;
85
+ tileheight: number;
86
+ tiles: TMXTileParsed[];
87
+ tilewidth: number;
88
+ }
89
+ interface TMXExternalTilesetParsed {
90
+ firstgid: number;
91
+ source: string;
92
+ }
93
+ interface TMXGroupLayerShared {
94
+ class?: string;
95
+ id: number;
96
+ name: string;
97
+ offsetx?: number;
98
+ offsety?: number;
99
+ opacity?: number;
100
+ parallaxx?: number;
101
+ parallaxy?: number;
102
+ tintcolor?: string;
103
+ type: string;
104
+ visible?: number;
105
+ }
106
+ interface TMXLayerParsed extends TMXGroupLayerShared {
107
+ chunks?: TMXChunkParsed[];
108
+ color?: string;
109
+ data?: number[];
110
+ draworder?: string;
111
+ flips?: TMXFlipsParsed[];
112
+ height?: number;
113
+ image?: TMXImageParsed;
114
+ locked?: number;
115
+ objects?: TMXObjectParsed[];
116
+ properties?: TMXPropertiesParsed;
117
+ width?: number;
118
+ x?: number;
119
+ y?: number;
120
+ }
121
+ interface TMXGroupLayerParsed extends TMXGroupLayerShared {
122
+ layers: (TMXGroupLayerParsed | TMXLayerParsed)[];
123
+ properties?: TMXPropertiesParsed;
124
+ }
125
+ type TMXTilesetParsed = TMXEmbeddedTilesetParsed | TMXExternalTilesetParsed;
126
+ export declare class TMXMapParsed {
127
+ backgroundcolor?: string;
128
+ compressionlevel?: number;
129
+ editorsettings: TMXEditorSettingsParsed;
130
+ height: number;
131
+ hexsidelength?: number;
132
+ infinite: number;
133
+ layers: (TMXGroupLayerParsed | TMXLayerParsed)[];
134
+ nextlayerid: number;
135
+ nextobjectid: number;
136
+ orientation: string;
137
+ parallaxoriginx?: number;
138
+ parallaxoriginy?: number;
139
+ properties: TMXPropertiesParsed;
140
+ renderorder: string;
141
+ staggeraxis?: string;
142
+ staggerindex?: string;
143
+ tiledversion: string;
144
+ tileheight: number;
145
+ tilesets: TMXTilesetParsed[];
146
+ tilewidth: number;
147
+ version: number;
148
+ width: number;
149
+ }
150
+ export declare class TMXParsed {
151
+ map: TMXMapParsed;
152
+ constructor(map?: TMXMapParsed);
153
+ }
154
+ export declare const parseTmx: (xmlString: string, translateFlips?: boolean) => ResultAsync<TMXParsed, Error>;
155
+ export declare const parseTileset: (xmlString: string) => Result<TMXEmbeddedTilesetParsed, Error>;
156
+ export type { TMXAnimationFrameParsed, TMXAnimationParsed, TMXChunkParsed, TMXEditorSettingsParsed, TMXEmbeddedTilesetParsed, TMXExportParsed, TMXExternalTilesetParsed, TMXFlipsParsed, TMXGroupLayerParsed, TMXImageParsed, TMXLayerParsed, TMXObjectParsed, TMXPropertiesParsed, TMXPropertyParsed, TMXTileParsed, TMXTilesetParsed };
package/dist/index.js ADDED
@@ -0,0 +1,736 @@
1
+ import { InvalidOperationError, Operation, exhaustiveGuard, getResult, getResultAsync, normalizeString } from "@esposter/shared";
2
+ import sax from "sax";
3
+ // src/models/Shape.ts
4
+ let Shape = /* @__PURE__ */ function(Shape) {
5
+ Shape["Ellipse"] = "ellipse";
6
+ Shape["Point"] = "point";
7
+ Shape["Polygon"] = "polygon";
8
+ Shape["Polyline"] = "polyline";
9
+ Shape["Rectangle"] = "rectangle";
10
+ return Shape;
11
+ }({});
12
+ // src/models/tmx/parsed/TMXMapParsed.ts
13
+ var TMXMapParsed = class {
14
+ editorsettings = {};
15
+ height = 0;
16
+ infinite = 0;
17
+ layers = [];
18
+ nextlayerid = 0;
19
+ nextobjectid = 0;
20
+ orientation = "";
21
+ properties = [];
22
+ renderorder = "";
23
+ tiledversion = "";
24
+ tileheight = 0;
25
+ tilesets = [];
26
+ tilewidth = 0;
27
+ version = 0;
28
+ width = 0;
29
+ };
30
+ // src/models/tmx/parsed/TMXParsed.ts
31
+ var TMXParsed = class {
32
+ map;
33
+ constructor(map = new TMXMapParsed()) {
34
+ this.map = map;
35
+ }
36
+ };
37
+ // src/models/tmx/TMXTagName.ts
38
+ let TMXTagName = /* @__PURE__ */ function(TMXTagName) {
39
+ TMXTagName["Animation"] = "animation";
40
+ TMXTagName["Chunk"] = "chunk";
41
+ TMXTagName["Data"] = "data";
42
+ TMXTagName["EditorSettings"] = "editorsettings";
43
+ TMXTagName["Ellipse"] = "ellipse";
44
+ TMXTagName["Export"] = "export";
45
+ TMXTagName["Frame"] = "frame";
46
+ TMXTagName["Group"] = "group";
47
+ TMXTagName["Image"] = "image";
48
+ TMXTagName["ImageLayer"] = "imagelayer";
49
+ TMXTagName["Layer"] = "layer";
50
+ TMXTagName["Map"] = "map";
51
+ TMXTagName["Object"] = "object";
52
+ TMXTagName["Objectgroup"] = "objectgroup";
53
+ TMXTagName["Point"] = "point";
54
+ TMXTagName["Polygon"] = "polygon";
55
+ TMXTagName["Polyline"] = "polyline";
56
+ TMXTagName["Properties"] = "properties";
57
+ TMXTagName["Property"] = "property";
58
+ TMXTagName["Text"] = "text";
59
+ TMXTagName["Tile"] = "tile";
60
+ TMXTagName["Tileset"] = "tileset";
61
+ return TMXTagName;
62
+ }({});
63
+ // src/models/Compression.ts
64
+ let Compression = /* @__PURE__ */ function(Compression) {
65
+ Compression["Gzip"] = "gzip";
66
+ Compression["Zlib"] = "zlib";
67
+ return Compression;
68
+ }({});
69
+ // src/models/Encoding.ts
70
+ let Encoding = /* @__PURE__ */ function(Encoding) {
71
+ Encoding["Base64"] = "base64";
72
+ Encoding["Csv"] = "csv";
73
+ return Encoding;
74
+ }({});
75
+ // src/services/getDecompressedBytes.ts
76
+ const getDecompressedBytes = async (bytes, compression) => {
77
+ const format = compression === "gzip" ? "gzip" : "deflate";
78
+ const decompressedTileStream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format));
79
+ return new Uint8Array(await new Response(decompressedTileStream).arrayBuffer());
80
+ };
81
+ // src/services/unpackTileBytes.ts
82
+ const unpackTileBytes = (bytes) => {
83
+ if (bytes.byteLength % Uint32Array.BYTES_PER_ELEMENT !== 0) throw new InvalidOperationError(Operation.Read, "TMXLayer", `expected whole ${Uint32Array.BYTES_PER_ELEMENT}-byte tiles, received ${bytes.byteLength} bytes`);
84
+ const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
85
+ const tiles = new Uint32Array(bytes.byteLength / Uint32Array.BYTES_PER_ELEMENT);
86
+ for (let index = 0; index < tiles.length; index++) tiles[index] = dataView.getUint32(index * Uint32Array.BYTES_PER_ELEMENT, true);
87
+ return [...tiles];
88
+ };
89
+ // src/services/decodeTileData.ts
90
+ const CSV_SEPARATOR_CODE_POINT = ",".codePointAt(0) ?? 0;
91
+ const ZERO_CODE_POINT = "0".codePointAt(0) ?? 0;
92
+ const NINE_CODE_POINT = "9".codePointAt(0) ?? 0;
93
+ const WHITESPACE_REGEX = /\s/u;
94
+ const decodeCsv = (text) => {
95
+ const tiles = [];
96
+ let tile = 0;
97
+ let hasDigit = false;
98
+ let isEnded = false;
99
+ for (let index = 0; index <= text.length; index++) {
100
+ const codePoint = text.codePointAt(index) ?? CSV_SEPARATOR_CODE_POINT;
101
+ if (codePoint === CSV_SEPARATOR_CODE_POINT) {
102
+ tiles.push(tile);
103
+ tile = 0;
104
+ hasDigit = false;
105
+ isEnded = false;
106
+ } else if (codePoint >= ZERO_CODE_POINT && codePoint <= NINE_CODE_POINT) {
107
+ if (isEnded) throw new InvalidOperationError(Operation.Read, decodeCsv.name, "has a cell with two numbers");
108
+ tile = tile * 10 + codePoint - ZERO_CODE_POINT;
109
+ hasDigit = true;
110
+ } else if (WHITESPACE_REGEX.test(text.charAt(index))) isEnded ||= hasDigit;
111
+ else throw new InvalidOperationError(Operation.Read, decodeCsv.name, `${text.charAt(index)} is not a tile gid`);
112
+ }
113
+ return tiles;
114
+ };
115
+ const decodeTileData = async (source) => {
116
+ const { compression, encoding, gids, text } = source;
117
+ if (encoding === void 0) return gids;
118
+ const data = normalizeString(text);
119
+ switch (encoding) {
120
+ case "base64": {
121
+ const bytes = Uint8Array.fromBase64(data);
122
+ switch (compression) {
123
+ case "gzip":
124
+ case "zlib": return unpackTileBytes(await getDecompressedBytes(bytes, compression));
125
+ case void 0: return unpackTileBytes(bytes);
126
+ default: return exhaustiveGuard(compression);
127
+ }
128
+ }
129
+ case "csv": return decodeCsv(data);
130
+ default: return exhaustiveGuard(encoding);
131
+ }
132
+ };
133
+ const Flippeds = Object.values(/* @__PURE__ */ function(Flipped) {
134
+ Flipped[Flipped["Diagonally"] = 536870912] = "Diagonally";
135
+ Flipped[Flipped["Vertically"] = 1073741824] = "Vertically";
136
+ Flipped[Flipped["Horizontally"] = 2147483648] = "Horizontally";
137
+ return Flipped;
138
+ }({})).filter((value) => typeof value === "number");
139
+ // src/services/parseFlips.ts
140
+ const parseFlips = (gid) => ({
141
+ Diagonal: Boolean(gid & 536870912),
142
+ Horizontal: Boolean(gid & 2147483648),
143
+ Vertical: Boolean(gid & 1073741824)
144
+ });
145
+ // src/services/parseTileId.ts
146
+ const FLIPPED_MASK = ~Flippeds.reduce((accumulator, flipped) => accumulator | flipped, 0);
147
+ const parseTileId = (gid) => gid & FLIPPED_MASK;
148
+ // src/services/assignTileData.ts
149
+ const assignTileData = async (pending, translateFlips) => {
150
+ await Promise.all(pending.map(async ({ expectedCount, owner, source }) => {
151
+ if (source.chunked) return;
152
+ const tiles = await decodeTileData(source);
153
+ if (expectedCount !== void 0 && tiles.length !== expectedCount) throw new InvalidOperationError(Operation.Read, "TMXLayer", `expected ${expectedCount} tiles, received ${tiles.length}`);
154
+ if (translateFlips) {
155
+ owner.data = tiles.map((gid) => parseTileId(gid));
156
+ owner.flips = tiles.map((gid) => parseFlips(gid));
157
+ } else owner.data = tiles;
158
+ }));
159
+ };
160
+ // src/models/PropertyType.ts
161
+ let PropertyType = /* @__PURE__ */ function(PropertyType) {
162
+ PropertyType["Bool"] = "bool";
163
+ PropertyType["Class"] = "class";
164
+ PropertyType["Color"] = "color";
165
+ PropertyType["File"] = "file";
166
+ PropertyType["Float"] = "float";
167
+ PropertyType["Int"] = "int";
168
+ PropertyType["Object"] = "object";
169
+ PropertyType["String"] = "string";
170
+ return PropertyType;
171
+ }({});
172
+ // src/services/closeTmxFrame.ts
173
+ const parsePropertyValue = (propertyType, value) => {
174
+ switch (propertyType) {
175
+ case "bool": return value === "true";
176
+ case "class":
177
+ case "color":
178
+ case "file":
179
+ case "object":
180
+ case "string": return value;
181
+ case "float":
182
+ case "int": return Number(value);
183
+ default: return exhaustiveGuard(propertyType);
184
+ }
185
+ };
186
+ const closeTmxFrame = (frame) => {
187
+ switch (frame.tag) {
188
+ case "chunk":
189
+ case "data":
190
+ case "text":
191
+ frame.target.text = frame.text;
192
+ return;
193
+ case "property":
194
+ if (frame.propertyType === "class") return;
195
+ frame.target.value = parsePropertyValue(frame.propertyType, frame.valueAttribute ?? frame.text);
196
+ return;
197
+ default: return;
198
+ }
199
+ };
200
+ // src/services/readTmxAttributes.ts
201
+ const readTmxAttributes = (attributes, table) => {
202
+ const values = {};
203
+ for (const [name, kind] of Object.entries(table)) {
204
+ const isNumber = kind === "number" || kind === "requiredNumber";
205
+ const isRequired = kind === "requiredNumber" || kind === "requiredString";
206
+ const text = attributes[name];
207
+ if (text === void 0 || isNumber && text === "") {
208
+ if (isRequired) values[name] = isNumber ? 0 : "";
209
+ } else if (isNumber) {
210
+ const number = Number(text);
211
+ if (Number.isNaN(number)) throw new InvalidOperationError(Operation.Read, readTmxAttributes.name, `${text} is not a number`);
212
+ values[name] = number;
213
+ } else values[name] = text;
214
+ }
215
+ return values;
216
+ };
217
+ // src/services/createTmxImage.ts
218
+ const imageAttributeKindMap = {
219
+ format: "string",
220
+ height: "requiredNumber",
221
+ source: "requiredString",
222
+ trans: "string",
223
+ width: "requiredNumber"
224
+ };
225
+ const createTmxImage = (attributes) => readTmxAttributes(attributes, imageAttributeKindMap);
226
+ // src/services/openTmxLayer.ts
227
+ const groupAttributeKindMap = {
228
+ class: "string",
229
+ id: "requiredNumber",
230
+ name: "requiredString",
231
+ offsetx: "number",
232
+ offsety: "number",
233
+ opacity: "number",
234
+ parallaxx: "number",
235
+ parallaxy: "number",
236
+ tintcolor: "string",
237
+ visible: "number"
238
+ };
239
+ const layerAttributeKindMap = {
240
+ ...groupAttributeKindMap,
241
+ color: "string",
242
+ draworder: "string",
243
+ height: "number",
244
+ locked: "number",
245
+ width: "number",
246
+ x: "number",
247
+ y: "number"
248
+ };
249
+ const openTmxLayer = (layers, tag, attributes) => {
250
+ if (tag === "group") {
251
+ const group = {
252
+ ...readTmxAttributes(attributes, groupAttributeKindMap),
253
+ layers: [],
254
+ type: tag
255
+ };
256
+ layers.push(group);
257
+ return {
258
+ tag,
259
+ target: group,
260
+ text: ""
261
+ };
262
+ }
263
+ const layer = {
264
+ ...readTmxAttributes(attributes, layerAttributeKindMap),
265
+ type: tag
266
+ };
267
+ layers.push(layer);
268
+ return {
269
+ tag,
270
+ target: layer,
271
+ text: ""
272
+ };
273
+ };
274
+ // src/services/openTmxProperties.ts
275
+ const openTmxProperties = (target) => {
276
+ target.properties ??= [];
277
+ return {
278
+ tag: "properties",
279
+ target: target.properties,
280
+ text: ""
281
+ };
282
+ };
283
+ // src/services/parseEnumAttribute.ts
284
+ const parseEnumAttribute = (operation, label, enumObject, value) => {
285
+ if (value === void 0) return void 0;
286
+ const enumValue = Object.values(enumObject).find((member) => member === value);
287
+ if (enumValue !== void 0) return enumValue;
288
+ throw new InvalidOperationError(Operation.Read, operation, `${value} is not a supported ${label}`);
289
+ };
290
+ // src/services/openTmxLayerChild.ts
291
+ const gidAttributeKindMap = { gid: "requiredNumber" };
292
+ const chunkAttributeKindMap = {
293
+ height: "requiredNumber",
294
+ width: "requiredNumber",
295
+ x: "requiredNumber",
296
+ y: "requiredNumber"
297
+ };
298
+ const parseCompression = (value) => parseEnumAttribute(parseCompression.name, "compression", Compression, value);
299
+ const parseEncoding = (value) => parseEnumAttribute(parseEncoding.name, "encoding", Encoding, value);
300
+ const createDataSource = (compression, encoding) => ({
301
+ chunked: false,
302
+ compression,
303
+ encoding,
304
+ gids: [],
305
+ text: ""
306
+ });
307
+ const openTileData = (layer, attributes, context) => {
308
+ const source = createDataSource(parseCompression(attributes.compression), parseEncoding(attributes.encoding));
309
+ context.pending.push({
310
+ expectedCount: context.tileCount,
311
+ owner: layer,
312
+ source
313
+ });
314
+ return {
315
+ owner: layer,
316
+ tag: "data",
317
+ target: source,
318
+ text: ""
319
+ };
320
+ };
321
+ const openTmxDataChild = (parent, tag, attributes, context) => {
322
+ const source = parent.target;
323
+ switch (tag) {
324
+ case "chunk": {
325
+ if (parent.tag !== "data") return void 0;
326
+ const chunk = readTmxAttributes(attributes, chunkAttributeKindMap);
327
+ parent.owner.chunks ??= [];
328
+ parent.owner.chunks.push(chunk);
329
+ source.chunked = true;
330
+ const chunkSource = createDataSource(source.compression, source.encoding);
331
+ context.pending.push({
332
+ owner: chunk,
333
+ source: chunkSource
334
+ });
335
+ return {
336
+ tag,
337
+ target: chunkSource,
338
+ text: ""
339
+ };
340
+ }
341
+ case "tile":
342
+ source.gids.push(readTmxAttributes(attributes, gidAttributeKindMap).gid);
343
+ return;
344
+ default: return;
345
+ }
346
+ };
347
+ const openTmxLayerChild = (parent, tag, attributes, context) => {
348
+ if (parent.tag === "chunk" || parent.tag === "data") return openTmxDataChild(parent, tag, attributes, context);
349
+ switch (tag) {
350
+ case "data":
351
+ if (parent.tag !== "layer") return void 0;
352
+ return openTileData(parent.target, attributes, context);
353
+ case "group":
354
+ case "imagelayer":
355
+ case "layer":
356
+ case "objectgroup":
357
+ if (parent.tag !== "group") return void 0;
358
+ return openTmxLayer(parent.target.layers, tag, attributes);
359
+ case "image":
360
+ if (parent.tag !== "imagelayer") return void 0;
361
+ parent.target.image = createTmxImage(attributes);
362
+ return {
363
+ tag,
364
+ target: parent.target.image,
365
+ text: ""
366
+ };
367
+ case "properties": return openTmxProperties(parent.target);
368
+ default: return;
369
+ }
370
+ };
371
+ // src/services/openTmxObjectChild.ts
372
+ const NUMBER_REGEX = /^[+-]?\d+(?:\.\d+)?$/u;
373
+ const objectAttributeKindMap = {
374
+ gid: "number",
375
+ height: "requiredNumber",
376
+ id: "requiredNumber",
377
+ name: "requiredString",
378
+ rotation: "number",
379
+ template: "string",
380
+ type: "requiredString",
381
+ visible: "number",
382
+ width: "requiredNumber",
383
+ x: "requiredNumber",
384
+ y: "requiredNumber"
385
+ };
386
+ const createObject = (attributes) => {
387
+ const { gid, ...object } = readTmxAttributes(attributes, objectAttributeKindMap);
388
+ const shaped = {
389
+ ...object,
390
+ shape: "rectangle"
391
+ };
392
+ if (gid === void 0) return shaped;
393
+ return gid ? {
394
+ ...shaped,
395
+ flips: parseFlips(gid),
396
+ gid: parseTileId(gid)
397
+ } : {
398
+ ...shaped,
399
+ gid
400
+ };
401
+ };
402
+ const parsePoints = (points) => points === "" ? [] : points.split(" ").map((point) => {
403
+ const [x = "", y = ""] = point.split(",");
404
+ return [Number(x), Number(y)];
405
+ });
406
+ const parseUntypedValue = (value) => {
407
+ if (value === "true") return true;
408
+ else if (value === "false") return false;
409
+ else if (NUMBER_REGEX.test(value)) return Number(value);
410
+ else return value;
411
+ };
412
+ const createTextProperties = (attributes) => Object.entries(attributes).map(([name, value]) => ({
413
+ name,
414
+ value: parseUntypedValue(value)
415
+ }));
416
+ const openTmxObjectChild = (parent, tag, attributes) => {
417
+ if (parent.tag === "objectgroup") {
418
+ if (tag === "properties") return openTmxProperties(parent.target);
419
+ if (tag !== "object") return void 0;
420
+ const object = createObject(attributes);
421
+ parent.target.objects ??= [];
422
+ parent.target.objects.push(object);
423
+ return {
424
+ tag,
425
+ target: object,
426
+ text: ""
427
+ };
428
+ }
429
+ const object = parent.target;
430
+ switch (tag) {
431
+ case "ellipse":
432
+ case "point":
433
+ object.shape = tag === "point" ? "point" : "ellipse";
434
+ return {
435
+ tag,
436
+ target: object,
437
+ text: ""
438
+ };
439
+ case "polygon":
440
+ case "polyline":
441
+ object.shape = tag === "polygon" ? "polygon" : "polyline";
442
+ object.points = parsePoints(attributes.points ?? "");
443
+ return {
444
+ tag,
445
+ target: object,
446
+ text: ""
447
+ };
448
+ case "properties": return openTmxProperties(object);
449
+ case "text":
450
+ object.properties = [...object.properties ?? [], ...createTextProperties(attributes)];
451
+ return {
452
+ tag,
453
+ target: object,
454
+ text: ""
455
+ };
456
+ default: return;
457
+ }
458
+ };
459
+ // src/services/openTmxFrame.ts
460
+ const mapAttributeKindMap = {
461
+ backgroundcolor: "string",
462
+ compressionlevel: "number",
463
+ height: "requiredNumber",
464
+ hexsidelength: "number",
465
+ infinite: "requiredNumber",
466
+ nextlayerid: "requiredNumber",
467
+ nextobjectid: "requiredNumber",
468
+ orientation: "requiredString",
469
+ parallaxoriginx: "number",
470
+ parallaxoriginy: "number",
471
+ renderorder: "requiredString",
472
+ staggeraxis: "string",
473
+ staggerindex: "string",
474
+ tiledversion: "requiredString",
475
+ tileheight: "requiredNumber",
476
+ tilewidth: "requiredNumber",
477
+ version: "requiredNumber",
478
+ width: "requiredNumber"
479
+ };
480
+ const embeddedTilesetAttributeKindMap = {
481
+ columns: "requiredNumber",
482
+ firstgid: "requiredNumber",
483
+ margin: "number",
484
+ name: "requiredString",
485
+ spacing: "number",
486
+ tilecount: "requiredNumber",
487
+ tileheight: "requiredNumber",
488
+ tilewidth: "requiredNumber"
489
+ };
490
+ const externalTilesetAttributeKindMap = {
491
+ firstgid: "requiredNumber",
492
+ source: "requiredString"
493
+ };
494
+ const tileAttributeKindMap = {
495
+ id: "requiredNumber",
496
+ probability: "number",
497
+ terrain: "string",
498
+ type: "string"
499
+ };
500
+ const animationFrameAttributeKindMap = {
501
+ duration: "requiredNumber",
502
+ tileid: "requiredNumber"
503
+ };
504
+ const exportAttributeKindMap = {
505
+ format: "requiredString",
506
+ target: "requiredString"
507
+ };
508
+ const propertyAttributeKindMap = { name: "requiredString" };
509
+ const createEmbeddedTileset = (attributes) => ({
510
+ ...readTmxAttributes(attributes, embeddedTilesetAttributeKindMap),
511
+ tiles: []
512
+ });
513
+ const parseTmxTagName = (name) => Object.values(TMXTagName).find((tagName) => tagName === name);
514
+ const parsePropertyType = (value) => parseEnumAttribute(parsePropertyType.name, "property type", PropertyType, value) ?? "string";
515
+ const openTmxRoot = (name, attributes, context) => {
516
+ const tag = parseTmxTagName(name);
517
+ switch (tag) {
518
+ case "map": {
519
+ const map = Object.assign(new TMXMapParsed(), readTmxAttributes(attributes, mapAttributeKindMap));
520
+ context.tileCount = map.width * map.height;
521
+ return {
522
+ tag,
523
+ target: map,
524
+ text: ""
525
+ };
526
+ }
527
+ case "tileset":
528
+ if (attributes.source !== void 0) return void 0;
529
+ return {
530
+ tag,
531
+ target: createEmbeddedTileset(attributes),
532
+ text: ""
533
+ };
534
+ default: throw new InvalidOperationError(Operation.Read, openTmxRoot.name, `${name} is not a map or a tileset`);
535
+ }
536
+ };
537
+ const openTmxMapChild = (parent, name, tag, attributes) => {
538
+ const map = parent.target;
539
+ switch (tag) {
540
+ case "data":
541
+ case "export":
542
+ case "image":
543
+ case "object":
544
+ case "property": return;
545
+ case "editorsettings": {
546
+ const editorSettings = {};
547
+ map.editorsettings = editorSettings;
548
+ return {
549
+ tag,
550
+ target: editorSettings,
551
+ text: ""
552
+ };
553
+ }
554
+ case "group":
555
+ case "imagelayer":
556
+ case "layer":
557
+ case "objectgroup": return openTmxLayer(map.layers, tag, attributes);
558
+ case "properties": return openTmxProperties(map);
559
+ case "tileset": {
560
+ if (attributes.source !== void 0) {
561
+ map.tilesets.push(readTmxAttributes(attributes, externalTilesetAttributeKindMap));
562
+ return;
563
+ }
564
+ const tileset = createEmbeddedTileset(attributes);
565
+ map.tilesets.push(tileset);
566
+ return {
567
+ tag,
568
+ target: tileset,
569
+ text: ""
570
+ };
571
+ }
572
+ default: throw new InvalidOperationError(Operation.Read, openTmxMapChild.name, `${name} is not a map child`);
573
+ }
574
+ };
575
+ const openTmxTileChild = (parent, tag, attributes) => {
576
+ const tile = parent.target;
577
+ switch (tag) {
578
+ case "animation": {
579
+ const animation = { frames: [] };
580
+ tile.animation = animation;
581
+ return {
582
+ tag,
583
+ target: animation,
584
+ text: ""
585
+ };
586
+ }
587
+ case "image": {
588
+ const image = createTmxImage(attributes);
589
+ tile.image = image;
590
+ return {
591
+ tag,
592
+ target: image,
593
+ text: ""
594
+ };
595
+ }
596
+ case "objectgroup": return {
597
+ tag,
598
+ target: tile,
599
+ text: ""
600
+ };
601
+ case "properties": return openTmxProperties(tile);
602
+ default: return;
603
+ }
604
+ };
605
+ const openTmxTilesetChild = (parent, tag, attributes) => {
606
+ const tileset = parent.target;
607
+ switch (tag) {
608
+ case "image": {
609
+ const image = createTmxImage(attributes);
610
+ tileset.image = image;
611
+ return {
612
+ tag,
613
+ target: image,
614
+ text: ""
615
+ };
616
+ }
617
+ case "tile": {
618
+ const tile = readTmxAttributes(attributes, tileAttributeKindMap);
619
+ tileset.tiles.push(tile);
620
+ return {
621
+ tag,
622
+ target: tile,
623
+ text: ""
624
+ };
625
+ }
626
+ default: return;
627
+ }
628
+ };
629
+ const openTmxPropertyChild = (parent, tag, attributes) => {
630
+ if (tag !== "property") return void 0;
631
+ const target = {
632
+ ...readTmxAttributes(attributes, propertyAttributeKindMap),
633
+ value: ""
634
+ };
635
+ parent.target.push(target);
636
+ return {
637
+ propertyType: parsePropertyType(attributes.type),
638
+ tag,
639
+ target,
640
+ text: "",
641
+ valueAttribute: attributes.value
642
+ };
643
+ };
644
+ const openTmxFrame = (parent, name, attributes, context) => {
645
+ if (parent === void 0) return openTmxRoot(name, attributes, context);
646
+ const tag = parseTmxTagName(name);
647
+ switch (parent.tag) {
648
+ case "animation": {
649
+ if (tag !== "frame") return void 0;
650
+ const frame = readTmxAttributes(attributes, animationFrameAttributeKindMap);
651
+ parent.target.frames.push(frame);
652
+ return {
653
+ tag,
654
+ target: frame,
655
+ text: ""
656
+ };
657
+ }
658
+ case "chunk":
659
+ case "data":
660
+ case "group":
661
+ case "imagelayer":
662
+ case "layer": return openTmxLayerChild(parent, tag, attributes, context);
663
+ case "editorsettings":
664
+ if (tag !== "export") return void 0;
665
+ parent.target.export = readTmxAttributes(attributes, exportAttributeKindMap);
666
+ return {
667
+ tag,
668
+ target: parent.target.export,
669
+ text: ""
670
+ };
671
+ case "map": return openTmxMapChild(parent, name, tag, attributes);
672
+ case "object":
673
+ case "objectgroup": return openTmxObjectChild(parent, tag, attributes);
674
+ case "properties": return openTmxPropertyChild(parent, tag, attributes);
675
+ case "property": return tag === "properties" ? openTmxProperties(parent.target) : void 0;
676
+ case "tile": return openTmxTileChild(parent, tag, attributes);
677
+ case "tileset": return openTmxTilesetChild(parent, tag, attributes);
678
+ default: return;
679
+ }
680
+ };
681
+ // src/services/readTmxDocument.ts
682
+ const BYTE_ORDER_MARK_REGEX = /^\uFEFF/u;
683
+ const toTmxAttributes = (attributes) => Object.fromEntries(Object.entries(attributes).map(([name, attribute]) => [name, typeof attribute === "string" ? attribute : attribute.value]));
684
+ const readTmxDocument = (xmlString, context) => {
685
+ const parser = sax.parser(true, {
686
+ normalize: false,
687
+ trim: false
688
+ });
689
+ const stack = [];
690
+ let root;
691
+ parser.onerror = (error) => {
692
+ throw new InvalidOperationError(Operation.Read, readTmxDocument.name, error.message);
693
+ };
694
+ parser.onopentag = (node) => {
695
+ if (stack.length === 0) {
696
+ root = openTmxFrame(void 0, node.name, toTmxAttributes(node.attributes), context);
697
+ stack.push(root);
698
+ return;
699
+ }
700
+ const parent = stack.at(-1);
701
+ stack.push(parent && openTmxFrame(parent, node.name, toTmxAttributes(node.attributes), context));
702
+ };
703
+ parser.onclosetag = () => {
704
+ const frame = stack.pop();
705
+ if (frame) closeTmxFrame(frame);
706
+ };
707
+ const appendText = (text) => {
708
+ const frame = stack.at(-1);
709
+ if (frame) frame.text += text;
710
+ };
711
+ parser.ontext = appendText;
712
+ parser.oncdata = appendText;
713
+ parser.write(xmlString.replace(BYTE_ORDER_MARK_REGEX, "")).close();
714
+ return root;
715
+ };
716
+ // src/parseTmx.ts
717
+ const parseTmx = (xmlString, translateFlips = false) => {
718
+ const context = {
719
+ pending: [],
720
+ tileCount: 0
721
+ };
722
+ return getResult(() => readTmxDocument(xmlString, context)).asyncAndThen((root) => getResultAsync(async () => {
723
+ if (root?.tag !== "map") throw new InvalidOperationError(Operation.Read, parseTmx.name, "has no map root element");
724
+ await assignTileData(context.pending, translateFlips);
725
+ return new TMXParsed(root.target);
726
+ }));
727
+ };
728
+ // src/services/parseTileset.ts
729
+ const parseTileset = (xmlString) => getResult(() => readTmxDocument(xmlString, {
730
+ pending: [],
731
+ tileCount: 0
732
+ })).andThen((root) => getResult(() => {
733
+ if (root?.tag !== "tileset") throw new InvalidOperationError(Operation.Read, parseTileset.name, "has no embedded tileset root element");
734
+ return root.target;
735
+ }));
736
+ export { Shape, TMXMapParsed, TMXParsed, parseTileset, parseTmx };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parse-tmx",
3
- "version": "4.2.0",
3
+ "version": "4.3.3",
4
4
  "description": "A parser for tmx.",
5
5
  "keywords": [
6
6
  "game-development",
@@ -44,26 +44,21 @@
44
44
  "scripts": {
45
45
  "build": "tsdown",
46
46
  "bench": "vitest bench --run",
47
- "format": "oxfmt",
48
- "format:check": "oxfmt --check",
49
47
  "lint": "eslint .",
50
48
  "lint:fix": "eslint --fix .",
51
49
  "test": "vitest",
52
50
  "typecheck": "tsc"
53
51
  },
54
52
  "dependencies": {
55
- "@esposter/shared": "4.2.0",
53
+ "@esposter/shared": "4.3.3",
56
54
  "neverthrow": "^8.2.0",
57
55
  "sax": "^1.6.1"
58
56
  },
59
57
  "devDependencies": {
60
- "@arethetypeswrong/core": "^0.18.5",
61
- "@esposter/configuration": "4.2.0",
62
- "@esposter/shared-node": "4.2.0",
58
+ "@esposter/configuration": "4.3.3",
59
+ "@esposter/shared-node": "4.3.3",
63
60
  "@types/sax": "^1.2.7",
64
- "publint": "^0.3.25",
65
- "tsdown": "^0.23.1",
66
- "vitest": "^5.0.4"
61
+ "tsdown": "^0.23.1"
67
62
  },
68
- "gitHead": "fef4971cf6a621a2c1850f32fcec19ee4ec0de99"
63
+ "gitHead": "955bbc130ad9b28877fd4ae8efc73a045141ce01"
69
64
  }