extwee 2.2.3 → 2.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/eslint.config.js +25 -0
- package/package.json +23 -28
- package/src/Config/parser.js +36 -0
- package/src/Config/reader.js +34 -0
- package/src/JSON/parse.js +1 -1
- package/src/Story.js +30 -25
- package/src/StoryFormat/parse.js +2 -2
- package/src/TWS/parse.js +1 -1
- package/src/Twee/parse.js +3 -1
- package/src/extwee.js +6 -6
- package/test/CLI/CLI.test.js +2 -2
- package/test/Config/Config.test.js +46 -0
- package/test/Config/files/empty.json +3 -0
- package/test/Config/files/invalid.json +1 -0
- package/test/Config/files/valid.json +5 -0
- package/test/Objects/Story.test.js +29 -3
- package/test/TWS/Parse.test.js +3 -3
- package/test/Twee/Twee.Parse.test.js +17 -0
- package/test/Twee/TweeParser/cursed.twee +16 -0
- package/test/Twee/TweeParser/malformed.twee +2 -0
- package/test/Twee/TweeParser/start.twee +2 -0
- package/types/Config/parser.d.ts +6 -0
- package/types/Config/reader.d.ts +11 -0
- package/types/Passage.d.ts +2 -2
- package/types/Story.d.ts +5 -5
- package/.eslintrc.json +0 -19
- package/build/extwee +0 -0
- package/build/extwee.exe +0 -0
- package/build/extwee.web.min.js +0 -2
- package/build/extwee.web.min.js.LICENSE.txt +0 -1
package/README.md
CHANGED
|
@@ -145,17 +145,17 @@ De-compile Twine 2 HTML into Twee 3:
|
|
|
145
145
|
|
|
146
146
|
### Compiling Twee 3 into Twine 1 HTML
|
|
147
147
|
|
|
148
|
-
Enabling Twine 1 mode requires using the
|
|
148
|
+
Enabling Twine 1 mode requires using the `--twine1` flag.
|
|
149
149
|
|
|
150
150
|
Because Twine 1 story formats can be split across files, compilation requires the "engine" from Twine 1 named `engine.js`, the name of the story format, and then its `header.html` template code and the optional but often included `code.js` file.
|
|
151
151
|
|
|
152
|
-
`extwee
|
|
152
|
+
`extwee --twine1 -c -i <tweeFile> -o <Twine1HTML> --engine <engineJS> --name <storyFormatName> --codejs <CodeJS> --header <header>`
|
|
153
153
|
|
|
154
154
|
### De-compiling Twine 1 HTML into Twee 3
|
|
155
155
|
|
|
156
|
-
Enabling Twine 1 mode requires using the
|
|
156
|
+
Enabling Twine 1 mode requires using the `--twine1` flag.
|
|
157
157
|
|
|
158
|
-
`extwee
|
|
158
|
+
`extwee --twine1 -d -i <twine1HTML> -o <outputTwee>`
|
|
159
159
|
|
|
160
160
|
<p align="right">(<a href="#readme-top">back to top</a>)</p>
|
|
161
161
|
|
|
@@ -163,7 +163,6 @@ Enabling Twine 1 mode requires using the `-t1` or `--twine1` flag.
|
|
|
163
163
|
|
|
164
164
|
Each major version has its own GitHub project:
|
|
165
165
|
|
|
166
|
-
- [Road to Extwee 2.2.0](https://github.com/users/videlais/projects/2)
|
|
167
166
|
- [Road to Extwee 2.4.0](https://github.com/users/videlais/projects/4)
|
|
168
167
|
|
|
169
168
|
## Tree Shaking Support
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import jest from "eslint-plugin-jest";
|
|
4
|
+
import jsdoc from 'eslint-plugin-jsdoc';
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
jsdoc.configs['flat/recommended'],
|
|
8
|
+
{
|
|
9
|
+
languageOptions: {
|
|
10
|
+
globals: {
|
|
11
|
+
...globals.browser,
|
|
12
|
+
...globals.node,
|
|
13
|
+
...globals.jest
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
plugins: {
|
|
17
|
+
jest: jest,
|
|
18
|
+
jsdoc: jsdoc
|
|
19
|
+
},
|
|
20
|
+
rules: {
|
|
21
|
+
'jsdoc/require-description': 'warn'
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
pluginJs.configs.recommended,
|
|
25
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extwee",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.5",
|
|
4
4
|
"description": "A story compiler tool using Twine-compatible formats",
|
|
5
5
|
"author": "Dan Cox",
|
|
6
6
|
"main": "index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"lint": "eslint ./src/**/*.js --fix",
|
|
13
13
|
"lint:test": "eslint ./test/**/*.test.js --fix",
|
|
14
14
|
"build:web": "webpack",
|
|
15
|
-
"build:bin": "esbuild ./src/extwee.js --bundle --platform=node --target=node12 --outfile=out.js
|
|
15
|
+
"build:bin": "esbuild ./src/extwee.js --bundle --platform=node --target=node12 --outfile=out.js",
|
|
16
16
|
"gen-types": "npx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types",
|
|
17
17
|
"all": "npm run lint && npm run lint:test && npm run test && npm run gen-types"
|
|
18
18
|
},
|
|
@@ -24,40 +24,35 @@
|
|
|
24
24
|
],
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"commander": "^
|
|
27
|
+
"commander": "^13.1.0",
|
|
28
28
|
"graphemer": "^1.4.0",
|
|
29
29
|
"html-entities": "^2.5.2",
|
|
30
|
-
"node-html-parser": "^
|
|
30
|
+
"node-html-parser": "^7.0.1",
|
|
31
31
|
"pickleparser": "^0.2.1",
|
|
32
|
-
"semver": "^7.
|
|
33
|
-
"uuid": "^
|
|
32
|
+
"semver": "^7.7.1",
|
|
33
|
+
"uuid": "^11.0.5"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@babel/cli": "^7.
|
|
37
|
-
"@babel/core": "^7.
|
|
38
|
-
"@babel/
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"eslint": "^
|
|
47
|
-
"eslint-
|
|
48
|
-
"
|
|
49
|
-
"eslint-plugin-jest": "^28.2.0",
|
|
50
|
-
"eslint-plugin-jsdoc": "^48.2.3",
|
|
51
|
-
"eslint-plugin-n": "^16.6.2",
|
|
52
|
-
"eslint-plugin-node": "^11.1.0",
|
|
53
|
-
"eslint-plugin-promise": "^6.1.1",
|
|
36
|
+
"@babel/cli": "^7.26.4",
|
|
37
|
+
"@babel/core": "^7.26.8",
|
|
38
|
+
"@babel/preset-env": "^7.26.8",
|
|
39
|
+
"@eslint/js": "^9.20.0",
|
|
40
|
+
"@types/uuid": "^10.0.0",
|
|
41
|
+
"babel-loader": "^9.2.1",
|
|
42
|
+
"clean-jsdoc-theme": "^4.3.0",
|
|
43
|
+
"core-js": "^3.40.0",
|
|
44
|
+
"esbuild": "^0.25.0",
|
|
45
|
+
"eslint": "^9.20.0",
|
|
46
|
+
"eslint-plugin-jest": "^28.11.0",
|
|
47
|
+
"eslint-plugin-jsdoc": "^50.6.3",
|
|
48
|
+
"globals": "^15.14.0",
|
|
54
49
|
"jest": "^29.7.0",
|
|
55
|
-
"pkg": "^5.8.1",
|
|
56
50
|
"regenerator-runtime": "^0.14.1",
|
|
57
51
|
"shelljs": "^0.8.5",
|
|
58
|
-
"typescript": "^5.
|
|
59
|
-
"
|
|
60
|
-
"webpack
|
|
52
|
+
"typescript": "^5.7.3",
|
|
53
|
+
"typescript-eslint": "^8.23.0",
|
|
54
|
+
"webpack": "^5.97.1",
|
|
55
|
+
"webpack-cli": "^6.0.1"
|
|
61
56
|
},
|
|
62
57
|
"repository": {
|
|
63
58
|
"type": "git",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a JSON object and extracts the StoryFormat, StoryTitle and StoryVersion.
|
|
3
|
+
* @param {object} obj Incoming JSON object.
|
|
4
|
+
* @returns {object} An object containing the extracted results.
|
|
5
|
+
*/
|
|
6
|
+
export function parser(obj) {
|
|
7
|
+
// Check if the object is a valid JSON object.
|
|
8
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
9
|
+
throw new Error('Error: Invalid JSON object');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Extracted results.
|
|
13
|
+
let results = {
|
|
14
|
+
StoryFormat: null,
|
|
15
|
+
StoryTitle: null,
|
|
16
|
+
StoryVersion: null
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Does the object contain 'StoryFormat'?
|
|
20
|
+
if (Object.hasOwnProperty.call(obj, 'story-format')) {
|
|
21
|
+
results.StoryFormat = obj['story-format'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Does the object contain 'StoryTitle'?
|
|
25
|
+
if (Object.hasOwnProperty.call(obj, 'story-title')) {
|
|
26
|
+
results.StoryTitle = obj['story-title'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Does the object contain 'StoryVersion'?
|
|
30
|
+
if (Object.hasOwnProperty.call(obj, 'story-version')) {
|
|
31
|
+
results.StoryVersion = obj['story-version'];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Return the extracted results.
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {readFileSync, existsSync} from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read a JSON file and return its contents.
|
|
5
|
+
* @param {string} path Path to the JSON file.
|
|
6
|
+
* @returns {object} Parsed JSON object.
|
|
7
|
+
* @throws {Error} If the file does not exist.
|
|
8
|
+
* @throws {Error} If the file is not a valid JSON file.
|
|
9
|
+
* @example
|
|
10
|
+
* const contents = reader('test/Config/files/valid.json');
|
|
11
|
+
* console.log(contents); // {"story-format": 'Harlowe', "story-title": "My Story"}
|
|
12
|
+
*/
|
|
13
|
+
export function reader(path) {
|
|
14
|
+
// Does the file exist?
|
|
15
|
+
if (!existsSync(path)) {
|
|
16
|
+
throw new Error(`Error: File ${path} not found`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Read the file.
|
|
20
|
+
const contents = readFileSync(path, 'utf8');
|
|
21
|
+
|
|
22
|
+
// Parsed contents.
|
|
23
|
+
let parsedContents = null;
|
|
24
|
+
|
|
25
|
+
// Try to parse the contents into JSON object.
|
|
26
|
+
try {
|
|
27
|
+
parsedContents = JSON.parse(contents);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(`Error: File ${path} is not a valid JSON file. ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Return the parsed contents.
|
|
33
|
+
return parsedContents;
|
|
34
|
+
}
|
package/src/JSON/parse.js
CHANGED
package/src/Story.js
CHANGED
|
@@ -3,7 +3,9 @@ import { generate as generateIFID } from './IFID/generate.js';
|
|
|
3
3
|
import { encode } from 'html-entities';
|
|
4
4
|
|
|
5
5
|
const creatorName = 'extwee';
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
// Set the creator version.
|
|
8
|
+
const creatorVersion = '2.2.5';
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* Story class.
|
|
@@ -396,6 +398,22 @@ class Story {
|
|
|
396
398
|
return this.#_passages.length;
|
|
397
399
|
}
|
|
398
400
|
|
|
401
|
+
// Parse Start
|
|
402
|
+
if (p.name === 'Start') {
|
|
403
|
+
// Have we already encountered StoryData?
|
|
404
|
+
if (this.start == '') {
|
|
405
|
+
// Set internal start based on Start.
|
|
406
|
+
/**
|
|
407
|
+
* Four possible scenarios:
|
|
408
|
+
* 1. StoryData has already been encountered, and we will never get here.
|
|
409
|
+
* 2. StoryData exists and will be encountered after Start.
|
|
410
|
+
* 3. StoryData does not exist.
|
|
411
|
+
* 4. Start is the first and only passage.
|
|
412
|
+
*/
|
|
413
|
+
this.start = p.name;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
399
417
|
// This is not StoryData or StoryTitle.
|
|
400
418
|
// Push the passage to the array.
|
|
401
419
|
return this.#_passages.push(p);
|
|
@@ -636,9 +654,6 @@ class Story {
|
|
|
636
654
|
storyData += ` ifid="${ generateIFID() }"`;
|
|
637
655
|
}
|
|
638
656
|
|
|
639
|
-
// 'Start' passage (if there is not a 'start' value set).
|
|
640
|
-
let startPassagePID = null;
|
|
641
|
-
|
|
642
657
|
// Passage Identification (PID) counter.
|
|
643
658
|
// (Twine 2 starts with 1, so we mirror that.)
|
|
644
659
|
let PIDcounter = 1;
|
|
@@ -655,35 +670,25 @@ class Story {
|
|
|
655
670
|
startPID = PIDcounter;
|
|
656
671
|
}
|
|
657
672
|
|
|
658
|
-
// Have we found the 'Start' passage?
|
|
659
|
-
if (p.name === 'Start') {
|
|
660
|
-
// If so, set the PID based on index.
|
|
661
|
-
startPassagePID = PIDcounter;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
673
|
// Increase and keep looking.
|
|
665
674
|
PIDcounter++;
|
|
666
675
|
});
|
|
667
676
|
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
// Check if startnode exists.
|
|
673
|
-
if(this.start !== '') {
|
|
674
|
-
// Set starting passage PID.
|
|
675
|
-
storyData += ` startnode="${startPID}"`;
|
|
677
|
+
// Are there any passages?
|
|
678
|
+
if (passages.length === 0) {
|
|
679
|
+
// No passages, so we can't set a startnode.
|
|
680
|
+
startPID = 0;
|
|
676
681
|
}
|
|
677
682
|
|
|
678
683
|
/**
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
684
|
+
* Multiple possible scenarios:
|
|
685
|
+
* 1. No passages. (StartPID is 0.)
|
|
686
|
+
* 2. Start is the first or only passage. (StartPID is 1.)
|
|
687
|
+
* 3. Starting passage is not the first passage. (StartPID is > 1.)
|
|
682
688
|
*/
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
689
|
+
|
|
690
|
+
// startnode: (integer) Optional. The PID of the starting passage.
|
|
691
|
+
storyData += ` startnode="${startPID}"`;
|
|
687
692
|
|
|
688
693
|
// creator: (string) Optional. The name of the program that created the story.
|
|
689
694
|
// Maps to <tw-storydata creator>.
|
package/src/StoryFormat/parse.js
CHANGED
|
@@ -90,8 +90,8 @@ function parse (contents) {
|
|
|
90
90
|
// Attempt to parse the JSON.
|
|
91
91
|
try {
|
|
92
92
|
jsonContent = JSON.parse(contents);
|
|
93
|
-
} catch (
|
|
94
|
-
throw new Error(
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw new Error(`Error: Unable to parse Twine 2 JSON chunk! ${error.message}`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
package/src/TWS/parse.js
CHANGED
|
@@ -29,7 +29,7 @@ function parse (binaryFileContents) {
|
|
|
29
29
|
pythonObject = parser.parse(binaryFileContents);
|
|
30
30
|
} catch (error) {
|
|
31
31
|
// This is a Buffer, but not pickle data.
|
|
32
|
-
throw new Error
|
|
32
|
+
throw new TypeError(`Error: Buffer does not contain Python pickle data! ${error}`);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Create Story object.
|
package/src/Twee/parse.js
CHANGED
|
@@ -68,7 +68,9 @@ function parse (fileContents) {
|
|
|
68
68
|
// Try to parse the metadata
|
|
69
69
|
try {
|
|
70
70
|
metadata = JSON.parse(metadata);
|
|
71
|
-
} catch (
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.info(`Info: Metadata could not be parsed. Setting to empty object. Reported error: ${error.message}`);
|
|
73
|
+
metadata = {};
|
|
72
74
|
}
|
|
73
75
|
} else {
|
|
74
76
|
// There wasn't any metadata, so set default
|
package/src/extwee.js
CHANGED
|
@@ -53,12 +53,12 @@ const isFile = (path) => {
|
|
|
53
53
|
program
|
|
54
54
|
.name('extwee')
|
|
55
55
|
.description('CLI for Extwee')
|
|
56
|
-
.
|
|
56
|
+
.option('-v, --version', '2.2.4')
|
|
57
57
|
.option('-c, --compile', 'Compile input into output')
|
|
58
58
|
.option('-d, --decompile', 'De-compile input into output')
|
|
59
|
-
.option('
|
|
60
|
-
.option('
|
|
61
|
-
.option('
|
|
59
|
+
.option('--twine1', 'Enable Twine 1 processing')
|
|
60
|
+
.option('--name <storyFormatName>', 'Name of the Twine 1 story format (needed for `code.js` inclusion)')
|
|
61
|
+
.option('--codejs <codeJSFile>', 'Twine 1 code.js file for use with Twine 1 HTML', (value) => {
|
|
62
62
|
// Does the input file exist?
|
|
63
63
|
if (isFile(value) === false) {
|
|
64
64
|
// We cannot do anything without valid input.
|
|
@@ -67,7 +67,7 @@ program
|
|
|
67
67
|
|
|
68
68
|
return value;
|
|
69
69
|
})
|
|
70
|
-
.option('
|
|
70
|
+
.option('--engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
|
|
71
71
|
// Does the input file exist?
|
|
72
72
|
if (isFile(value) === false) {
|
|
73
73
|
// We cannot do anything without valid input.
|
|
@@ -76,7 +76,7 @@ program
|
|
|
76
76
|
|
|
77
77
|
return value;
|
|
78
78
|
})
|
|
79
|
-
.option('
|
|
79
|
+
.option('--header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
|
|
80
80
|
// Does the input file exist?
|
|
81
81
|
if (isFile(value) === false) {
|
|
82
82
|
// We cannot do anything without valid input.
|
package/test/CLI/CLI.test.js
CHANGED
|
@@ -27,12 +27,12 @@ describe('CLI', () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it('Twine 1 - compile: Twee 3 + Twine 1 engine.js + Twine 1 code.js + Twine 1 header.html', () => {
|
|
30
|
-
shell.exec(`node ${currentPath}/src/extwee.js
|
|
30
|
+
shell.exec(`node ${currentPath}/src/extwee.js --twine1 -c -i ${testFilePath}/example6.twee -o ${testFilePath}/output/test3.html --codejs ${testFilePath}/twine1/code.js --engine ${testFilePath}/twine1/engine.js --header ${testFilePath}/twine1/header.html --name Test`);
|
|
31
31
|
expect(shell.test('-e', `${testFilePath}/output/test3.html`)).toBe(true);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('Twine 1 - de-compile: Twine 1 HTML into Twee 3', () => {
|
|
35
|
-
shell.exec(`node ${currentPath}/src/extwee.js
|
|
35
|
+
shell.exec(`node ${currentPath}/src/extwee.js --twine1 -d -i ${testFilePath}/twine1Test.html -o ${testFilePath}/output/test.twee`);
|
|
36
36
|
expect(shell.test('-e', `${testFilePath}/output/test.twee`)).toBe(true);
|
|
37
37
|
});
|
|
38
38
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {reader as ConfigReader} from '../../src/Config/reader.js';
|
|
2
|
+
import {parser as ConfigParser} from '../../src/Config/parser.js';
|
|
3
|
+
|
|
4
|
+
describe('src/Config/reader.js', () => {
|
|
5
|
+
describe('reader()', () => {
|
|
6
|
+
it('should throw an error if the file does not exist', () => {
|
|
7
|
+
expect(() => ConfigReader('non-existent-file.json')).toThrow('Error: File non-existent-file.json not found');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should throw an error if the file is not a valid JSON file', () => {
|
|
11
|
+
expect(() => ConfigReader('test/Config/files/invalid.json')).toThrow();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should return the parsed JSON contents of the file', () => {
|
|
15
|
+
const contents = ConfigReader('test/Config/files/valid.json');
|
|
16
|
+
expect(contents).toEqual({
|
|
17
|
+
"story-format": 'Harlowe',
|
|
18
|
+
"story-title": "My Story",
|
|
19
|
+
"story-version": "2.0.1"
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('parser()', () => {
|
|
25
|
+
|
|
26
|
+
it('should throw an error if the object is not a valid JSON object', () => {
|
|
27
|
+
expect(() => ConfigParser('{')).toThrow();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should extract the StoryFormat, StoryTitle, and StoryVersion from the JSON object', () => {
|
|
31
|
+
const jsonObject = ConfigReader('test/Config/files/valid.json');
|
|
32
|
+
const contents = ConfigParser(jsonObject);
|
|
33
|
+
expect(contents.StoryFormat).toEqual('Harlowe');
|
|
34
|
+
expect(contents.StoryTitle).toEqual('My Story');
|
|
35
|
+
expect(contents.StoryVersion).toEqual('2.0.1');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should not extract the StoryFormat, StoryTitle, and StoryVersion if they do not exist in the JSON object', () => {
|
|
39
|
+
const jsonObject = ConfigReader('test/Config/files/empty.json');
|
|
40
|
+
const contents = ConfigParser(jsonObject);
|
|
41
|
+
expect(contents.StoryFormat).toBeNull();
|
|
42
|
+
expect(contents.StoryTitle).toBeNull();
|
|
43
|
+
expect(contents.StoryVersion).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
invalid
|
|
@@ -311,6 +311,27 @@ describe('Story', () => {
|
|
|
311
311
|
// Test for format.
|
|
312
312
|
expect(s.formatVersion).toBe('2.28.2');
|
|
313
313
|
});
|
|
314
|
+
|
|
315
|
+
it('addPassage() - should override StoryData: zoom', function () {
|
|
316
|
+
// Generate object.
|
|
317
|
+
const o = {
|
|
318
|
+
zoom: 0.5
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Add the passage.
|
|
322
|
+
s.addPassage(new Passage('StoryData', JSON.stringify(o)));
|
|
323
|
+
|
|
324
|
+
// Test for zoom.
|
|
325
|
+
expect(s.zoom).toBe(0.5);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('addPassage() - should set start if Start passage and StoryData is not present', function () {
|
|
329
|
+
// Add the passage.
|
|
330
|
+
s.addPassage(new Passage('Start'));
|
|
331
|
+
|
|
332
|
+
// Test for start.
|
|
333
|
+
expect(s.start).toBe('Start');
|
|
334
|
+
});
|
|
314
335
|
});
|
|
315
336
|
|
|
316
337
|
describe('removePassageByName()', () => {
|
|
@@ -726,17 +747,22 @@ describe('Story', () => {
|
|
|
726
747
|
expect(result.includes('zoom="2"')).toBe(true);
|
|
727
748
|
});
|
|
728
749
|
|
|
729
|
-
it('Should encode
|
|
750
|
+
it('Should encode startnode as Start as single and only passage', () => {
|
|
730
751
|
// Add passage.
|
|
731
752
|
s.addPassage(new Passage('Start', 'Word'));
|
|
732
|
-
// Set start.
|
|
733
|
-
s.start = 'Start';
|
|
734
753
|
// Create HTML.
|
|
735
754
|
const result = s.toTwine2HTML();
|
|
736
755
|
// Expect the start to be encoded.
|
|
737
756
|
expect(result.includes('startnode="1"')).toBe(true);
|
|
738
757
|
});
|
|
739
758
|
|
|
759
|
+
it('Should encode startnode as 0 if no passages', () => {
|
|
760
|
+
// Create HTML.
|
|
761
|
+
const result = s.toTwine2HTML();
|
|
762
|
+
// Expect the start to be encoded.
|
|
763
|
+
expect(result.includes('startnode="0"')).toBe(true);
|
|
764
|
+
});
|
|
765
|
+
|
|
740
766
|
it('Should encode start if property is not set but Start passage is', () => {
|
|
741
767
|
// Add passage.
|
|
742
768
|
s.addPassage(new Passage('Start', 'Word'));
|
package/test/TWS/Parse.test.js
CHANGED
|
@@ -11,9 +11,9 @@ describe('TWSParser', () => {
|
|
|
11
11
|
let r = null;
|
|
12
12
|
|
|
13
13
|
beforeAll(() => {
|
|
14
|
-
const contents = fs.readFileSync('test/TWS/TWSParser/Example1.tws'
|
|
15
|
-
const b = Buffer.from(contents, 'binary');
|
|
16
|
-
r = parseTWS(
|
|
14
|
+
const contents = fs.readFileSync('test/TWS/TWSParser/Example1.tws');
|
|
15
|
+
//const b = Buffer.from(contents, 'binary');
|
|
16
|
+
r = parseTWS(contents);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it('Should parse StoryTitle', function () {
|
|
@@ -15,6 +15,14 @@ describe('Twee', () => {
|
|
|
15
15
|
expect(() => { parseTwee('()()'); }).toThrow();
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
+
it('Should ignore malformed passage metadata and create empty object', () => {
|
|
19
|
+
const fr = readFileSync('test/Twee/TweeParser/malformed.twee', 'utf-8');
|
|
20
|
+
const story = parseTwee(fr);
|
|
21
|
+
const metadata = story.getPassageByName('Start').metadata;
|
|
22
|
+
const numberOfMetadataProperties = Object.keys(metadata).length;
|
|
23
|
+
expect(numberOfMetadataProperties).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
18
26
|
it('Should throw error if it detects malformed passage headers', () => {
|
|
19
27
|
expect(() => { parseTwee('::{}[]\nNo name'); }).toThrow();
|
|
20
28
|
});
|
|
@@ -72,5 +80,14 @@ describe('Twee', () => {
|
|
|
72
80
|
const p = story.getPassageByName('StoryAuthor');
|
|
73
81
|
expect(p).not.toBe(null);
|
|
74
82
|
});
|
|
83
|
+
|
|
84
|
+
it('Should parse single and only passage Start', () => {
|
|
85
|
+
const fr = readFileSync('test/Twee/TweeParser/start.twee', 'utf-8');
|
|
86
|
+
const story = parseTwee(fr);
|
|
87
|
+
const p = story.getPassageByName('Start');
|
|
88
|
+
const startingPassage = story.start;
|
|
89
|
+
expect(p).not.toBe(null);
|
|
90
|
+
expect(startingPassage).toBe('Start');
|
|
91
|
+
});
|
|
75
92
|
});
|
|
76
93
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
:: StoryTitle
|
|
2
|
+
Cursed
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
:: StoryData
|
|
6
|
+
{
|
|
7
|
+
"ifid": "22F25A58-7062-4927-95B6-F424DDB2EC65",
|
|
8
|
+
"format": "Harlowe",
|
|
9
|
+
"format-version": "3.3.8",
|
|
10
|
+
"start": "[Hello] {world} \\\\",
|
|
11
|
+
"zoom": 1
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
:: \[Hello\] \{world\} \\\\ {"position":"400,200","size":"100,100"}
|
|
16
|
+
\:: Extra header
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a JSON file and return its contents.
|
|
3
|
+
* @param {string} path Path to the JSON file.
|
|
4
|
+
* @returns {object} Parsed JSON object.
|
|
5
|
+
* @throws {Error} If the file does not exist.
|
|
6
|
+
* @throws {Error} If the file is not a valid JSON file.
|
|
7
|
+
* @example
|
|
8
|
+
* const contents = reader('test/Config/files/valid.json');
|
|
9
|
+
* console.log(contents); // {"story-format": 'Harlowe', "story-title": "My Story"}
|
|
10
|
+
*/
|
|
11
|
+
export function reader(path: string): object;
|
package/types/Passage.d.ts
CHANGED
|
@@ -68,12 +68,12 @@ export default class Passage {
|
|
|
68
68
|
* @param {object} m - Replacement object
|
|
69
69
|
* @throws {Error} Metadata must be an object literal!
|
|
70
70
|
*/
|
|
71
|
-
set metadata(m:
|
|
71
|
+
set metadata(m: object);
|
|
72
72
|
/**
|
|
73
73
|
* Metadata
|
|
74
74
|
* @returns {object} Metadata
|
|
75
75
|
*/
|
|
76
|
-
get metadata():
|
|
76
|
+
get metadata(): object;
|
|
77
77
|
/**
|
|
78
78
|
* @param {string} t - Replacement text
|
|
79
79
|
* @throws {Error} Text should be a String!
|
package/types/Story.d.ts
CHANGED
|
@@ -50,12 +50,12 @@ export class Story {
|
|
|
50
50
|
/**
|
|
51
51
|
* @param {object} a - Replacement tag colors
|
|
52
52
|
*/
|
|
53
|
-
set tagColors(a:
|
|
53
|
+
set tagColors(a: object);
|
|
54
54
|
/**
|
|
55
55
|
* Tag Colors object (each property is a tag and its color)
|
|
56
56
|
* @returns {object} tag colors array
|
|
57
57
|
*/
|
|
58
|
-
get tagColors():
|
|
58
|
+
get tagColors(): object;
|
|
59
59
|
/**
|
|
60
60
|
* @param {string} i - Replacement IFID.
|
|
61
61
|
*/
|
|
@@ -86,12 +86,12 @@ export class Story {
|
|
|
86
86
|
/**
|
|
87
87
|
* @param {object} o - Replacement metadata
|
|
88
88
|
*/
|
|
89
|
-
set metadata(o:
|
|
89
|
+
set metadata(o: object);
|
|
90
90
|
/**
|
|
91
91
|
* Metadata of Story.
|
|
92
92
|
* @returns {object} metadata of story
|
|
93
93
|
*/
|
|
94
|
-
get metadata():
|
|
94
|
+
get metadata(): object;
|
|
95
95
|
/**
|
|
96
96
|
* @param {string} f - Replacement format
|
|
97
97
|
*/
|
|
@@ -226,5 +226,5 @@ export class Story {
|
|
|
226
226
|
#private;
|
|
227
227
|
}
|
|
228
228
|
export const creatorName: "extwee";
|
|
229
|
-
export const creatorVersion: "2.2.
|
|
229
|
+
export const creatorVersion: "2.2.5";
|
|
230
230
|
import Passage from './Passage.js';
|