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