parse-tmx 3.9.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,166 +1,82 @@
1
- import { InvalidOperationError, Operation, exhaustiveGuard, normalizeString, takeOne } from "@esposter/shared";
2
- import { parseStringPromise } from "@esposter/xml2js";
3
- //#region src/models/Compression.ts
4
- let Compression = /* @__PURE__ */ function(Compression) {
5
- Compression["Gzip"] = "gzip";
6
- Compression["Zlib"] = "zlib";
7
- return Compression;
8
- }({});
9
- //#endregion
10
- //#region src/models/Encoding.ts
11
- let Encoding = /* @__PURE__ */ function(Encoding) {
12
- Encoding["Base64"] = "base64";
13
- Encoding["Csv"] = "csv";
14
- return Encoding;
15
- }({});
16
- //#endregion
17
- //#region src/models/Flipped.ts
18
- let Flipped = /* @__PURE__ */ function(Flipped) {
19
- Flipped[Flipped["Diagonally"] = 536870912] = "Diagonally";
20
- Flipped[Flipped["Vertically"] = 1073741824] = "Vertically";
21
- Flipped[Flipped["Horizontally"] = 2147483648] = "Horizontally";
22
- return Flipped;
23
- }({});
24
- const Flippeds = Object.values(Flipped).filter((value) => typeof value === "number");
25
- //#endregion
1
+ import { InvalidOperationError, Operation, exhaustiveGuard, getResult, getResultAsync, normalizeString } from "@esposter/shared";
2
+ import sax from "sax";
26
3
  //#region src/models/Shape.ts
27
4
  let Shape = /* @__PURE__ */ function(Shape) {
28
5
  Shape["Ellipse"] = "ellipse";
29
6
  Shape["Point"] = "point";
30
7
  Shape["Polygon"] = "polygon";
8
+ Shape["Polyline"] = "polyline";
31
9
  Shape["Rectangle"] = "rectangle";
32
10
  return Shape;
33
11
  }({});
34
12
  //#endregion
