extwee 1.5.3 → 2.0.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.
Files changed (144) hide show
  1. package/.eslintrc.json +25 -0
  2. package/.github/workflows/nodejs.yml +24 -0
  3. package/.travis.yml +13 -0
  4. package/CODE_OF_CONDUCT.md +82 -0
  5. package/LICENSE +21 -21
  6. package/README.md +36 -203
  7. package/SECURITY.md +12 -0
  8. package/babel.config.json +22 -0
  9. package/bin/extwee.js +49 -0
  10. package/index.js +31 -26
  11. package/package.json +60 -39
  12. package/src/FileReader.js +33 -0
  13. package/src/HTMLParser.js +343 -0
  14. package/src/HTMLWriter.js +196 -0
  15. package/src/Passage.js +202 -0
  16. package/src/Story.js +461 -0
  17. package/src/StoryFormat.js +300 -0
  18. package/src/StoryFormatParser.js +142 -0
  19. package/src/TweeParser.js +166 -0
  20. package/src/TweeWriter.js +98 -0
  21. package/story-formats/chapbook-1.2.0/format.js +1 -0
  22. package/story-formats/chapbook-1.2.0/logo.svg +1 -0
  23. package/story-formats/harlowe-1.2.4/format.js +1 -0
  24. package/story-formats/harlowe-1.2.4/icon.svg +78 -0
  25. package/story-formats/harlowe-2.1.0/format.js +2 -0
  26. package/story-formats/harlowe-2.1.0/icon.svg +78 -0
  27. package/story-formats/harlowe-3.1.0/format.js +3 -0
  28. package/story-formats/harlowe-3.1.0/icon.svg +78 -0
  29. package/story-formats/paperthin-1.0.0/format.js +1 -0
  30. package/story-formats/paperthin-1.0.0/icon.svg +5 -0
  31. package/story-formats/snowman-1.4.0/format.js +1 -0
  32. package/story-formats/snowman-1.4.0/icon.svg +436 -0
  33. package/story-formats/snowman-2.0.2/format.js +1 -0
  34. package/story-formats/snowman-2.0.2/icon.svg +436 -0
  35. package/story-formats/sugarcube-1.0.35/LICENSE +23 -0
  36. package/story-formats/sugarcube-1.0.35/format.js +1 -0
  37. package/story-formats/sugarcube-1.0.35/icon.svg +56 -0
  38. package/story-formats/sugarcube-2.31.1/LICENSE +22 -0
  39. package/story-formats/sugarcube-2.31.1/format.js +1 -0
  40. package/story-formats/sugarcube-2.31.1/icon.svg +56 -0
  41. package/test/CLI/example6.twee +16 -0
  42. package/test/CLI/harlowe.js +3 -0
  43. package/test/CLI/input.html +47 -0
  44. package/test/CLI/test.twee +18 -0
  45. package/test/CLI/test2.html +47 -0
  46. package/test/CLI/tweeExample.twee +17 -0
  47. package/test/CLI/twineExample.html +15 -0
  48. package/test/CLI.test.js +30 -0
  49. package/test/FileReader/t1.txt +1 -0
  50. package/test/FileReader.test.js +14 -0
  51. package/test/HTMLParser/Example1.html +53 -0
  52. package/test/HTMLParser/Tags.html +15 -0
  53. package/test/HTMLParser/lyingStartnode.html +15 -0
  54. package/test/HTMLParser/lyingTagColors.html +48 -0
  55. package/test/HTMLParser/missingCreator.html +11 -0
  56. package/test/HTMLParser/missingCreatorVersion.html +11 -0
  57. package/test/HTMLParser/missingFormat.html +11 -0
  58. package/test/HTMLParser/missingFormatVersion.html +11 -0
  59. package/test/HTMLParser/missingIFID.html +11 -0
  60. package/test/HTMLParser/missingName.html +33 -0
  61. package/test/HTMLParser/missingPID.html +15 -0
  62. package/test/HTMLParser/missingPassageName.html +15 -0
  63. package/test/HTMLParser/missingPassageTags.html +15 -0
  64. package/test/HTMLParser/missingPosition.html +15 -0
  65. package/test/HTMLParser/missingScript.html +14 -0
  66. package/test/HTMLParser/missingSize.html +35 -0
  67. package/test/HTMLParser/missingStartnode.html +11 -0
  68. package/test/HTMLParser/missingStyle.html +14 -0
  69. package/test/HTMLParser/missingZoom.html +11 -0
  70. package/test/HTMLParser/tagColors.html +31 -0
  71. package/test/HTMLParser/twineExample.html +15 -0
  72. package/test/HTMLParser/twineExample2.html +15 -0
  73. package/test/HTMLParser/twineExample3.html +15 -0
  74. package/test/HTMLParser.test.js +177 -0
  75. package/test/HTMLWriter/TestTags.html +42 -0
  76. package/test/HTMLWriter/creator.html +51 -0
  77. package/test/HTMLWriter/example6.twee +16 -0
  78. package/test/HTMLWriter/missingStoryTitle.twee +29 -0
  79. package/test/HTMLWriter/test11.html +123 -0
  80. package/test/HTMLWriter/test2.html +59 -0
  81. package/test/HTMLWriter/test3.html +50 -0
  82. package/test/HTMLWriter/test4.html +51 -0
  83. package/test/HTMLWriter/test6.html +50 -0
  84. package/test/HTMLWriter.test.js +279 -0
  85. package/test/Passage.test.js +104 -0
  86. package/test/Roundtrip/Example1.html +64 -0
  87. package/test/Roundtrip/example1.twee +21 -0
  88. package/test/Roundtrip/example2.twee +18 -0
  89. package/test/Roundtrip/harlowe.js +3 -0
  90. package/test/Roundtrip/round.html +50 -0
  91. package/test/Roundtrip.test.js +48 -0
  92. package/test/Story/startmeta.twee +29 -0
  93. package/test/Story/test.twee +25 -0
  94. package/test/Story.test.js +282 -0
  95. package/test/StoryFormat.test.js +152 -0
  96. package/test/StoryFormatParser/example.js +3 -0
  97. package/test/StoryFormatParser/example2.js +3 -0
  98. package/test/StoryFormatParser/format.js +1 -0
  99. package/test/StoryFormatParser/format_doublename.js +1 -0
  100. package/test/StoryFormatParser/harlowe.js +3 -0
  101. package/test/StoryFormatParser/missingAuthor.js +1 -0
  102. package/test/StoryFormatParser/missingDescription.js +1 -0
  103. package/test/StoryFormatParser/missingImage.js +1 -0
  104. package/test/StoryFormatParser/missingLicense.js +1 -0
  105. package/test/StoryFormatParser/missingName.js +1 -0
  106. package/test/StoryFormatParser/missingProofing.js +1 -0
  107. package/test/StoryFormatParser/missingSource.js +1 -0
  108. package/test/StoryFormatParser/missingURL.js +1 -0
  109. package/test/StoryFormatParser/missingVersion.js +1 -0
  110. package/test/StoryFormatParser/versionWrong.js +1 -0
  111. package/test/StoryFormatParser.test.js +91 -0
  112. package/test/TweeParser/emptytags.twee +2 -0
  113. package/test/TweeParser/example.twee +32 -0
  114. package/test/TweeParser/missing.twee +19 -0
  115. package/test/TweeParser/multipleScriptPassages.twee +19 -0
  116. package/test/TweeParser/multipleStyleTag.twee +19 -0
  117. package/test/TweeParser/multipletags.twee +10 -0
  118. package/test/TweeParser/noTitle.twee +2 -0
  119. package/test/TweeParser/notes.twee +16 -0
  120. package/test/TweeParser/pasagemetadataerror.twee +2 -0
  121. package/test/TweeParser/scriptPassage.twee +16 -0
  122. package/test/TweeParser/singletag.twee +13 -0
  123. package/test/TweeParser/startMetadata.twee +14 -0
  124. package/test/TweeParser/storydataerror.twee +25 -0
  125. package/test/TweeParser/stylePassage.twee +16 -0
  126. package/test/TweeParser/test.twee +25 -0
  127. package/test/TweeParser.test.js +79 -0
  128. package/test/TweeWriter/test1.twee +18 -0
  129. package/test/TweeWriter/test3.twee +12 -0
  130. package/test/TweeWriter/test4.twee +14 -0
  131. package/test/TweeWriter/test5.twee +20 -0
  132. package/test/TweeWriter.test.js +82 -0
  133. package/DirectoryReader.js +0 -128
  134. package/DirectoryWatcher.js +0 -63
  135. package/FileReader.js +0 -36
  136. package/HTMLParser.js +0 -206
  137. package/HTMLWriter.js +0 -177
  138. package/Passage.js +0 -20
  139. package/Story.js +0 -148
  140. package/StoryFormat.js +0 -41
  141. package/StoryFormatParser.js +0 -65
  142. package/TweeParser.js +0 -255
  143. package/TweeWriter.js +0 -111
  144. package/main.js +0 -103
