extwee 1.6.2 → 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 (151) hide show
  1. package/.eslintrc.json +25 -24
  2. package/.github/workflows/nodejs.yml +24 -24
  3. package/.travis.yml +13 -13
  4. package/CODE_OF_CONDUCT.md +82 -0
  5. package/LICENSE +21 -21
  6. package/README.md +36 -205
  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 -45
  12. package/src/FileReader.js +33 -35
  13. package/src/HTMLParser.js +343 -206
  14. package/src/HTMLWriter.js +196 -172
  15. package/src/Passage.js +202 -24
  16. package/src/Story.js +461 -122
  17. package/src/StoryFormat.js +300 -28
  18. package/src/StoryFormatParser.js +142 -59
  19. package/src/TweeParser.js +166 -207
  20. package/src/TweeWriter.js +98 -96
  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/{HTMLWriter/example7.twee → CLI/example6.twee} +16 -16
  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.test.js +14 -0
  50. package/test/HTMLParser/Example1.html +53 -0
  51. package/test/HTMLParser/Tags.html +15 -0
  52. package/test/HTMLParser/lyingStartnode.html +15 -0
  53. package/test/HTMLParser/lyingTagColors.html +48 -0
  54. package/test/HTMLParser/missingCreator.html +11 -0
  55. package/test/HTMLParser/missingCreatorVersion.html +11 -0
  56. package/test/HTMLParser/missingFormat.html +11 -0
  57. package/test/HTMLParser/missingFormatVersion.html +11 -0
  58. package/test/HTMLParser/missingIFID.html +11 -0
  59. package/test/HTMLParser/missingName.html +33 -0
  60. package/test/HTMLParser/missingPID.html +15 -0
  61. package/test/HTMLParser/missingPassageName.html +15 -0
  62. package/test/HTMLParser/missingPassageTags.html +15 -0
  63. package/test/HTMLParser/missingPosition.html +15 -0
  64. package/test/HTMLParser/missingScript.html +14 -0
  65. package/test/HTMLParser/missingSize.html +35 -0
  66. package/test/HTMLParser/missingStartnode.html +11 -0
  67. package/test/HTMLParser/missingStyle.html +14 -0
  68. package/test/HTMLParser/missingZoom.html +11 -0
  69. package/test/HTMLParser/tagColors.html +31 -0
  70. package/test/HTMLParser/twineExample.html +15 -46
  71. package/test/HTMLParser/twineExample2.html +15 -0
  72. package/test/HTMLParser/twineExample3.html +15 -0
  73. package/test/HTMLParser.test.js +177 -0
  74. package/test/HTMLWriter/TestTags.html +42 -0
  75. package/test/HTMLWriter/{test10.html → creator.html} +8 -5
  76. package/test/HTMLWriter/example6.twee +16 -16
  77. package/test/HTMLWriter/{example.twee → missingStoryTitle.twee} +29 -29
  78. package/test/HTMLWriter/test11.html +123 -0
  79. package/test/HTMLWriter/test2.html +15 -12
  80. package/test/HTMLWriter/test3.html +7 -13
  81. package/test/HTMLWriter/test4.html +8 -5
  82. package/test/HTMLWriter/test6.html +7 -5
  83. package/test/HTMLWriter.test.js +279 -0
  84. package/test/Passage.test.js +104 -0
  85. package/test/Roundtrip/Example1.html +64 -0
  86. package/test/Roundtrip/example1.twee +21 -0
  87. package/test/Roundtrip/example2.twee +18 -0
  88. package/test/Roundtrip/harlowe.js +3 -0
  89. package/test/Roundtrip/round.html +50 -0
  90. package/test/Roundtrip.test.js +48 -0
  91. package/test/Story/startmeta.twee +29 -29
  92. package/test/Story/test.twee +25 -25
  93. package/test/Story.test.js +282 -0
  94. package/test/StoryFormat.test.js +152 -0
  95. package/test/StoryFormatParser/example.js +3 -0
  96. package/test/StoryFormatParser/{test2.js → example2.js} +3 -3
  97. package/test/StoryFormatParser/format_doublename.js +1 -0
  98. package/test/StoryFormatParser/harlowe.js +2 -2
  99. package/test/StoryFormatParser/missingAuthor.js +1 -0
  100. package/test/StoryFormatParser/missingDescription.js +1 -0
  101. package/test/StoryFormatParser/missingImage.js +1 -0
  102. package/test/StoryFormatParser/missingLicense.js +1 -0
  103. package/test/StoryFormatParser/missingName.js +1 -0
  104. package/test/StoryFormatParser/missingProofing.js +1 -0
  105. package/test/StoryFormatParser/missingSource.js +1 -0
  106. package/test/StoryFormatParser/missingURL.js +1 -0
  107. package/test/StoryFormatParser/missingVersion.js +1 -0
  108. package/test/StoryFormatParser/versionWrong.js +1 -0
  109. package/test/StoryFormatParser.test.js +91 -0
  110. package/test/TweeParser/emptytags.twee +2 -2
  111. package/test/TweeParser/example.twee +32 -29
  112. package/test/TweeParser/missing.twee +19 -0
  113. package/test/{HTMLWriter/example5.twee → TweeParser/multipleScriptPassages.twee} +19 -13
  114. package/test/{HTMLWriter/example4.twee → TweeParser/multipleStyleTag.twee} +19 -13
  115. package/test/TweeParser/multipletags.twee +10 -2
  116. package/test/TweeParser/noTitle.twee +2 -0
  117. package/test/TweeParser/notes.twee +16 -32
  118. package/test/TweeParser/pasagemetadataerror.twee +2 -2
  119. package/test/{HTMLWriter/example2.twee → TweeParser/scriptPassage.twee} +16 -13
  120. package/test/TweeParser/singletag.twee +13 -2
  121. package/test/TweeParser/startMetadata.twee +14 -0
  122. package/test/TweeParser/storydataerror.twee +25 -25
  123. package/test/{HTMLWriter/example3.twee → TweeParser/stylePassage.twee} +16 -13
  124. package/test/TweeParser/test.twee +25 -25
  125. package/test/TweeParser.test.js +79 -0
  126. package/test/TweeWriter/test1.twee +14 -9
  127. package/test/TweeWriter/test3.twee +7 -10
  128. package/test/TweeWriter/test4.twee +14 -0
  129. package/test/TweeWriter/test5.twee +20 -0
  130. package/test/TweeWriter.test.js +82 -0
  131. package/main.js +0 -106
  132. package/src/DirectoryReader.js +0 -114
  133. package/src/DirectoryWatcher.js +0 -87
  134. package/test/DirectoryReader/css/test.css +0 -3
  135. package/test/DirectoryReader1/js/Site.js +0 -1
  136. package/test/DirectoryReader2/error.js +0 -1
  137. package/test/DirectoryReader2/example.css +0 -3
  138. package/test/DirectoryReader2/index.twee +0 -6
  139. package/test/DirectoryReader3/twee/index.twee +0 -6
  140. package/test/DirectoryWatcher/example.txt +0 -0
  141. package/test/DirectoryWatcher/test.txt +0 -0
  142. package/test/DirectoryWatcher/test1.txt +0 -0
  143. package/test/HTMLWriter/test5.html +0 -48
  144. package/test/HTMLWriter/test7.html +0 -48
  145. package/test/HTMLWriter/test8.html +0 -48
  146. package/test/HTMLWriter/test9.html +0 -48
  147. package/test/StoryFormatParser/test.js +0 -2
  148. package/test/TweeParser/test.twee3 +0 -11
  149. package/test/TweeWriter/metatest.twee +0 -12
  150. package/test/TweeWriter/test2.twee +0 -15
  151. package/test/test.js +0 -736
