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/src/Passage.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @class Passage
3
+ * @module Passage
4
+ */
5
+ export default class Passage {
6
+ /**
7
+ * Name of the Passage
8
+ *
9
+ * @private
10
+ */
11
+ #_name = null;
12
+
13
+ /**
14
+ * Internal array of tags
15
+ *
16
+ * @private
17
+ */
18
+ #_tags = [];
19
+
20
+ /**
21
+ * Internal metadata of passage
22
+ *
23
+ * @private
24
+ */
25
+ #_metadata = {};
26
+
27
+ /**
28
+ * Internal text of the passage
29
+ *
30
+ * @private
31
+ */
32
+ #_text = '';
33
+
34
+ /**
35
+ * Internal PID of passage
36
+ *
37
+ * @private
38
+ */
39
+ #_pid = -1;
40
+
41
+ /**
42
+ * @function Passage
43
+ * @class
44
+ * @param {string} name - Name
45
+ * @param {string} text - Content
46
+ * @param {Array} tags - Tags
47
+ * @param {object} metadata - Metadata
48
+ * @param {number} pid - Passage ID (PID)
49
+ */
50
+ constructor (name = '', text = '', tags = [], metadata = {}, pid = -1) {
51
+ // Set name
52
+ this.name = name;
53
+
54
+ // Set tags
55
+ this.tags = tags;
56
+
57
+ // Set metadata
58
+ this.metadata = metadata;
59
+
60
+ // Sets text
61
+ this.text = text;
62
+
63
+ // Sets pid
64
+ this.pid = pid;
65
+ }
66
+
67
+ /**
68
+ * Name
69
+ *
70
+ * @public
71
+ * @memberof Passage
72
+ * @returns {string} Name
73
+ */
74
+ get name () { return this.#_name; }
75
+
76
+ /**
77
+ * @param {string} s - Name to replace
78
+ */
79
+ set name (s) {
80
+ if (typeof s === 'string') {
81
+ this.#_name = s;
82
+ } else {
83
+ throw new Error('Name must be a String!');
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Tags
89
+ *
90
+ * @public
91
+ * @memberof Passage
92
+ * @returns {Array} Tags
93
+ */
94
+ get tags () { return this.#_tags; }
95
+
96
+ /**
97
+ * @param {Array} t - Replacement array
98
+ */
99
+ set tags (t) {
100
+ // Test if tags is an array
101
+ if (Array.isArray(t)) {
102
+ this.#_tags = t;
103
+ } else {
104
+ throw new Error('Tags must be an array!');
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Metadata
110
+ *
111
+ * @public
112
+ * @memberof Passage
113
+ * @returns {object} Metadata
114
+ */
115
+ get metadata () { return this.#_metadata; }
116
+
117
+ /**
118
+ * @param {object} m - Replacement object
119
+ */
120
+ set metadata (m) {
121
+ // Test if metadata was an object
122
+ if (typeof m === 'object') {
123
+ this.#_metadata = m;
124
+ } else {
125
+ throw new Error('Metadata should be an object literal!');
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Text
131
+ *
132
+ * @public
133
+ * @memberof Passage
134
+ * @returns {string} Text
135
+ */
136
+ get text () { return this.#_text; }
137
+
138
+ /**
139
+ * @param {string} t - Replacement text
140
+ */
141
+ set text (t) {
142
+ // Test if text is a String
143
+ if (typeof t === 'string') {
144
+ this.#_text = t;
145
+ } else {
146
+ throw new Error('Text should be a String!');
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Passage ID (PID)
152
+ *
153
+ * @public
154
+ * @memberof Passage
155
+ * @returns {number} Passage ID (PID)
156
+ */
157
+ get pid () { return this.#_pid; }
158
+
159
+ /**
160
+ * @param {number} p - Replacement PID
161
+ */
162
+ set pid (p) {
163
+ // Test if PID is a number
164
+ if (Number.isInteger(p)) {
165
+ this.#_pid = p;
166
+ } else {
167
+ throw new Error('PID should be a number!');
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Return a String representation
173
+ *
174
+ * @public
175
+ * @memberof Passage
176
+ * @returns {string} String form of passage
177
+ */
178
+ toString () {
179
+ // Start empty string.
180
+ let content = '';
181
+ // Write the name
182
+ content += `:: ${this.name}`;
183
+
184
+ // Test if it has any tags
185
+ if (this.tags.length > 0) {
186
+ // Write output of tags
187
+ content += ` [${this.tags.join(' ')}]`;
188
+ }
189
+
190
+ // Check if any properties exist
191
+ if (Object.keys(this.metadata).length > 0) {
192
+ // Write out a space and then passage metadata
193
+ content += ` ${JSON.stringify(this.metadata)}`;
194
+ }
195
+
196
+ // Add newline, text, and two newlines
197
+ content += `\n${this.text}\n\n`;
198
+
199
+ // Return string.
200
+ return content;
201
+ }
202
+ }
package/src/Story.js ADDED
@@ -0,0 +1,461 @@
1
+ import Passage from './Passage.js';
2
+
3
+ const name = 'extwee';
4
+ const version = '2.0.0';
5
+
6
+ /**
7
+ * @class Story
8
+ * @module Story
9
+ */
10
+ export default class Story {
11
+ /**
12
+ * Internal start
13
+ *
14
+ * @private
15
+ */
16
+ #_start = '';
17
+
18
+ /**
19
+ * Internal IFID
20
+ *
21
+ * @private
22
+ */
23
+ #_IFID = ''
24
+
25
+ /**
26
+ * Internal story format
27
+ *
28
+ * @private
29
+ */
30
+ #_format = '';
31
+
32
+ /**
33
+ * Internal version of story format
34
+ */
35
+ #_formatVersion = '';
36
+
37
+ /**
38
+ * Internal zoom level
39
+ */
40
+ #_zoom = 0;
41
+
42
+ /**
43
+ * Passages
44
+ *
45
+ * @private
46
+ */
47
+ #_passages = [];
48
+
49
+ /**
50
+ * Creator
51
+ *
52
+ * @private
53
+ */
54
+ #_creator = '';
55
+
56
+ /**
57
+ * CreatorVersion
58
+ *
59
+ * @private
60
+ */
61
+ #_creatorVersion = '';
62
+
63
+ /**
64
+ * Metadata
65
+ *
66
+ * @private
67
+ */
68
+ #_metadata = null;
69
+
70
+ /**
71
+ * Tag Colors
72
+ *
73
+ * @private
74
+ */
75
+ #_tagColors = {};
76
+
77
+ /**
78
+ * Internal PID counter
79
+ *
80
+ * @private
81
+ */
82
+ #_PIDCounter = 1;
83
+
84
+ /**
85
+ * @function Story
86
+ * @class
87
+ */
88
+ constructor () {
89
+ // Store the creator and version
90
+ this.#_creator = name;
91
+ this.#_creatorVersion = version;
92
+ // Set metadata to an object
93
+ this.#_metadata = {};
94
+ }
95
+
96
+ /**
97
+ * Tag Colors object (each property is a tag and its color)
98
+ *
99
+ * @public
100
+ * @readonly
101
+ * @memberof Story
102
+ * @returns {object} tag colors array
103
+ */
104
+ get tagColors () { return this.#_tagColors; }
105
+
106
+ /**
107
+ * @param {object} a - Replacement tag colors
108
+ */
109
+ set tagColors (a) {
110
+ if (a instanceof Object) {
111
+ this.#_tagColors = a;
112
+ } else {
113
+ throw new Error('Tag colors must be an object!');
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Interactive Fiction ID (IFID) of Story
119
+ *
120
+ * @public
121
+ * @readonly
122
+ * @memberof Story
123
+ * @returns {string} IFID
124
+ */
125
+ get IFID () { return this.#_IFID; }
126
+
127
+ /**
128
+ * @param {string} i - Replacement IFID
129
+ */
130
+ set IFID (i) {
131
+ if (typeof i === 'string') {
132
+ this.#_IFID = i;
133
+ } else {
134
+ throw new Error('IFID must be a String!');
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Name of start passage
140
+ *
141
+ * @public
142
+ * @readonly
143
+ * @memberof Story
144
+ * @returns {string} start
145
+ */
146
+ get start () { return this.#_start; }
147
+
148
+ /**
149
+ * @param {string} s - Replacement start
150
+ */
151
+ set start (s) {
152
+ if (typeof s === 'string') {
153
+ this.#_start = s;
154
+ } else {
155
+ throw new Error('start (passage name) must be a String!');
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Story format version of Story
161
+ *
162
+ * @public
163
+ * @readonly
164
+ * @memberof Story
165
+ * @returns {string} story format version
166
+ */
167
+ get formatVersion () { return this.#_formatVersion; }
168
+
169
+ /**
170
+ * @param {string} f - Replacement format version
171
+ */
172
+ set formatVersion (f) {
173
+ if (typeof f === 'string') {
174
+ this.#_formatVersion = f;
175
+ } else {
176
+ throw new Error('Story format version must be a String!');
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Metadata of Story
182
+ *
183
+ * @public
184
+ * @readonly
185
+ * @memberof Story
186
+ * @returns {object} metadata of story
187
+ */
188
+ get metadata () { return this.#_metadata; }
189
+
190
+ /**
191
+ * @param {object} o - Replacement metadata
192
+ */
193
+ set metadata (o) {
194
+ if (typeof o === 'object') {
195
+ this.#_metadata = o;
196
+ } else {
197
+ throw new Error('Story metadata must be Object!');
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Story format of Story
203
+ *
204
+ * @public
205
+ * @readonly
206
+ * @memberof Story
207
+ * @returns {string} format
208
+ */
209
+ get format () { return this.#_format; }
210
+
211
+ /**
212
+ * @param {string} f - Replacement format
213
+ */
214
+ set format (f) {
215
+ if (typeof f === 'string') {
216
+ this.#_format = f;
217
+ } else {
218
+ throw new Error('Story format must be a String!');
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Program used to create Story
224
+ *
225
+ * @public
226
+ * @memberof Story
227
+ * @returns {string} Creator Program
228
+ */
229
+ get creator () { return this.#_creator; }
230
+
231
+ /**
232
+ * @param {string} c - Creator Program of Story
233
+ */
234
+ set creator (c) {
235
+ if (typeof c === 'string') {
236
+ this.#_creator = c;
237
+ } else {
238
+ throw new Error('Creator must be String');
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Version used to create Story
244
+ *
245
+ * @public
246
+ * @memberof Story
247
+ * @returns {string} Version
248
+ */
249
+ get creatorVersion () { return this.#_creatorVersion; }
250
+
251
+ /**
252
+ * @param {string} c - Version of creator program
253
+ */
254
+ set creatorVersion (c) {
255
+ if (typeof c === 'string') {
256
+ this.#_creatorVersion = c;
257
+ } else {
258
+ throw new Error('Creator version must be a string!');
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Zoom level
264
+ *
265
+ * @public
266
+ * @memberof Story
267
+ * @returns {number} Zoom level
268
+ */
269
+ get zoom () { return this.#_zoom; }
270
+
271
+ /**
272
+ * @param {number} n - Replacement zoom level
273
+ */
274
+ set zoom (n) {
275
+ if (typeof n === 'number') {
276
+ // Parse float with a fixed length and then force into Number
277
+ this.#_zoom = Number(Number.parseFloat(n).toFixed(2));
278
+ } else {
279
+ throw new Error('Zoom level must be a Number!');
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Add a passage to the story
285
+ *
286
+ * @public
287
+ * @function addPassage
288
+ * @memberof Story
289
+ * @param {Passage} p - Passage to add to Story
290
+ */
291
+ addPassage (p) {
292
+ // Check if passed argument is a Passage
293
+ if (p instanceof Passage) {
294
+ // Does this passage already exist in the collection?
295
+ if (this.getPassageByName(p.name) === null) {
296
+ // StoryData is the only passage with special parsing needs.
297
+ // All other passages are added to the internal passages array.
298
+ if (p.name === 'StoryData') {
299
+ // Try to parse JSON
300
+ try {
301
+ // Attempt to parse storyData JSON
302
+ const metadata = JSON.parse(p.text);
303
+
304
+ // IFID
305
+ if (Object.prototype.hasOwnProperty.call(metadata, 'ifid')) {
306
+ this.IFID = metadata.ifid;
307
+ }
308
+
309
+ // format
310
+ if (Object.prototype.hasOwnProperty.call(metadata, 'format')) {
311
+ this.format = metadata.format;
312
+ }
313
+
314
+ // formatVersion
315
+ if (Object.prototype.hasOwnProperty.call(metadata, 'formatVersion')) {
316
+ this.formatVersion = metadata.formatVersion;
317
+ }
318
+
319
+ // zoom
320
+ if (Object.prototype.hasOwnProperty.call(metadata, 'zoom')) {
321
+ this.zoom = metadata.zoom;
322
+ }
323
+
324
+ // start
325
+ if (Object.prototype.hasOwnProperty.call(metadata, 'start')) {
326
+ this.start = metadata.start;
327
+ }
328
+
329
+ // tag colors
330
+ if (Object.prototype.hasOwnProperty.call(metadata, 'tag-colors')) {
331
+ this.tagColors = metadata['tag-colors'];
332
+ }
333
+ } catch (event) {
334
+ // Ignore errors
335
+ }
336
+ } else {
337
+ // Look for Start
338
+ if (p.name === 'Start' && this.start === '') {
339
+ // Set Start as starting passage (unless overwritten by start property)
340
+ this.start = 'Start';
341
+ }
342
+
343
+ // Test for default value.
344
+ // (This might occur if using Story directory
345
+ // outside of using HTMLParser or TweeParser.)
346
+ if (p.pid === -1) {
347
+ // Set the internal counter.
348
+ p.pid = this.#_PIDCounter;
349
+ // Update the internal counter.
350
+ this.#_PIDCounter++;
351
+ }
352
+
353
+ // Push the passage to the array
354
+ this.#_passages.push(p);
355
+ }
356
+ } else {
357
+ // Warn user
358
+ console.warn('Ignored passage with same name as existing one!');
359
+ }
360
+ } else {
361
+ // We can only add passages to array
362
+ throw new Error('Can only add Passages to the story!');
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Remove a passage from the story by name
368
+ *
369
+ * @public
370
+ * @function removePassage
371
+ * @memberof Story
372
+ * @param {string} name - Passage name to remove
373
+ */
374
+ removePassage (name) {
375
+ this.#_passages = this.#_passages.filter(passage => passage.name !== name);
376
+ }
377
+
378
+ /**
379
+ * Find passages by tag
380
+ *
381
+ * @public
382
+ * @function getPassagesByTag
383
+ * @memberof Story
384
+ * @param {string} t - Passage name to search for
385
+ * @returns {Array} Return array of passages
386
+ */
387
+ getPassagesByTag (t) {
388
+ // Look through passages
389
+ return this.#_passages.filter((passage) => {
390
+ // Look through each passage's tags
391
+ return passage.tags.some((tag) => t === tag);
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Find passage by name
397
+ *
398
+ * @public
399
+ * @function getPassageByName
400
+ * @memberof Story
401
+ * @param {string} name - Passage name to search for
402
+ * @returns {Passage | null} Return passage or null
403
+ */
404
+ getPassageByName (name) {
405
+ // Look through passages
406
+ const results = this.#_passages.find((passage) => passage.name === name);
407
+ // Return entry or null, if not found
408
+ return results !== undefined ? results : null;
409
+ }
410
+
411
+ /**
412
+ * Find passage by PID
413
+ *
414
+ * @public
415
+ * @function getPassageByPID
416
+ * @memberof Story
417
+ * @param {number} pid - Passage PID to search for
418
+ * @returns {Passage | null} Return passage or null
419
+ */
420
+ getPassageByPID (pid) {
421
+ // Look through passages
422
+ const results = this.#_passages.find((passage) => passage.pid === pid);
423
+ // Return passages or null
424
+ return results !== undefined ? results : null;
425
+ }
426
+
427
+ /**
428
+ * Size (number of passages).
429
+ * Does not include StoryAuthor, StoryTitle, UserScript, or UserStylesheet passages
430
+ *
431
+ * @public
432
+ * @function size
433
+ * @memberof Story
434
+ * @returns {number} Return number of passages
435
+ */
436
+ size () {
437
+ return this.#_passages.length;
438
+ }
439
+
440
+ /**
441
+ * forEach-style iterator of passages in Story
442
+ *
443
+ * @public
444
+ * @function forEach
445
+ * @memberof Story
446
+ * @param {Function} callback - Callback function
447
+ */
448
+ forEach (callback) {
449
+ // Check if argument is a function
450
+ if (typeof callback !== 'function') {
451
+ // Throw error
452
+ throw new Error('Callback must be a function!');
453
+ }
454
+
455
+ // Use internal forEach
456
+ this.#_passages.forEach((element, index) => {
457
+ // Call callback function with element and index
458
+ callback(element, index);
459
+ });
460
+ }
461
+ }