package/package.json CHANGED
@@ -1,39 +1,60 @@
1
- {
2
- "name": "extwee",
3
- "version": "1.5.3",
4
- "description": "A Twee 3 compiler written in JS.",
5
- "author": "Dan Cox",
6
- "main": "index.js",
7
- "scripts": {
8
- "start": "node main.js",
9
- "test": "nyc mocha test/test.js"
10
- },
11
- "keywords": [
12
- "twine",
13
- "twee",
14
- "parser",
15
- "compiler"
16
- ],
17
- "license": "MIT",
18
- "dependencies": {
19
- "@babel/core": "^7.6.0",
20
- "@babel/preset-env": "^7.6.0",
21
- "chokidar": "^3.0.2",
22
- "clean-css": "^4.2.1",
23
- "mocha": "^6.2.0",
24
- "node-html-parser": "^1.1.16",
25
- "shelljs": "^0.8.3",
26
- "uglify-js": "^3.6.0",
27
- "yargs": "^13.3.0"
28
- },
29
- "devDependencies": {
30
- "nyc": "^14.1.1"
31
- },
32
- "repository": {
33
- "type": "git",
34
- "url": "https://github.com/videlais/extwee"
35
- },
36
- "bugs": {
37
- "url": "https://github.com/videlais/extwee/issues"
38
- }
39
- }
1
+ {
2
+ "name": "extwee",
3
+ "version": "2.0.0",
4
+ "description": "A Twee 3 compiler written in JS.",
5
+ "author": "Dan Cox",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "extwee": "bin/extwee.js"
9
+ },
10
+ "scripts": {
11
+ "test": "jest --silent --runInBand --coverage --colors",
12
+ "test:watch": "jest --watch",
13
+ "lint": "eslint ./src/**/*.js --fix",
14
+ "lint:test": "eslint ./test/*.test.js --fix",
15
+ "all": "npm run lint && npm run lint:test && npm run test"
16
+ },
17
+ "keywords": [
18
+ "twine",
19
+ "twee",
20
+ "parser",
21
+ "compiler"
22
+ ],
23
+ "license": "MIT",
24
+ "dependencies": {
25
+ "commander": "^4.1.1",
26
+ "node-html-parser": "^1.2.16",
27
+ "uuid": "^8.3.2"
28
+ },
29
+ "devDependencies": {
30
+ "@babel/cli": "^7.12.10",
31
+ "@babel/core": "^7.12.10",
32
+ "@babel/eslint-parser": "^7.12.1",
33
+ "@babel/eslint-plugin": "^7.12.1",
34
+ "@babel/plugin-proposal-class-properties": "^7.12.1",
35
+ "@babel/plugin-transform-runtime": "^7.12.10",
36
+ "@babel/polyfill": "^7.12.1",
37
+ "@babel/preset-env": "^7.12.11",
38
+ "babel-loader": "^8.2.2",
39
+ "core-js": "^3.8.1",
40
+ "eslint": "^7.7.0",
41
+ "eslint-config-standard": "^14.1.0",
42
+ "eslint-plugin-import": "^2.20.1",
43
+ "eslint-plugin-jest": "^24.1.3",
44
+ "eslint-plugin-jsdoc": "^30.2.1",
45
+ "eslint-plugin-node": "^11.0.0",
46
+ "eslint-plugin-promise": "^4.2.1",
47
+ "eslint-plugin-standard": "^4.0.1",
48
+ "jest": "^27.3.1",
49
+ "semver": "^5.7.1",
50
+ "shelljs": "^0.8.4"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/videlais/extwee"
55
+ },
56
+ "bugs": {
57
+ "url": "https://github.com/videlais/extwee/issues"
58
+ },
59
+ "type": "module"
60
+ }
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ /**
3
+ * @class FileReader
4
+ * @module FileReader
5
+ */
6
+ export default class FileReader {
7
+ /**
8
+ * Read a text file
9
+ *
10
+ * @static
11
+ * @public
12
+ * @function readFile
13
+ * @memberof FileReader
14
+ * @param {string} file - Path of file to read
15
+ * @returns {string} Content of file
16
+ */
17
+ static read (file) {
18
+ // Setup default value
19
+ let contents = '';
20
+
21
+ // Attempt to find the file
22
+ if (fs.existsSync(file)) {
23
+ // The file exists.
24
+ contents = fs.readFileSync(file, 'utf8');
25
+ } else {
26
+ // Throw error if file does not exist
27
+ throw new Error('Error: File not found!');
28
+ }
29
+
30
+ // Return default or updated values
31
+ return contents;
32
+ }
33
+ }
@@ -0,0 +1,343 @@
1
+ /**
2
+ * @external HTML
3
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML|HTML}
4
+ */
5
+
6
+ import { parse as HtmlParser } from 'node-html-parser';
7
+ import Story from './Story.js';
8
+ import Passage from './Passage.js';
9
+ /**
10
+ * @class HTMLParser
11
+ * @module HTMLParser
12
+ */
13
+ export default class HTMLParser {
14
+ /**
15
+ * Parse HTML text into a JS DOM-like object
16
+ *
17
+ * @public
18
+ * @static
19
+ * @function parse
20
+ * @param {string} content - Content to parse
21
+ * @returns {Story} story
22
+ */
23
+ static parse (content) {
24
+ let story = null;
25
+ let startNode = null;
26
+
27
+ // Send to node-html-parser
28
+ // Enable getting the content of 'script', 'style', and 'pre' elements
29
+ // Get back a DOM
30
+ const dom = new HtmlParser(
31
+ content,
32
+ {
33
+ lowerCaseTagName: false,
34
+ script: true,
35
+ style: true,
36
+ pre: true
37
+ });
38
+
39
+ // Pull out the tw-storydata element
40
+ const storyData = dom.querySelector('tw-storydata');
41
+
42
+ // Does the <tw-storydata> element exist?
43
+ if (storyData !== null) {
44
+ // Create a Story.
45
+ story = new Story();
46
+
47
+ /**
48
+ * name: (string) Required.
49
+ * The name of the story.
50
+ */
51
+ if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
52
+ // Create StoryTitle passage based on name
53
+ story.addPassage(new Passage('StoryTitle', storyData.attributes.name));
54
+ } else {
55
+ // Name is a required filed. Warn user.
56
+ console.warn('Twine 2 HTML must have a name!');
57
+ // Set a default name
58
+ story.addPassage(new Passage('StoryTitle', 'Untitled'));
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
+ }
128
+ } else {
129
+ // If there is not a <tw-storydata> element, this is not a Twine 2 story!
130
+ throw new Error('Not a Twine 2-style file!');
131
+ }
132
+
133
+ // Pull out the tw-passagedata elements
134
+ const storyPassages = dom.querySelectorAll('tw-passagedata');
135
+
136
+ // Move through the passages
137
+ for (const passage in storyPassages) {
138
+ // Get the passage attributes
139
+ const attr = storyPassages[passage].attributes;
140
+ // Get the passage text
141
+ const text = storyPassages[passage].rawText;
142
+
143
+ // Set a default position.
144
+ let position = null;
145
+ // Does position exist?
146
+ if (Object.prototype.hasOwnProperty.call(attr, 'position')) {
147
+ // Update position.
148
+ position = attr.position;
149
+ }
150
+
151
+ // Set a default size.
152
+ let size = null;
153
+ // Does size exist?
154
+ if (Object.prototype.hasOwnProperty.call(attr, 'size')) {
155
+ // Update size.
156
+ size = attr.size;
157
+ }
158
+
159
+ /**
160
+ * name: (string) Required.
161
+ * The name of the passage.
162
+ *
163
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
164
+ */
165
+ // Create a default value
166
+ let name = null;
167
+ // Does name exist?
168
+ if (Object.prototype.hasOwnProperty.call(attr, 'name')) {
169
+ // Escape the name
170
+ name = HTMLParser.escapeMetacharacters(attr.name);
171
+ } else {
172
+ console.warn('Encountered passage without a name! Will not add.');
173
+ }
174
+
175
+ // Create empty tag array.
176
+ let tags = [];
177
+ // Does the tags attribute exist?
178
+ if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
179
+ // Escape any tags
180
+ // (Attributes can, themselves, be empty strings.)
181
+ if (attr.tags.length > 0 && attr.tags !== '""') {
182
+ // Escape the tags
183
+ tags = HTMLParser.escapeMetacharacters(attr.tags);
184
+ // Split by spaces into an array
185
+ tags = tags.split(' ');
186
+ }
187
+
188
+ // Remove any empty strings.
189
+ tags = tags.filter(tag => tag !== '');
190
+ }
191
+
192
+ // Create metadata for passage.
193
+ const metadata = {};
194
+
195
+ // Does position exist?
196
+ if (position !== null) {
197
+ // Add the property to metadata
198
+ metadata.position = position;
199
+ }
200
+
201
+ // Does size exist?
202
+ if (size !== null) {
203
+ // Add the property to metadata
204
+ metadata.size = size;
205
+ }
206
+
207
+ /**
208
+ * pid: (string) Required.
209
+ * The Passage ID (PID).
210
+ * (Note: This is subject to change during editing with Twine 2.)
211
+ *
212
+ * https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md#passages
213
+ */
214
+ // Create a default PID
215
+ let pid = -1;
216
+ // Does pid exist?
217
+ if (Object.prototype.hasOwnProperty.call(attr, 'pid')) {
218
+ // Parse string into int
219
+ // Update PID
220
+ pid = Number.parseInt(attr.pid, 10);
221
+ } else {
222
+ console.warn('Passages are required to have PID. Will not add!');
223
+ }
224
+
225
+ // If passage is missing name and PID (required attributes),
226
+ // they are not added.
227
+ if (name !== null && pid !== -1) {
228
+ // Add a new Passage into an array
229
+ story.addPassage(
230
+ new Passage(
231
+ name,
232
+ text,
233
+ tags,
234
+ metadata,
235
+ pid
236
+ )
237
+ );
238
+ }
239
+ }
240
+
241
+ // Look for the style element
242
+ const styleElement = dom.querySelector('#twine-user-stylesheet');
243
+
244
+ // Does the style element exist?
245
+ if (styleElement !== null) {
246
+ // Check if there is any content.
247
+ if (styleElement.rawText.length > 0) {
248
+ // Update stylesheet passage
249
+ story.addPassage(new Passage(
250
+ 'UserStylesheet',
251
+ styleElement.rawText,
252
+ ['stylesheet'])
253
+ );
254
+ }
255
+ }
256
+
257
+ // Look for the script element
258
+ const scriptElement = dom.querySelector('#twine-user-script');
259
+
260
+ // Does the script element exist?
261
+ if (scriptElement !== null) {
262
+ // Check if there is any content.
263
+ if (scriptElement.rawText.length > 0) {
264
+ story.addPassage(new Passage(
265
+ 'UserScript',
266
+ scriptElement.rawText,
267
+ ['script'])
268
+ );
269
+ }
270
+ }
271
+
272
+ // Was there a startNode?
273
+ if (startNode !== null) {
274
+ // Try to find starting passage by PID.
275
+ const startingPassage = story.getPassageByPID(startNode);
276
+ // Does the passage exist (yet)?
277
+ if (startingPassage !== null) {
278
+ // If so, update property to name of passage.
279
+ story.start = startingPassage.name;
280
+ } else {
281
+ throw new Error('Invalid startnode detected in <tw-storydata>!');
282
+ }
283
+ }
284
+
285
+ // Look for all <tw-tag> elements
286
+ const twTags = dom.querySelectorAll('tw-tag');
287
+
288
+ // Parse through the entries
289
+ twTags.forEach((tags) => {
290
+ // Parse each tag element
291
+ const attributes = tags.attributes;
292
+
293
+ // Create default value for name
294
+ let name = '';
295
+
296
+ // Create default value for color
297
+ let color = '';
298
+
299
+ // Check for name
300
+ if (Object.prototype.hasOwnProperty.call(attributes, 'name')) {
301
+ name = attributes.name;
302
+ }
303
+
304
+ // Check for color
305
+ if (Object.prototype.hasOwnProperty.call(attributes, 'color')) {
306
+ color = attributes.color;
307
+ }
308
+
309
+ // If both are not empty strings, use them.
310
+ if (name !== '' && color !== '') {
311
+ // Add name and color to the object
312
+ story.tagColors[name] = color;
313
+ }
314
+ });
315
+
316
+ // Return the parsed story
317
+ return story;
318
+ }
319
+
320
+ /**
321
+ * Try to escape meta-characters
322
+ *
323
+ * @public
324
+ * @static
325
+ * @function escapeMetacharacters
326
+ * @param {string} result - Text to parse
327
+ * @returns {string} Escaped characters
328
+ */
329
+ static escapeMetacharacters (result) {
330
+ // Replace any single backslash with two of them
331
+ result = result.replace(/\\/g, '\\');
332
+ // Double-escape escaped {
333
+ result = result.replace(/\\\{/g, '\\\\{');
334
+ // Double-escape escaped }
335
+ result = result.replace(/\\\}/g, '\\\\}');
336
+ // Double-escape escaped [
337
+ result = result.replace(/\\\[/g, '\\\\[');
338
+ // Double-escape escaped ]
339
+ result = result.replace(/\\\]/g, '\\\\]');
340
+
341
+ return result;
342
+ }
343
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * @external Story
3
+ * @see Story.js
4
+ * @external StoryFormat
5
+ * @see StoryFormat.js
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import Story from './Story.js';
10
+ import StoryFormat from './StoryFormat.js';
11
+ import { v4 as uuidv4 } from 'uuid';
12
+
13
+ /**
14
+ * @class HTMLWriter
15
+ * @module HTMLWriter
16
+ */
17
+ export default class HTMLWriter {
18
+ /**
19
+ * Write story to file using story format and adding any CSS and JS
20
+ *
21
+ * @public
22
+ * @static
23
+ * @function writeFile
24
+ * @param {string} file - File to write
25
+ * @param {Story} story - Story object to write
26
+ * @param {StoryFormat} storyFormat - StoryFormat to write
27
+ */
28
+ static write (file, story, storyFormat) {
29
+ if (!(story instanceof Story)) {
30
+ throw new Error('Error: story must be a Story object!');
31
+ }
32
+
33
+ if (!(storyFormat instanceof StoryFormat)) {
34
+ throw new Error('storyFormat must be a StoryFormat object!');
35
+ }
36
+
37
+ let outputContents = '';
38
+ let storyData = '';
39
+
40
+ // Look for StoryTitle
41
+ const storyTitle = story.getPassageByName('StoryTitle');
42
+
43
+ if (storyTitle != null) {
44
+ // Use StoryTitle for name
45
+ storyData += `<tw-storydata name="${storyTitle.text}"`;
46
+ } else {
47
+ throw new Error("'name' is required attribute. (Add StoryTitle to story.)");
48
+ }
49
+
50
+ // Does start exist?
51
+ if (story.start !== '') {
52
+ // Try to get starting passage
53
+ const startingPassage = story.getPassageByName(story.start);
54
+ // Does it exist currently?
55
+ if (startingPassage !== null) {
56
+ // Add the starting passage
57
+ storyData += ` startnode="${startingPassage.pid}"`;
58
+ } else {
59
+ // Throw error if no starting passage exists
60
+ throw new Error('Starting passage not found');
61
+ }
62
+ } else {
63
+ // Throw error if no starting passage exists
64
+ throw new Error('No starting passage found!');
65
+ }
66
+
67
+ // Defaults to 'extwee' if missing.
68
+ storyData += ` creator="${story.creator}"`;
69
+
70
+ // Default to extwee version.
71
+ storyData += ` creator-version="${story.creatorVersion}"`;
72
+
73
+ // Check if IFID exists.
74
+ if (story.IFID !== '') {
75
+ // Write the existing IFID
76
+ storyData += ` ifid="${story.IFID}"`;
77
+ } else {
78
+ // Generate a new IFID
79
+ // Twine 2 uses v4 (random) UUIDs, using only capital letters
80
+ storyData += ` ifid="${uuidv4().toUpperCase()}"`;
81
+ }
82
+
83
+ // Write existing or default value.
84
+ storyData += ` zoom="${story.zoom}"`;
85
+
86
+ // Write existing or default value.
87
+ storyData += ` format="${storyFormat.name}"`;
88
+
89
+ // Write existing or default value.
90
+ storyData += ` format-version="${storyFormat.version}"`;
91
+
92
+ // Add the default.
93
+ storyData += ' options hidden>\n';
94
+
95
+ // Start the STYLE.
96
+ storyData += '\t<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">';
97
+
98
+ // Get stylesheet passages
99
+ const stylesheetPassages = story.getPassagesByTag('stylesheet');
100
+
101
+ // Concatenate passages
102
+ stylesheetPassages.forEach((passage) => {
103
+ // Add text of passages
104
+ storyData += passage.text;
105
+ // Remove from story
106
+ story.removePassage(passage);
107
+ });
108
+
109
+ // Close the STYLE
110
+ storyData += '</style>\n';
111
+
112
+ // Start the SCRIPT
113
+ storyData += '\t<script role="script" id="twine-user-script" type="text/twine-javascript">';
114
+
115
+ // Get stylesheet passages
116
+ const scriptPassages = story.getPassagesByTag('script');
117
+
118
+ // Concatenate passages
119
+ scriptPassages.forEach((passage) => {
120
+ // Add text of passages
121
+ storyData += passage.text;
122
+ // Remove from story
123
+ story.removePassage(passage);
124
+ });
125
+
126
+ // Close SCRIPT
127
+ storyData += '</script>\n';
128
+
129
+ // Build the passages
130
+ story.forEach((passage) => {
131
+ // Start the passage element
132
+ storyData += '\t<tw-passagedata';
133
+
134
+ /**
135
+ * pid: (string) Required.
136
+ * The Passage ID (PID).
137
+ */
138
+ storyData += ` pid="${passage.pid}"`;
139
+
140
+ /**
141
+ * name: (string) Required.
142
+ * The name of the passage.
143
+ */
144
+ storyData += ` name="${passage.name}"`;
145
+
146
+ /**
147
+ * tags: (string) Optional.
148
+ * Any tags for the passage separated by spaces.
149
+ */
150
+ if (passage.tags.length > 1) {
151
+ storyData += ` tags="${passage.tags.join(' ')}" `;
152
+ } else if (passage.tags.length === 1) {
153
+ storyData += ` tags="${passage.tags[0]}" `;
154
+ }
155
+
156
+ /**
157
+ * position: (string) Optional.
158
+ * Comma-separated X and Y position of the upper-left of the passage
159
+ * when viewed within the Twine 2 editor.
160
+ */
161
+ if (Object.prototype.hasOwnProperty.call(passage.metadata, 'position')) {
162
+ storyData += ` position="${passage.metadata.position}" `;
163
+ }
164
+
165
+ /**
166
+ * size: (string) Optional.
167
+ * Comma-separated width and height of the passage
168
+ * when viewed within the Twine 2 editor.
169
+ */
170
+ if (Object.prototype.hasOwnProperty.call(passage.metadata, 'size')) {
171
+ storyData += `size="${passage.metadata.size}" `;
172
+ }
173
+
174
+ storyData += `>${passage.text}</tw-passagedata>\n`;
175
+ });
176
+
177
+ storyData += '</tw-storydata>';
178
+
179
+ // Replace the story name in the source file
180
+ storyFormat.source = storyFormat.source.replace(/{{STORY_NAME}}/g, story.name);
181
+
182
+ // Replace the story data
183
+ storyFormat.source = storyFormat.source.replace('{{STORY_DATA}}', storyData);
184
+
185
+ // Combine everything together.
186
+ outputContents += storyFormat.source;
187
+
188
+ try {
189
+ // Try to write.
190
+ fs.writeFileSync(file, outputContents);
191
+ } catch (event) {
192
+ // Throw error
193
+ throw new Error('Error: Cannot write HTML file!');
194
+ }
195
+ }
196
+ }