extwee 2.2.5 → 2.3.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 (63) hide show
  1. package/.github/workflows/dependabot-automerge.yml +23 -0
  2. package/.github/workflows/nodejs.yml +4 -1
  3. package/README.md +29 -14
  4. package/SECURITY.md +1 -1
  5. package/build/extwee.web.min.js +2 -0
  6. package/build/extwee.web.min.js.LICENSE.txt +1 -0
  7. package/extwee.config.json +6 -0
  8. package/extwee.config.md +67 -0
  9. package/index.js +2 -0
  10. package/package.json +24 -23
  11. package/src/CLI/CommandLineProcessing.js +196 -0
  12. package/src/CLI/ProcessConfig/loadStoryFormat.js +102 -0
  13. package/src/CLI/ProcessConfig/readDirectories.js +46 -0
  14. package/src/CLI/ProcessConfig.js +175 -0
  15. package/src/CLI/isDirectory.js +27 -0
  16. package/src/CLI/isFile.js +28 -0
  17. package/src/Config/parser.js +30 -8
  18. package/src/Passage.js +17 -2
  19. package/src/Story.js +101 -1
  20. package/src/StoryFormat/compile.js +19 -0
  21. package/src/StoryFormat.js +51 -0
  22. package/src/extwee.js +20 -195
  23. package/test/Config/Config.test.js +40 -10
  24. package/test/Config/files/full.json +8 -0
  25. package/test/Config/files/valid.json +4 -3
  26. package/test/Config/isDirectory.test.js +44 -0
  27. package/test/Config/isFile.test.js +50 -0
  28. package/test/Config/loadStoryFormat.test.js +101 -0
  29. package/test/Config/readDirectories.test.js +68 -0
  30. package/test/Objects/Passage.test.js +5 -0
  31. package/test/Objects/Story.test.js +174 -0
  32. package/test/Objects/StoryFormat.test.js +60 -0
  33. package/test/TWS/Parse.test.js +0 -22
  34. package/test/Web/window.Extwee.test.js +85 -0
  35. package/types/Story.d.ts +26 -1
  36. package/types/StoryFormat/compile.d.ts +8 -0
  37. package/types/StoryFormat.d.ts +7 -0
  38. package/types/index.d.ts +4 -2
  39. package/types/src/CLI/CommandLineProcessing.d.ts +8 -0
  40. package/types/src/CLI/ProcessConfig/loadStoryFormat.d.ts +20 -0
  41. package/types/src/CLI/ProcessConfig/readDirectories.d.ts +9 -0
  42. package/types/src/CLI/ProcessConfig.d.ts +12 -0
  43. package/types/src/CLI/isDirectory.d.ts +1 -0
  44. package/types/src/CLI/isFile.d.ts +1 -0
  45. package/types/src/Config/parser.d.ts +6 -0
  46. package/types/src/Config/reader.d.ts +11 -0
  47. package/types/src/IFID/generate.d.ts +14 -0
  48. package/types/src/JSON/parse.d.ts +44 -1
  49. package/types/src/Passage.d.ts +49 -4
  50. package/types/src/Story.d.ts +110 -16
  51. package/types/src/StoryFormat/compile.d.ts +8 -0
  52. package/types/src/StoryFormat/parse.d.ts +46 -3
  53. package/types/src/StoryFormat.d.ts +69 -38
  54. package/types/src/TWS/parse.d.ts +3 -3
  55. package/types/src/Twee/parse.d.ts +3 -4
  56. package/types/src/Twine1HTML/compile.d.ts +3 -1
  57. package/types/src/Twine1HTML/parse.d.ts +3 -4
  58. package/types/src/Twine2ArchiveHTML/compile.d.ts +8 -0
  59. package/types/src/Twine2ArchiveHTML/parse.d.ts +31 -1
  60. package/types/src/Twine2HTML/compile.d.ts +7 -2
  61. package/types/src/Twine2HTML/parse.d.ts +12 -9
  62. package/index.html +0 -22
  63. package/test/TWS/TWSParser/Example1.tws +0 -150
