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.
- package/.github/codeql-analysis.yml +51 -0
- package/README.md +9 -3
- package/build/extwee +0 -0
- package/build/extwee.web.min.js +1 -1
- package/docs/_sidebar.md +1 -0
- package/docs/examples/dynamicPassages.md +1 -1
- package/docs/examples/twsToTwee.md +1 -1
- package/docs/objects/story.md +1 -2
- package/index.js +3 -1
- package/package.json +22 -19
- package/src/IFID/generate.js +20 -0
- package/src/JSON/parse.js +44 -1
- package/src/Passage.js +61 -31
- package/src/Story.js +272 -110
- package/src/StoryFormat/parse.js +190 -80
- package/src/StoryFormat.js +78 -88
- package/src/TWS/parse.js +3 -3
- package/src/Twee/parse.js +3 -4
- package/src/Twine1HTML/compile.js +3 -1
- package/src/Twine1HTML/parse.js +3 -4
- package/src/Twine2ArchiveHTML/compile.js +9 -1
- package/src/Twine2ArchiveHTML/parse.js +33 -3
- package/src/Twine2HTML/compile.js +32 -7
- package/src/Twine2HTML/parse.js +51 -55
- package/test/IFID/IFID.Generate.test.js +10 -0
- package/test/JSON/JSON.Parse.test.js +24 -24
- package/test/Passage.test.js +69 -0
- package/test/Story.test.js +298 -49
- package/test/StoryFormat/StoryFormat.Parse.test.js +442 -55
- package/test/StoryFormat.test.js +9 -2
- package/test/TWS/Parse.test.js +1 -1
- package/test/Twine1HTML/Twine1HTML.Compile.test.js +1 -1
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Compile.test.js +1 -1
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +20 -4
- package/test/Twine2HTML/Twine2HTML.Compile.test.js +36 -121
- package/test/Twine2HTML/Twine2HTML.Parse.test.js +63 -43
- package/test/Twine2HTML/Twine2HTMLCompiler/format.js +9 -0
- package/test/Twine2HTML/Twine2HTMLParser/missingZoom.html +1 -1
- package/test/Twine2HTML/Twine2HTMLParser/unescaping.html +33 -0
- package/tsconfig.json +21 -0
- package/types/index.d.ts +14 -0
- package/types/src/JSON/parse.d.ts +8 -0
- package/types/src/Passage.d.ts +72 -0
- package/types/src/Story.d.ts +161 -0
- package/types/src/StoryFormat/parse.d.ts +7 -0
- package/types/src/StoryFormat.d.ts +97 -0
- package/types/src/TWS/parse.d.ts +10 -0
- package/types/src/Twee/parse.d.ts +10 -0
- package/types/src/Twine1HTML/compile.d.ts +17 -0
- package/types/src/Twine1HTML/parse.d.ts +10 -0
- package/types/src/Twine2ArchiveHTML/compile.d.ts +6 -0
- package/types/src/Twine2ArchiveHTML/parse.d.ts +6 -0
- package/types/src/Twine2HTML/compile.d.ts +9 -0
- package/types/src/Twine2HTML/parse.d.ts +17 -0
- package/types/src/extwee.d.ts +2 -0
- package/web-index.js +3 -1
- package/test/StoryFormat/StoryFormatParser/example.js +0 -3
- package/test/StoryFormat/StoryFormatParser/example2.js +0 -3
- package/test/StoryFormat/StoryFormatParser/format.js +0 -1
- package/test/StoryFormat/StoryFormatParser/format_doublename.js +0 -1
- package/test/StoryFormat/StoryFormatParser/harlowe.js +0 -3
- package/test/StoryFormat/StoryFormatParser/missingAuthor.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingDescription.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingImage.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingLicense.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingName.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingProofing.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingSource.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingURL.js +0 -1
- package/test/StoryFormat/StoryFormatParser/missingVersion.js +0 -1
- package/test/StoryFormat/StoryFormatParser/versionWrong.js +0 -1
- package/test/Twine2HTML/Twine2HTMLParser/missingName.html +0 -33
- package/test/Twine2HTML/Twine2HTMLParser/missingPID.html +0 -15
- package/test/Twine2HTML/Twine2HTMLParser/missingPassageName.html +0 -15
package/src/Twine1HTML/parse.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
//
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
57
|
+
return storyFormat.source;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
60
|
export { compile };
|
package/src/Twine2HTML/parse.js
CHANGED
|
@@ -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
|
|
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('
|
|
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('
|
|
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 =
|
|
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 =
|
|
193
|
+
name = attr.name;
|
|
180
194
|
} else {
|
|
181
|
-
|
|
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 =
|
|
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 =
|
|
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('
|
|
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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
26
|
-
expect(s.creatorVersion).toBe(
|
|
27
|
-
expect(s.zoom).toBe(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
175
|
-
expect(r.creatorVersion).toBe(
|
|
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(
|
|
190
|
+
expect(r.creator).toBe(creatorName);
|
|
191
191
|
expect(r.creatorVersion).toBe('2.2.0');
|
|
192
|
-
expect(r.zoom).toBe(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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);
|
package/test/Passage.test.js
CHANGED
|
@@ -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 "word"')).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('Should escape ampersands', function () {
|
|
184
|
+
const p = new Passage('Test', 'Word & word');
|
|
185
|
+
expect(p.toTwine2HTML().includes('Word & 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 < 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 > 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 &<>"' 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=""Test""')).toBe(true);
|
|
206
|
+
expect(p.toTwine1HTML().includes('tiddler=""Test""')).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('"Word"')).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="&tag "bad""')).toBe(true);
|
|
217
|
+
expect(p.toTwine1HTML().includes('tags="&tag "bad""')).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=""Test"" tags="&tag "bad"" 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
|
});
|