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
@@ -0,0 +1,300 @@
1
+ /**
2
+ * @class StoryFormat
3
+ * @module StoryFormat
4
+ */
5
+ export default class StoryFormat {
6
+ /**
7
+ * Internal name
8
+ *
9
+ * @private
10
+ */
11
+ #_name = '';
12
+
13
+ /**
14
+ * Internal version
15
+ *
16
+ * @private
17
+ */
18
+ #_version = '';
19
+
20
+ /**
21
+ * Internal description
22
+ *
23
+ * @private
24
+ */
25
+ #_description = '';
26
+
27
+ /**
28
+ * Internal author
29
+ *
30
+ * @private
31
+ */
32
+ #_author = '';
33
+
34
+ /**
35
+ * Internal image
36
+ *
37
+ * @private
38
+ */
39
+ #_image = '';
40
+
41
+ /**
42
+ * Internal URL
43
+ *
44
+ * @private
45
+ */
46
+ #_url = '';
47
+
48
+ /**
49
+ * Internal license
50
+ *
51
+ * @private
52
+ */
53
+ #_license = '';
54
+
55
+ /**
56
+ * Internal proofing
57
+ *
58
+ * @private
59
+ */
60
+ #_proofing = '';
61
+
62
+ /**
63
+ * Internal source
64
+ *
65
+ * @private
66
+ */
67
+ #_source = '';
68
+
69
+ /**
70
+ * @class
71
+ * @function StoryFormat
72
+ * @param {string} name - Name
73
+ * @param {string} version - Version
74
+ * @param {string} description - Description
75
+ * @param {string} author - Author
76
+ * @param {string} image - Image
77
+ * @param {string} url - URL
78
+ * @param {string} license - License
79
+ * @param {boolean} proofing - If proofing or not
80
+ * @param {string} source - Source
81
+ */
82
+ constructor (
83
+ name = '',
84
+ version = '',
85
+ description = '',
86
+ author = '',
87
+ image = '',
88
+ url = '',
89
+ license = '',
90
+ proofing = false,
91
+ source = ''
92
+ ) {
93
+ // Set name
94
+ this.name = name;
95
+
96
+ // Set version
97
+ this.version = version;
98
+
99
+ // Set description
100
+ this.description = description;
101
+
102
+ // Set author
103
+ this.author = author;
104
+
105
+ // Set image
106
+ this.image = image;
107
+
108
+ // Set URL
109
+ this.url = url;
110
+
111
+ // Set license
112
+ this.license = license;
113
+
114
+ // Set proofing
115
+ this.proofing = proofing;
116
+
117
+ // Set source
118
+ this.source = source;
119
+ }
120
+
121
+ /**
122
+ * Name
123
+ *
124
+ * @public
125
+ * @memberof StoryFormat
126
+ * @returns {string} Name
127
+ */
128
+ get name () { return this.#_name; }
129
+
130
+ /**
131
+ * @param {string} n - Replacement name
132
+ */
133
+ set name (n) {
134
+ if (typeof n === 'string') {
135
+ this.#_name = n;
136
+ } else {
137
+ throw new Error('Name must be a string!');
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Version
143
+ *
144
+ * @public
145
+ * @memberof StoryFormat
146
+ * @returns {string} Version
147
+ */
148
+ get version () { return this.#_version; }
149
+
150
+ /**
151
+ * @param {string} n - Replacement version
152
+ */
153
+ set version (n) {
154
+ if (typeof n === 'string') {
155
+ this.#_version = n;
156
+ } else {
157
+ throw new Error('Version must be a string!');
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Description
163
+ *
164
+ * @public
165
+ * @memberof StoryFormat
166
+ * @returns {string} Description
167
+ */
168
+ get description () { return this.#_description; }
169
+
170
+ /**
171
+ * @param {string} d - Replacement description
172
+ */
173
+ set description (d) {
174
+ if (typeof d === 'string') {
175
+ this.#_description = d;
176
+ } else {
177
+ throw new Error('Description must be a string!');
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Author
183
+ *
184
+ * @public
185
+ * @memberof StoryFormat
186
+ * @returns {string} Author
187
+ */
188
+ get author () { return this.#_author; }
189
+
190
+ /**
191
+ * @param {string} a - Replacement author
192
+ */
193
+ set author (a) {
194
+ if (typeof a === 'string') {
195
+ this.#_author = a;
196
+ } else {
197
+ throw new Error('Author must be a string!');
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Image
203
+ *
204
+ * @public
205
+ * @memberof StoryFormat
206
+ * @returns {string} Image
207
+ */
208
+ get image () { return this.#_image; }
209
+
210
+ /**
211
+ * @param {string} i - Replacement image
212
+ */
213
+ set image (i) {
214
+ if (typeof i === 'string') {
215
+ this.#_image = i;
216
+ } else {
217
+ throw new Error('Image must be a string!');
218
+ }
219
+ }
220
+
221
+ /**
222
+ * URL
223
+ *
224
+ * @public
225
+ * @memberof StoryFormat
226
+ * @returns {string} URL
227
+ */
228
+ get url () { return this.#_url; }
229
+
230
+ /**
231
+ * @param {string} u - Replacement URL
232
+ */
233
+ set url (u) {
234
+ if (typeof u === 'string') {
235
+ this.#_url = u;
236
+ } else {
237
+ throw new Error('URL must be a string!');
238
+ }
239
+ }
240
+
241
+ /**
242
+ * License
243
+ *
244
+ * @public
245
+ * @memberof StoryFormat
246
+ * @returns {string} License
247
+ */
248
+ get license () { return this.#_license; }
249
+
250
+ /**
251
+ * @param {string} l - Replacement license
252
+ */
253
+ set license (l) {
254
+ if (typeof l === 'string') {
255
+ this.#_license = l;
256
+ } else {
257
+ throw new Error('License must be a string!');
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Proofing
263
+ *
264
+ * @public
265
+ * @memberof StoryFormat
266
+ * @returns {boolean} Proofing
267
+ */
268
+ get proofing () { return this.#_proofing; }
269
+
270
+ /**
271
+ * @param {boolean} p - Replacement proofing
272
+ */
273
+ set proofing (p) {
274
+ if (typeof p === 'boolean') {
275
+ this.#_proofing = p;
276
+ } else {
277
+ throw new Error('Proofing must be a Boolean!');
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Source
283
+ *
284
+ * @public
285
+ * @memberof StoryFormat
286
+ * @returns {string} Source
287
+ */
288
+ get source () { return this.#_source; }
289
+
290
+ /**
291
+ * @param {string} s - Replacement source
292
+ */
293
+ set source (s) {
294
+ if (typeof s === 'string') {
295
+ this.#_source = s;
296
+ } else {
297
+ throw new Error('Source must be a String!');
298
+ }
299
+ }
300
+ }
@@ -0,0 +1,142 @@
1
+ import StoryFormat from './StoryFormat.js';
2
+ import semver from 'semver';
3
+ /**
4
+ * @class StoryFormatParser
5
+ * @module StoryFormatParser
6
+ */
7
+ export default class StoryFormatParser {
8
+ /**
9
+ * Parse a Story Format file
10
+ *
11
+ * @public
12
+ * @static
13
+ * @memberof StoryFormatParser
14
+ * @function parse
15
+ * @param {string} contents - Content
16
+ * @returns {StoryFormat} StoryFormat object
17
+ */
18
+ static parse (contents) {
19
+ // Harlowe has malformed JSON, so we have to test for it
20
+ const harlowePosition = contents.indexOf('harlowe');
21
+
22
+ if (harlowePosition !== -1) {
23
+ // The 'setup' property is malformed
24
+ const setupPosition = contents.lastIndexOf(',"setup": function');
25
+ contents = contents.slice(0, setupPosition) + '}';
26
+ }
27
+
28
+ // Find the start of story format or -1, if not found
29
+ const openingCurlyBracketPosition = contents.indexOf('{');
30
+ // Find the end of story format or -1, if not found
31
+ const closingCurlyBracketPosition = contents.lastIndexOf('}');
32
+
33
+ // Look for JSON among the story format
34
+ // If either is -1, this is not valid JSON
35
+ if (openingCurlyBracketPosition === -1 || closingCurlyBracketPosition === -1) {
36
+ // Either start or end curly brackets were now found!
37
+ throw new Error('Unable to find Twine2 JSON chunk!');
38
+ } else {
39
+ // Slice out the JSON based on curly brackets
40
+ contents = contents.slice(openingCurlyBracketPosition, closingCurlyBracketPosition + 1);
41
+ }
42
+
43
+ // Create an object literal
44
+ let jsonContent = {};
45
+
46
+ try {
47
+ jsonContent = JSON.parse(contents);
48
+ } catch (event) {
49
+ throw new Error('Unable to parse Twine2 JSON chunk!');
50
+ }
51
+
52
+ /**
53
+ * The following keys are found in most or all story formats:
54
+ * - name: (string) Optional. Name of the story format.
55
+ * (Omitting the name will lead to an Untitled Story Format.)
56
+ * - version: (string) Required, and semantic version-style formatting
57
+ * (x.y.z, e.g., 1.2.1) of the version is also required.
58
+ * - author: (string) Optional.
59
+ * - description: (string) Optional.
60
+ * - image: (string) Optional. The filename of an image (ideally SVG)
61
+ * served from the same directory as the format.js file.
62
+ * - url: (string) Optional. The URL of the directory containing the format.js file.
63
+ * - license: (string) Optional.
64
+ * - proofing: (boolean) Optional (defaults to false). True if the story format
65
+ * is a "proofing" format. The distinction is relevant only in the Twine 2 UI.
66
+ * - source: (string) Required. Full HTML output of the story format.
67
+ * (See: https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-storyformats-spec.md)
68
+ */
69
+
70
+ // Name is optional, so we have to test for it
71
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'name')) {
72
+ // Use the default name
73
+ jsonContent.name = 'Untitled Story Format';
74
+ }
75
+
76
+ // Author is optional, so we have to test for it
77
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'author')) {
78
+ // Use the default author
79
+ jsonContent.author = '';
80
+ }
81
+
82
+ // Description is optional, so we have to test for it
83
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'description')) {
84
+ // Use the default description
85
+ jsonContent.description = '';
86
+ }
87
+
88
+ // Image is optional, so we have to test for it
89
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'image')) {
90
+ // Use the default image
91
+ jsonContent.image = '';
92
+ }
93
+
94
+ // URL is optional, so we have to test for it
95
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'url')) {
96
+ // Use the default url
97
+ jsonContent.url = '';
98
+ }
99
+
100
+ // License is optional, so we have to test for it
101
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'license')) {
102
+ // Use the default license
103
+ jsonContent.license = '';
104
+ }
105
+
106
+ // Proofing is optional, so we have to test for it
107
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'proofing')) {
108
+ // Use the default proofing
109
+ jsonContent.proofing = false;
110
+ }
111
+
112
+ // Version is required, so we have to test for it
113
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'version')) {
114
+ // Throw error
115
+ throw new Error('Processed story format does not have required version property!');
116
+ }
117
+
118
+ // Test if version is semantic-style, which is required
119
+ if (semver.valid(jsonContent.version) === null) {
120
+ throw new Error('Processed story format\'s version is not a valid semantic value!');
121
+ }
122
+
123
+ // Source is required, so we have to test for it
124
+ if (!Object.prototype.hasOwnProperty.call(jsonContent, 'source')) {
125
+ // Throw error
126
+ throw new Error('Processed story format does not have required source property!');
127
+ }
128
+
129
+ // Pass all values to the constructor and return the result
130
+ return new StoryFormat(
131
+ jsonContent.name,
132
+ jsonContent.version,
133
+ jsonContent.description,
134
+ jsonContent.author,
135
+ jsonContent.image,
136
+ jsonContent.url,
137
+ jsonContent.license,
138
+ jsonContent.proofing,
139
+ jsonContent.source
140
+ );
141
+ }
142
+ }
@@ -0,0 +1,166 @@
1
+ import Passage from './Passage.js';
2
+ import Story from './Story.js';
3
+ /**
4
+ * @class TweeParser
5
+ * @module TweeParser
6
+ */
7
+ export default class TweeParser {
8
+ /**
9
+ * Parse Twee
10
+ *
11
+ * @public
12
+ * @static
13
+ * @function parse
14
+ * @param {string} fileContents - File contents to parse
15
+ * @returns {Story} story
16
+ */
17
+ static parse (fileContents) {
18
+ // Create Story.
19
+ const story = new Story();
20
+
21
+ // Check if argument is a string
22
+ const isString = (x) => {
23
+ return Object.prototype.toString.call(x) === '[object String]';
24
+ };
25
+
26
+ // Throw error if fileContents is empty
27
+ if (!isString(fileContents)) {
28
+ throw new Error('Contents not a String');
29
+ }
30
+
31
+ let adjusted = '';
32
+
33
+ // Check if there are extra content in the files
34
+ // If so, cut it all out for the parser
35
+ if (fileContents[0] !== ':' && fileContents[1] !== ':') {
36
+ adjusted = fileContents.slice(fileContents.indexOf('::'), fileContents.length);
37
+ } else {
38
+ adjusted = fileContents;
39
+ }
40
+
41
+ // Split the file based on the passage sigil (::) proceeded by a newline
42
+ const parsingPassages = adjusted.split('\n::');
43
+
44
+ // Fix the first result
45
+ parsingPassages[0] = parsingPassages[0].slice(2, parsingPassages[0].length);
46
+
47
+ // Set the initial pid
48
+ let pid = 1;
49
+
50
+ // Iterate through the passages
51
+ parsingPassages.forEach((passage) => {
52
+ // Set default values
53
+ let tags = '';
54
+ let metadata = '';
55
+ let text = '';
56
+ let name = '';
57
+
58
+ // Header is everything to the first newline
59
+ let header = passage.slice(0, passage.indexOf('\n'));
60
+ // Text is everything else
61
+ // (Also eat the leading newline character.)
62
+ // (And trim any remaining whitespace.)
63
+ text = passage.substring(header.length + 1, passage.length).trim();
64
+
65
+ // Test for metadata
66
+ const openingCurlyBracketPosition = header.lastIndexOf('{');
67
+ const closingCurlyBracketPosition = header.lastIndexOf('}');
68
+
69
+ if (openingCurlyBracketPosition !== -1 && closingCurlyBracketPosition !== -1) {
70
+ // Save the text metadata
71
+ metadata = header.slice(openingCurlyBracketPosition, closingCurlyBracketPosition + 1);
72
+
73
+ // Remove the metadata from the header
74
+ header = header.substring(0, openingCurlyBracketPosition) + header.substring(closingCurlyBracketPosition + 1);
75
+ }
76
+
77
+ // There was passage metadata
78
+ if (metadata.length > 0) {
79
+ // Try to parse the metadata
80
+ try {
81
+ metadata = JSON.parse(metadata);
82
+ } catch (event) {
83
+ }
84
+ } else {
85
+ // There wasn't any metadata, so set default
86
+ metadata = {};
87
+ }
88
+
89
+ // Test for tags
90
+ const openingSquareBracketPosition = header.lastIndexOf('[');
91
+ const closingSquareBracketPosition = header.lastIndexOf(']');
92
+
93
+ if (openingSquareBracketPosition !== -1 && closingSquareBracketPosition !== -1) {
94
+ tags = header.slice(openingSquareBracketPosition, closingSquareBracketPosition + 1);
95
+
96
+ // Remove the tags from the header
97
+ header = header.substring(0, openingSquareBracketPosition) + header.substring(closingSquareBracketPosition + 1);
98
+ }
99
+
100
+ // Parse tags
101
+ if (tags.length > 0) {
102
+ // Eat the opening and closing square brackets
103
+ tags = tags.substring(1, tags.length - 1);
104
+
105
+ // Set empty default
106
+ let tagsArray = [];
107
+
108
+ // Test if tags is not single, empty string
109
+ if (!(tags === '')) {
110
+ tagsArray = tags.split(' ');
111
+ }
112
+
113
+ // There are multiple tags
114
+ if (tagsArray.length > 1) {
115
+ // Create future array
116
+ const futureTagArray = [];
117
+
118
+ // Move through entries
119
+ // Add a trimmed version into future array
120
+ tagsArray.forEach((tag) => { futureTagArray.push(tag.trim()); });
121
+
122
+ // Set the tags back to the future array
123
+ tags = futureTagArray;
124
+ } else if (tagsArray.length === 1) {
125
+ // There was only one tag
126
+ // Store it
127
+ const temp = tags;
128
+
129
+ // Switch tags over to an array
130
+ tags = [];
131
+ // Push the single entry
132
+ tags.push(temp);
133
+ } else {
134
+ // Make sure tags is set to empty array if no tags were found
135
+ tags = [];
136
+ }
137
+ } else {
138
+ // There were no tags, so set it to an empty array;
139
+ tags = [];
140
+ }
141
+
142
+ // Filter out any empty string tags
143
+ tags = tags.filter(tag => tag !== '');
144
+
145
+ // Trim any remaining whitespace
146
+ header = header.trim();
147
+
148
+ // Check if there is a name left
149
+ if (header.length > 0) {
150
+ name = header;
151
+ } else {
152
+ // No name left. Something went wrong. Blame user.
153
+ throw new Error('Malformed passage header!');
154
+ }
155
+
156
+ // addPassage() method does all the work
157
+ story.addPassage(new Passage(name, text, tags, metadata, pid));
158
+
159
+ // Increase pid
160
+ pid++;
161
+ });
162
+
163
+ // Return Story.
164
+ return story;
165
+ }
166
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * @external Story
3
+ * @see Story.js
4
+ * @external Passage
5
+ * @see Passage.js
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import Story from './Story.js';
10
+ import { v4 as uuidv4 } from 'uuid';
11
+
12
+ /**
13
+ * @class TweeWriter
14
+ * @module TweeWriter
15
+ */
16
+ export default class TweeWriter {
17
+ /**
18
+ * Write to a file using a Story object
19
+ *
20
+ * @static
21
+ * @param {Story} story - Story format to write
22
+ * @param {string} file - File to write to
23
+ * @returns {void}
24
+ */
25
+ static write (story, file) {
26
+ if (!(story instanceof Story)) {
27
+ throw new Error('Not a Story object!');
28
+ }
29
+
30
+ // Write the StoryData first.
31
+ let outputContents = ':: StoryData\n';
32
+
33
+ // Create default object.
34
+ const metadata = {};
35
+
36
+ // Is there an IFID?
37
+ if (story.IFID === '') {
38
+ // Generate a new IFID for this work.
39
+ // Twine 2 uses v4 (random) UUIDs, using only capital letters.
40
+ metadata.ifid = uuidv4().toUpperCase();
41
+ } else {
42
+ // Use existing (non-default) value.
43
+ metadata.ifid = story.IFID;
44
+ }
45
+
46
+ // Is there a format?
47
+ if (story.format !== '') {
48
+ // Using existing format
49
+ metadata.format = story.format;
50
+ }
51
+
52
+ // Is there a formatVersion?
53
+ if (story.formatVersion !== '') {
54
+ // Using existing format version
55
+ metadata['format-version'] = story.formatVersion;
56
+ }
57
+
58
+ // Is there a zoom?
59
+ if (story.zoom !== 0) {
60
+ // Using existing zoom.
61
+ metadata.zoom = story.zoom;
62
+ }
63
+
64
+ // Is there a start?
65
+ if (story.start !== '') {
66
+ // Using existing start
67
+ metadata.start = story.start;
68
+ }
69
+
70
+ // Get number of colors.
71
+ const numberOfColors = Object.keys(story.tagColors).length;
72
+ // Are there any colors?
73
+ if (numberOfColors > 0) {
74
+ // Add a tag-colors property
75
+ metadata['tag-colors'] = story.tagColors;
76
+ }
77
+
78
+ // Write out the story metadata.
79
+ outputContents += `${JSON.stringify(metadata, undefined, 2)}`;
80
+
81
+ // Add two newlines.
82
+ outputContents += '\n\n';
83
+
84
+ // For each passage, append it to the output.
85
+ story.forEach((passage) => {
86
+ // For each passage, append it to the output.
87
+ outputContents += passage.toString();
88
+ });
89
+
90
+ try {
91
+ // Try to write
92
+ fs.writeFileSync(file, outputContents);
93
+ } catch (event) {
94
+ // Throw error
95
+ throw new Error('Error: Cannot write Twee file!');
96
+ }
97
+ }
98
+ }