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/HTMLWriter.js CHANGED
@@ -1,177 +1,227 @@
1
- const fs = require("fs");
2
- const Story = require('./Story.js');
3
- const StoryFormat = require('./StoryFormat.js');
4
- /**
5
- * @class HTMLWriter
6
- * @module HTMLWriter
7
- */
8
- class HTMLWriter {
9
- /**
10
- * @method HTMLWriter
11
- * @constructor
12
- */
13
- constructor (file, story, storyFormat, css, js) {
14
-
15
- if( !(story instanceof Story) ) {
16
- throw new Error("Error: story must be a Story object!");
17
- }
18
-
19
- if( !(storyFormat instanceof StoryFormat)) {
20
-
21
- throw new Error("storyFormat must be a StoryFormat object!");
22
-
23
- }
24
-
25
- let cssContent = css || null;
26
- let jsContent = js || null;
27
-
28
- this.writeFile(file, story, storyFormat, cssContent, jsContent);
29
- }
30
-
31
- writeFile(file, story, storyFormat, cssContent, jsContent) {
32
-
33
- let outputContents = "";
34
-
35
- // Build <tw-storydata>
36
- let storyData =
37
- '<tw-storydata name="' + story.name + '" ' +
38
- 'startnode="' + story.getStartingPassage() + '" ' +
39
- 'creator="' + story.creator + '" ' +
40
- 'creator-version="' + story.creatorVersion + '" ' +
41
- 'ifid="' + story.metadata.ifid + '" ' +
42
- 'zoom="' + story.metadata.zoom + '" ' +
43
- 'format="' + storyFormat.name + '" ' +
44
- 'format-version="' + storyFormat.version + '" ' +
45
- 'options hidden>\n';
46
-
47
- // Build the STYLE
48
- storyData += '<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">';
49
-
50
- // Get any passages tagged with 'stylesheet'
51
- let stylePassages = story.getStylePassages();
52
-
53
- // CSS passed through from DirectoryReader
54
- if(cssContent != null) {
55
- storyData += cssContent;
56
- }
57
-
58
- // Iterate through the collection and add to storyData
59
- for(let content of stylePassages) {
60
- storyData += content.text;
61
- }
62
-
63
- storyData += '</style>\n';
64
-
65
- // Build the SCRIPT
66
- storyData += '<script role="script" id="twine-user-script" type="text/twine-javascript">';
67
-
68
- // Get any passages tagged with 'script'
69
- let scriptPassages = story.getScriptPassages();
70
-
71
- // JS passed through from DirectoryReader
72
- if(jsContent != null) {
73
- storyData += jsContent;
74
- }
75
-
76
- // Iterate through the collection and add to storyData
77
- for(let content of scriptPassages) {
78
- storyData += content.text;
79
- }
80
-
81
- storyData += '</script>\n';
82
-
83
- // All the script data has been written.
84
- // Delete all 'script'-tagged passages
85
- story.deleteAllByTag("script");
86
-
87
- // All the style data has been written.
88
- // Delete all 'style'-tagged passages
89
- story.deleteAllByTag("stylesheet");
90
-
91
- // Build the passages
92
- for(let passage of story.passages) {
93
-
94
- storyData += '<tw-passagedata pid="' + passage.pid + '" name="' + passage.name + '"';
95
-
96
- // Write out any tags
97
- if(passage.tags.length > 1) {
98
-
99
- storyData += ' tags="';
100
-
101
- for(let t of passage.tags) {
102
-
103
- storyData += t + ', ';
104
-
105
- }
106
-
107
- storyData += '" ';
108
-
109
- } else if(passage.tags.length == 1) {
110
-
111
- storyData += ' tags="' + passage.tags[0] + '" ';
112
-
113
- } else {
114
-
115
- storyData += ' tags ';
116
-
117
- }
118
-
119
- // Write out position
120
- if(passage.metadata.hasOwnProperty("position")) {
121
-
122
- storyData += 'position="' +
123
- passage.metadata.position + '" ';
124
-
125
- } else {
126
-
127
- // Didn't have a position.
128
- // Make one up.
129
- storyData += 'position="100,100" ';
130
-
131
- }
132
-
133
- // Write out size
134
- if(passage.metadata.hasOwnProperty("size") ) {
135
-
136
- storyData += 'size="' +
137
- passage.metadata.size + '" ';
138
-
139
- } else {
140
-
141
- // Didn't have a size.
142
- // Make one up.
143
- storyData += 'size="100,100" ';
144
-
145
- }
146
-
147
- storyData += '>' + passage.text + '</tw-passagedata>\n';
148
-
149
- }
150
-
151
- storyData += '</tw-storydata>';
152
-
153
- // Replace the story name in the source file
154
- storyFormat.source = storyFormat.source.replace("{{STORY_NAME}}", story.name);
155
-
156
- // Replace the story data
157
- storyFormat.source = storyFormat.source.replace("{{STORY_DATA}}", storyData);
158
-
159
- outputContents += storyFormat.source
160
-
161
- try {
162
-
163
- // Try to write
164
- fs.writeFileSync(file, outputContents);
165
-
166
- } catch(event) {
167
-
168
- // Throw error
169
- throw new Error("Error: Cannot write HTML file!");
170
-
171
- }
172
-
173
- }
174
-
175
- }
176
-
177
- module.exports = HTMLWriter;
1
+ /**
2
+ * @external Story
3
+ * @see Story.js
4
+ * @external StoryFormat
5
+ * @see StoryFormat.js
6
+ */
7
+
8
+ import fs from 'fs';
9
+ import Story from './Story.js';
10
+ import StoryFormat from './StoryFormat.js';
11
+ import { v4 as uuidv4 } from 'uuid';
12
+
13
+ /**
14
+ * @class HTMLWriter
15
+ * @module HTMLWriter
16
+ */
17
+ export default class HTMLWriter {
18
+ /**
19
+ * Write story to file using story format and adding any CSS and JS
20
+ *
21
+ * @public
22
+ * @static
23
+ * @function writeFile
24
+ * @param {string} file - File to write
25
+ * @param {Story} story - Story object to write
26
+ * @param {StoryFormat} storyFormat - StoryFormat to write
27
+ */
28
+ static write (file, story, storyFormat) {
29
+ if (!(story instanceof Story)) {
30
+ throw new Error('Error: story must be a Story object!');
31
+ }
32
+
33
+ if (!(storyFormat instanceof StoryFormat)) {
34
+ throw new Error('storyFormat must be a StoryFormat object!');
35
+ }
36
+
37
+ let outputContents = '';
38
+ let storyData = '';
39
+
40
+ // Look for StoryTitle
41
+ const storyTitle = story.getPassageByName('StoryTitle');
42
+
43
+ if (storyTitle != null) {
44
+ // Use StoryTitle for name
45
+ storyData += `<tw-storydata name="${storyTitle.text}"`;
46
+ } else {
47
+ throw new Error("'name' is required attribute. (Add StoryTitle to story.)");
48
+ }
49
+
50
+ // Does start exist?
51
+ if (story.start !== '') {
52
+ // Try to get starting passage
53
+ const startingPassage = story.getPassageByName(story.start);
54
+ // Does it exist currently?
55
+ if (startingPassage !== null) {
56
+ // Add the starting passage
57
+ storyData += ` startnode="${startingPassage.pid}"`;
58
+ } else {
59
+ // Throw error if no starting passage exists
60
+ throw new Error('Starting passage not found');
61
+ }
62
+ } else {
63
+ // Throw error if no starting passage exists
64
+ throw new Error('No starting passage found!');
65
+ }
66
+
67
+ // Defaults to 'extwee' if missing.
68
+ storyData += ` creator="${story.creator}"`;
69
+
70
+ // Default to extwee version.
71
+ storyData += ` creator-version="${story.creatorVersion}"`;
72
+
73
+ // Check if IFID exists.
74
+ if (story.IFID !== '') {
75
+ // Write the existing IFID
76
+ storyData += ` ifid="${story.IFID}"`;
77
+ } else {
78
+ // Generate a new IFID
79
+ // Twine 2 uses v4 (random) UUIDs, using only capital letters
80
+ storyData += ` ifid="${uuidv4().toUpperCase()}"`;
81
+ }
82
+
83
+ // Write existing or default value.
84
+ storyData += ` zoom="${story.zoom}"`;
85
+
86
+ // Write existing or default value.
87
+ storyData += ` format="${storyFormat.name}"`;
88
+
89
+ // Write existing or default value.
90
+ storyData += ` format-version="${storyFormat.version}"`;
91
+
92
+ // Add the default.
93
+ storyData += ' options hidden>\n';
94
+
95
+ // Start the STYLE.
96
+ storyData += '\t<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">';
97
+
98
+ // Get stylesheet passages
99
+ const stylesheetPassages = story.getPassagesByTag('stylesheet');
100
+
101
+ // Concatenate passages
102
+ stylesheetPassages.forEach((passage) => {
103
+ // Add text of passages
104
+ storyData += passage.text;
105
+ // Remove from story
106
+ story.removePassage(passage);
107
+ });
108
+
109
+ // Close the STYLE
110
+ storyData += '</style>\n';
111
+
112
+ // Start the SCRIPT
113
+ storyData += '\t<script role="script" id="twine-user-script" type="text/twine-javascript">';
114
+
115
+ // Get stylesheet passages
116
+ const scriptPassages = story.getPassagesByTag('script');
117
+
118
+ // Concatenate passages
119
+ scriptPassages.forEach((passage) => {
120
+ // Add text of passages
121
+ storyData += passage.text;
122
+ // Remove from story
123
+ story.removePassage(passage);
124
+ });
125
+
126
+ // Close SCRIPT
127
+ storyData += '</script>\n';
128
+
129
+ // Build the passages
130
+ story.forEach((passage) => {
131
+ // Start the passage element
132
+ storyData += '\t<tw-passagedata';
133
+
134
+ /**
135
+ * pid: (string) Required.
136
+ * The Passage ID (PID).
137
+ */
138
+ storyData += ` pid="${passage.pid}"`;
139
+
140
+ /**
141
+ * name: (string) Required.
142
+ * The name of the passage.
143
+ */
144
+ storyData += ` name="${passage.name}"`;
145
+
146
+ /**
147
+ * tags: (string) Optional.
148
+ * Any tags for the passage separated by spaces.
149
+ */
150
+ if (passage.tags.length > 1) {
151
+ storyData += ` tags="${passage.tags.join(' ')}" `;
152
+ } else if (passage.tags.length === 1) {
153
+ storyData += ` tags="${passage.tags[0]}" `;
154
+ }
155
+
156
+ /**
157
+ * position: (string) Optional.
158
+ * Comma-separated X and Y position of the upper-left of the passage
159
+ * when viewed within the Twine 2 editor.
160
+ */
161
+ if (Object.prototype.hasOwnProperty.call(passage.metadata, 'position')) {
162
+ storyData += ` position="${passage.metadata.position}" `;
163
+ }
164
+
165
+ /**
166
+ * size: (string) Optional.
167
+ * Comma-separated width and height of the passage
168
+ * when viewed within the Twine 2 editor.
169
+ */
170
+ if (Object.prototype.hasOwnProperty.call(passage.metadata, 'size')) {
171
+ storyData += `size="${passage.metadata.size}" `;
172
+ }
173
+
174
+ storyData += `>${HTMLWriter.escape(passage.text)}</tw-passagedata>\n`;
175
+ });
176
+
177
+ storyData += '</tw-storydata>';
178
+
179
+ // Replace the story name in the source file
180
+ storyFormat.source = storyFormat.source.replaceAll(/{{STORY_NAME}}/g, story.name);
181
+
182
+ // Replace the story data
183
+ storyFormat.source = storyFormat.source.replace('{{STORY_DATA}}', storyData);
184
+
185
+ // Combine everything together.
186
+ outputContents += storyFormat.source;
187
+
188
+ try {
189
+ // Try to write.
190
+ fs.writeFileSync(file, outputContents);
191
+ } catch (event) {
192
+ // Throw error
193
+ throw new Error('Error: Cannot write HTML file!');
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Escape HTML characters
199
+ *
200
+ * @public
201
+ * @static
202
+ * @function escape
203
+ * @param {string} text - Text to escape
204
+ * @returns {string} Escaped text
205
+ */
206
+ static escape (text) {
207
+ // Throw error if text is not a string
208
+ if (Object.prototype.toString.call(text) !== '[object String]') {
209
+ throw new Error('Text argument is not a String');
210
+ }
211
+
212
+ const rules = [
213
+ ['&', '&amp;'],
214
+ ['<', '&lt;'],
215
+ ['>', '&gt;'],
216
+ ['"', '&quot;'],
217
+ ["'", '&#x27;'],
218
+ ['`', '&#x60;']
219
+ ];
220
+
221
+ rules.forEach(([rule, template]) => {
222
+ text = text.replaceAll(rule, template);
223
+ });
224
+
225
+ return text;
226
+ }
227
+ }
package/src/Passage.js CHANGED
@@ -1,20 +1,202 @@
1
- /**
2
- * @class Passage
3
- * @module Passage
4
- */
5
- class Passage {
6
- /**
7
- * @method Passage
8
- * @constructor
9
- */
10
- constructor (name = "", tags = [], metadata = {}, text = "", pid = 1) {
11
- this.name = name;
12
- this.tags = tags;
13
- this.metadata = metadata;
14
- this.text = text;
15
- this.pid = pid;
16
- }
17
-
18
- }
19
-
20
- module.exports = Passage;
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
+ }