extwee 2.2.0 → 2.2.2

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 (74) hide show
  1. package/.github/codeql-analysis.yml +51 -0
  2. package/README.md +9 -3
  3. package/build/extwee +0 -0
  4. package/build/extwee.web.min.js +1 -1
  5. package/docs/_sidebar.md +1 -0
  6. package/docs/examples/dynamicPassages.md +1 -1
  7. package/docs/examples/twsToTwee.md +1 -1
  8. package/docs/objects/story.md +1 -2
  9. package/index.js +3 -1
  10. package/package.json +22 -19
  11. package/src/IFID/generate.js +20 -0
  12. package/src/JSON/parse.js +44 -1
  13. package/src/Passage.js +61 -31
  14. package/src/Story.js +272 -110
  15. package/src/StoryFormat/parse.js +190 -80
  16. package/src/StoryFormat.js +78 -88
  17. package/src/TWS/parse.js +3 -3
  18. package/src/Twee/parse.js +3 -4
  19. package/src/Twine1HTML/compile.js +3 -1
  20. package/src/Twine1HTML/parse.js +3 -4
  21. package/src/Twine2ArchiveHTML/compile.js +9 -1
  22. package/src/Twine2ArchiveHTML/parse.js +33 -3
  23. package/src/Twine2HTML/compile.js +32 -7
  24. package/src/Twine2HTML/parse.js +51 -55
  25. package/test/IFID/IFID.Generate.test.js +10 -0
  26. package/test/JSON/JSON.Parse.test.js +24 -24
  27. package/test/Passage.test.js +69 -0
  28. package/test/Story.test.js +298 -49
  29. package/test/StoryFormat/StoryFormat.Parse.test.js +442 -55
  30. package/test/StoryFormat.test.js +9 -2
  31. package/test/TWS/Parse.test.js +1 -1
  32. package/test/Twine1HTML/Twine1HTML.Compile.test.js +1 -1
  33. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Compile.test.js +1 -1
  34. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +20 -4
  35. package/test/Twine2HTML/Twine2HTML.Compile.test.js +36 -121
  36. package/test/Twine2HTML/Twine2HTML.Parse.test.js +63 -43
  37. package/test/Twine2HTML/Twine2HTMLCompiler/format.js +9 -0
  38. package/test/Twine2HTML/Twine2HTMLParser/missingZoom.html +1 -1
  39. package/test/Twine2HTML/Twine2HTMLParser/unescaping.html +33 -0
  40. package/tsconfig.json +21 -0
  41. package/types/index.d.ts +14 -0
  42. package/types/src/JSON/parse.d.ts +8 -0
  43. package/types/src/Passage.d.ts +72 -0
  44. package/types/src/Story.d.ts +161 -0
  45. package/types/src/StoryFormat/parse.d.ts +7 -0
  46. package/types/src/StoryFormat.d.ts +97 -0
  47. package/types/src/TWS/parse.d.ts +10 -0
  48. package/types/src/Twee/parse.d.ts +10 -0
  49. package/types/src/Twine1HTML/compile.d.ts +17 -0
  50. package/types/src/Twine1HTML/parse.d.ts +10 -0
  51. package/types/src/Twine2ArchiveHTML/compile.d.ts +6 -0
  52. package/types/src/Twine2ArchiveHTML/parse.d.ts +6 -0
  53. package/types/src/Twine2HTML/compile.d.ts +9 -0
  54. package/types/src/Twine2HTML/parse.d.ts +17 -0
  55. package/types/src/extwee.d.ts +2 -0
  56. package/web-index.js +3 -1
  57. package/test/StoryFormat/StoryFormatParser/example.js +0 -3
  58. package/test/StoryFormat/StoryFormatParser/example2.js +0 -3
  59. package/test/StoryFormat/StoryFormatParser/format.js +0 -1
  60. package/test/StoryFormat/StoryFormatParser/format_doublename.js +0 -1
  61. package/test/StoryFormat/StoryFormatParser/harlowe.js +0 -3
  62. package/test/StoryFormat/StoryFormatParser/missingAuthor.js +0 -1
  63. package/test/StoryFormat/StoryFormatParser/missingDescription.js +0 -1
  64. package/test/StoryFormat/StoryFormatParser/missingImage.js +0 -1
  65. package/test/StoryFormat/StoryFormatParser/missingLicense.js +0 -1
  66. package/test/StoryFormat/StoryFormatParser/missingName.js +0 -1
  67. package/test/StoryFormat/StoryFormatParser/missingProofing.js +0 -1
  68. package/test/StoryFormat/StoryFormatParser/missingSource.js +0 -1
  69. package/test/StoryFormat/StoryFormatParser/missingURL.js +0 -1
  70. package/test/StoryFormat/StoryFormatParser/missingVersion.js +0 -1
  71. package/test/StoryFormat/StoryFormatParser/versionWrong.js +0 -1
  72. package/test/Twine2HTML/Twine2HTMLParser/missingName.html +0 -33
  73. package/test/Twine2HTML/Twine2HTMLParser/missingPID.html +0 -15
  74. package/test/Twine2HTML/Twine2HTMLParser/missingPassageName.html +0 -15
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.
@@ -12,7 +12,7 @@ Depending on the incoming format or creation method, many possible properties ca
12
12
  - format ( string ) Name of the story format for Twine 2 HTML.
