extwee 2.2.0 → 2.2.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.
Files changed (42) hide show
  1. package/build/extwee +0 -0
  2. package/build/extwee.web.min.js +1 -1
  3. package/docs/_sidebar.md +1 -0
  4. package/docs/examples/dynamicPassages.md +1 -1
  5. package/docs/examples/twsToTwee.md +1 -1
  6. package/index.js +1 -1
  7. package/package.json +15 -12
  8. package/src/JSON/parse.js +1 -1
  9. package/src/Passage.js +9 -28
  10. package/src/Story.js +9 -6
  11. package/src/TWS/parse.js +1 -1
  12. package/src/Twee/parse.js +1 -1
  13. package/src/Twine1HTML/compile.js +1 -1
  14. package/src/Twine1HTML/parse.js +1 -1
  15. package/src/Twine2ArchiveHTML/compile.js +1 -1
  16. package/src/Twine2HTML/compile.js +1 -1
  17. package/src/Twine2HTML/parse.js +5 -4
  18. package/test/JSON/JSON.Parse.test.js +20 -20
  19. package/test/Passage.test.js +69 -0
  20. package/test/Story.test.js +42 -3
  21. package/test/Twine1HTML/Twine1HTML.Compile.test.js +1 -1
  22. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Compile.test.js +1 -1
  23. package/test/Twine2HTML/Twine2HTML.Compile.test.js +1 -1
  24. package/test/Twine2HTML/Twine2HTML.Parse.test.js +6 -5
  25. package/test/Twine2HTML/Twine2HTMLParser/unescaping.html +33 -0
  26. package/tsconfig.json +21 -0
  27. package/types/index.d.ts +14 -0
  28. package/types/src/JSON/parse.d.ts +8 -0
  29. package/types/src/Passage.d.ts +72 -0
  30. package/types/src/Story.d.ts +161 -0
  31. package/types/src/StoryFormat/parse.d.ts +7 -0
  32. package/types/src/StoryFormat.d.ts +97 -0
  33. package/types/src/TWS/parse.d.ts +10 -0
  34. package/types/src/Twee/parse.d.ts +10 -0
  35. package/types/src/Twine1HTML/compile.d.ts +17 -0
  36. package/types/src/Twine1HTML/parse.d.ts +10 -0
  37. package/types/src/Twine2ArchiveHTML/compile.d.ts +6 -0
  38. package/types/src/Twine2ArchiveHTML/parse.d.ts +6 -0
  39. package/types/src/Twine2HTML/compile.d.ts +9 -0
  40. package/types/src/Twine2HTML/parse.d.ts +17 -0
  41. package/types/src/extwee.d.ts +2 -0
  42. package/web-index.js +1 -1
package/docs/_sidebar.md CHANGED
@@ -17,3 +17,4 @@
17
17
  - Examples
18
18
  - [Dynamically Generating Passages](/examples/dynamicPassages.md)
19
19
  - [Converting Twine 2 JSON into Twee 3](/examples/jsonToTwee.md)
20
+ - [Converting Twine 1 TWS into Twee 3](/examples/twsToTwee.md)
@@ -16,7 +16,7 @@ import { writeFileSync } from 'node:fs';
16
16
  const example = new Story( 'Example' );
17
17
 
18
18
  // Generate 20 passages and add them to the story.
