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
@@ -1,12 +1,11 @@
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.
7
- *
8
- * See: Twine 1 HTML Output Documentation
9
- * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md)
7
+ * @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md Twine 1 HTML Documentation}
8
+ * @function parse
10
9
  * @param {string} content - Twine 1 HTML content to parse.
11
10
  * @returns {Story} Story object
12
11
  */
@@ -1,9 +1,17 @@
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.
5
+ * @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-archive-spec.md Twine 2 Archive Specification}
6
+ * @function compile
5
7
  * @param {Array} stories - Array of Story objects.
6
8
  * @returns {string} Twine 2 Archive HTML.
9
+ * @example
10
+ * const story1 = new Story();
11
+ * const story2 = new Story();
12
+ * const stories = [story1, story2];
13
+ * console.log(compile(stories));
14
+ * // => '<tw-storydata name="Untitled" startnode="1" creator="Twine" creator-version="2.3.9" ifid="A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6" zoom="1" format="Harlowe" format-version="3.1.0" options="" 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="Untitled Passage" tags="" position="0,0" size="100,100"></tw-passagedata></tw-storydata>\n\n<tw-storydata name="Untitled" startnode="1" creator="Twine" creator-version="2.3.9" ifid="A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6" zoom="1" format="Harlowe" format-version="3.1.0" options="" 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="Untitled Passage" tags="" position="0,0" size="100,100"></tw-passagedata></tw-storydata>\n\n'
7
15
  */