package/src/TweeParser.js CHANGED
@@ -1,207 +1,166 @@
1
- const Passage = require('./Passage.js');
2
- const Story = require('./Story.js');
3
- /**
4
- * @class TweeParser
5
- * @module TweeParser
6
- */
7
- class TweeParser {
8
- /**
9
- * @function TweeParser
10
- * @class
11
- * @param {string} content - Twee content to parse
12
- */
13
- constructor (content = '') {
14
- this.story = new Story();
15
- this._passageMetadataError = false;
16
- this._storydataError = false;
17
-
18
- this.parse(content);
19
- }
20
-
21
- /**
22
- * Parse Twee
23
- *
24
- * @function parse
25
- * @param {string} fileContents - File contents to parse
26
- */
27
- parse (fileContents) {
28
- // Throw error if fileContents is empty
29
- if (fileContents.length === 0) {
30
- throw new Error('No file contents!');
31
- }
32
-
33
- let passages = [];
34
- let adjusted = '';
35
-
36
- // Check if there are extra content in the files
37
- // If so, cut it all out for the parser
38
- if (fileContents[0] !== ':' && fileContents[1] !== ':') {
39
- adjusted = fileContents.slice(fileContents.indexOf('::'), fileContents.length);
40
- } else {
41
- adjusted = fileContents;
42
- }
43
-
44
- // Split the file based on the passage sigil (::) proceeded by a newline
45
- const parsingPassages = adjusted.split('\n::');
46
-
47
- // Fix the first result
48
- parsingPassages[0] = parsingPassages[0].slice(2, parsingPassages[0].length);
49
-
50
- // Set the initial pid
51
- let pid = 0;
52
-
53
- // Iterate through the passages
54
- parsingPassages.forEach((passage) => {
55
- // Set default values
56
- let tags = '';
57
- let metadata = '';
58
- let text = '';
59
- let name = '';
60
-
61
- // Header is everything to the first newline
62
- let header = passage.slice(0, passage.indexOf('\n'));
63
- // Text is everything else
64
- // (Also eat the leading newline character.)
65
- // (And trim any remaining whitespace.)
66
- text = passage.substring(header.length + 1, passage.length).trim();
67
-
68
- // Test for metadata
69
- const openingCurlyBracketPosition = header.lastIndexOf('{');
70
- const closingCurlyBracketPosition = header.lastIndexOf('}');
71
-
72
- if (openingCurlyBracketPosition !== -1 && closingCurlyBracketPosition !== -1) {
73
- // Save the text metadata
74
- metadata = header.slice(openingCurlyBracketPosition, closingCurlyBracketPosition + 1);
75
-
76
- // Remove the metadata from the header
77
- header = header.substring(0, openingCurlyBracketPosition) + header.substring(closingCurlyBracketPosition + 1);
78
- }
79
-
80
- // There was passage metadata
81
- if (metadata.length > 0) {
82
- // Try to parse the metadata
83
- try {
84
- metadata = JSON.parse(metadata);
85
- } catch (event) {
86
- this._passageMetadataError = true;
87
- }
88
- }
89
-
90
- // Test for tags
91
- const openingSquareBracketPosition = header.lastIndexOf('[');
92
- const closingSquareBracketPosition = header.lastIndexOf(']');
93
-
94
- if (openingSquareBracketPosition !== -1 && closingSquareBracketPosition !== -1) {
95
- tags = header.slice(openingSquareBracketPosition, closingSquareBracketPosition + 1);
96
-
97
- // Remove the tags from the header
98
- header = header.substring(0, openingSquareBracketPosition) + header.substring(closingSquareBracketPosition + 1);
99
- }
100
-
101
- // Parse tags
102
- if (tags.length > 0) {
103
- // Eat the opening and closing square brackets
104
- tags = tags.substring(1, tags.length - 1);
105
-
106
- // Set empty default
107
- let tagsArray = [];
108
-
109
- // Test if tags is not single, empty string
110
- if (!(tags === '')) {
111
- tagsArray = tags.split(' ');
112
- }
113
-
114
- // There are multiple tags
115
- if (tagsArray.length > 1) {
116
- // Create future array
117
- const futureTagArray = [];
118
-
119
- // Move through entries
120
- // Add a trimmed version into future array
121
- tagsArray.forEach((tag) => { futureTagArray.push(tag.trim()); });
122
-
123
- // Set the tags back to the future array
124
- tags = futureTagArray;
125
- } else if (tagsArray.length === 1) {
126
- // There was only one tag
127
- // Store it
128
- const temp = tags;
129
-
130
- // Switch tags over to an array
131
- tags = [];
132
- // Push the single entry
133
- tags.push(temp);
134
- } else {
135
- // Make sure tags is set to empty array if no tags were found
136
- tags = [];
137
- }
138
- } else {
139
- // There were no tags, so set it to an empty array;
140
- tags = [];
141
- }
142
-
143
- // Filter out any empty string tags
144
- tags = tags.filter(tag => tag !== '');
145
-
146
- // Trim any remaining whitespace
147
- header = header.trim();
148
-
149
- // Check if there is a name left
150
- if (header.length > 0) {
151
- name = header;
152
- } else {
153
- // No name left. Something went wrong. Blame user.
154
- throw new Error('Malformed passage header!');
155
- }
156
-
157
- if (this._passageMetadataError) {
158
- console.warn('Error parsing metadata for "' + name + '". It was ignored.');
159
- }
160
-
161
- // Add the new Passage to the internal array
162
- passages.push(new Passage(name, tags, metadata, text, pid));
163
-
164
- // Increase pid
165
- pid++;
166
- });
167
-
168
- // All formats share StoryTitle
169
- // Find it and set it
170
- let pos = passages.find((el) => { return el.name === 'StoryTitle'; });
171
-
172
- if (pos !== undefined) {
173
- this.story.name = pos.text;
174
- // Remove the StoryTitle passage
175
- passages = passages.filter(p => p.name !== 'StoryTitle');
176
- } else {
177
- // There was no StoryTitle passage
178
- // Set a value of "Unknown"
179
- this.story.name = 'Unknown';
180
- }
181
-
182
- // Look for StoryData
183
- pos = passages.find((el) => { return el.name === 'StoryData'; });
184
-
185
- if (pos !== undefined) {
186
- // Try to parse the StoryData
187
- try {
188
- this.story.metadata = JSON.parse(pos.text);
189
- } catch (event) {
190
- // Silently fail with default values
191
- this._storydataError = true;
192
- }
193
-
194
- // Remove the StoryData passage
195
- passages = passages.filter(p => p.name !== 'StoryData');
196
- }
197
-
198
- if (this._storydataError) {
199
- console.warn('Error with processing StoryData JSON data. It was ignored.');
200
- }
201
-
202
- // Set the passages to the internal story
203
- this.story.passages = passages;
204
- }
205
- }
206
-
207
- module.exports = TweeParser;
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
+ }
package/src/TweeWriter.js CHANGED
@@ -1,96 +1,98 @@
1
- /**
2
- * @external Story
3
- * @see Story.js
4
- */
5
-
6
- const fs = require('fs');
7
- const Story = require('./Story.js');
8
-
9
- /**
10
- * @class TweeWriter
11
- * @module TweeWriter
12
- */
13
- class TweeWriter {
14
- /**
15
- * @function TweeWriter
16
- * @class
17
- * @param {Story} story - Story object to write
18
- * @param {string} file - File to write to
19
- */
20
- constructor (story, file) {
21
- if (!(story instanceof Story)) {
22
- throw new Error('Not a Story object!');
23
- }
24
-
25
- this.writeFile(file, story);
26
- }
27
-
28
- /**
29
- * Write to a file using a Story object
30
- *
31
- * @param {string} file - File to write to
32
- * @param {Story} story - Story format to write
33
- * @returns {void}
34
- */
35
- writeFile (file, story) {
36
- // Write StoryTitle first
37
- let outputContents = ':: StoryTitle\n' + story.name + '\n\n';
38
-
39
- // Write the StoryData second
40
- outputContents += ':: StoryData\n';
41
-
42
- // Test if story.metadata is an object or not
43
- if (typeof story.metadata === 'object') {
44
- // Write any metadata in pretty format
45
- outputContents += ' ' + JSON.stringify(story.metadata, undefined, 2);
46
- } else {
47
- // If, for whatever reason, story.metadata is not an object, throw error.
48
- throw new Error('Story Metadata MUST be an object!');
49
- }
50
-
51
- // Add two newlines
52
- outputContents += '\n\n';
53
-
54
- // Are there any passages?
55
- if (story.passages.length > 0) {
56
- // Build the contents
57
- for (const passage in story.passages) {
58
- // Write the name
59
- outputContents += ':: ' + story.passages[passage].name;
60
-
61
- // Test if it has any tags
62
- if (story.passages[passage].tags.length > 0) {
63
- outputContents += ' [';
64
-
65
- for (const tag of story.passages[passage].tags) {
66
- outputContents += ' ' + tag;
67
- }
68
-
69
- outputContents += ']';
70
- }
71
-
72
- // Write out any passage metadata
73
- outputContents += JSON.stringify(story.passages[passage].metadata);
74
-
75
- // Add the text and two newlines
76
- outputContents += '\n' + story.passages[passage].text + '\n\n';
77
- }
78
- } else {
79
- // Create empty Start passage
80
- outputContents += ':: Start\n';
81
- }
82
-
83
- try {
84
- // Try to write
85
- fs.writeFileSync(file, outputContents);
86
- } catch (event) {
87
- // Throw error
88
- throw new Error('Error: Cannot write Twee file!');
89
- }
90
-
91
- // Writing was successful
92
- console.info('Created ' + fs.realpathSync(file));
93
- }
94
- }
95
-
96
- module.exports = TweeWriter;
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
+ }