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/Story.js CHANGED
@@ -1,122 +1,461 @@
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
+
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
+ }