extwee 1.6.2 → 2.0.3

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