extwee 2.0.5 → 2.2.0
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/.eslintrc.json +25 -25
- package/.github/FUNDING.yml +3 -0
- package/.github/dependabot.yml +11 -0
- package/.github/workflows/nodejs.yml +25 -24
- package/.travis.yml +13 -13
- package/CODE_OF_CONDUCT.md +82 -82
- package/LICENSE +21 -21
- package/README.md +173 -36
- package/SECURITY.md +12 -12
- package/babel.config.json +18 -22
- package/build/extwee +0 -0
- package/build/extwee.exe +0 -0
- package/build/extwee.web.min.js +2 -0
- package/build/extwee.web.min.js.LICENSE.txt +1 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +167 -0
- package/docs/_sidebar.md +19 -0
- package/docs/examples/dynamicPassages.md +28 -0
- package/docs/examples/jsonToTwee.md +23 -0
- package/docs/examples/twsToTwee.md +25 -0
- package/docs/formats/json.md +17 -0
- package/docs/formats/twee.md +13 -0
- package/docs/formats/twine1HTML.md +13 -0
- package/docs/formats/twine2ArchiveHTML.md +13 -0
- package/docs/formats/twine2HTML.md +13 -0
- package/docs/formats/tws.md +9 -0
- package/docs/index.html +26 -0
- package/docs/install/binaries.md +9 -0
- package/docs/install/npm.md +20 -0
- package/docs/install/npx.md +9 -0
- package/docs/objects/passage.md +47 -0
- package/docs/objects/story.md +70 -0
- package/docs/objects/storyformat.md +27 -0
- package/index.html +22 -0
- package/index.js +29 -31
- package/package.json +65 -58
- package/src/JSON/parse.js +128 -0
- package/src/Passage.js +298 -202
- package/src/Story.js +650 -489
- package/src/StoryFormat/parse.js +134 -0
- package/src/StoryFormat.js +259 -300
- package/src/TWS/parse.js +86 -0
- package/src/Twee/parse.js +157 -0
- package/src/Twine1HTML/compile.js +58 -0
- package/src/Twine1HTML/parse.js +134 -0
- package/src/Twine2ArchiveHTML/compile.js +36 -0
- package/src/Twine2ArchiveHTML/parse.js +49 -0
- package/src/Twine2HTML/compile.js +35 -0
- package/src/Twine2HTML/parse.js +348 -0
- package/src/extwee.js +206 -0
- package/test/CLI/CLI.test.js +49 -0
- package/test/CLI/files/example.json +1 -0
- package/test/CLI/files/example6.twee +22 -0
- package/test/{Roundtrip → CLI/files}/harlowe.js +2 -2
- package/test/CLI/{input.html → files/input.html} +47 -47
- package/test/CLI/files/output/test.twee +0 -0
- package/test/CLI/{tweeExample.twee → files/tweeExample.twee} +17 -17
- package/test/CLI/files/twine1/LICENSE.txt +32 -0
- package/test/CLI/files/twine1/code.js +5 -0
- package/test/CLI/files/twine1/engine.js +43 -0
- package/test/CLI/files/twine1/header.html +325 -0
- package/test/CLI/files/twine1Test.html +371 -0
- package/test/{HTMLParser → CLI/files}/twineExample.html +16 -15
- package/test/JSON/JSON.Parse.test.js +316 -0
- package/test/Passage.test.js +175 -104
- package/test/Roundtrip/{Example1.html → Files/Example1.html} +63 -63
- package/test/Roundtrip/Files/LICENSE +19 -0
- package/test/Roundtrip/Files/example1.twee +10 -0
- package/test/Roundtrip/{example2.twee → Files/example2.twee} +27 -18
- package/test/Roundtrip/Files/example4.twee +27 -0
- package/test/{StoryFormatParser → Roundtrip/Files}/harlowe.js +2 -2
- package/test/Roundtrip/{round.html → Files/round.html} +6 -7
- package/test/Roundtrip/Roundtrip.test.js +54 -0
- package/test/Story.test.js +624 -355
- package/test/StoryFormat/StoryFormat.Parse.test.js +91 -0
- package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/example.js +3 -3
- package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/example2.js +3 -3
- package/test/{CLI → StoryFormat/StoryFormatParser}/harlowe.js +2 -2
- package/test/StoryFormat.test.js +152 -152
- package/test/TWS/Parse.test.js +78 -0
- package/test/TWS/TWSParser/Example1.tws +150 -0
- package/test/TWS/TWSParser/Example5.tws +414 -0
- package/test/TWS/TWSParser/noscale.tws +0 -0
- package/test/TWS/TWSParser/nostory.tws +0 -0
- package/test/Twee/Twee.Parse.test.js +76 -0
- package/test/{TweeParser → Twee/TweeParser}/emptytags.twee +2 -2
- package/test/{TweeParser → Twee/TweeParser}/example.twee +32 -32
- package/test/{TweeParser → Twee/TweeParser}/missing.twee +19 -19
- package/test/{TweeParser → Twee/TweeParser}/multipleScriptPassages.twee +19 -19
- package/test/{TweeParser → Twee/TweeParser}/multipleStyleTag.twee +19 -19
- package/test/{TweeParser → Twee/TweeParser}/multipletags.twee +10 -10
- package/test/{TweeParser → Twee/TweeParser}/noTitle.twee +2 -2
- package/test/{TweeParser → Twee/TweeParser}/notes.twee +16 -16
- package/test/{TweeParser → Twee/TweeParser}/pasagemetadataerror.twee +2 -2
- package/test/{TweeParser → Twee/TweeParser}/scriptPassage.twee +16 -16
- package/test/{TweeParser → Twee/TweeParser}/singletag.twee +13 -13
- package/test/{TweeParser → Twee/TweeParser}/startMetadata.twee +14 -14
- package/test/{TweeParser → Twee/TweeParser}/storydataerror.twee +25 -25
- package/test/{TweeParser → Twee/TweeParser}/stylePassage.twee +16 -16
- package/test/{Story → Twee/TweeParser}/test.twee +25 -25
- package/test/Twine1HTML/Twine1HTML.Compile.test.js +180 -0
- package/test/Twine1HTML/Twine1HTML.Parse.test.js +183 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/LICENSE +674 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/engine.js +43 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/jquery.js +4 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/Twine1/modernizr.js +4 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/engineTest.html +1 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/jonah-1.4.2/LICENSE +32 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/jonah-1.4.2/code.js +4 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/jonah-1.4.2/header.html +327 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/test.html +0 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/test1.html +6 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/test2.html +6 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/test3.html +43 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/test4.html +372 -0
- package/test/Twine1HTML/Twine1HTMLCompiler/test5.html +372 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Compile.test.js +35 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +25 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTMLCompiler/test1.html +6 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTMLParser/test1.html +3 -0
- package/test/Twine2HTML/Twine2HTML.Compile.test.js +224 -0
- package/test/Twine2HTML/Twine2HTML.Parse.test.js +172 -0
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/creator.html +4 -5
- package/test/{CLI → Twine2HTML/Twine2HTMLCompiler}/example6.twee +15 -15
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/missingStoryTitle.twee +29 -29
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test11.html +1 -3
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test2.html +10 -11
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test3.html +1 -2
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test4.html +4 -5
- package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/test6.html +1 -2
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/Example1.html +52 -52
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/Tags.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/lyingStartnode.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/lyingTagColors.html +48 -48
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingCreator.html +11 -11
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingCreatorVersion.html +11 -11
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingFormat.html +11 -11
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingFormatVersion.html +11 -11
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingIFID.html +11 -11
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingName.html +33 -33
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPID.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPassageName.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPassageTags.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingPosition.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingScript.html +14 -14
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingSize.html +35 -35
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingStartnode.html +11 -11
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingStyle.html +14 -14
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/missingZoom.html +11 -11
- package/test/Twine2HTML/Twine2HTMLParser/twineExample.html +23 -0
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/twineExample2.html +15 -15
- package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/twineExample3.html +15 -15
- package/web-index.js +29 -0
- package/webpack.config.js +12 -0
- package/bin/extwee.js +0 -47
- package/src/FileReader.js +0 -33
- package/src/HTMLParser.js +0 -343
- package/src/HTMLWriter.js +0 -231
- package/src/StoryFormatParser.js +0 -142
- package/src/TweeParser.js +0 -161
- package/src/TweeWriter.js +0 -98
- package/story-formats/chapbook-1.2.0/format.js +0 -1
- package/story-formats/chapbook-1.2.0/logo.svg +0 -1
- package/story-formats/harlowe-1.2.4/format.js +0 -1
- package/story-formats/harlowe-1.2.4/icon.svg +0 -78
- package/story-formats/harlowe-2.1.0/format.js +0 -2
- package/story-formats/harlowe-2.1.0/icon.svg +0 -78
- package/story-formats/harlowe-3.1.0/format.js +0 -3
- package/story-formats/harlowe-3.1.0/icon.svg +0 -78
- package/story-formats/paperthin-1.0.0/format.js +0 -1
- package/story-formats/paperthin-1.0.0/icon.svg +0 -5
- package/story-formats/snowman-1.4.0/format.js +0 -1
- package/story-formats/snowman-1.4.0/icon.svg +0 -436
- package/story-formats/snowman-2.0.2/format.js +0 -1
- package/story-formats/snowman-2.0.2/icon.svg +0 -436
- package/story-formats/sugarcube-1.0.35/LICENSE +0 -23
- package/story-formats/sugarcube-1.0.35/format.js +0 -1
- package/story-formats/sugarcube-1.0.35/icon.svg +0 -56
- package/story-formats/sugarcube-2.31.1/LICENSE +0 -22
- package/story-formats/sugarcube-2.31.1/format.js +0 -1
- package/story-formats/sugarcube-2.31.1/icon.svg +0 -56
- package/test/CLI/test2.html +0 -47
- package/test/CLI/twineExample.html +0 -15
- package/test/CLI.test.js +0 -30
- package/test/FileReader/t1.txt +0 -1
- package/test/FileReader.test.js +0 -14
- package/test/HTMLParser.test.js +0 -177
- package/test/HTMLWriter/example6.twee +0 -16
- package/test/HTMLWriter.test.js +0 -289
- package/test/Roundtrip/example1.twee +0 -21
- package/test/Roundtrip.test.js +0 -48
- package/test/Story/startmeta.twee +0 -29
- package/test/StoryFormatParser.test.js +0 -91
- package/test/TweeParser/test.twee +0 -25
- package/test/TweeParser.test.js +0 -79
- package/test/TweeWriter/test1.twee +0 -18
- package/test/TweeWriter/test3.twee +0 -12
- package/test/TweeWriter/test4.twee +0 -14
- package/test/TweeWriter/test5.twee +0 -20
- package/test/TweeWriter.test.js +0 -85
- /package/test/CLI/{test.twee → files/test.twee} +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/format.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/format_doublename.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingAuthor.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingDescription.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingImage.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingLicense.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingName.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingProofing.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingSource.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingURL.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/missingVersion.js +0 -0
- /package/test/{StoryFormatParser → StoryFormat/StoryFormatParser}/versionWrong.js +0 -0
- /package/test/{HTMLWriter → Twine2HTML/Twine2HTMLCompiler}/TestTags.html +0 -0
- /package/test/{HTMLParser → Twine2HTML/Twine2HTMLParser}/tagColors.html +0 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { parse as HtmlParser } from 'node-html-parser';
|
|
2
|
+
import Story from '../Story.js';
|
|
3
|
+
import Passage from '../Passage.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Parse Twine 2 HTML into Story object.
|
|
7
|
+
*
|
|
8
|
+
* See: Twine 2 HTML Output Specification
|
|
9
|
+
* (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md)
|
|
10
|
+
* @param {string} content - Twine 2 HTML content to parse.
|
|
11
|
+
* @returns {Story} Story
|
|
12
|
+
*/
|
|
13
|
+
function parse (content) {
|
|
14
|
+
// Create new story.
|
|
15
|
+
const story = new Story();
|
|
16
|
+
|
|
17
|
+
// Can only parse string values.
|
|
18
|
+
if (typeof content !== 'string') {
|
|
19
|
+
throw new TypeError('Content is not a string!');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Set default start node.
|
|
23
|
+
let startNode = null;
|
|
24
|
+
|
|
25
|
+
// Send to node-html-parser
|
|
26
|
+
// Enable getting the content of 'script', 'style', and 'pre' elements
|
|
27
|
+
// Get back a DOM
|
|
28
|
+
const dom = new HtmlParser(
|
|
29
|
+
content,
|
|
30
|
+
{
|
|
31
|
+
lowerCaseTagName: false,
|
|
32
|
+
script: true,
|
|
33
|
+
style: true,
|
|
34
|
+
pre: true
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Pull out the `<tw-storydata>` element.
|
|
38
|
+
const storyDataElements = dom.getElementsByTagName('tw-storydata');
|
|
39
|
+
|
|
40
|
+
// Did we find any elements?
|
|
41
|
+
if (storyDataElements.length === 0) {
|
|
42
|
+
// 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!');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// We only parse the first element found.
|
|
47
|
+
const storyData = storyDataElements[0];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* name: (string) Required.
|
|
51
|
+
* The name of the story.
|
|
52
|
+
*/
|
|
53
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
|
|
54
|
+
// Set the story name
|
|
55
|
+
story.name = storyData.attributes.name;
|
|
56
|
+
} else {
|
|
57
|
+
// Name is a required field. Warn user.
|
|
58
|
+
console.warn('Twine 2 HTML must have a name!');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ifid: (string) Required.
|
|
63
|
+
* An IFID is a sequence of between 8 and 63 characters,
|
|
64
|
+
* each of which shall be a digit, a capital letter or a
|
|
65
|
+
* hyphen that uniquely identify a story (see Treaty of Babel).
|
|
66
|
+
*/
|
|
67
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'ifid')) {
|
|
68
|
+
// Update story IFID.
|
|
69
|
+
story.IFID = storyData.attributes.ifid;
|
|
70
|
+
} else {
|
|
71
|
+
// Name is a required filed. Warn user.
|
|
72
|
+
console.warn('Twine 2 HTML must have an IFID!');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* creator: (string) Optional.
|
|
77
|
+
* The name of program used to create the file.
|
|
78
|
+
*/
|
|
79
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator')) {
|
|
80
|
+
// Update story creator
|
|
81
|
+
story.creator = storyData.attributes.creator;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* creator-version: (string) Optional.
|
|
86
|
+
* The version of the program used to create the file.
|
|
87
|
+
*/
|
|
88
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator-version')) {
|
|
89
|
+
// Update story creator version
|
|
90
|
+
story.creatorVersion = storyData.attributes['creator-version'];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* format: (string) Optional.
|
|
95
|
+
* The story format used to create the story.
|
|
96
|
+
*/
|
|
97
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format')) {
|
|
98
|
+
// Update story format
|
|
99
|
+
story.format = storyData.attributes.format;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* format-version: (string) Optional.
|
|
104
|
+
* The version of the story format used to create the story.
|
|
105
|
+
*/
|
|
106
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format-version')) {
|
|
107
|
+
// Update story format version
|
|
108
|
+
story.formatVersion = storyData.attributes['format-version'];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* zoom: (string) Optional.
|
|
113
|
+
* The decimal level of zoom (i.e. 1.0 is 100% and 1.2 would be 120% zoom level).
|
|
114
|
+
*/
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'zoom')) {
|
|
116
|
+
// Update story zoom
|
|
117
|
+
story.zoom = Number(Number.parseFloat(storyData.attributes.zoom).toFixed(2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* startnode: (string) Optional.
|
|
122
|
+
* The PID matching a `<tw-passagedata>` element whose content should be displayed first.
|
|
123
|
+
*/
|
|
124
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'startnode')) {
|
|
125
|
+
// Take string value and convert to Int
|
|
126
|
+
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
|
+
}
|
|
131
|
+
|
|
132
|
+
// Pull out the `<tw-passagedata>` element.
|
|
133
|
+
const storyPassages = dom.querySelectorAll('tw-passagedata');
|
|
134
|
+
|
|
135
|
+
// Move through the passages
|
|
136
|
+
for (const passage in storyPassages) {
|
|
137
|
+
// Get the passage attributes
|
|
138
|
+
const attr = storyPassages[passage].attributes;
|
|
139
|
+
// Get the passage text
|
|
140
|
+
const text = storyPassages[passage].rawText;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* position: (string) Optional.
|
|
144
|
+
* Comma-separated X and Y position of the upper-left
|
|
145
|
+
* of the passage when viewed within the Twine 2 editor.
|
|
146
|
+
*/
|
|
147
|
+
// Set a default position.
|
|
148
|
+
let position = null;
|
|
149
|
+
// Does position exist?
|
|
150
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'position')) {
|
|
151
|
+
// Update position.
|
|
152
|
+
position = attr.position;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* size: (string) Optional.
|
|
157
|
+
* Comma-separated width and height of the
|
|
158
|
+
* passage when viewed within the Twine 2 editor.
|
|
159
|
+
*/
|
|
160
|
+
// Set a default size.
|
|
161
|
+
let size = null;
|
|
162
|
+
// Does size exist?
|
|
163
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'size')) {
|
|
164
|
+
// Update size.
|
|
165
|
+
size = attr.size;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* name: (string) Required.
|
|
170
|
+
* The name of the passage.
|
|
171
|
+
*
|
|
172
|
+
* https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
|
|
173
|
+
*/
|
|
174
|
+
// Create a default value
|
|
175
|
+
let name = null;
|
|
176
|
+
// Does name exist?
|
|
177
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'name')) {
|
|
178
|
+
// Escape the name
|
|
179
|
+
name = escapeMetacharacters(attr.name);
|
|
180
|
+
} else {
|
|
181
|
+
throw new Error('Cannot parse passage data without name!');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Create empty tag array.
|
|
185
|
+
let tags = [];
|
|
186
|
+
// Does the tags attribute exist?
|
|
187
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
|
|
188
|
+
// Escape any tags
|
|
189
|
+
// (Attributes can, themselves, be empty strings.)
|
|
190
|
+
if (attr.tags.length > 0 && attr.tags !== '""') {
|
|
191
|
+
// Escape the tags
|
|
192
|
+
tags = escapeMetacharacters(attr.tags);
|
|
193
|
+
// Split by spaces into an array
|
|
194
|
+
tags = tags.split(' ');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Remove any empty strings.
|
|
198
|
+
tags = tags.filter(tag => tag !== '');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create metadata for passage.
|
|
202
|
+
const metadata = {};
|
|
203
|
+
|
|
204
|
+
// Does position exist?
|
|
205
|
+
if (position !== null) {
|
|
206
|
+
// Add the property to metadata
|
|
207
|
+
metadata.position = position;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Does size exist?
|
|
211
|
+
if (size !== null) {
|
|
212
|
+
// Add the property to metadata
|
|
213
|
+
metadata.size = size;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* pid: (string) Required.
|
|
218
|
+
* The Passage ID (PID).
|
|
219
|
+
* (Note: This is subject to change during editing with Twine 2.)
|
|
220
|
+
*
|
|
221
|
+
* https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
|
|
222
|
+
*/
|
|
223
|
+
// Create a default PID
|
|
224
|
+
let pid = -1;
|
|
225
|
+
// Does pid exist?
|
|
226
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'pid')) {
|
|
227
|
+
// Parse string into int
|
|
228
|
+
// Update PID
|
|
229
|
+
pid = Number.parseInt(attr.pid, 10);
|
|
230
|
+
} else {
|
|
231
|
+
console.warn('Passages are required to have PID. Will not add!');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check the current PID against startNode number.
|
|
235
|
+
if (pid === startNode) {
|
|
236
|
+
// These match.
|
|
237
|
+
// Save the passage name.
|
|
238
|
+
story.start = name;
|
|
239
|
+
}
|
|
240
|
+
|
|
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!');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Look for the style element
|
|
262
|
+
const styleElement = dom.querySelector('#twine-user-stylesheet');
|
|
263
|
+
|
|
264
|
+
// Does the style element exist?
|
|
265
|
+
if (styleElement !== null) {
|
|
266
|
+
// Check if there is any content.
|
|
267
|
+
if (styleElement.rawText.length > 0) {
|
|
268
|
+
// Update stylesheet passage
|
|
269
|
+
story.addPassage(new Passage(
|
|
270
|
+
'UserStylesheet',
|
|
271
|
+
styleElement.rawText,
|
|
272
|
+
['stylesheet'])
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Look for the script element
|
|
278
|
+
const scriptElement = dom.querySelector('#twine-user-script');
|
|
279
|
+
|
|
280
|
+
// Does the script element exist?
|
|
281
|
+
if (scriptElement !== null) {
|
|
282
|
+
// Check if there is any content.
|
|
283
|
+
if (scriptElement.rawText.length > 0) {
|
|
284
|
+
story.addPassage(new Passage(
|
|
285
|
+
'UserScript',
|
|
286
|
+
scriptElement.rawText,
|
|
287
|
+
['script'])
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Look for all <tw-tag> elements.
|
|
293
|
+
const twTags = dom.querySelectorAll('tw-tag');
|
|
294
|
+
|
|
295
|
+
// Parse through the entries.
|
|
296
|
+
twTags.forEach((tags) => {
|
|
297
|
+
// Parse each tag element
|
|
298
|
+
const attributes = tags.attributes;
|
|
299
|
+
|
|
300
|
+
// Create default value for name
|
|
301
|
+
let name = '';
|
|
302
|
+
|
|
303
|
+
// Create default value for color
|
|
304
|
+
let color = '';
|
|
305
|
+
|
|
306
|
+
// Check for name
|
|
307
|
+
if (Object.prototype.hasOwnProperty.call(attributes, 'name')) {
|
|
308
|
+
name = attributes.name;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check for color
|
|
312
|
+
if (Object.prototype.hasOwnProperty.call(attributes, 'color')) {
|
|
313
|
+
color = attributes.color;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// If both are not empty strings, use them.
|
|
317
|
+
if (name !== '' && color !== '') {
|
|
318
|
+
// Add name and color to the object
|
|
319
|
+
story.tagColors[name] = color;
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Return the parsed story.
|
|
324
|
+
return story;
|
|
325
|
+
}
|
|
326
|
+
|
|
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;
|
package/src/extwee.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file CLI for Extwee
|
|
5
|
+
* @author Dan Cox
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Import functions we need.
|
|
9
|
+
import {
|
|
10
|
+
parseTwine2HTML,
|
|
11
|
+
parseTwee,
|
|
12
|
+
parseStoryFormat,
|
|
13
|
+
parseTwine1HTML,
|
|
14
|
+
compileTwine2HTML,
|
|
15
|
+
compileTwine1HTML
|
|
16
|
+
} from '../index.js';
|
|
17
|
+
|
|
18
|
+
// Import fs.
|
|
19
|
+
import { readFileSync, writeFileSync, statSync } from 'node:fs';
|
|
20
|
+
|
|
21
|
+
// Import Commander.
|
|
22
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
23
|
+
|
|
24
|
+
// Create a new Command.
|
|
25
|
+
const program = new Command();
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Check if a passed option is a valid file.
|
|
29
|
+
* @function isFile
|
|
30
|
+
* @description Check if a file exists.
|
|
31
|
+
* @param {string} path - Path to file.
|
|
32
|
+
* @returns {boolean} True if file exists, false if not.
|
|
33
|
+
*/
|
|
34
|
+
const isFile = (path) => {
|
|
35
|
+
// set default.
|
|
36
|
+
let result = false;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Attempt tp get stats.
|
|
40
|
+
const stats = statSync(path);
|
|
41
|
+
|
|
42
|
+
// Return if path is a file.
|
|
43
|
+
result = stats.isFile();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// There was an error, so return false.
|
|
46
|
+
result = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Return either the default (false) or the result (true).
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
program
|
|
54
|
+
.name('extwee')
|
|
55
|
+
.description('CLI for Extwee')
|
|
56
|
+
.version('2.2.0', '-v, -V, --version', 'Output the current version')
|
|
57
|
+
.option('-c, --compile', 'Compile input into output')
|
|
58
|
+
.option('-d, --decompile', 'De-compile input into output')
|
|
59
|
+
.option('-t1, --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
|
+
// Does the input file exist?
|
|
63
|
+
if (isFile(value) === false) {
|
|
64
|
+
// We cannot do anything without valid input.
|
|
65
|
+
throw new InvalidArgumentError(`Twine 1 code.js ${value} does not exist.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return value;
|
|
69
|
+
})
|
|
70
|
+
.option('-engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
|
|
71
|
+
// Does the input file exist?
|
|
72
|
+
if (isFile(value) === false) {
|
|
73
|
+
// We cannot do anything without valid input.
|
|
74
|
+
throw new InvalidArgumentError(`Twine 1 engine.js ${value} does not exist.`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return value;
|
|
78
|
+
})
|
|
79
|
+
.option('-header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
|
|
80
|
+
// Does the input file exist?
|
|
81
|
+
if (isFile(value) === false) {
|
|
82
|
+
// We cannot do anything without valid input.
|
|
83
|
+
throw new InvalidArgumentError(`Twine 1 header.html ${value} does not exist.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return value;
|
|
87
|
+
})
|
|
88
|
+
.option('-s <storyformat>, --storyformat <storyformat>', 'Path to story format file for Twine 2', (value) => {
|
|
89
|
+
// Does the input file exist?
|
|
90
|
+
if (isFile(value) === false) {
|
|
91
|
+
// We cannot do anything without valid input.
|
|
92
|
+
throw new InvalidArgumentError(`Story format ${value} does not exist.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return value;
|
|
96
|
+
})
|
|
97
|
+
.option('-i <inputFile>, --input <inputFile>', 'Path to input file', (value) => {
|
|
98
|
+
// Does the input file exist?
|
|
99
|
+
if (isFile(value) === false) {
|
|
100
|
+
// We cannot do anything without valid input.
|
|
101
|
+
throw new InvalidArgumentError(`Input file ${value} does not exist.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return value;
|
|
105
|
+
})
|
|
106
|
+
.option('-o <outputFile>, --output <outputFile>', 'Path to output file');
|
|
107
|
+
|
|
108
|
+
// Parse the passed arguments.
|
|
109
|
+
program.parse(process.argv);
|
|
110
|
+
|
|
111
|
+
// Create object of passed arguments parsed by Commander.
|
|
112
|
+
const options = program.opts();
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* Prepare some (soon to be) global variables.
|
|
116
|
+
*/
|
|
117
|
+
// Check if Twine 1 is enabled.
|
|
118
|
+
const isTwine1Mode = (options.twine1 === true);
|
|
119
|
+
|
|
120
|
+
// Check if Twine 2 is enabled.
|
|
121
|
+
const isTwine2Mode = (isTwine1Mode === false);
|
|
122
|
+
|
|
123
|
+
// Check if de-compile mode is enabled.
|
|
124
|
+
const isDecompileMode = (options.decompile === true);
|
|
125
|
+
|
|
126
|
+
// Check if compile mode is enabled.
|
|
127
|
+
const isCompileMode = (options.compile === true);
|
|
128
|
+
|
|
129
|
+
// De-compile Twine 2 HTML into Twee 3 branch.
|
|
130
|
+
// If -d is passed, -i and -o are required.
|
|
131
|
+
if (isTwine2Mode === true && isDecompileMode === true) {
|
|
132
|
+
// Read the input HTML file.
|
|
133
|
+
const inputHTML = readFileSync(options.i, 'utf-8');
|
|
134
|
+
|
|
135
|
+
// Parse the input HTML file into Story object.
|
|
136
|
+
const storyObject = parseTwine2HTML(inputHTML);
|
|
137
|
+
|
|
138
|
+
// Write the output file from Story as Twee 3.
|
|
139
|
+
writeFileSync(options.o, storyObject.toTwee());
|
|
140
|
+
|
|
141
|
+
// Exit the process.
|
|
142
|
+
process.exit();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Compile Twee 3 into Twine 2 HTML branch.
|
|
146
|
+
// If -c is passed, -i, -o, and -s are required.
|
|
147
|
+
if (isTwine2Mode === true && isCompileMode === true) {
|
|
148
|
+
// Read the input file.
|
|
149
|
+
const inputTwee = readFileSync(options.i, 'utf-8');
|
|
150
|
+
|
|
151
|
+
// Parse the input file.
|
|
152
|
+
const story = parseTwee(inputTwee);
|
|
153
|
+
|
|
154
|
+
// Read the story format file.
|
|
155
|
+
const inputStoryFormat = readFileSync(options.s, 'utf-8');
|
|
156
|
+
|
|
157
|
+
// Parse the story format file.
|
|
158
|
+
const parsedStoryFormat = parseStoryFormat(inputStoryFormat);
|
|
159
|
+
|
|
160
|
+
// Compile the story.
|
|
161
|
+
const Twine2HTML = compileTwine2HTML(story, parsedStoryFormat);
|
|
162
|
+
|
|
163
|
+
// Write the output file.
|
|
164
|
+
writeFileSync(options.o, Twine2HTML);
|
|
165
|
+
|
|
166
|
+
// Exit the process.
|
|
167
|
+
process.exit();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Compile Twee 3 into Twine 1 HTML branch.
|
|
171
|
+
// Twine 1 compilation is complicated, so we have to check for all the required options.
|
|
172
|
+
// * options.engine (from Twine 1 itself)
|
|
173
|
+
// * options.header (from Twine 1 story format)
|
|
174
|
+
// * options.name (from Twine 1 story format)
|
|
175
|
+
// * options.codejs (from Twine 1 story format)
|
|
176
|
+
if (isTwine1Mode === true && isCompileMode === true) {
|
|
177
|
+
// Read the input file.
|
|
178
|
+
const inputTwee = readFileSync(options.i, 'utf-8');
|
|
179
|
+
|
|
180
|
+
// Parse the input file.
|
|
181
|
+
const story = parseTwee(inputTwee);
|
|
182
|
+
|
|
183
|
+
// Does the engine file exist?
|
|
184
|
+
const Twine1HTML = compileTwine1HTML(story, options.engine, options.header, options.name, options.codejs);
|
|
185
|
+
|
|
186
|
+
// Write the output file.
|
|
187
|
+
writeFileSync(options.o, Twine1HTML);
|
|
188
|
+
|
|
189
|
+
// Exit the process.
|
|
190
|
+
process.exit();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// De-compile Twine 1 HTML into Twee 3 branch.
|
|
194
|
+
if (isTwine1Mode === true && isDecompileMode === true) {
|
|
195
|
+
// Read the input HTML file.
|
|
196
|
+
const inputHTML = readFileSync(options.i, 'utf-8');
|
|
197
|
+
|
|
198
|
+
// Parse the input HTML file into Story object.
|
|
199
|
+
const storyObject = parseTwine1HTML(inputHTML);
|
|
200
|
+
|
|
201
|
+
// Write the output file from Story as Twee 3.
|
|
202
|
+
writeFileSync(options.o, storyObject.toTwee());
|
|
203
|
+
|
|
204
|
+
// Exit the process.
|
|
205
|
+
process.exit();
|
|
206
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import shell from 'shelljs';
|
|
2
|
+
|
|
3
|
+
// We could get this from process,
|
|
4
|
+
// but since we are using shelljs,
|
|
5
|
+
// we ask for the pwd() instead of cwd().
|
|
6
|
+
const currentPath = shell.pwd().stdout;
|
|
7
|
+
const testFilePath = currentPath + '/test/CLI/files';
|
|
8
|
+
|
|
9
|
+
describe('CLI', () => {
|
|
10
|
+
// Remove the test files, if they exist.
|
|
11
|
+
beforeAll(() => {
|
|
12
|
+
// Test for files beginning with "test." in the output directory.
|
|
13
|
+
if (shell.ls('-A', `${testFilePath}/output/`).length > 0) {
|
|
14
|
+
// Remove the files.
|
|
15
|
+
shell.rm(`${testFilePath}/output/*`);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('Twine 2 - de-compile: Twine 2 HTML into Twee 3', () => {
|
|
20
|
+
shell.exec(`node ${currentPath}/src/extwee.js -d -i ${testFilePath}/input.html -o ${testFilePath}/output/test.twee`);
|
|
21
|
+
expect(shell.test('-e', `${testFilePath}/output/test.twee`)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('Twine 2 - compile: Twee 3 + StoryFormat into Twine 2 HTML', () => {
|
|
25
|
+
shell.exec(`node ${currentPath}/src/extwee.js -c -i ${testFilePath}/example6.twee -s ${testFilePath}/harlowe.js -o ${testFilePath}/output/test2.html`);
|
|
26
|
+
expect(shell.test('-e', `${testFilePath}/output/test2.html`)).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
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 -t1 -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
|
+
expect(shell.test('-e', `${testFilePath}/output/test3.html`)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('Twine 1 - de-compile: Twine 1 HTML into Twee 3', () => {
|
|
35
|
+
shell.exec(`node ${currentPath}/src/extwee.js -t1 -d -i ${testFilePath}/twine1Test.html -o ${testFilePath}/output/test.twee`);
|
|
36
|
+
expect(shell.test('-e', `${testFilePath}/output/test.twee`)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Remove the test files, if they exist.
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
// Test for files in the output directory.
|
|
42
|
+
if (shell.ls('-A', `${testFilePath}/output/`).length > 0) {
|
|
43
|
+
// Remove the files.
|
|
44
|
+
shell.rm(`${testFilePath}/output/*`);
|
|
45
|
+
}
|
|
46
|
+
// Create one file to prevent git from ignoring the folder.
|
|
47
|
+
shell.touch(`${testFilePath}/output/test.twee`);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"Test","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":{"s":"e"},"text":"Word"}]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
:: StoryData
|
|
2
|
+
{
|
|
3
|
+
"ifid": "D674C58C-DEFA-4F70-B7A2-27742230C0FC",
|
|
4
|
+
"format": "SugarCube",
|
|
5
|
+
"format-version": "2.28.2",
|
|
6
|
+
"start": "Start",
|
|
7
|
+
"tag-colors": {
|
|
8
|
+
"bar": "green",
|
|
9
|
+
"foo": "red",
|
|
10
|
+
"qaz": "blue"
|
|
11
|
+
},
|
|
12
|
+
"zoom": 0.25
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:: StoryTitle
|
|
16
|
+
twineExample
|
|
17
|
+
|
|
18
|
+
:: Start [tag tags] {"position": "200,200", "size": "100,100"}
|
|
19
|
+
Content
|
|
20
|
+
|
|
21
|
+
:: Style1 [stylesheet]
|
|
22
|
+
body {background-color: green;}
|