extwee 1.6.0 → 2.0.2

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 -0
  2. package/.github/workflows/nodejs.yml +24 -25
  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 +59 -37
  12. package/src/FileReader.js +33 -36
  13. package/src/HTMLParser.js +343 -206
  14. package/src/HTMLWriter.js +227 -177
  15. package/src/Passage.js +202 -20
  16. package/src/Story.js +461 -148
  17. package/src/StoryFormat.js +300 -41
  18. package/src/StoryFormatParser.js +142 -65
  19. package/src/TweeParser.js +161 -255
  20. package/src/TweeWriter.js +98 -111
  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 +289 -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 -0
  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 +85 -0
  131. package/main.js +0 -106
  132. package/src/DirectoryReader.js +0 -107
  133. package/src/DirectoryWatcher.js +0 -92
  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 -722
package/src/HTMLParser.js CHANGED
@@ -1,206 +1,343 @@
1
- const { parse } = require('node-html-parser');
2
- const htmlparser = parse;
3
- const Story = require('./Story.js');
4
- const Passage = require('./Passage.js');
5
- /**
6
- * @class HTMLParser
7
- * @module HTMLParser
8
- */
9
- class HTMLParser {
10
- /**
11
- * @method HTMLParser
12
- * @constructor
13
- */
14
- constructor (content) {
15
-
16
- this.story = null;
17
-
18
- this.parse(content);
19
- }
20
-
21
- parse(content) {
22
-
23
- // Send to node-html-parser
24
- // Enable getting the content of 'script', 'style', and 'pre' elements
25
- // Get back a DOM
26
- let dom = new htmlparser(
27
- content,
28
- {
29
- lowerCaseTagName: false,
30
- script: true,
31
- style: true,
32
- pre: true
33
- });
34
-
35
- // Pull out the tw-storydata element
36
- let storyData = dom.querySelector('tw-storydata');
37
-
38
- if(storyData != null) {
39
-
40
- this.story = new Story();
41
- this.story.name = storyData.attributes["name"];
42
- this.story.creator = storyData.attributes["creator"];
43
- this.story.creatorVersion = storyData.attributes["creator-version"];
44
-
45
- this.story.metadata = {};
46
- this.story.metadata.ifid = storyData.attributes["ifid"];
47
- this.story.metadata.format = storyData.attributes["format"];
48
- this.story.metadata.formatVersion = storyData.attributes["format-version"];
49
- this.story.metadata.zoom = storyData.attributes["zoom"];
50
- this.story.metadata.start = storyData.attributes["startnode"];
51
-
52
- } else {
53
-
54
- throw new Error("Error: Not a Twine 2-style file!");
55
-
56
- }
57
-
58
- // Pull out the tw-passagedata elements
59
- let storyPassages = dom.querySelectorAll("tw-passagedata");
60
-
61
- // Create an empty array
62
- this.story.passages = new Array();
63
-
64
- // Set default pid
65
- let pid = 1;
66
-
67
- // Add StoryTitle
68
- this.story.passages.push(
69
- new Passage(
70
- "StoryTitle",
71
- [],
72
- {},
73
- this.story.name,
74
- pid
75
- )
76
- );
77
-
78
- // Increase PID by one before parsing any other passages
79
- pid++;
80
-
81
- // Move through the passages
82
- for(let passage in storyPassages) {
83
-
84
- // Get the passage attributes
85
- let attr = storyPassages[passage].attributes;
86
- // Get the passage text
87
- let text = storyPassages[passage].rawText;
88
-
89
- // Save position
90
- let position = attr.position;
91
-
92
- // Save size
93
- let size = attr.size;
94
-
95
- // Escape the name
96
- let name = this._escapeMetacharacters(attr.name);
97
-
98
- // Create empty tags
99
- let tags = "";
100
-
101
- // Escape any tags
102
- // (Attributes can, themselves, be emtpy strings.)
103
- if(attr.tags.length > 0 && attr.tags != '""') {
104
-
105
- tags = this._escapeMetacharacters(attr.tags);
106
-
107
- }
108
-
109
- // Split by spaces
110
- tags = tags.split(" ");
111
-
112
- // Remove any empty strings
113
- tags = tags.filter(tag => tag != "");
114
-
115
- // Add a new Passage into an array
116
- this.story.passages.push(
117
- new Passage(
118
- name,
119
- tags,
120
- {
121
- "position": position,
122
- "size": size
123
-
124
- },
125
- text,
126
- pid
127
- )
128
- );
129
-
130
- pid++;
131
-
132
- }
133
-
134
- // Look for the style element
135
- let styleElement = dom.querySelector('#twine-user-stylesheet');
136
-
137
- // Check if there is any content.
138
- // If not, we won't add empty passages
139
- if(styleElement.rawText.length > 0) {
140
-
141
- // Add UserStylesheet
142
- this.story.passages.push(
143
- new Passage(
144
- "UserStylesheet",
145
- ["stylesheet"],
146
- {},
147
- styleElement.rawText
148
- )
149
- );
150
- }
151
-
152
- // Look for the script element
153
- let scriptElement = dom.querySelector('#twine-user-script');
154
-
155
- // Check if there is any content.
156
- // If not, we won't add empty passages
157
- if(scriptElement.rawText.length > 0) {
158
-
159
- // Add UserScript
160
- this.story.passages.push(
161
- new Passage(
162
- "UserScript",
163
- ["script"],
164
- {},
165
- scriptElement.rawText
166
- )
167
- );
168
-
169
- }
170
-
171
- // Now that all passages have been handled,
172
- // change the start name
173
- this.story.metadata.start = this.story.getStartingPassage();
174
-
175
- // Add StoryData
176
- this.story.passages.push(
177
- new Passage(
178
- "StoryData",
179
- [],
180
- {},
181
- JSON.stringify(this.story.metadata, null, 4)
182
- )
183
- );
184
-
185
- }
186
-
187
- _escapeMetacharacters(result) {
188
-
189
- // Replace any single backslash with two of them
190
- result = result.replace(/\\/g, "\\");
191
- // Double-escape escaped {
192
- result = result.replace(/\\\{/g, "\\\\{");
193
- // Double-escape escaped }
194
- result = result.replace(/\\\}/g, "\\\\}");
195
- // Double-escape escaped [
196
- result = result.replace(/\\\[/g, "\\\\[");
197
- // Double-escape escaped ]
198
- result = result.replace(/\\\]/g, "\\\\]");
199
-
200
- return result;
201
-
202
- }
203
-
204
- }
205
-
206
- module.exports = HTMLParser;
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
+ }