13
13
  - formatVersion ( string ) Semantic version of the named story format for Twine 2 HTML or Twee 3.
14
14
  - zoom ( float ) Zoom level for Twine 2 HTML or Twee 3.
15
- - passages ( array(Passage) ) **[Read-only]** Collection of internal passages.
15
+ - passages ( array(Passage) ) Collection of internal passages.
16
16
  - creator ( string ) Name of story creation tool. (Defaults to "Extwee").
17
17
  - creatorVersion ( string ) Semantic version of named creation tool.
18
18
  - metadata ( object ) Key-value pairs of metadata values.
@@ -41,7 +41,6 @@ As collections of passages, each **Story** has multiple methods for accessing an
41
41
  - `getPassagesByTags(string)`: Returns an array of any passages containing a particular tag value.
42
42
  - `getPassageByName(string)`: Returns either `null`` or the named passage.
43
43
  - `size()`: Returns the number of passages in the collection.
44
- - `forEachPassage(callback)`: Allows for iterating over the passage collection.
45
44
 
46
45
  ## Passage Creation Example
47
46
 
package/index.js CHANGED
@@ -8,7 +8,8 @@ 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 { generate as generateIFID } from './src/IFID/generate.js';
12
+ import { Story } from './src/Story.js';
12
13
  import Passage from './src/Passage.js';
13
14
  import StoryFormat from './src/StoryFormat.js';
14
15
 
@@ -23,6 +24,7 @@ export {
23
24
  compileTwine1HTML,
24
25
  compileTwine2HTML,
25
26
  compileTwine2ArchiveHTML,
27
+ generateIFID,
26
28
  Story,
27
29
  Passage,
28
30
  StoryFormat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
@@ -8,7 +8,7 @@
8
8
  "extwee": "src/extwee.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "jest --silent --runInBand --coverage --colors",
11
+ "test": "jest --runInBand --coverage --colors",
12
12
  "lint": "eslint ./src/**/*.js --fix",
13
13
  "lint:test": "eslint ./test/**/*.test.js --fix",
14
14
  "build:web": "webpack",
@@ -23,43 +23,46 @@
23
23
  ],
24
24
  "license": "MIT",
25
25
  "dependencies": {
26
- "commander": "^11.1.0",
27
- "node-html-parser": "^6.1.11",
26
+ "commander": "^12.0.0",
27
+ "html-entities": "^2.5.2",
28
+ "node-html-parser": "^6.1.12",
28
29
  "pickleparser": "^0.2.1",
29
- "semver": "^7.3.7",
30
+ "semver": "^7.6.0",
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/eslint-parser": "^7.23.3",
35
+ "@babel/core": "^7.24.0",
36
+ "@babel/eslint-parser": "^7.23.10",
36
37
  "@babel/eslint-plugin": "^7.23.5",
37
- "@babel/preset-env": "^7.23.6",
38
+ "@babel/preset-env": "^7.24.0",
39
+ "@types/uuid": "^9.0.7",
38
40
  "babel-loader": "^9.1.3",
39
41
  "clean-jsdoc-theme": "^4.2.17",
40
- "core-js": "^3.34.0",
41
- "docsify-cli": "^4.4.4",
42
- "esbuild": "0.19.10",
43
- "eslint": "^8.56.0",
42
+ "core-js": "^3.36.0",
43
+ "esbuild": "^0.20.1",
44
+ "eslint": "^8.57.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.9.0",
48
+ "eslint-plugin-jsdoc": "^48.2.0",
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
- "webpack": "^5.89.0",
54
+ "shelljs": "^0.8.5",
55
+ "typescript": "^5.4.2",
56
+ "webpack": "^5.90.3",
55
57
  "webpack-cli": "^5.1.4"
56
58
  },
57
59
  "repository": {
58
60
  "type": "git",
59
- "url": "https://github.com/videlais/extwee"
61
+ "url": "git+https://github.com/videlais/extwee.git"
60
62
  },
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
  }
@@ -0,0 +1,20 @@
1
+ import { v4 } from 'uuid';
2
+
3
+ /**
4
+ * Generates an Interactive Fiction Identification (IFID) based the Treaty of Babel.
5
+ *
6
+ * For Twine works, the IFID is a UUID (v4) in uppercase.
7
+ * @see Treaty of Babel ({@link https://babel.ifarchive.org/babel_rev11.html#the-ifid-for-an-html-story-file})
8
+ * @function generate
9
+ * @description Generates a new IFID.
10
+ * @returns {string} IFID
11
+ * @example
12
+ * const ifid = generate();
13
+ * console.log(ifid);
14
+ * // => 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6'
15
+ */
16
+ function generate () {
17
+ return v4().toUpperCase();
18
+ }
19
+
20
+ export { generate };
package/src/JSON/parse.js CHANGED
@@ -1,11 +1,54 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
  import Passage from '../Passage.js';
3
3
 
4
4
  /**
5
5
  * Parse JSON representation into Story.
6
+ * @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-jsonoutput-doc.md Twine 2 JSON Specification}
6
7
  * @function parse
7
8
  * @param {string} jsonString - JSON string to convert to Story.
9
+ * @throws {Error} - Invalid JSON!
8
10
  * @returns {Story} Story object.
11
+ * @example
12
+ * const jsonString = `{
13
+ * "name": "My Story",
14
+ * "start": "First Passage",
15
+ * "ifid": "A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6",
16
+ * "format": "SugarCube",
17
+ * "formatVersion": "2.31.0",
18
+ * "creator": "Twine",
19
+ * "creatorVersion": "2.3.9",
20
+ * "zoom": 1,
21
+ * "passages": [
22
+ * {
23
+ * "name": "First Passage",
24
+ * "tags": "",
25
+ * "metadata": "",
26
+ * "text": "This is the first passage."
27
+ * },
28
+ * ]
29
+ * }`;
30
+ * const story = parse(jsonString);
31
+ * console.log(story);
32
+ * // => Story {
33
+ * // name: 'My Story',
34
+ * // start: 'First Passage',
35
+ * // IFID: 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6',
36
+ * // format: 'SugarCube',
37
+ * // formatVersion: '2.31.0',
38
+ * // creator: 'Twine',
39
+ * // creatorVersion: '2.3.9',
40
+ * // zoom: 1,
41
+ * // tagColors: {},
42
+ * // metadata: {},
43
+ * // passages: [
44
+ * // Passage {
45
+ * // name: 'First Passage',
46
+ * // tags: '',
47
+ * // metadata: '',
48
+ * // text: 'This is the first passage.'
49
+ * // }
50
+ * // ]
51
+ * // }
9
52
  */
10
53
  function parse (jsonString) {
11
54
  // Create future object.
package/src/Passage.js CHANGED
@@ -1,28 +1,68 @@
1
+ import { encode } from 'html-entities';
2
+
1
3
  /**
2
- * A passage is the smallest unit of a Twine story.
3
- */
4
+ * Passage class.
5
+ * @class
6
+ * @classdesc Represents a passage in a Twine story.
7
+ * @property {string} name - Name of the passage.
8
+ * @property {Array} tags - Tags for the passage.
9
+ * @property {object} metadata - Metadata for the passage.
10
+ * @property {string} text - Text content of the passage.
11
+ * @method {string} toTwee - Return a Twee representation.
12
+ * @method {string} toJSON - Return JSON representation.
13
+ * @method {string} toTwine2HTML - Return Twine 2 HTML representation.
14
+ * @method {string} toTwine1HTML - Return Twine 1 HTML representation.
15
+ * @example
16
+ * const p = new Passage('Start', 'This is the start of the story.');
17
+ * console.log(p.toTwee());
18
+ * // :: Start
19
+ * // This is the start of the story.
20
+ * //
21
+ * console.log(p.toJSON());
22
+ * // {"name":"Start","tags":[],"metadata":{},"text":"This is the start of the story."}
23
+ * console.log(p.toTwine2HTML());
24
+ * // <tw-passagedata pid="1" name="Start" tags="" >This is the start of the story.</tw-passagedata>
25
+ * console.log(p.toTwine1HTML());
26
+ * // <div tiddler="Start" tags="" modifier="extwee" twine-position="10,10">This is the start of the story.</div>
27
+ * @example
28
+ * const p = new Passage('Start', 'This is the start of the story.', ['start', 'beginning'], {position: '10,10', size: '100,100'});
29
+ * console.log(p.toTwee());
30
+ * // :: Start [start beginning] {"position":"10,10","size":"100,100"}
31
+ * // This is the start of the story.
32
+ * //
33
+ * console.log(p.toJSON());
34
+ * // {"name":"Start","tags":["start","beginning"],"metadata":{"position":"10,10","size":"100,100"},"text":"This is the start of the story."}
35
+ * console.log(p.toTwine2HTML());
36
+ * // <tw-passagedata pid="1" name="Start" tags="start beginning" position="10,10" size="100,100">This is the start of the story.</tw-passagedata>
37
+ * console.log(p.toTwine1HTML());
38
+ * // <div tiddler="Start" tags="start beginning" modifier="extwee" twine-position="10,10">This is the start of the story.</div>
39
+ */
4
40
  export default class Passage {
5
41
  /**
6
42
  * Name of the Passage
7
43
  * @private
44
+ * @type {string}
8
45
  */
9
- #_name = null;
46
+ #_name = '';
10
47
 
11
48
  /**
12
49
  * Internal array of tags
13
50
  * @private
51
+ * @type {Array}
14
52
  */
15
53
  #_tags = [];
16
54
 
17
55
  /**
18
56
  * Internal metadata of passage
19
57
  * @private
58
+ * @type {object}
20
59
  */
21
60
  #_metadata = {};
22
61
 
23
62
  /**
24
63
  * Internal text of the passage
25
64
  * @private
65
+ * @type {string}
26
66
  */
27
67
  #_text = '';
28
68
 
@@ -55,6 +95,7 @@ export default class Passage {
55
95
 
56
96
  /**
57
97
  * @param {string} s - Name to replace
98
+ * @throws {Error} Name must be a String!
58
99
  */
59
100
  set name (s) {
60
101
  if (typeof s === 'string') {
@@ -72,10 +113,12 @@ export default class Passage {
72
113
 
73
114
  /**
74
115
  * @param {Array} t - Replacement array
116
+ * @throws {Error} Tags must be an array!
75
117
  */
76
118
  set tags (t) {
77
119
  // Test if tags is an array
78
120
  if (Array.isArray(t)) {
121
+ // Set the tags.
79
122
  this.#_tags = t;
80
123
  } else {
81
124
  throw new Error('Tags must be an array!');
@@ -90,6 +133,7 @@ export default class Passage {
90
133
 
91
134
  /**
92
135
  * @param {object} m - Replacement object
136
+ * @throws {Error} Metadata must be an object literal!
93
137
  */
94
138
  set metadata (m) {
95
139
  // Test if metadata was an object
@@ -108,6 +152,7 @@ export default class Passage {
108
152
 
109
153
  /**
110
154
  * @param {string} t - Replacement text
155
+ * @throws {Error} Text should be a String!
111
156
  */
112
157
  set text (t) {
113
158
  // Test if text is a String
@@ -120,6 +165,10 @@ export default class Passage {
120
165
 
121
166
  /**
122
167
  * Return a Twee representation.
168
+ *
169
+ * See: https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md
170
+ *
171
+ * @method toTwee
123
172
  * @returns {string} String form of passage.
124
173
  */
125
174
  toTwee () {
@@ -150,6 +199,7 @@ export default class Passage {
150
199
 
151
200
  /**
152
201
  * Return JSON representation.
202
+ * @method toJSON
153
203
  * @returns {string} JSON string.
154
204
  */
155
205
  toJSON () {
@@ -168,6 +218,7 @@ export default class Passage {
168
218
  /**
169
219
  * Return Twine 2 HTML representation.
170
220
  * (Default Passage ID is 1.)
221
+ * @method toTwine2HTML
171
222
  * @param {number} pid - Passage ID (PID) to record in HTML.
172
223
  * @returns {string} Twine 2 HTML string.
173
224
  */
@@ -185,13 +236,13 @@ export default class Passage {
185
236
  * name: (string) Required.
186
237
  * The name of the passage.
187
238
  */
188
- passageData += ` name="${this.name}"`;
239
+ passageData += ` name="${ encode( this.name ) }"`;
189
240
 
190
241
  /**
191
242
  * tags: (string) Optional.
192
243
  * Any tags for the passage separated by spaces.
193
244
  */
194
- passageData += ` tags="${this.#_tags.join(' ')}" `;
245
+ passageData += ` tags="${ encode( this.#_tags.join(' ') ) }" `;
195
246
 
196
247
  /**
197
248
  * position: (string) Optional.
@@ -211,30 +262,8 @@ export default class Passage {
211
262
  passageData += `size="${this.#_metadata.size}" `;
212
263
  }
213
264
 
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
265
  // Add the text and close the element.
237
- passageData += `>${escape(this.text)}</tw-passagedata>\n`;
266
+ passageData += `>${ encode( this.text ) }</tw-passagedata>\n`;
238
267
 
239
268
  // Return the Twine 2 HTML element.
240
269
  return passageData;
@@ -242,6 +271,7 @@ export default class Passage {
242
271
 
243
272
  /**
244
273
  * Return Twine 1 HTML representation.
274
+ * @method toTwine1HTML
245
275
  * @returns {string} Twine 1 HTML string.
246
276
  */
247
277
  toTwine1HTML () {
@@ -258,13 +288,13 @@ export default class Passage {
258
288
  * tiddler: (string) Required.
259
289
  * The name of the passage.
260
290
  */
261
- passageData += ` tiddler="${this.name}"`;
291
+ passageData += ` tiddler="${ encode( this.name ) }"`;
262
292
 
263
293
  /**
264
294
  * tags: (string) Required.
265
295
  * Any tags for the passage separated by spaces.
266
296
  */
267
- passageData += ` tags="${this.#_tags.join(' ')}" `;
297
+ passageData += ` tags="${ encode( this.#_tags.join(' ') ) }" `;
268
298
 
269
299
  /**
270
300
  * modifier: (string) Optional.
@@ -290,7 +320,7 @@ export default class Passage {
290
320
  * text: (string) Required.
291
321
  * Text content of the passage.
292
322
  */
293
- passageData += `>${this.#_text}</div>`;
323
+ passageData += `>${ encode( this.#_text ) }</div>`;
294
324
 
295
325
  // Return the HTML representation.
296
326
  return passageData;