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