parse-tmx 4.2.0 → 4.3.1

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