35
- //#region src/models/tmx/node/BaseTMXNode.ts
36
- var BaseTMXNode = class {
37
- "#name";
38
- $;
39
- };
40
- //#endregion
41
- //#region src/models/tmx/node/TMXNodeType.ts
42
- let TMXNodeType = /* @__PURE__ */ function(TMXNodeType) {
43
- TMXNodeType["Data"] = "data";
44
- TMXNodeType["EditorSettings"] = "editorsettings";
45
- TMXNodeType["Export"] = "export";
46
- TMXNodeType["Group"] = "group";
47
- TMXNodeType["Image"] = "image";
48
- TMXNodeType["ImageLayer"] = "imagelayer";
49
- TMXNodeType["Layer"] = "layer";
50
- TMXNodeType["Object"] = "object";
51
- TMXNodeType["Objectgroup"] = "objectgroup";
52
- TMXNodeType["Properties"] = "properties";
53
- TMXNodeType["Property"] = "property";
54
- TMXNodeType["Tileset"] = "tileset";
55
- return TMXNodeType;
56
- }({});
57
- //#endregion
58
- //#region src/models/tmx/shared/TMXMapShared.ts
59
- var TMXMapShared = class {
13
+ //#region src/models/tmx/parsed/TMXMapParsed.ts
14
+ var TMXMapParsed = class {
15
+ editorsettings = {};
60
16
  height = 0;
61
17
  infinite = 0;
18
+ layers = [];
62
19
  nextlayerid = 0;
63
20
  nextobjectid = 0;
64
21
  orientation = "";
22
+ properties = [];
65
23
  renderorder = "";
66
24
  tiledversion = "";
67
25
  tileheight = 0;
26
+ tilesets = [];
68
27
  tilewidth = 0;
69
28
  version = 0;
70
29
  width = 0;
71
30
  };
72
31
  //#endregion
73
- //#region src/models/tmx/parsed/TMXMapParsed.ts
74
- var TMXMapParsed = class extends TMXMapShared {
75
- editorsettings = {};
76
- layers = [];
77
- properties = [];
78
- tilesets = [];
79
- constructor(init) {
80
- super();
81
- Object.assign(this, init);
82
- }
83
- };
84
- //#endregion
85
32
  //#region src/models/tmx/parsed/TMXParsed.ts
86
33
  var TMXParsed = class {
87
- map = new TMXMapParsed();
88
- constructor(map) {
89
- if (map) this.map = map;
34
+ map;
35
+ constructor(map = new TMXMapParsed()) {
36
+ this.map = map;
90
37
  }
91
38
  };
92
39
  //#endregion
93
- //#region src/services/cloneNodeWithType.ts
94
- const cloneNodeWithType = (node) => {
95
- const attributes = structuredClone(node.$);
96
- attributes.type = node["#name"];
97
- return attributes;
98
- };
99
- //#endregion
100
- //#region src/services/parseFlips.ts
101
- const parseFlips = (gid) => ({
102
- Diagonal: Boolean(gid & 536870912),
103
- Horizontal: Boolean(gid & 2147483648),
104
- Vertical: Boolean(gid & 1073741824)
105
- });
106
- //#endregion
107
- //#region src/services/parseObjectShape.ts
108
- const parseObjectShape = (node) => {
109
- if ("point" in node) return "point";
110
- else if ("ellipse" in node) return "ellipse";
111
- else if ("polygon" in node) return "polygon";
112
- else return "rectangle";
113
- };
114
- //#endregion
115
- //#region src/services/parseProperties.ts
116
- const parseProperties = (properties) => properties.flatMap(({ property }) => property.map(({ $: { name, value }, _ }) => ({
117
- name,
118
- value: value ?? _
119
- })));
120
- //#endregion
121
- //#region src/services/parseTileId.ts
122
- const FLIPPED_MASK = ~Flippeds.reduce((accumulator, flipped) => accumulator | flipped, 0);
123
- const parseTileId = (gid) => gid & FLIPPED_MASK;
124
- //#endregion
125
- //#region src/services/parseObject.ts
126
- const parseObject = (node) => {
127
- const { $, polygon, properties, text } = node;
128
- const object = structuredClone($);
129
- object.shape = parseObjectShape(node);
130
- if (properties) object.properties = parseProperties(properties);
131
- if (polygon) object.points = takeOne(polygon).$.points.split(" ").map((point) => {
132
- const points = point.split(",");
133
- const x = takeOne(points);
134
- const y = takeOne(points, 1);
135
- return [Number(x), Number(y)];
136
- });
137
- if (text) {
138
- const textNode = takeOne(text);
139
- object.text = textNode._;
140
- object.properties = [...object.properties ?? [], ...Object.entries(textNode.$ ?? {}).map(([name, value]) => ({
141
- name,
142
- value
143
- }))];
144
- }
145
- if (object.gid) {
146
- object.flips = parseFlips(object.gid);
147
- object.gid = parseTileId(object.gid);
148
- }
149
- return object;
150
- };
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
+ }({});
151
66
  //#endregion
152
- //#region src/services/parseLayer.ts
153
- const parseLayer = (node) => {
154
- const { image, object, properties } = node;
155
- const layer = cloneNodeWithType(node);
156
- if (image) layer.image = structuredClone(takeOne(image).$);
157
- if (object) layer.objects = object.map((objectNode) => parseObject(objectNode));
158
- if (properties) layer.properties = parseProperties(properties);
159
- return layer;
160
- };
67
+ //#region src/models/Compression.ts
68
+ let Compression = /* @__PURE__ */ function(Compression) {
69
+ Compression["Gzip"] = "gzip";
70
+ Compression["Zlib"] = "zlib";
71
+ return Compression;
72
+ }({});
161
73
  //#endregion
162
- //#region src/services/checkIsTMXEmbeddedTilesetNode.ts
163
- const checkIsTMXEmbeddedTilesetNode = (node) => "tile" in node;
74
+ //#region src/models/Encoding.ts
75
+ let Encoding = /* @__PURE__ */ function(Encoding) {
76
+ Encoding["Base64"] = "base64";
77
+ Encoding["Csv"] = "csv";
78
+ return Encoding;
79
+ }({});
164
80
  //#endregion
165
81
  //#region src/services/getDecompressedBytes.ts
166
82
  const getDecompressedBytes = async (bytes, compression) => {
@@ -172,21 +88,45 @@ const getDecompressedBytes = async (bytes, compression) => {
172
88
  //#region src/services/unpackTileBytes.ts
173
89
  const unpackTileBytes = (bytes) => {
174
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`);
175
- const unpackedTiles = [];
176
91
  const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
177
- for (let byteIndex = 0; byteIndex < bytes.byteLength; byteIndex += Uint32Array.BYTES_PER_ELEMENT) unpackedTiles.push(dataView.getUint32(byteIndex, true));
178
- return unpackedTiles;
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];
179
95
  };
180
96
  //#endregion
181
97
  //#region src/services/decodeTileData.ts
182
- const decodeTileData = async (nodeData) => {
183
- if (checkIsTMXEmbeddedTilesetNode(nodeData)) return nodeData.tile?.map(({ $ }) => $.gid ?? 0) ?? [];
184
- const { $, _ } = nodeData;
185
- const { compression, encoding } = $;
186
- const layerData = normalizeString(_);
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);
187
127
  switch (encoding) {
188
128
  case "base64": {
189
- const bytes = Uint8Array.fromBase64(layerData);
129
+ const bytes = Uint8Array.fromBase64(data);
190
130
  switch (compression) {
191
131
  case "gzip":
192
132
  case "zlib": return unpackTileBytes(await getDecompressedBytes(bytes, compression));
@@ -194,124 +134,628 @@ const decodeTileData = async (nodeData) => {
194
134
  default: return exhaustiveGuard(compression);
195
135
  }
196
136
  }
197
- case "csv": return layerData.split(",").map(Number);
137
+ case "csv": return decodeCsv(data);
198
138
  default: return exhaustiveGuard(encoding);
199
139
  }
200
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
+ };
201
213
  //#endregion
202
- //#region src/services/parseTileLayer.ts
203
- const parseTileLayer = async (node, tileCount, translateFlips) => {
204
- const { data, properties } = node;
205
- if (!data) throw new InvalidOperationError(Operation.Read, "TMXLayer", "data is missing");
206
- const layer = cloneNodeWithType(node);
207
- if (properties) layer.properties = parseProperties(properties);
208
- const nodeData = takeOne(data);
209
- const tiles = await decodeTileData(nodeData);
210
- if (tiles.length !== tileCount) throw new InvalidOperationError(Operation.Read, "TMXLayer", `expected ${tileCount} tiles, received ${tiles.length}`);
211
- if (translateFlips) {
212
- layer.data = tiles.map((gid) => parseTileId(gid));
213
- layer.flips = tiles.map((gid) => parseFlips(gid));
214
- } else layer.data = tiles;
215
- return layer;
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;
216
230
  };
217
231
  //#endregion
218
- //#region src/services/parseNode.ts
219
- const parseGroup = async (node, tileCount, translateFlips) => {
220
- const group = cloneNodeWithType(node);
221
- group.layers = await Promise.all(node.$$.map((layerNode) => parseNode(layerNode, tileCount, translateFlips)));
222
- return group;
223
- };
224
- const parseNode = (node, tileCount, translateFlips) => {
225
- const tmxNodeType = node["#name"];
226
- switch (tmxNodeType) {
227
- case "group": return parseGroup(node, tileCount, translateFlips);
228
- case "imagelayer":
229
- case "objectgroup": return Promise.resolve(parseLayer(node));
230
- case "layer": return parseTileLayer(node, tileCount, translateFlips);
231
- default: throw new InvalidOperationError(Operation.Read, parseNode.name, tmxNodeType);
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
+ };
232
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
+ };
233
289
  };
234
290
  //#endregion
235
- //#region src/services/checkIsExternalTileset.ts
236
- const checkIsExternalTileset = (tileset) => "source" in tileset;
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
+ };
237
300
  //#endregion
238
- //#region src/services/parseTile.ts
239
- const parseTile = (node) => {
240
- const { $, animation, objectgroup, properties } = node;
241
- const tile = structuredClone($);
242
- if (animation) tile.animation = { frames: takeOne(takeOne(animation), "frame").map(({ $: frameData }) => structuredClone(frameData)) };
243
- if (objectgroup) tile.objects = takeOne(takeOne(objectgroup), "object").map((objectNode) => parseObject(objectNode));
244
- if (properties) tile.properties = parseProperties(properties);
245
- return tile;
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}`);
246
307
  };
247
308
  //#endregion
248
- //#region src/services/parseTileset.ts
249
- const parseTileset = (node) => {
250
- if (checkIsExternalTileset(node.$)) return structuredClone(node.$);
251
- const { $, $$, tile } = node;
252
- const imageNode = $$.find((childNode) => childNode["#name"] === "image");
253
- if (!imageNode) throw new InvalidOperationError(Operation.Read, parseTileset.name, $.name);
254
- const image = structuredClone(imageNode.$);
255
- const tiles = tile?.map((tileNode) => parseTile(tileNode)) ?? [];
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
+ });
256
333
  return {
257
- ...$,
258
- image,
259
- tiles
334
+ owner: layer,
335
+ tag: "data",
336
+ target: source,
337
+ text: ""
260
338
  };
261
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
+ };
262
390
  //#endregion