8
16
  function compile (stories) {
9
17
  // Can only accept array.
@@ -2,9 +2,39 @@ import { parse as HtmlParser } from 'node-html-parser';
2
2
  import { parse as parseTwine2HTML } from '../Twine2HTML/parse.js';
3
3
 
4
4
  /**
5
- * Parse HTML for one or more Twine 2 HTML elements and return array of story objects.
5
+ * Parse Twine 2 Archive HTML and returns an array of story objects.
6
+ * @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-archive-spec.md Twine 2 Archive Specification}
7
+ * @function parse
6
8
  * @param {string} content - Content to parse for Twine 2 HTML elements.
9
+ * @throws {TypeError} - Content is not a string!
7
10
  * @returns {Array} Array of stories found in content.
11
+ * @example
12
+ * const content = '<tw-storydata name="Untitled" startnode="1" creator="Twine" creator-version="2.3.9" ifid="A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6" zoom="1" format="Harlowe" format-version="3.1.0" options="" 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="Untitled Passage" tags="" position="0,0" size="100,100"></tw-passagedata></tw-storydata>';
13
+ * console.log(parse(content));
14
+ * // => [
15
+ * // Story {
16
+ * // name: 'Untitled',
17
+ * // startnode: '1',
18
+ * // creator: 'Twine',
19
+ * // creatorVersion: '2.3.9',
20
+ * // ifid: 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6',
21
+ * // zoom: '1',
22
+ * // format: 'Harlowe',
23
+ * // formatVersion: '3.1.0',
24
+ * // options: '',
25
+ * // hidden: '',
26
+ * // passages: [
27
+ * // Passage {
28
+ * // pid: '1',
29
+ * // name: 'Untitled Passage',
30
+ * // tags: '',
31
+ * // position: '0,0',
32
+ * // size: '100,100',
33
+ * // text: ''
34
+ * // }
35
+ * // ]
36
+ * // }
37
+ * // ]
8
38
  */
9
39
  function parse (content) {
10
40
  // Can only parse string values.
@@ -32,8 +62,8 @@ function parse (content) {
32
62
 
33
63
  // Did we find any elements?
34
64
  if (storyDataElements.length === 0) {
35
- // If there is not a single `<tw-storydata>` element, this is not a Twine 2 story!
36
- throw new Error('Not Twine 2 HTML content!');
65
+ // Produce a warning if no Twine 2 HTML content is found.
66
+ console.warn('Warning: No Twine 2 HTML content found!');
37
67
  }
38
68
 
39
69
  // Iterate through all `<tw-storydata>` elements.
@@ -1,22 +1,50 @@
1
- import Story from '../Story.js';
1
+ import { Story } from '../Story.js';
2
2
  import StoryFormat from '../StoryFormat.js';
3
3
 
4
4
  /**
5
5
  * Write a combination of Story + StoryFormat into Twine 2 HTML file.
6
+ * @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md Twine 2 HTML Output Specification}
7
+ * @function compile
6
8
  * @param {Story} story - Story object to write.
7
9
  * @param {StoryFormat} storyFormat - StoryFormat to write.
8
- * @returns {string} Twine 2 HTML.
10
+ * @returns {string} Twine 2 HTML based on StoryFormat and Story.
11
+ * @throws {Error} If story is not instance of Story.
12
+ * @throws {Error} If storyFormat is not instance of StoryFormat.
13
+ * @throws {Error} If storyFormat.source is empty string.
9
14
  */
10
15
  function compile (story, storyFormat) {
16
+ // Check if story is instanceof Story.
11
17
  if (!(story instanceof Story)) {
12
18
  throw new Error('Error: story must be a Story object!');
13
19
  }
14
20
 
21
+ // Check if storyFormat is instanceof StoryFormat.
15
22
  if (!(storyFormat instanceof StoryFormat)) {
16
23
  throw new Error('storyFormat must be a StoryFormat object!');
17
24
  }
18
25
 
19
- let outputContents = '';
26
+ // Check if storyFormat.source is empty string.
27
+ if (storyFormat.source === '') {
28
+ throw new Error('StoryFormat source empty string!');
29
+ }
30
+
31
+ /**
32
+ * There are two required attributes:
33
+ * - story.IFID: UUIDv4
34
+ * - story.name: string (non-empty)
35
+ */
36
+
37
+ // Check if story.IFID is UUIDv4 formatted.
38
+ if (story.IFID.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[89ABab][0-9A-F]{3}-[0-9A-F]{12}$/) === null) {
39
+ throw new Error('Story IFID is invalid!');
40
+ }
41
+
42
+ // Check if story.name is empty string.
43
+ if (story.name === '') {
44
+ throw new Error('Story name empty string!');
45
+ }
46
+
47
+ // Translate story to Twine 2 HTML.
20
48
  const storyData = story.toTwine2HTML();
21
49
 
22
50
  // Replace the story name in the source file.
@@ -25,11 +53,8 @@ function compile (story, storyFormat) {
25
53
  // Replace the story data.
26
54
  storyFormat.source = storyFormat.source.replaceAll(/{{STORY_DATA}}/gm, storyData);
27
55
 
28
- // Combine everything together.
29
- outputContents += storyFormat.source;
30
-
31
56
  // Return content.
32
- return outputContents;
57
+ return storyFormat.source;
33
58
  }
34
59
 
35
60
  export { compile };
@@ -1,14 +1,25 @@
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.
7
8
  *
8
9
  * See: Twine 2 HTML Output Specification
9
10
  * (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md)
11
+ *
12
+ * Produces warnings for:
13
+ * - Missing name attribute on `<tw-storydata>` element.
14
+ * - Missing IFID attribute on `<tw-storydata>` element.
15
+ * - Malformed IFID attribute on `<tw-storydata>` element.
16
+ * @function parse
10
17
  * @param {string} content - Twine 2 HTML content to parse.
11
- * @returns {Story} Story
18
+ * @returns {Story} Story object based on Twine 2 HTML content.
19
+ * @throws {TypeError} Content is not a string.
20
+ * @throws {Error} Not Twine 2 HTML content!
21
+ * @throws {Error} Cannot parse passage data without name!
22
+ * @throws {Error} Passages are required to have PID!
12
23
  */
13
24
  function parse (content) {
14
25
  // Create new story.
@@ -16,7 +27,7 @@ function parse (content) {
16
27
 
17
28
  // Can only parse string values.
18
29
  if (typeof content !== 'string') {
19
- throw new TypeError('Content is not a string!');
30
+ throw new TypeError('TypeError: Content is not a string!');
20
31
  }
21
32
 
22
33
  // Set default start node.
@@ -40,7 +51,7 @@ function parse (content) {
40
51
  // Did we find any elements?
41
52
  if (storyDataElements.length === 0) {
42
53
  // If there is not a single `<tw-storydata>` element, this is not a Twine 2 story!
43
- throw new Error('Not Twine 2 HTML content!');
54
+ throw new TypeError('TypeError: Not Twine 2 HTML content!');
44
55
  }
45
56
 
46
57
  // We only parse the first element found.
@@ -55,7 +66,7 @@ function parse (content) {
55
66
  story.name = storyData.attributes.name;
56
67
  } else {
57
68
  // Name is a required field. Warn user.
58
- console.warn('Twine 2 HTML must have a name!');
69
+ console.warn('Warning: The name attribute is missing from tw-storydata!');
59
70
  }
60
71
 
61
72
  /**
@@ -69,7 +80,13 @@ function parse (content) {
69
80
  story.IFID = storyData.attributes.ifid;
70
81
  } else {
71
82
  // Name is a required filed. Warn user.
72
- console.warn('Twine 2 HTML must have an IFID!');
83
+ console.warn('Warning: The ifid attribute is missing from tw-storydata!');
84
+ }
85
+
86
+ // Check if the IFID has valid formatting.
87
+ if (story.IFID.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/) === null) {
88
+ // IFID is not valid.
89
+ console.warn('Warning: The IFID is not in valid UUIDv4 formatting on tw-storydata!');
73
90
  }
74
91
 
75
92
  /**
@@ -124,9 +141,6 @@ function parse (content) {
124
141
  if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'startnode')) {
125
142
  // Take string value and convert to Int
126
143
  startNode = Number.parseInt(storyData.attributes.startnode, 10);
127
- } else {
128
- // Throw error without start node.
129
- throw new Error('Missing startnode in <tw-storydata>!');
130
144
  }
131
145
 
132
146
  // Pull out the `<tw-passagedata>` element.
@@ -172,15 +186,21 @@ function parse (content) {
172
186
  * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
173
187
  */
174
188
  // Create a default value
175
- let name = null;
189
+ let name = 'Untitled Passage';
176
190
  // Does name exist?
177
191
  if (Object.prototype.hasOwnProperty.call(attr, 'name')) {
178
192
  // Escape the name
179
- name = escapeMetacharacters(attr.name);
193
+ name = attr.name;
180
194
  } else {
181
- throw new Error('Cannot parse passage data without name!');
195
+ console.warn('Warning: name attribute is missing! Default passage name will be used.');
182
196
  }
183
197
 
198
+ /**
199
+ * tags: (string) Optional.
200
+ * A space-separated list of tags for the passage.
201
+ *
202
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
203
+ */
184
204
  // Create empty tag array.
185
205
  let tags = [];
186
206
  // Does the tags attribute exist?
@@ -189,7 +209,7 @@ function parse (content) {
189
209
  // (Attributes can, themselves, be empty strings.)
190
210
  if (attr.tags.length > 0 && attr.tags !== '""') {
191
211
  // Escape the tags
192
- tags = escapeMetacharacters(attr.tags);
212
+ tags = attr.tags;
193
213
  // Split by spaces into an array
194
214
  tags = tags.split(' ');
195
215
  }
@@ -198,6 +218,12 @@ function parse (content) {
198
218
  tags = tags.filter(tag => tag !== '');
199
219
  }
200
220
 
221
+ /**
222
+ * metadata: (object) Optional.
223
+ * An object containing additional metadata about the passage.
224
+ *
225
+ * Twine 2 HTML does not support metadata, but other formats do.
226
+ */
201
227
  // Create metadata for passage.
202
228
  const metadata = {};
203
229
 
@@ -221,14 +247,14 @@ function parse (content) {
221
247
  * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
222
248
  */
223
249
  // Create a default PID
224
- let pid = -1;
250
+ let pid = 1;
225
251
  // Does pid exist?
226
252
  if (Object.prototype.hasOwnProperty.call(attr, 'pid')) {
227
253
  // Parse string into int
228
254
  // Update PID
229
255
  pid = Number.parseInt(attr.pid, 10);
230
256
  } else {
231
- console.warn('Passages are required to have PID. Will not add!');
257
+ console.warn('Warning: pid attribute is missing! Default PID will be used.');
232
258
  }
233
259
 
234
260
  // Check the current PID against startNode number.
@@ -238,24 +264,15 @@ function parse (content) {
238
264
  story.start = name;
239
265
  }
240
266
 
241
- // If passage is missing name and PID (required attributes),
242
- // they are not added.
243
- if (name !== null && pid !== -1) {
244
- // Add a new Passage into an array
245
- story.addPassage(
246
- new Passage(
247
- name,
248
- text,
249
- tags,
250
- metadata
251
- )
252
- );
253
- }
254
- }
255
-
256
- // There was an invalid startNode.
257
- if (story.start === '') {
258
- throw new Error('startNode does not exist within passages!');
267
+ // Add a new Passage into an array
268
+ story.addPassage(
269
+ new Passage(
270
+ decode(name),
271
+ decode(text),
272
+ tags.map(tag => decode(tag)),
273
+ metadata
274
+ )
275
+ );
259
276
  }
260
277
 
261
278
  // Look for the style element
@@ -324,25 +341,4 @@ function parse (content) {
324
341
  return story;
325
342
  }
326
343
 
327
- /**
328
- * Try to escape Twine 2 meta-characters.
329
- * @param {string} result - Text to parse.
330
- * @returns {string} Escaped characters.
331
- */
332
- function escapeMetacharacters (result) {
333
- // Replace any single backslash, \, with two of them, \\.
334
- result = result.replace(/\\/g, '\\');
335
- // Double-escape escaped {
336
- result = result.replace(/\\\{/g, '\\\\{');
337
- // Double-escape escaped }
338
- result = result.replace(/\\\}/g, '\\\\}');
339
- // Double-escape escaped [
340
- result = result.replace(/\\\[/g, '\\\\[');
341
- // Double-escape escaped ]
342
- result = result.replace(/\\\]/g, '\\\\]');
343
-
344
- return result;
345
- }
346
-
347
- export { parse, escapeMetacharacters };
348
- export default parse;
344
+ export { parse };
@@ -0,0 +1,10 @@
1
+ import { generate } from '../../src/IFID/generate.js';
2
+
3
+ describe('src/IFID/generate.js', () => {
4
+ describe('generate()', () => {
5
+ it('should generate a valid IFID', () => {
6
+ const ifid = generate();
7
+ expect(ifid).toMatch(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/);
8
+ });
9
+ });
10
+ });
@@ -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
 
@@ -16,15 +16,15 @@ describe('JSON', () => {
16
16
  const s = parseJSON(r.toJSON());
17
17
 
18
18
  // Check all properties.
19
- expect(s.name).toBe('');
19
+ expect(s.name).toBe('Untitled Story');
20
20
  expect(Object.keys(s.tagColors).length).toBe(0);
21
21
  expect(s.IFID).toBe('');
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');
27
- expect(s.zoom).toBe(0);
25
+ expect(s.creator).toBe(creatorName);
26
+ expect(s.creatorVersion).toBe(creatorVersion);
27
+ expect(s.zoom).toBe(1);
28
28
  expect(Object.keys(s.metadata).length).toBe(0);
29
29
  });
30
30
 
@@ -52,14 +52,14 @@ describe('JSON', () => {
52
52
  it('Should parse everything but name', function () {
53
53
  const s = '{"tagColors":{"r":"red"},"ifid":"dd","start":"Start","formatVersion":"1.0","metadata":{"some":"thing"},"format":"Snowman","creator":"extwee","creatorVersion":"2.2.0","zoom":1,"passages":[{"name":"Start","tags":["tag1"],"metadata":{},"text":"Word"}]}';
54
54
  const r = parseJSON(s);
55
- expect(r.name).toBe('');
55
+ expect(r.name).toBe('Untitled Story');
56
56
  expect(Object.keys(r.tagColors).length).toBe(1);
57
57
  expect(r.IFID).toBe('DD');
58
58
  expect(r.start).toBe('Start');
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,9 +187,9 @@ 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
- expect(r.zoom).toBe(0);
192
+ expect(r.zoom).toBe(1);
193
193
  expect(r.size()).toBe(1);
194
194
  });
195
195
 
@@ -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
  });