extwee 2.2.2 → 2.2.4
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/build/extwee +0 -0
- package/build/extwee.web.min.js +1 -1
- package/eslint.config.js +19 -0
- package/jest.config.json +5 -0
- package/package.json +24 -27
- 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 +2 -1
- package/src/Twee/parse.js +3 -1
- package/test/{Passage.test.js → Objects/Passage.test.js} +4 -4
- package/test/{Story.test.js → Objects/Story.test.js} +39 -14
- package/test/{StoryFormat.test.js → Objects/StoryFormat.test.js} +2 -2
- 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/IFID/generate.d.ts +14 -0
- package/types/JSON/parse.d.ts +51 -0
- package/types/Passage.d.ts +117 -0
- package/types/Story.d.ts +230 -0
- package/types/StoryFormat/parse.d.ts +50 -0
- package/types/StoryFormat.d.ts +121 -0
- package/types/TWS/parse.d.ts +10 -0
- package/types/Twee/parse.d.ts +9 -0
- package/types/Twine1HTML/compile.d.ts +19 -0
- package/types/Twine1HTML/parse.d.ts +9 -0
- package/types/Twine2ArchiveHTML/compile.d.ts +14 -0
- package/types/Twine2ArchiveHTML/parse.d.ts +36 -0
- package/types/Twine2HTML/compile.d.ts +14 -0
- package/types/Twine2HTML/parse.d.ts +20 -0
- package/.eslintrc.json +0 -25
package/eslint.config.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import globals from "globals";
|
|
2
|
+
import pluginJs from "@eslint/js";
|
|
3
|
+
import jest from "eslint-plugin-jest";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
languageOptions: {
|
|
8
|
+
globals: {
|
|
9
|
+
...globals.browser,
|
|
10
|
+
...globals.node,
|
|
11
|
+
...globals.jest
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
plugins: {
|
|
15
|
+
jest: jest
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
pluginJs.configs.recommended,
|
|
19
|
+
];
|
package/jest.config.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "extwee",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "A story compiler tool using Twine-compatible formats",
|
|
5
5
|
"author": "Dan Cox",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
"extwee": "src/extwee.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "jest --runInBand
|
|
11
|
+
"test": "jest --runInBand",
|
|
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
|
|
16
|
-
"
|
|
15
|
+
"build:bin": "esbuild ./src/extwee.js --bundle --platform=node --target=node12 --outfile=out.js",
|
|
16
|
+
"gen-types": "npx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types",
|
|
17
|
+
"all": "npm run lint && npm run lint:test && npm run test && npm run gen-types"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"twine",
|
|
@@ -23,37 +24,33 @@
|
|
|
23
24
|
],
|
|
24
25
|
"license": "MIT",
|
|
25
26
|
"dependencies": {
|
|
26
|
-
"commander": "^12.
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"graphemer": "^1.4.0",
|
|
27
29
|
"html-entities": "^2.5.2",
|
|
28
|
-
"node-html-parser": "^6.1.
|
|
30
|
+
"node-html-parser": "^6.1.13",
|
|
29
31
|
"pickleparser": "^0.2.1",
|
|
30
|
-
"semver": "^7.6.
|
|
31
|
-
"uuid": "^
|
|
32
|
+
"semver": "^7.6.2",
|
|
33
|
+
"uuid": "^10.0.0"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
34
|
-
"@babel/cli": "^7.
|
|
35
|
-
"@babel/core": "^7.24.
|
|
36
|
-
"@babel/
|
|
37
|
-
"@
|
|
38
|
-
"@
|
|
39
|
-
"@types/uuid": "^9.0.7",
|
|
36
|
+
"@babel/cli": "^7.25.6",
|
|
37
|
+
"@babel/core": "^7.24.6",
|
|
38
|
+
"@babel/preset-env": "^7.24.6",
|
|
39
|
+
"@eslint/js": "^9.10.0",
|
|
40
|
+
"@types/uuid": "^10.0.0",
|
|
40
41
|
"babel-loader": "^9.1.3",
|
|
41
|
-
"clean-jsdoc-theme": "^4.
|
|
42
|
-
"core-js": "^3.
|
|
43
|
-
"esbuild": "^0.
|
|
44
|
-
"eslint": "^
|
|
45
|
-
"eslint-
|
|
46
|
-
"
|
|
47
|
-
"eslint-plugin-jest": "^27.9.0",
|
|
48
|
-
"eslint-plugin-jsdoc": "^48.2.0",
|
|
49
|
-
"eslint-plugin-node": "^11.1.0",
|
|
50
|
-
"eslint-plugin-promise": "^6.1.1",
|
|
42
|
+
"clean-jsdoc-theme": "^4.3.0",
|
|
43
|
+
"core-js": "^3.37.1",
|
|
44
|
+
"esbuild": "^0.23.0",
|
|
45
|
+
"eslint": "^9.10.0",
|
|
46
|
+
"eslint-plugin-jest": "^28.8.3",
|
|
47
|
+
"globals": "^15.9.0",
|
|
51
48
|
"jest": "^29.7.0",
|
|
52
|
-
"pkg": "^5.8.1",
|
|
53
49
|
"regenerator-runtime": "^0.14.1",
|
|
54
50
|
"shelljs": "^0.8.5",
|
|
55
|
-
"typescript": "^5.4.
|
|
56
|
-
"
|
|
51
|
+
"typescript": "^5.4.5",
|
|
52
|
+
"typescript-eslint": "^8.4.0",
|
|
53
|
+
"webpack": "^5.91.0",
|
|
57
54
|
"webpack-cli": "^5.1.4"
|
|
58
55
|
},
|
|
59
56
|
"repository": {
|
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.4';
|
|
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
|
@@ -28,8 +28,9 @@ function parse (binaryFileContents) {
|
|
|
28
28
|
// Try to parse the pickle data, assuming it is pickle data.
|
|
29
29
|
pythonObject = parser.parse(binaryFileContents);
|
|
30
30
|
} catch (error) {
|
|
31
|
+
console.log(error);
|
|
31
32
|
// This is a Buffer, but not pickle data.
|
|
32
|
-
throw new
|
|
33
|
+
throw new TypeError('Error: Buffer does not contain Python pickle data!');
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// 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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import Passage from '
|
|
1
|
+
import Passage from '../../src/Passage.js';
|
|
2
2
|
import { parse as HTMLParser } from 'node-html-parser';
|
|
3
3
|
|
|
4
4
|
describe('Passage', () => {
|
|
@@ -218,8 +218,8 @@ describe('Passage', () => {
|
|
|
218
218
|
});
|
|
219
219
|
|
|
220
220
|
it('Should escape meta-characters safely in Twee header', function () {
|
|
221
|
-
const p = new Passage('Where do tags begin? [well',
|
|
222
|
-
expect(p.toTwee().includes('Where do tags begin?
|
|
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
223
|
});
|
|
224
224
|
|
|
225
225
|
it('Should produce valid HTML attributes', function () {
|
|
@@ -232,7 +232,7 @@ describe('Passage', () => {
|
|
|
232
232
|
expect(d.querySelector('tw-passagedata').getAttribute('tags')).toBe('&tag "bad"');
|
|
233
233
|
expect(d.querySelector('tw-passagedata').getAttribute('position')).toBe('100,100');
|
|
234
234
|
// Use Twine 2 result.
|
|
235
|
-
const s =
|
|
235
|
+
const s = '<tw-passagedata pid="1" name=""Test"" tags="&tag "bad"" position="100,100" size="100,100"></tw-passagedata>';
|
|
236
236
|
// Parse HTML.
|
|
237
237
|
const t = new HTMLParser(s);
|
|
238
238
|
// Test Twine 2 attributes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Story, creatorName, creatorVersion } from '
|
|
2
|
-
import Passage from '
|
|
3
|
-
import { parse as parseTwee } from '
|
|
1
|
+
import { Story, creatorName, creatorVersion } from '../../src/Story.js';
|
|
2
|
+
import Passage from '../../src/Passage.js';
|
|
3
|
+
import { parse as parseTwee } from '../../src/Twee/parse.js';
|
|
4
4
|
import { readFileSync } from 'node:fs';
|
|
5
5
|
import { parse as HTMLParser } from 'node-html-parser';
|
|
6
|
-
import { generate as generateIFID } from '
|
|
6
|
+
import { generate as generateIFID } from '../../src/IFID/generate.js';
|
|
7
7
|
|
|
8
8
|
// Pull the name and version of this project from package.json.
|
|
9
9
|
// These are used as the 'creator' and 'creator-version'.
|
|
@@ -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()', () => {
|
|
@@ -444,7 +465,7 @@ describe('Story', () => {
|
|
|
444
465
|
const t = s.toTwee();
|
|
445
466
|
|
|
446
467
|
// Test for format in metadata, should not exist.
|
|
447
|
-
expect(t.includes(
|
|
468
|
+
expect(t.includes('"\'format":')).not.toBe(true);
|
|
448
469
|
});
|
|
449
470
|
|
|
450
471
|
it('Should not generate formatVersion if empty', function () {
|
|
@@ -461,7 +482,7 @@ describe('Story', () => {
|
|
|
461
482
|
const t = s.toTwee();
|
|
462
483
|
|
|
463
484
|
// Test for formatVersion in metadata, should not exist.
|
|
464
|
-
expect(t.includes(
|
|
485
|
+
expect(t.includes('"\'format-version":')).not.toBe(true);
|
|
465
486
|
});
|
|
466
487
|
|
|
467
488
|
it('Should not generate zoom if zero', function () {
|
|
@@ -478,7 +499,7 @@ describe('Story', () => {
|
|
|
478
499
|
const t = s.toTwee();
|
|
479
500
|
|
|
480
501
|
// Test for zoom in metadata, should not exist.
|
|
481
|
-
expect(t.includes(
|
|
502
|
+
expect(t.includes('"\'zoom":')).not.toBe(true);
|
|
482
503
|
});
|
|
483
504
|
|
|
484
505
|
it('Should not generate start if empty', function () {
|
|
@@ -495,7 +516,7 @@ describe('Story', () => {
|
|
|
495
516
|
const t = s.toTwee();
|
|
496
517
|
|
|
497
518
|
// Test for start in metadata, should not exist.
|
|
498
|
-
expect(t.includes(
|
|
519
|
+
expect(t.includes('"\'start":')).not.toBe(true);
|
|
499
520
|
});
|
|
500
521
|
|
|
501
522
|
it('Should detect StoryTitle text', function () {
|
|
@@ -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'));
|
|
@@ -781,7 +807,7 @@ describe('Story', () => {
|
|
|
781
807
|
expect(result.includes(`creator-version="${creatorVersion}"`)).not.toBe(true);
|
|
782
808
|
});
|
|
783
809
|
|
|
784
|
-
|
|
810
|
+
it('Should not encode creator if not set', () => {
|
|
785
811
|
// Add passage.
|
|
786
812
|
s.addPassage(new Passage('Start', 'Word'));
|
|
787
813
|
// Set start.
|
|
@@ -793,7 +819,6 @@ describe('Story', () => {
|
|
|
793
819
|
// Expect the creator to be encoded.
|
|
794
820
|
expect(result.includes(`creator="${creatorName}"`)).not.toBe(true);
|
|
795
821
|
});
|
|
796
|
-
|
|
797
822
|
});
|
|
798
823
|
|
|
799
824
|
describe('toTwine1HTML()', function () {
|
|
@@ -828,7 +853,7 @@ describe('Story', () => {
|
|
|
828
853
|
// Expect correct name attribute for tw-passagedata.
|
|
829
854
|
expect(root.querySelector('tw-passagedata').getAttribute('name')).toBe('"Test"');
|
|
830
855
|
// Use Twine 2 result.
|
|
831
|
-
const s2 =
|
|
856
|
+
const s2 = '<tw-storydata name=""Abuse" &test" startnode="1" creator="Twine" creator-version="2.8.1" format="Harlowe" format-version="3.3.8" ifid="452A9D80-C759-42C5-B001-5B861A2410C5" options="" tags="" zoom="1" hidden><style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css"></style><script role="script" id="twine-user-script" type="text/twine-javascript"></script><tw-passagedata pid="1" name=""Test"" tags="&tag "bad"" position="300,100" size="100,100"></tw-passagedata></tw-storydata>';
|
|
832
857
|
// Parse HTML.
|
|
833
858
|
const root2 = HTMLParser(s2);
|
|
834
859
|
// Expect correct name attribute for tw-storydata.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import StoryFormat from '
|
|
1
|
+
import StoryFormat from '../../src/StoryFormat.js';
|
|
2
2
|
|
|
3
3
|
describe('StoryFormat', () => {
|
|
4
4
|
describe('Default values', () => {
|
|
@@ -153,7 +153,7 @@ describe('StoryFormat', () => {
|
|
|
153
153
|
describe('toString', () => {
|
|
154
154
|
it('Should return string representation', () => {
|
|
155
155
|
const sf = new StoryFormat();
|
|
156
|
-
expect(sf.toString()).toBe(JSON.stringify(sf, null,
|
|
156
|
+
expect(sf.toString()).toBe(JSON.stringify(sf, null, '\t'));
|
|
157
157
|
});
|
|
158
158
|
});
|
|
159
159
|
});
|
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,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates an Interactive Fiction Identification (IFID) based the Treaty of Babel.
|
|
3
|
+
*
|
|
4
|
+
* For Twine works, the IFID is a UUID (v4) in uppercase.
|
|
5
|
+
* @see Treaty of Babel ({@link https://babel.ifarchive.org/babel_rev11.html#the-ifid-for-an-html-story-file})
|
|
6
|
+
* @function generate
|
|
7
|
+
* @description Generates a new IFID.
|
|
8
|
+
* @returns {string} IFID
|
|
9
|
+
* @example
|
|
10
|
+
* const ifid = generate();
|
|
11
|
+
* console.log(ifid);
|
|
12
|
+
* // => 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6'
|
|
13
|
+
*/
|
|
14
|
+
export function generate(): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse JSON representation into Story.
|
|
3
|
+
* @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-jsonoutput-doc.md Twine 2 JSON Specification}
|
|
4
|
+
* @function parse
|
|
5
|
+
* @param {string} jsonString - JSON string to convert to Story.
|
|
6
|
+
* @throws {Error} - Invalid JSON!
|
|
7
|
+
* @returns {Story} Story object.
|
|
8
|
+
* @example
|
|
9
|
+
* const jsonString = `{
|
|
10
|
+
* "name": "My Story",
|
|
11
|
+
* "start": "First Passage",
|
|
12
|
+
* "ifid": "A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6",
|
|
13
|
+
* "format": "SugarCube",
|
|
14
|
+
* "formatVersion": "2.31.0",
|
|
15
|
+
* "creator": "Twine",
|
|
16
|
+
* "creatorVersion": "2.3.9",
|
|
17
|
+
* "zoom": 1,
|
|
18
|
+
* "passages": [
|
|
19
|
+
* {
|
|
20
|
+
* "name": "First Passage",
|
|
21
|
+
* "tags": "",
|
|
22
|
+
* "metadata": "",
|
|
23
|
+
* "text": "This is the first passage."
|
|
24
|
+
* },
|
|
25
|
+
* ]
|
|
26
|
+
* }`;
|
|
27
|
+
* const story = parse(jsonString);
|
|
28
|
+
* console.log(story);
|
|
29
|
+
* // => Story {
|
|
30
|
+
* // name: 'My Story',
|
|
31
|
+
* // start: 'First Passage',
|
|
32
|
+
* // IFID: 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6',
|
|
33
|
+
* // format: 'SugarCube',
|
|
34
|
+
* // formatVersion: '2.31.0',
|
|
35
|
+
* // creator: 'Twine',
|
|
36
|
+
* // creatorVersion: '2.3.9',
|
|
37
|
+
* // zoom: 1,
|
|
38
|
+
* // tagColors: {},
|
|
39
|
+
* // metadata: {},
|
|
40
|
+
* // passages: [
|
|
41
|
+
* // Passage {
|
|
42
|
+
* // name: 'First Passage',
|
|
43
|
+
* // tags: '',
|
|
44
|
+
* // metadata: '',
|
|
45
|
+
* // text: 'This is the first passage.'
|
|
46
|
+
* // }
|
|
47
|
+
* // ]
|
|
48
|
+
* // }
|
|
49
|
+
*/
|
|
50
|
+
export function parse(jsonString: string): Story;
|
|
51
|
+
import { Story } from '../Story.js';
|