263
- //#region src/services/parseXmlValue.ts
264
- const NUMBER_REGEX = /^[+-]?\d+(?<decimal>\.\d+)?$/u;
265
- const parseXmlValue = (value) => {
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) => {
266
427
  if (value === "true") return true;
267
428
  else if (value === "false") return false;
268
429
  else if (NUMBER_REGEX.test(value)) return Number(value);
269
430
  else return value;
270
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
+ };
271
479
  //#endregion
272
- //#region src/services/parseXmlString.ts
273
- const parseXmlString = (xmlString) => parseStringPromise(xmlString, {
274
- attrValueProcessors: [(value) => parseXmlValue(value)],
275
- explicitChildren: true,
276
- preserveChildrenOrder: true
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: []
277
533
  });
278
- //#endregion
279
- //#region src/parseTmx.ts
280
- const parseTmx = async (xmlString, translateFlips = false) => {
281
- const { map: { $, $$ } } = await parseXmlString(xmlString);
282
- const map = new TMXMapParsed($);
283
- const tileCount = map.width * map.height;
284
- for (const node of $$) {
285
- const tmxNodeType = node["#name"];
286
- switch (tmxNodeType) {
287
- case "data": break;
288
- case "editorsettings":
289
- if (!node.$$) break;
290
- map.editorsettings = Object.fromEntries(node.$$.map((exportNode) => [exportNode["#name"], exportNode.$]));
291
- break;
292
- case "export":
293
- case "image":
294
- case "object":
295
- case "property": break;
296
- case "group":
297
- case "imagelayer":
298
- case "layer":
299
- case "objectgroup": {
300
- const layer = await parseNode(node, tileCount, translateFlips);
301
- map.layers.push(layer);
302
- break;
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;
303
584
  }
304
- case "properties":
305
- if (!node.$$) break;
306
- map.properties = parseProperties([{ property: node.$$ }]);
307
- break;
308
- case "tileset":
309
- map.tilesets.push(parseTileset(node));
310
- break;
311
- default: exhaustiveGuard(tmxNodeType);
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
+ };
312
616
  }
617
+ case "objectgroup": return {
618
+ tag,
619
+ target: tile,
620
+ text: ""
621
+ };
622
+ case "properties": return openTmxProperties(tile);
623
+ default: return;
313
624
  }
314
- return new TMXParsed(map);
315
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
+ }));
316
760
  //#endregion
317
- export { BaseTMXNode, Compression, Encoding, Flipped, Flippeds, Shape, TMXMapParsed, TMXMapShared, TMXNodeType, TMXParsed, checkIsExternalTileset, checkIsTMXEmbeddedTilesetNode, cloneNodeWithType, decodeTileData, getDecompressedBytes, parseFlips, parseLayer, parseNode, parseObject, parseObjectShape, parseProperties, parseTile, parseTileId, parseTileLayer, parseTileset, parseTmx, parseXmlString, parseXmlValue, unpackTileBytes };
761
+ export { Shape, TMXMapParsed, TMXParsed, parseTileset, parseTmx };