@@ -0,0 +1,175 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { isFile } from './isFile.js';
3
+ import {
4
+ parseTwine2HTML,
5
+ parseTwee,
6
+ parseStoryFormat,
7
+ parseTwine1HTML,
8
+ compileTwine2HTML
9
+ } from '../../index.js';
10
+ import { loadStoryFormat } from './ProcessConfig/loadStoryFormat.js';
11
+
12
+ // Look for the config file in the current directory.
13
+ const configFile = 'extwee.config.json';
14
+
15
+ /**
16
+ * Check if the config file exists.
17
+ * @function ConfigFilePresent
18
+ * @description This function checks if the config file exists in the current directory.
19
+ * If the config file does not exist, the function will return false.
20
+ * If the config file exists, the function will return true.
21
+ * The config file is used to store configuration options for the Extwee CLI.
22
+ * @returns {boolean} - true if the config file exists, false otherwise.
23
+ */
24
+ export const ConfigFilePresent = () => {
25
+ // Check if the config file exists.
26
+ return isFile(configFile);
27
+ }
28
+
29
+ /**
30
+ * Processes the config file, if present.
31
+ * @function ConfigFileProcessing
32
+ * @description This function processes the config file.
33
+ * It checks if the config file exists and if it does, it reads the config file.
34
+ * If the config file does not exist, the function will exit the process with an error message.
35
+ * The config file is used to store configuration options for the Extwee CLI.
36
+ * @returns {void}
37
+ * @throws {Error} - If the config file does not exist or if there is an error parsing the config file.
38
+ */
39
+ export function ConfigFileProcessing() {
40
+ // Check if the config file exists.
41
+ const configFileExists = isFile(configFile);
42
+
43
+ // If the config file does not exist, exit the process.
44
+ if (configFileExists === false) {
45
+ console.error(`Error: ${configFile} does not exist.`);
46
+ // Exit the process.
47
+ process.exit(1);
48
+ }
49
+
50
+ // If the config file exists, read it.
51
+ let configFileContents = {};
52
+ // If the config file exists and no command-line arguments are passed, read the config file.
53
+
54
+ // Read the config file.
55
+ const configFileData = readFileSync(configFile, 'utf-8');
56
+
57
+ // Parse the config file.
58
+ try {
59
+ configFileContents = JSON.parse(configFileData);
60
+ console.log(`Using config file: ${configFile}`, configFileContents);
61
+ } catch (e) {
62
+ // There was an error parsing the config file.
63
+ console.error(`Error parsing ${configFile}: ${e}`);
64
+ // Exit the process.
65
+ process.exit(1);
66
+ }
67
+
68
+ // First, we need to know the mode (compile or decompile).
69
+
70
+ // Does the config file the "mode" property?
71
+ if (configFileContents.mode === undefined) {
72
+ // The config file does not have a "mode" property.
73
+ console.error(`Error: ${configFile} does not have a "mode" property.`);
74
+ // Exit the process.
75
+ process.exit(1);
76
+ }
77
+
78
+ // Check if the mode is valid.
79
+ if (configFileContents.mode !== 'compile' && configFileContents.mode !== 'decompile') {
80
+ // The mode is not valid.
81
+ console.error(`Error: ${configFile} has an invalid "mode" property. Must be "compile" or "decompile".`);
82
+ // Exit the process.
83
+ process.exit(1);
84
+ }
85
+
86
+ // Check if the "input" property is present.
87
+ if (configFileContents.input === undefined) {
88
+ // The config file does not have an "input" property.
89
+ console.error(`Error: ${configFile} does not have an "input" property.`);
90
+ // Exit the process.
91
+ process.exit(1);
92
+ }
93
+
94
+ // Check if the "output" property is present.
95
+ if (configFileContents.output === undefined) {
96
+ // The config file does not have an "input" property.
97
+ console.error(`Error: ${configFile} does not have an "output" property.`);
98
+ // Exit the process.
99
+ process.exit(1);
100
+ }
101
+
102
+ // Check if de-compile mode is enabled.
103
+ const isDecompileMode = (configFileContents.mode === "decompile");
104
+
105
+ // Check if compile mode is enabled.
106
+ const isCompileMode = (configFileContents.mode === "compile");
107
+
108
+ // Check if Twine 1 is enabled.
109
+ const isTwine1Mode = (configFileContents.twine1Project === true);
110
+
111
+ // Check if story-format-version is present.
112
+ if (configFileContents.storyFormatVersion === undefined) {
113
+ // If not present, assume "latest" version.
114
+ configFileContents.storyFormatVersion = 'latest';
115
+ }
116
+
117
+ // Check if Twine 2 is enabled.
118
+ // By default, Twine 2 is enabled and only disabled if Twine 1 is enabled.
119
+ // This is because Twine 2 is the default mode for Extwee.
120
+ const isTwine2Mode = (isTwine1Mode === false ? true : false);
121
+
122
+ // De-compile Twine 2 HTML into Twee 3 branch.
123
+ if (isTwine2Mode === true && isDecompileMode === true) {
124
+ // Read the input HTML file.
125
+ const inputHTML = readFileSync(configFileContents.input, 'utf-8');
126
+
127
+ // Parse the input HTML file into Story object.
128
+ const storyObject = parseTwine2HTML(inputHTML);
129
+
130
+ // Write the output file from Story as Twee 3.
131
+ writeFileSync(configFileContents.output, storyObject.toTwee());
132
+
133
+ // Exit the process.
134
+ return;
135
+ }
136
+
137
+ // Compile Twee 3 into Twine 2 HTML branch.
138
+ if (isTwine2Mode === true && isCompileMode === true) {
139
+ // Read the input file.
140
+ const inputTwee = readFileSync(configFileContents.input, 'utf-8');
141
+
142
+ // Parse the input file.
143
+ const story = parseTwee(inputTwee);
144
+
145
+ // Read the story format file.
146
+ const inputStoryFormat = loadStoryFormat(configFileContents.storyFormatName, configFileContents.storyFormatVersion);
147
+
148
+ // Parse the story format file.
149
+ const parsedStoryFormat = parseStoryFormat(inputStoryFormat);
150
+
151
+ // Compile the story.
152
+ const Twine2HTML = compileTwine2HTML(story, parsedStoryFormat);
153
+
154
+ // Write the output file.
155
+ writeFileSync(configFileContents.output, Twine2HTML);
156
+
157
+ // Exit the process.
158
+ return;
159
+ }
160
+
161
+ // De-compile Twine 1 HTML into Twee 3 branch.
162
+ if (isTwine1Mode === true && isDecompileMode === true) {
163
+ // Read the input HTML file.
164
+ const inputHTML = readFileSync(configFileContents.input, 'utf-8');
165
+
166
+ // Parse the input HTML file into Story object.
167
+ const storyObject = parseTwine1HTML(inputHTML);
168
+
169
+ // Write the output file from Story as Twee 3.
170
+ writeFileSync(configFileContents.output, storyObject.toTwee());
171
+
172
+ return;
173
+ }
174
+
175
+ }
@@ -0,0 +1,27 @@
1
+ import { statSync } from 'node:fs';
2
+
3
+ /*
4
+ * Check if a passed option is a valid directory.
5
+ * @function isDirectory
6
+ * @description Check if a directory exists.
7
+ * @param {string} path - Path to directory.
8
+ * @returns {boolean} True if directory exists, false if not.
9
+ */
10
+ export const isDirectory = (path) => {
11
+ // set default.
12
+ let result = false;
13
+
14
+ try {
15
+ // Attempt t0 get stats.
16
+ const stats = statSync(path);
17
+
18
+ // Return if path is a directory.
19
+ result = stats.isDirectory();
20
+ } catch (e) {
21
+ // If there was an error, log it.
22
+ console.error(`Error: ${e}`);
23
+ }
24
+
25
+ // Return either the default (false) or the result (true).
26
+ return result;
27
+ };
@@ -0,0 +1,28 @@
1
+ // Import fs.
2
+ import { statSync } from 'node:fs';
3
+
4
+ /*
5
+ * Check if a passed option is a valid file.
6
+ * @function isFile
7
+ * @description Check if a file exists.
8
+ * @param {string} path - Path to file.
9
+ * @returns {boolean} True if file exists, false if not.
10
+ */
11
+ export const isFile = (path) => {
12
+ // set default.
13
+ let result = false;
14
+
15
+ try {
16
+ // Attempt t0 get stats.
17
+ const stats = statSync(path);
18
+
19
+ // Return if path is a file.
20
+ result = stats.isFile();
21
+ } catch (e) {
22
+ // If there was an error, log it.
23
+ console.error(`Error: ${e}`);
24
+ }
25
+
26
+ // Return either the default (false) or the result (true).
27
+ return result;
28
+ };
@@ -12,8 +12,11 @@ export function parser(obj) {
12
12
  // Extracted results.
13
13
  let results = {
14
14
  StoryFormat: null,
15
- StoryTitle: null,
16
- StoryVersion: null
15
+ Input: null,
16
+ Output: null,
17
+ Mode: null,
18
+ Twine1Project: false,
19
+ StoryFormatVersion: null
17
20
  };
18
21
 
19
22
  // Does the object contain 'StoryFormat'?
@@ -21,14 +24,33 @@ export function parser(obj) {
21
24
  results.StoryFormat = obj['story-format'];
22
25
  }
23
26
 
24
- // Does the object contain 'StoryTitle'?
25
- if (Object.hasOwnProperty.call(obj, 'story-title')) {
26
- results.StoryTitle = obj['story-title'];
27
+ // Does the object contain 'StoryFormatVersion'?
28
+ if (Object.hasOwnProperty.call(obj, 'story-format-version')) {
29
+ results.StoryFormatVersion = obj['story-format-version'];
30
+ } else {
31
+ results.StoryFormatVersion = "latest";
27
32
  }
28
33
 
29
- // Does the object contain 'StoryVersion'?
30
- if (Object.hasOwnProperty.call(obj, 'story-version')) {
31
- results.StoryVersion = obj['story-version'];
34
+ // Does the object contain 'mode'?
35
+ if (Object.hasOwnProperty.call(obj, 'mode')) {
36
+ results.Mode = obj['mode'];
37
+ }
38
+
39
+ // Does the object contain 'input'?
40
+ if (Object.hasOwnProperty.call(obj, 'input')) {
41
+ results.Input = obj['input'];
42
+ }
43
+
44
+ // Does the object contain 'output'?
45
+ if (Object.hasOwnProperty.call(obj, 'output')) {
46
+ results.Output = obj['output'];
47
+ }
48
+
49
+ // Does the object contain 'twine1-project'?
50
+ if (Object.hasOwnProperty.call(obj, 'twine1-project')) {
51
+ results.Twine1Project = obj['twine1-project'];
52
+ } else {
53
+ results.Twine1Project = false;
32
54
  }
33
55
 
34
56
  // Return the extracted results.
package/src/Passage.js CHANGED
@@ -190,8 +190,23 @@ export default class Passage {
190
190
  content += ` ${JSON.stringify(this.metadata)}`;
191
191
  }
192
192
 
193
- // Add newline, text, and two newlines.
194
- content += `\n${this.text}\n\n`;
193
+ // Split the text into lines.
194
+ const lines = this.text.split('\n');
195
+
196
+ // For each line, check if it begins with a double-colon.
197
+ for (let i = 0; i < lines.length; i++) {
198
+ // Check if the line begins with a double-colon.
199
+ if (lines[i].startsWith('::')) {
200
+ // Escape the double-colon.
201
+ lines[i] = `\\${lines[i]}`;
202
+ }
203
+ }
204
+
205
+ // Rejoin the lines.
206
+ const output = lines.join('\n');
207
+
208
+ // Add newline and text.
209
+ content += `\n${output}\n\n`;
195
210
 
196
211
  // Return string.
197
212
  return content;
package/src/Story.js CHANGED
@@ -2,10 +2,12 @@ import Passage from './Passage.js';
2
2
  import { generate as generateIFID } from './IFID/generate.js';
3
3
  import { encode } from 'html-entities';
4
4
 
5
+ // Set the creator name.
6
+ // This is used to identify the program that created the story.
5
7
  const creatorName = 'extwee';
6
8
 
7
9
  // Set the creator version.
8
- const creatorVersion = '2.2.5';
10
+ const creatorVersion = '2.3.0';
9
11
 
10
12
  /**
11
13
  * Story class.
@@ -22,6 +24,8 @@ const creatorVersion = '2.2.5';
22
24
  * @property {string} creatorVersion - Version used to create Story.
23
25
  * @property {object} metadata - Metadata of Story.
24
26
  * @property {object} tagColors - Tag Colors
27
+ * @property {string} storyJavaScript - Story JavaScript
28
+ * @property {string} storyStylesheet - Story Stylesheet
25
29
  * @method {number} addPassage - Add a passage to the story and returns the new length of the passages array.
26
30
  * @method {number} removePassageByName - Remove a passage from the story by name and returns the new length of the passages array.
27
31
  * @method {Array} getPassagesByTag - Find passages by tag.
@@ -107,6 +111,18 @@ class Story {
107
111
  */
108
112
  #_tagColors = {};
109
113
 
114
+ /**
115
+ * Story JavaScript
116
+ * @private
117
+ */
118
+ #_storyJavaScript = '';
119
+
120
+ /**
121
+ * Story Stylesheet
122
+ * @private
123
+ */
124
+ #_storyStylesheet = '';
125
+
110
126
  /**
111
127
  * Creates a story.
112
128
  * @param {string} name - Name of the story.
@@ -322,6 +338,45 @@ class Story {
322
338
  }
323
339
  }
324
340
 
341
+ /**
342
+ * Story stylesheet data can be set as a passage, property value, or both.
343
+ * @returns {string} storyStylesheet
344
+ */
345
+ get storyStylesheet () {
346
+ return this.#_storyStylesheet;
347
+ }
348
+
349
+ /**
350
+ * @param {string} s - Replacement story stylesheet
351
+ */
352
+ set storyStylesheet (s) {
353
+ if (typeof s === 'string') {
354
+ this.#_storyStylesheet = s;
355
+ } else {
356
+ throw new Error('Story stylesheet must be a string!');
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Get story JavaScript.
362
+ * @returns {string} storyJavaScript
363
+ */
364
+ get storyJavaScript () {
365
+ return this.#_storyJavaScript;
366
+ }
367
+
368
+ /**
369
+ * Set story JavaScript.
370
+ * @param {string} s - Replacement story JavaScript
371
+ */
372
+ set storyJavaScript (s) {
373
+ if (typeof s === 'string') {
374
+ this.#_storyJavaScript = s;
375
+ } else {
376
+ throw new Error('Story JavaScript must be a string!');
377
+ }
378
+ }
379
+
325
380
  /**
326
381
  * Add a passage to the story.
327
382
  * Passing `StoryData` will override story metadata and `StoryTitle` will override story name.
@@ -484,6 +539,8 @@ class Story {
484
539
  creator: this.creator,
485
540
  creatorVersion: this.creatorVersion,
486
541
  zoom: this.zoom,
542
+ style: this.storyStylesheet,
543
+ script: this.storyJavaScript,
487
544
  passages: []
488
545
  };
489
546
 
@@ -595,6 +652,16 @@ class Story {
595
652
  // Add two newlines.
596
653
  outputContents += '\n\n';
597
654
 
655
+ // Write out the story stylesheet, if any.
656
+ if (this.#_storyStylesheet.length > 0) {
657
+ outputContents += ':: StoryStylesheet [stylesheet]\n' + this.#_storyStylesheet + '\n\n';
658
+ }
659
+
660
+ // Write out the story JavaScript, if any.
661
+ if (this.#_storyJavaScript.length > 0) {
662
+ outputContents += ':: StoryJavaScript [script]\n' + this.#_storyJavaScript + '\n\n';
663
+ }
664
+
598
665
  // For each passage, append it to the output.
599
666
  this.passages.forEach((passage) => {
600
667
  outputContents += passage.toTwee();
@@ -620,6 +687,10 @@ class Story {
620
687
  * - `format`: (string) Optional. The format of the story.
621
688
  * - `format-version`: (string) Optional. The version of the format of the story.
622
689
  *
690
+ * Because story stylesheet data can be represented as a passage, property value, or both, all approaches are encoded.
691
+ *
692
+ * Because story JavaScript can be represented as a passage, property value, or both, all approaches are encoded.
693
+ *
623
694
  * @method toTwine2HTML
624
695
  * @returns {string} Twine 2 HTML string
625
696
  */
@@ -728,6 +799,9 @@ class Story {
728
799
  // Add the default attributes.
729
800
  storyData += ' options hidden>\n';
730
801
 
802
+ // We may have passages with tags of 'stylesheet', story stylesheet data, both, or none.
803
+
804
+ // Step 1: Add all passages with tag of 'stylesheet' to the stylesheet element.
731
805
  // Filter out passages with tag of 'stylesheet'.
732
806
  const stylesheetPassages = passages.filter((passage) => passage.tags.includes('stylesheet'));
733
807
 
@@ -749,6 +823,16 @@ class Story {
749
823
  storyData += '</style>\n';
750
824
  }
751
825
 
826
+ // Step 2: Check if the internal stylesheet data is empty.
827
+ // If it is not empty, add it to the stylesheet element.
828
+ if (this.#_storyStylesheet.length > 0) {
829
+ // Add the internal stylesheet.
830
+ storyData += `\t<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">${this.#_storyStylesheet}</style>\n`;
831
+ }
832
+
833
+ // We may have passages with tags of 'script', story JavaScript data, both, or none.
834
+
835
+ // Step 1: Add all passages with tag of 'script' to the script element.
752
836
  // Filter out passages with tag of 'script'.
753
837
  const scriptPassages = passages.filter((passage) => passage.tags.includes('script'));
754
838
 
@@ -770,6 +854,13 @@ class Story {
770
854
  storyData += '</script>\n';
771
855
  }
772
856
 
857
+ // Step 2: Check if the internal JavaScript data is empty.
858
+ // If it is not empty, add it to the script element.
859
+ if (this.#_storyJavaScript.length > 0) {
860
+ // Add the internal JavaScript.
861
+ storyData += `\t<script role="script" id="twine-user-script" type="text/twine-javascript">${this.#_storyJavaScript}</script>\n`;
862
+ }
863
+
773
864
  // Reset the PID counter.
774
865
  PIDcounter = 1;
775
866
 
@@ -781,6 +872,15 @@ class Story {
781
872
  PIDcounter++;
782
873
  });
783
874
 
875
+ // Generate <tw-tag> elements for each tag, if any.
876
+ const tagList = Object.keys(this.tagColors);
877
+
878
+ // For each tag, generate a <tw-tag> element.
879
+ tagList.forEach((tag) => {
880
+ // Add the <tw-tag> element.
881
+ storyData += `\t<tw-tag name="${tag}" color="${this.tagColors[tag]}"></tw-tag>\n`;
882
+ });
883
+
784
884
  // Close the HTML element.
785
885
  storyData += '</tw-storydata>';
786
886
 
@@ -0,0 +1,19 @@
1
+ import StoryFormat from '../StoryFormat.js';
2
+
3
+ /**
4
+ * Compiles a {@link StoryFormat} object into a JSONP string for writing to a `format.js` file.
5
+ * @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-storyformats-spec.md Twine 2 Story Formats Specification}
6
+ * @param {StoryFormat} storyFormat Story format object to compile.
7
+ * @returns {string} JSONP string.
8
+ */
9
+ function compile (storyFormat) {
10
+ // Test if storyFormat is a StoryFormat object.
11
+ if (!(storyFormat instanceof StoryFormat)) {
12
+ throw new TypeError('Error: Incoming object is not a storyFormat object');
13
+ }
14
+
15
+ // Create a JSONP string wrapped with the function window.StoryFormat.
16
+ return `window.storyFormat(${JSON.stringify(storyFormat)})`;
17
+ }
18
+
19
+ export { compile };
@@ -1,3 +1,5 @@
1
+ import { valid } from 'semver';
2
+
1
3
  /**
2
4
  * StoryFormat representing a Twine 2 story format.
3
5
  *
@@ -85,6 +87,19 @@ export default class StoryFormat {
85
87
  */
86
88
  #_source = '';
87
89
 
90
+ // Constructor with all parameters.
91
+ constructor(name = 'Untitled Story Format', version = '', description = '', author = '', image = '', url = '', license = '', proofing = false, source = '') {
92
+ this.name = name;
93
+ this.version = version;
94
+ this.description = description;
95
+ this.author = author;
96
+ this.image = image;
97
+ this.url = url;
98
+ this.license = license;
99
+ this.proofing = proofing;
100
+ this.source = source;
101
+ }
102
+
88
103
  /**
89
104
  * Name
90
105
  * @returns {string} Name.
@@ -246,4 +261,40 @@ export default class StoryFormat {
246
261
  toString() {
247
262
  return JSON.stringify(this, null, "\t");
248
263
  }
264
+
265
+ /**
266
+ * Produces a JSON representation of the story format object.
267
+ * @method toJSON
268
+ * @returns {object} - A JSON representation of the story format.
269
+ */
270
+ toJSON() {
271
+ // name: (string) Optional. The name of the story format. (Omitting the name will lead to an Untitled Story Format.)
272
+ // Set a default name.
273
+ let name = "Untitled Story Format";
274
+
275
+ // Check if the story format is not an empty string.
276
+ if (this.name.length > 0) {
277
+ // Update the name.
278
+ name = this.name;
279
+ }
280
+
281
+ // version: (string) Required, and semantic version-style formatting (x.y.z, e.g., 1.2.1) of the version is also required.
282
+
283
+ // Check if the version is valid. If not, throw an error.
284
+ if (!valid(this.version)) {
285
+ throw new TypeError('ERROR: Version must be a valid semantic version!');
286
+ }
287
+
288
+ return JSON.stringify({
289
+ name,
290
+ version: this.version,
291
+ description: this.description,
292
+ author: this.author,
293
+ image: this.image,
294
+ url: this.url,
295
+ license: this.license,
296
+ proofing: this.proofing,
297
+ source: this.source
298
+ });
299
+ }
249
300
  }