19
- for(let i = 0; i < 20; i++;) {
19
+ for(let i = 0; i < 20; i++) {
20
20
  example.addPassage( new Passage( `Passage ${i}`, 'Some Text') );
21
21
  }
22
22
 
@@ -1,6 +1,6 @@
1
1
  # TWS To Twee
2
2
 
3
- Converting from TWS to Twee 3 is similar to many other conversion processes:
3
+ Converting from TWS to Twee 3 is similar to many other conversion processes with one small difference. TWS conversion needs to be begin from the [**Buffer** data type in JavaScript](https://nodejs.org/api/buffer.html).
4
4
 
5
5
  1. Read the binary file.
6
6
  2. Convert the binary files into a Buffer.
package/index.js CHANGED
@@ -8,7 +8,7 @@ import { parse as parseTWS } from './src/TWS/parse.js';
8
8
  import { compile as compileTwine1HTML } from './src/Twine1HTML/compile.js';
9
9
  import { compile as compileTwine2HTML } from './src/Twine2HTML/compile.js';
10
10
  import { compile as compileTwine2ArchiveHTML } from './src/Twine2ArchiveHTML/compile.js';
11
- import Story from './src/Story.js';
11
+ import { Story } from './src/Story.js';
12
12
  import Passage from './src/Passage.js';
13
13
  import StoryFormat from './src/StoryFormat.js';
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
@@ -24,33 +24,35 @@
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
26
  "commander": "^11.1.0",
27
- "node-html-parser": "^6.1.11",
27
+ "html-entities": "^2.4.0",
28
+ "node-html-parser": "^6.1.12",
28
29
  "pickleparser": "^0.2.1",
29
- "semver": "^7.3.7",
30
+ "semver": "^7.5.4",
30
31
  "uuid": "^9.0.1"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@babel/cli": "^7.23.4",
34
- "@babel/core": "^7.23.6",
35
+ "@babel/core": "^7.23.7",
35
36
  "@babel/eslint-parser": "^7.23.3",
36
37
  "@babel/eslint-plugin": "^7.23.5",
37
- "@babel/preset-env": "^7.23.6",
38
+ "@babel/preset-env": "^7.23.8",
38
39
  "babel-loader": "^9.1.3",
39
40
  "clean-jsdoc-theme": "^4.2.17",
40
- "core-js": "^3.34.0",
41
+ "core-js": "^3.35.0",
41
42
  "docsify-cli": "^4.4.4",
42
- "esbuild": "0.19.10",
43
+ "esbuild": "0.19.11",
43
44
  "eslint": "^8.56.0",
44
45
  "eslint-config-standard": "^17.1.0",
45
46
  "eslint-plugin-import": "^2.29.1",
46
- "eslint-plugin-jest": "^27.6.0",
47
- "eslint-plugin-jsdoc": "^46.9.1",
48
- "eslint-plugin-node": "^11.0.0",
47
+ "eslint-plugin-jest": "^27.6.1",
48
+ "eslint-plugin-jsdoc": "^48.0.2",
49
+ "eslint-plugin-node": "^11.1.0",
49
50
  "eslint-plugin-promise": "^6.1.1",
50
51
  "jest": "^29.7.0",
51
52
  "pkg": "^5.8.1",
52
53
  "regenerator-runtime": "^0.14.1",
53
- "shelljs": "^0.8.4",
54
+ "shelljs": "^0.8.5",
55
+ "typescript": "^5.3.3",
54
56
  "webpack": "^5.89.0",
55
57
  "webpack-cli": "^5.1.4"
56
58
  },
@@ -61,5 +63,6 @@
61
63
  "bugs": {
62
64
  "url": "https://github.com/videlais/extwee/issues"
63
65
  },
64
- "type": "module"
66
+ "type": "module",
67
+ "types": "./types/index.d.ts"
65
68
  }
package/src/JSON/parse.js CHANGED
@@ -1,4 +1,4 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
  import Passage from '../Passage.js';
3
3
 
4
4
  /**
package/src/Passage.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { encode } from 'html-entities';
2
+
1
3
  /**
2
4
  * A passage is the smallest unit of a Twine story.
3
5
  */
@@ -76,6 +78,7 @@ export default class Passage {
76
78
  set tags (t) {
77
79
  // Test if tags is an array
78
80
  if (Array.isArray(t)) {
81
+ // Set the tags.
79
82
  this.#_tags = t;
80
83
  } else {
81
84
  throw new Error('Tags must be an array!');
@@ -185,13 +188,13 @@ export default class Passage {
185
188
  * name: (string) Required.
186
189
  * The name of the passage.
187
190
  */
188
- passageData += ` name="${this.name}"`;
191
+ passageData += ` name="${ encode( this.name ) }"`;
189
192
 
190
193
  /**
191
194
  * tags: (string) Optional.
192
195
  * Any tags for the passage separated by spaces.
193
196
  */
194
- passageData += ` tags="${this.#_tags.join(' ')}" `;
197
+ passageData += ` tags="${ encode( this.#_tags.join(' ') ) }" `;
195
198
 
196
199
  /**
197
200
  * position: (string) Optional.
@@ -211,30 +214,8 @@ export default class Passage {
211
214
  passageData += `size="${this.#_metadata.size}" `;
212
215
  }
213
216
 
214
- /**
215
- * Escape passage Twine 2 passage text.
216
- * @param {string} text - Text to escape.
217
- * @returns {string} Escaped text.
218
- */
219
- const escape = function (text) {
220
- const rules = [
221
- ['&', '&amp;'],
222
- ['<', '&lt;'],
223
- ['>', '&gt;'],
224
- ['"', '&quot;'],
225
- ["'", '&#x27;'],
226
- ['`', '&#x60;']
227
- ];
228
-
229
- rules.forEach(([rule, template]) => {
230
- text = text.replaceAll(rule, template);
231
- });
232
-
233
- return text;
234
- };
235
-
236
217
  // Add the text and close the element.
237
- passageData += `>${escape(this.text)}</tw-passagedata>\n`;
218
+ passageData += `>${ encode( this.text ) }</tw-passagedata>\n`;
238
219
 
239
220
  // Return the Twine 2 HTML element.
240
221
  return passageData;
@@ -258,13 +239,13 @@ export default class Passage {
258
239
  * tiddler: (string) Required.
259
240
  * The name of the passage.
260
241
  */
261
- passageData += ` tiddler="${this.name}"`;
242
+ passageData += ` tiddler="${ encode( this.name ) }"`;
262
243
 
263
244
  /**
264
245
  * tags: (string) Required.
265
246
  * Any tags for the passage separated by spaces.
266
247
  */
267
- passageData += ` tags="${this.#_tags.join(' ')}" `;
248
+ passageData += ` tags="${ encode( this.#_tags.join(' ') ) }" `;
268
249
 
269
250
  /**
270
251
  * modifier: (string) Optional.
@@ -290,7 +271,7 @@ export default class Passage {
290
271
  * text: (string) Required.
291
272
  * Text content of the passage.
292
273
  */
293
- passageData += `>${this.#_text}</div>`;
274
+ passageData += `>${ encode( this.#_text ) }</div>`;
294
275
 
295
276
  // Return the HTML representation.
296
277
  return passageData;
package/src/Story.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import Passage from './Passage.js';
2
2
  import { v4 as uuidv4 } from 'uuid';
3
+ import { encode } from 'html-entities';
3
4
 
4
5
  const creatorName = 'extwee';
5
- const version = '2.2.0';
6
+ const creatorVersion = '2.2.1';
6
7
 
7
- export default class Story {
8
+ class Story {
8
9
  /**
9
10
  * Internal name of story
10
11
  * @private
@@ -78,7 +79,7 @@ export default class Story {
78
79
  this.name = name;
79
80
  // Store the creator.
80
81
  this.#_creator = creatorName;
81
- this.#_creatorVersion = version;
82
+ this.#_creatorVersion = creatorVersion;
82
83
  // Set metadata to an object.
83
84
  this.#_metadata = {};
84
85
  }
@@ -516,7 +517,7 @@ export default class Story {
516
517
  */
517
518
  toTwine2HTML () {
518
519
  // Prepare HTML content.
519
- let storyData = `<tw-storydata name="${this.name}"`;
520
+ let storyData = `<tw-storydata name="${ encode( this.name ) }"`;
520
521
  // Passage Identification (PID) counter.
521
522
  // (Twine 2 starts with 1, so we mirror that.)
522
523
  let PIDcounter = 1;
@@ -552,7 +553,7 @@ export default class Story {
552
553
  storyData += ` startnode="${startPID}"`;
553
554
 
554
555
  // Defaults to 'extwee' if missing.
555
- storyData += ` creator="${this.creator}"`;
556
+ storyData += ` creator="${ encode( this.creator ) }"`;
556
557
 
557
558
  // Default to extwee version.
558
559
  storyData += ` creator-version="${this.creatorVersion}"`;
@@ -571,7 +572,7 @@ export default class Story {
571
572
  storyData += ` zoom="${this.zoom}"`;
572
573
 
573
574
  // Write existing or default value.
574
- storyData += ` format="${this.#_format}"`;
575
+ storyData += ` format="${ encode(this.#_format) }"`;
575
576
 
576
577
  // Write existing or default value.
577
578
  storyData += ` format-version="${this.#_formatVersion}"`;
@@ -648,3 +649,5 @@ export default class Story {
648
649
  return outputContents;
649
650
  }
650
651
  }
652
+
653
+ export { Story, creatorName, creatorVersion };
package/src/TWS/parse.js CHANGED
@@ -1,4 +1,4 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
  import Passage from '../Passage.js';
3
3
  import { Parser } from 'pickleparser';
4
4
 
package/src/Twee/parse.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Passage from '../Passage.js';
2
- import Story from '../Story.js';
2
+ import { Story } from '../Story.js';
3
3
 
4
4
  /**
5
5
  * Parses Twee 3 text into a Story object.
@@ -1,4 +1,4 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
 
3
3
  /**
4
4
  * Write a combination of Story object, `engine.js` (from Twine 1), `header.html`, and optional `code.js`.
@@ -1,6 +1,6 @@
1
1
  import { parse as HtmlParser } from 'node-html-parser';
2
2
  import Passage from '../Passage.js';
3
- import Story from '../Story.js';
3
+ import { Story } from '../Story.js';
4
4
 
5
5
  /**
6
6
  * Parses Twine 1 HTML into a Story object.
@@ -1,4 +1,4 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
 
3
3
  /**
4
4
  * Write array of Story objects into Twine 2 Archive HTML.
@@ -1,4 +1,4 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
  import StoryFormat from '../StoryFormat.js';
3
3
 
4
4
  /**
@@ -1,6 +1,7 @@
1
1
  import { parse as HtmlParser } from 'node-html-parser';
2
- import Story from '../Story.js';
2
+ import { Story } from '../Story.js';
3
3
  import Passage from '../Passage.js';
4
+ import { decode } from 'html-entities';
4
5
 
5
6
  /**
6
7
  * Parse Twine 2 HTML into Story object.
@@ -244,9 +245,9 @@ function parse (content) {
244
245
  // Add a new Passage into an array
245
246
  story.addPassage(
246
247
  new Passage(
247
- name,
248
- text,
249
- tags,
248
+ decode(name),
249
+ decode(text),
250
+ tags.map(tag => decode(tag)),
250
251
  metadata
251
252
  )
252
253
  );
@@ -1,4 +1,4 @@
1
- import Story from '../../src/Story.js';
1
+ import { Story, creatorVersion, creatorName } from '../../src/Story.js';
2
2
  import Passage from '../../src/Passage.js';
3
3
  import { parse as parseJSON } from '../../src/JSON/parse.js';
4
4
 
@@ -22,8 +22,8 @@ describe('JSON', () => {
22
22
  expect(s.start).toBe('');
23
23
  expect(s.formatVersion).toBe('');
24
24
  expect(s.format).toBe('');
25
- expect(s.creator).toBe('extwee');
26
- expect(s.creatorVersion).toBe('2.2.0');
25
+ expect(s.creator).toBe(creatorName);
26
+ expect(s.creatorVersion).toBe(creatorVersion);
27
27
  expect(s.zoom).toBe(0);
28
28
  expect(Object.keys(s.metadata).length).toBe(0);
29
29
  });
@@ -59,7 +59,7 @@ describe('JSON', () => {
59
59
  expect(r.formatVersion).toBe('1.0');
60
60
  expect(Object.keys(r.metadata).length).toBe(1);
61
61
  expect(r.format).toBe('Snowman');
62
- expect(r.creator).toBe('extwee');
62
+ expect(r.creator).toBe(creatorName);
63
63
  expect(r.creatorVersion).toBe('2.2.0');
64
64
  expect(r.zoom).toBe(1);
65
65
  expect(r.size()).toBe(1);
@@ -75,7 +75,7 @@ describe('JSON', () => {
75
75
  expect(r.formatVersion).toBe('1.0');
76
76
  expect(Object.keys(r.metadata).length).toBe(1);
77
77
  expect(r.format).toBe('Snowman');
78
- expect(r.creator).toBe('extwee');
78
+ expect(r.creator).toBe(creatorName);
79
79
  expect(r.creatorVersion).toBe('2.2.0');
80
80
  expect(r.zoom).toBe(1);
81
81
  expect(r.size()).toBe(1);
@@ -91,7 +91,7 @@ describe('JSON', () => {
91
91
  expect(r.formatVersion).toBe('1.0');
92
92
  expect(Object.keys(r.metadata).length).toBe(1);
93
93
  expect(r.format).toBe('Snowman');
94
- expect(r.creator).toBe('extwee');
94
+ expect(r.creator).toBe(creatorName);
95
95
  expect(r.creatorVersion).toBe('2.2.0');
96
96
  expect(r.zoom).toBe(1);
97
97
  expect(r.size()).toBe(1);
@@ -107,7 +107,7 @@ describe('JSON', () => {
107
107
  expect(r.formatVersion).toBe('1.0');
108
108
  expect(Object.keys(r.metadata).length).toBe(1);
109
109
  expect(r.format).toBe('Snowman');
110
- expect(r.creator).toBe('extwee');
110
+ expect(r.creator).toBe(creatorName);
111
111
  expect(r.creatorVersion).toBe('2.2.0');
112
112
  expect(r.zoom).toBe(1);
113
113
  expect(r.size()).toBe(1);
@@ -123,7 +123,7 @@ describe('JSON', () => {
123
123
  expect(r.formatVersion).toBe('');
124
124
  expect(Object.keys(r.metadata).length).toBe(1);
125
125
  expect(r.format).toBe('Snowman');
126
- expect(r.creator).toBe('extwee');
126
+ expect(r.creator).toBe(creatorName);
127
127
  expect(r.creatorVersion).toBe('2.2.0');
128
128
  expect(r.zoom).toBe(1);
129
129
  expect(r.size()).toBe(1);
@@ -139,7 +139,7 @@ describe('JSON', () => {
139
139
  expect(r.formatVersion).toBe('1.0');
140
140
  expect(Object.keys(r.metadata).length).toBe(1);
141
141
  expect(r.format).toBe('');
142
- expect(r.creator).toBe('extwee');
142
+ expect(r.creator).toBe(creatorName);
143
143
  expect(r.creatorVersion).toBe('2.2.0');
144
144
  expect(r.zoom).toBe(1);
145
145
  expect(r.size()).toBe(1);
@@ -155,7 +155,7 @@ describe('JSON', () => {
155
155
  expect(r.formatVersion).toBe('1.0');
156
156
  expect(Object.keys(r.metadata).length).toBe(1);
157
157
  expect(r.format).toBe('Snowman');
158
- expect(r.creator).toBe('extwee');
158
+ expect(r.creator).toBe(creatorName);
159
159
  expect(r.creatorVersion).toBe('2.2.0');
160
160
  expect(r.zoom).toBe(1);
161
161
  expect(r.size()).toBe(1);
@@ -171,8 +171,8 @@ describe('JSON', () => {
171
171
  expect(r.formatVersion).toBe('1.0');
172
172
  expect(Object.keys(r.metadata).length).toBe(1);
173
173
  expect(r.format).toBe('Snowman');
174
- expect(r.creator).toBe('extwee');
175
- expect(r.creatorVersion).toBe('2.2.0');
174
+ expect(r.creator).toBe(creatorName);
175
+ expect(r.creatorVersion).toBe(creatorVersion);
176
176
  expect(r.zoom).toBe(1);
177
177
  expect(r.size()).toBe(1);
178
178
  });
@@ -187,7 +187,7 @@ describe('JSON', () => {
187
187
  expect(r.formatVersion).toBe('1.0');
188
188
  expect(Object.keys(r.metadata).length).toBe(1);
189
189
  expect(r.format).toBe('Snowman');
190
- expect(r.creator).toBe('extwee');
190
+ expect(r.creator).toBe(creatorName);
191
191
  expect(r.creatorVersion).toBe('2.2.0');
192
192
  expect(r.zoom).toBe(0);
193
193
  expect(r.size()).toBe(1);
@@ -203,7 +203,7 @@ describe('JSON', () => {
203
203
  expect(r.formatVersion).toBe('1.0');
204
204
  expect(Object.keys(r.metadata).length).toBe(0);
205
205
  expect(r.format).toBe('Snowman');
206
- expect(r.creator).toBe('extwee');
206
+ expect(r.creator).toBe(creatorName);
207
207
  expect(r.creatorVersion).toBe('2.2.0');
208
208
  expect(r.zoom).toBe(1);
209
209
  expect(r.size()).toBe(1);
@@ -219,7 +219,7 @@ describe('JSON', () => {
219
219
  expect(r.formatVersion).toBe('1.0');
220
220
  expect(Object.keys(r.metadata).length).toBe(1);
221
221
  expect(r.format).toBe('Snowman');
222
- expect(r.creator).toBe('extwee');
222
+ expect(r.creator).toBe(creatorName);
223
223
  expect(r.creatorVersion).toBe('2.2.0');
224
224
  expect(r.zoom).toBe(1);
225
225
  expect(r.size()).toBe(0);
@@ -235,7 +235,7 @@ describe('JSON', () => {
235
235
  expect(r.formatVersion).toBe('1.0');
236
236
  expect(Object.keys(r.metadata).length).toBe(1);
237
237
  expect(r.format).toBe('Snowman');
238
- expect(r.creator).toBe('extwee');
238
+ expect(r.creator).toBe(creatorName);
239
239
  expect(r.creatorVersion).toBe('2.2.0');
240
240
  expect(r.zoom).toBe(1);
241
241
  expect(r.size()).toBe(0);
@@ -251,7 +251,7 @@ describe('JSON', () => {
251
251
  expect(r.formatVersion).toBe('1.0');
252
252
  expect(Object.keys(r.metadata).length).toBe(1);
253
253
  expect(r.format).toBe('Snowman');
254
- expect(r.creator).toBe('extwee');
254
+ expect(r.creator).toBe(creatorName);
255
255
  expect(r.creatorVersion).toBe('2.2.0');
256
256
  expect(r.zoom).toBe(1);
257
257
  expect(r.size()).toBe(1);
@@ -268,7 +268,7 @@ describe('JSON', () => {
268
268
  expect(r.formatVersion).toBe('1.0');
269
269
  expect(Object.keys(r.metadata).length).toBe(1);
270
270
  expect(r.format).toBe('Snowman');
271
- expect(r.creator).toBe('extwee');
271
+ expect(r.creator).toBe(creatorName);
272
272
  expect(r.creatorVersion).toBe('2.2.0');
273
273
  expect(r.zoom).toBe(1);
274
274
  expect(r.size()).toBe(1);
@@ -286,7 +286,7 @@ describe('JSON', () => {
286
286
  expect(r.formatVersion).toBe('1.0');
287
287
  expect(Object.keys(r.metadata).length).toBe(1);
288
288
  expect(r.format).toBe('Snowman');
289
- expect(r.creator).toBe('extwee');
289
+ expect(r.creator).toBe(creatorName);
290
290
  expect(r.creatorVersion).toBe('2.2.0');
291
291
  expect(r.zoom).toBe(1);
292
292
  expect(r.size()).toBe(1);
@@ -304,7 +304,7 @@ describe('JSON', () => {
304
304
  expect(r.formatVersion).toBe('1.0');
305
305
  expect(Object.keys(r.metadata).length).toBe(1);
306
306
  expect(r.format).toBe('Snowman');
307
- expect(r.creator).toBe('extwee');
307
+ expect(r.creator).toBe(creatorName);
308
308
  expect(r.creatorVersion).toBe('2.2.0');
309
309
  expect(r.zoom).toBe(1);
310
310
  expect(r.size()).toBe(1);
@@ -1,4 +1,5 @@
1
1
  import Passage from '../src/Passage.js';
2
+ import { parse as HTMLParser } from 'node-html-parser';
2
3
 
3
4
  describe('Passage', () => {
4
5
  describe('constructor()', () => {
@@ -172,4 +173,72 @@ describe('Passage', () => {
172
173
  expect(result.includes('position="10,10"')).toBe(true);
173
174
  });
174
175
  });
176
+
177
+ describe('Escaping', function () {
178
+ it('Should escape double quotes', function () {
179
+ const p = new Passage('Test', 'Word "word"');
180
+ expect(p.toTwine2HTML().includes('Word &quot;word&quot;')).toBe(true);
181
+ });
182
+
183
+ it('Should escape ampersands', function () {
184
+ const p = new Passage('Test', 'Word & word');
185
+ expect(p.toTwine2HTML().includes('Word &amp; word')).toBe(true);
186
+ });
187
+
188
+ it('Should escape less than', function () {
189
+ const p = new Passage('Test', 'Word < word');
190
+ expect(p.toTwine2HTML().includes('Word &lt; word')).toBe(true);
191
+ });
192
+
193
+ it('Should escape greater than', function () {
194
+ const p = new Passage('Test', 'Word > word');
195
+ expect(p.toTwine2HTML().includes('Word &gt; word')).toBe(true);
196
+ });
197
+
198
+ it('Should escape all', function () {
199
+ const p = new Passage('Test', 'Word &<>"\' word');
200
+ expect(p.toTwine2HTML().includes('>Word &amp;&lt;&gt;&quot;&apos; word<')).toBe(true);
201
+ });
202
+
203
+ it('Should escape meta-characters safely in name', function () {
204
+ const p = new Passage('"Test"');
205
+ expect(p.toTwine2HTML().includes('name="&quot;Test&quot;"')).toBe(true);
206
+ expect(p.toTwine1HTML().includes('tiddler="&quot;Test&quot;"')).toBe(true);
207
+ });
208
+
209
+ it('Should escape meta-characters safely in text', function () {
210
+ const p = new Passage('Test', '"Word"');
211
+ expect(p.toTwine2HTML().includes('&quot;Word&quot;')).toBe(true);
212
+ });
213
+
214
+ it('Should escape meta-characters safely in tags', function () {
215
+ const p = new Passage('Test', 'Word', ['&tag', '"bad"']);
216
+ expect(p.toTwine2HTML().includes('tags="&amp;tag &quot;bad&quot;"')).toBe(true);
217
+ expect(p.toTwine1HTML().includes('tags="&amp;tag &quot;bad&quot;"')).toBe(true);
218
+ });
219
+
220
+ it('Should escape meta-characters safely in Twee header', function () {
221
+ const p = new Passage('Where do tags begin? [well', '', ['hmm']);
222
+ expect(p.toTwee().includes('Where do tags begin? \[well [hmm]')).toBe(true);
223
+ });
224
+
225
+ it('Should produce valid HTML attributes', function () {
226
+ // Generate passage.
227
+ const p = new Passage('"Test"', '"Word"', ['&tag', '"bad"'], { position: '100,100' });
228
+ // Parse HTML.
229
+ const d = new HTMLParser(p.toTwine2HTML());
230
+ // Test attributes.
231
+ expect(d.querySelector('tw-passagedata').getAttribute('name')).toBe('"Test"');
232
+ expect(d.querySelector('tw-passagedata').getAttribute('tags')).toBe('&tag "bad"');
233
+ expect(d.querySelector('tw-passagedata').getAttribute('position')).toBe('100,100');
234
+ // Use Twine 2 result.
235
+ const s = `<tw-passagedata pid="1" name="&quot;Test&quot;" tags="&amp;tag &quot;bad&quot;" position="100,100" size="100,100"></tw-passagedata>`;
236
+ // Parse HTML.
237
+ const t = new HTMLParser(s);
238
+ // Test Twine 2 attributes.
239
+ expect(t.querySelector('tw-passagedata').getAttribute('name')).toBe('"Test"');
240
+ expect(t.querySelector('tw-passagedata').getAttribute('tags')).toBe('&tag "bad"');
241
+ expect(t.querySelector('tw-passagedata').getAttribute('position')).toBe('100,100');
242
+ });
243
+ });
175
244
  });
@@ -1,7 +1,8 @@
1
- import Story from '../src/Story.js';
1
+ import { Story, creatorName, creatorVersion } from '../src/Story.js';
2
2
  import Passage from '../src/Passage';
3
3
  import { parse as parseTwee } from '../src/Twee/parse.js';
4
4
  import { readFileSync } from 'node:fs';
5
+ import { parse as HTMLParser } from 'node-html-parser';
5
6
 
6
7
  // Pull the name and version of this project from package.json.
7
8
  // These are used as the 'creator' and 'creator-version'.
@@ -405,8 +406,8 @@ describe('Story', () => {
405
406
  expect(result.start).toBe('');
406
407
  expect(result.formatVersion).toBe('');
407
408
  expect(result.format).toBe('');
408
- expect(result.creator).toBe('extwee');
409
- expect(result.creatorVersion).toBe('2.2.0');
409
+ expect(result.creator).toBe(creatorName);
410
+ expect(result.creatorVersion).toBe(creatorVersion);
410
411
  expect(result.zoom).toBe(0);
411
412
  expect(Object.keys(result.metadata).length).toBe(0);
412
413
  });
@@ -635,4 +636,42 @@ describe('Story', () => {
635
636
  expect(result.includes('<div tiddler="Start"')).toBe(true);
636
637
  });
637
638
  });
639
+
640
+ describe('Escaping', function () {
641
+ it('Should produce valid Twine 2 Story HTML', function () {
642
+ // Create a new Story.
643
+ const s = new Story('"Abuse" &test');
644
+ // Add a passage.
645
+ s.addPassage(new Passage('"Test"', 'Word'));
646
+ // Set start.
647
+ s.start = '"Test"';
648
+ // Parse HTML.
649
+ const root = HTMLParser(s.toTwine2HTML());
650
+ // Expect correct name attribute for tw-storydata.
651
+ expect(root.querySelector('tw-storydata').getAttribute('name')).toBe('"Abuse" &test');
652
+ // Expect correct name attribute for tw-passagedata.
653
+ expect(root.querySelector('tw-passagedata').getAttribute('name')).toBe('"Test"');
654
+ // Use Twine 2 result.
655
+ const s2 = `<tw-storydata name="&quot;Abuse&quot; &amp;test" startnode="1" creator="Twine" creator-version="2.8.1" format="Harlowe" format-version="3.3.8" ifid="452A9D80-C759-42C5-B001-5B861A2410C5" options="" tags="" zoom="1" hidden><style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css"></style><script role="script" id="twine-user-script" type="text/twine-javascript"></script><tw-passagedata pid="1" name="&quot;Test&quot;" tags="&amp;tag &quot;bad&quot;" position="300,100" size="100,100"></tw-passagedata></tw-storydata>`;
656
+ // Parse HTML.
657
+ const root2 = HTMLParser(s2);
658
+ // Expect correct name attribute for tw-storydata.
659
+ expect(root2.querySelector('tw-storydata').getAttribute('name')).toBe('"Abuse" &test');
660
+ // Expect correct name attribute for tw-passagedata.
661
+ expect(root2.querySelector('tw-passagedata').getAttribute('name')).toBe('"Test"');
662
+ });
663
+
664
+ it('Should produce valid Twine 1 Story HTML', function () {
665
+ // Create a new Story.
666
+ const s = new Story('"Abuse" &test');
667
+ // Add a passage.
668
+ s.addPassage(new Passage('"Test"', 'Word'));
669
+ // Set start.
670
+ s.start = '"Test"';
671
+ // Parse HTML.
672
+ const root = HTMLParser(s.toTwine1HTML());
673
+ // Expect correct name attribute for div.
674
+ expect(root.querySelector('div').getAttribute('tiddler')).toBe('"Test"');
675
+ });
676
+ });
638
677
  });
@@ -1,5 +1,5 @@
1
1
  import { compile as compileTwine1HTML } from '../../src/Twine1HTML/compile.js';
2
- import Story from '../../src/Story.js';
2
+ import { Story } from '../../src/Story.js';
3
3
  import { readFileSync } from 'fs';
4
4
  import Passage from '../../src/Passage.js';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import { compile as compileTwine2ArchiveHTML } from '../../src/Twine2ArchiveHTML/compile.js';
2
- import Story from '../../src/Story.js';
2
+ import { Story } from '../../src/Story.js';
3
3
  import Passage from '../../src/Passage.js';
4
4
 
5
5
  describe('Twine2ArchiveHTML', function () {
@@ -1,7 +1,7 @@
1
1
  import { parse as parseStoryFormat } from '../../src/StoryFormat/parse.js';
2
2
  import { parse as parseTwine2HTML } from '../../src/Twine2HTML/parse.js';
3
3
  import { compile as compileTwine2HTML } from '../../src/Twine2HTML/compile.js';
4
- import Story from '../../src/Story.js';
4
+ import { Story } from '../../src/Story.js';
5
5
  import Passage from '../../src/Passage.js';
6
6
  import { readFileSync } from 'node:fs';
7
7