extwee 2.2.4 → 2.2.6

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.
package/README.md CHANGED
@@ -27,7 +27,7 @@
27
27
 
28
28
  ## Story Compilation
29
29
 
30
- The process of *story compilation* converts human-readable content, what is generally called a *story*, into a playable format such as HyperText Markup Language (HTML). The result is then presented as links or other visual, interactive elements for another party to interact with to see its content.
30
+ The process of *story compilation* converts human-readable content, what is generally called a *story*, into a playable format such as HyperText Markup Language (HTML). The result is then presented as links or other visual, interactive elements for another party.
31
31
 
32
32
  The term *compilation* is used because different parts of code are put together in a specific arrangement to enable later play. As part of Twine-compatible HTML, this means combining JavaScript code (generally a "story format") with story HTML data.
33
33
 
@@ -59,12 +59,13 @@ Extwee supports multiple historical and current Twine-compatible formats.
59
59
 
60
60
  | Format | Input | Output |
61
61
  |----------------------------------------------------------------------------------------------------------------------------------|-------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
62
- | [ Twine 1 HTML (2006 - 2015) ]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md ) | Yes | Partial support. Twine 1 HTML can be produced, but the `StorySettings` optional passage introduced in Twine 1.4.0 requires external libraries like jQuery not included with Extwee. |
63
- | [ Twine 1 TWS (2009 - 2015) ]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-twsoutput.md ) | Yes | Extwee does not support TWS (Python pickle) output because no current version of Twine or other story compilation tool produces this historical format. |
64
- | [ Twine 2 HTML (2015 - Present) ]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md ) | Yes | Yes |
65
- | [ Twine 2 Archive HTML (2015 - Present) ]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-archive-spec.md ) | Yes | Yes |
66
- | [ Twee 3 (2021 - Present) ]( https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md ) | Yes | Yes |
67
- | [ Twine 2 JSON (2023 - Present) ]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-jsonoutput-doc.md ) | Yes | Yes |
62
+ | [Twine 1 HTML (2006 - 2015)]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md ) | Yes | Partial support. Twine 1 HTML can be produced, but the `StorySettings` optional passage introduced in Twine 1.4.0 requires external libraries like jQuery not included with Extwee. |
63
+ | [Twine 1 TWS (2009 - 2015)]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-twsoutput.md ) | Yes | Extwee does not support TWS (Python pickle) output because no current version of Twine or other story compilation tool produces this historical format. |
64
+ | [Twine 2 HTML (2015 - Present)]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md ) | Yes | Yes |
65
+ | [Twine 2 Archive HTML (2015 - Present)]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-archive-spec.md ) | Yes | Yes |
66
+ | [Twee 3 (2021 - Present)]( https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md ) | Yes | Yes |
67
+ | [Twine 2 JSON (2023 - Present)]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-jsonoutput-doc.md ) | Yes | Yes |
68
+ | [Twine 2 Story Format (2015 - Present)]( https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-storyformats-spec.md ) | Yes | Yes |
68
69
 
69
70
  **Note:** Round-trip translations can present problems because of required fields and properties per format. Some metadata may be added or removed based on the specification being followed.
70
71
 
@@ -86,6 +87,8 @@ An object must be created using either the `new` keyword in JavaScript or as the
86
87
 
87
88
  Story and Passage objects can generate multiple output formats: `toTwee()`, `toTwine1HTML()`, `toTwine2HTML()`, and `toJSON()`. Stories cannot be played in a browser without the corresponding compiler combining it with story format data.
88
89
 
90
+ The StoryFormat object supports `toString()` method of producing a tab-separated JSON output and `toJSON()` method of generating JSON output matching the Twine 2 Story Format Specification.
91
+
89
92
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
90
93
 
91
94
  ### Parsers
@@ -110,8 +113,11 @@ Compiles story, story formats, or other data into an archive or playable format.
110
113
  - `compileTwine2HTML()`
111
114
  - `compileTwine1HTML()`
112
115
  - `compileTwine2ArchiveHTML()`
116
+ - `compileStoryFormat()`
117
+
118
+ To create playable Twine 1 HTML, an `engine.js` file must be supplied. (See [Story Format Archive](https://github.com/videlais/story-formats-archive/tree/main/official/twine1/engine/1.4.2) for available Twine 1 `engine.js` file.)
113
119
 
114
- **Note:** In order to create playable Twine 1 HTML, an `engine.js` file must be supplied.
120
+ Compilation of a story format adds the necessary function wrapper to convert the JSON output, via `toJSON()`, to JSONP.
115
121
 
116
122
  ### Support Functionality
117
123
 
@@ -129,7 +135,9 @@ Extwee has [documentation hosted on GitHub Pages](https://videlais.github.io/ext
129
135
 
130
136
  ## Command-Line Usage
131
137
 
132
- Extwee supports a command-line interface for four general scenarios:
138
+ Extwee supports a command-line interface for four general scenarios.
139
+
140
+ **Notes:** As of Extwee 2.2.5, short and long command-line option flags are separated. Short options use one hyphen followed by one character and all long options begin with two hyphens and a name as word.
133
141
 
134
142
  ### Compiling Twee 3 + Twine 2 Story Format into Twine 2 HTML
135
143
 
@@ -145,30 +153,27 @@ De-compile Twine 2 HTML into Twee 3:
145
153
 
146
154
  ### Compiling Twee 3 into Twine 1 HTML
147
155
 
148
- Enabling Twine 1 mode requires using the `-t1` or `--twine1` flag.
156
+ Enabling Twine 1 mode requires using the `--twine1` long flag.
149
157
 
150
158
  Because Twine 1 story formats can be split across files, compilation requires the "engine" from Twine 1 named `engine.js`, the name of the story format, and then its `header.html` template code and the optional but often included `code.js` file.
151
159
 
152
- `extwee -t1 -c -i <tweeFile> -o <Twine1HTML> -engine <engineJS> -name <storyFormatName> -codejs <CodeJS> -header <header>`
160
+ (Refer to the [Story Formats Archive](https://github.com/videlais/story-formats-archive) for access to historic `engine.js` and other files.)
161
+
162
+ `extwee --twine1 -c -i <tweeFile> -o <Twine1HTML> --engine <engineJS> --name <storyFormatName> --codejs <CodeJS> --header <header>`
153
163
 
154
164
  ### De-compiling Twine 1 HTML into Twee 3
155
165
 
156
- Enabling Twine 1 mode requires using the `-t1` or `--twine1` flag.
166
+ Enabling Twine 1 mode requires using the `--twine1` flag.
157
167
 
158
- `extwee -t1 -d -i <twine1HTML> -o <outputTwee>`
168
+ `extwee --twine1 -d -i <twine1HTML> -o <outputTwee>`
159
169
 
160
170
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
161
171
 
162
- ## Roadmap
163
-
164
- Each major version has its own GitHub project:
165
-
166
- - [Road to Extwee 2.2.0](https://github.com/users/videlais/projects/2)
167
- - [Road to Extwee 2.4.0](https://github.com/users/videlais/projects/4)
168
-
169
172
  ## Tree Shaking Support
170
173
 
171
- For [advanced tree shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking) patterns, most formats are broke into `compile.js` and `parse.js` files exporting associated `compile()` and `parse()` functions. When using the API, it is possible to only import a single function or object to produce smaller and potentially faster code in projects dependent on Extwee functionality.
174
+ For [advanced tree shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking) patterns, most formats are broke into `compile.js` and `parse.js` files exporting associated `compile()` and `parse()` functions.
175
+
176
+ When using the API, it is possible to only import a single function or object to produce smaller and potentially faster code in projects dependent on Extwee functionality.
172
177
 
173
178
  <p align="right">(<a href="#readme-top">back to top</a>)</p>
174
179
 
package/eslint.config.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import globals from "globals";
2
2
  import pluginJs from "@eslint/js";
3
3
  import jest from "eslint-plugin-jest";
4
+ import jsdoc from 'eslint-plugin-jsdoc';
4
5
 
5
6
  export default [
7
+ jsdoc.configs['flat/recommended'],
6
8
  {
7
9
  languageOptions: {
8
10
  globals: {
@@ -12,7 +14,11 @@ export default [
12
14
  }
13
15
  },
14
16
  plugins: {
15
- jest: jest
17
+ jest: jest,
18
+ jsdoc: jsdoc
19
+ },
20
+ rules: {
21
+ 'jsdoc/require-description': 'warn'
16
22
  }
17
23
  },
18
24
  pluginJs.configs.recommended,
package/index.js CHANGED
@@ -8,6 +8,7 @@ import { parse as parseTWS } from './src/TWS/parse.js';
8
8
  import { compile as compileTwine1HTML } from './src/Twine1HTML/compile.js';
9
9
  import { compile as compileTwine2HTML } from './src/Twine2HTML/compile.js';
10
10
  import { compile as compileTwine2ArchiveHTML } from './src/Twine2ArchiveHTML/compile.js';
11
+ import { compile as compileStoryFormat } from './src/StoryFormat/compile.js';
11
12
  import { generate as generateIFID } from './src/IFID/generate.js';
12
13
  import { Story } from './src/Story.js';
13
14
  import Passage from './src/Passage.js';
@@ -24,6 +25,7 @@ export {
24
25
  compileTwine1HTML,
25
26
  compileTwine2HTML,
26
27
  compileTwine2ArchiveHTML,
28
+ compileStoryFormat,
27
29
  generateIFID,
28
30
  Story,
29
31
  Passage,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.2.4",
3
+ "version": "2.2.6",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
@@ -24,34 +24,36 @@
24
24
  ],
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
- "commander": "^12.1.0",
27
+ "commander": "^13.1.0",
28
28
  "graphemer": "^1.4.0",
29
29
  "html-entities": "^2.5.2",
30
- "node-html-parser": "^6.1.13",
30
+ "node-html-parser": "^7.0.1",
31
31
  "pickleparser": "^0.2.1",
32
- "semver": "^7.6.2",
33
- "uuid": "^10.0.0"
32
+ "semver": "^7.7.1",
33
+ "uuid": "^11.1.0"
34
34
  },
35
35
  "devDependencies": {
36
- "@babel/cli": "^7.25.6",
37
- "@babel/core": "^7.24.6",
38
- "@babel/preset-env": "^7.24.6",
39
- "@eslint/js": "^9.10.0",
36
+ "@babel/cli": "^7.26.4",
37
+ "@babel/core": "^7.26.10",
38
+ "@babel/preset-env": "^7.26.9",
39
+ "@eslint/js": "^9.22.0",
40
+ "@types/semver": "^7.5.8",
40
41
  "@types/uuid": "^10.0.0",
41
- "babel-loader": "^9.1.3",
42
+ "babel-loader": "^10.0.0",
42
43
  "clean-jsdoc-theme": "^4.3.0",
43
- "core-js": "^3.37.1",
44
- "esbuild": "^0.23.0",
45
- "eslint": "^9.10.0",
46
- "eslint-plugin-jest": "^28.8.3",
47
- "globals": "^15.9.0",
44
+ "core-js": "^3.41.0",
45
+ "esbuild": "^0.25.1",
46
+ "eslint": "^9.22.0",
47
+ "eslint-plugin-jest": "^28.11.0",
48
+ "eslint-plugin-jsdoc": "^50.6.6",
49
+ "globals": "^16.0.0",
48
50
  "jest": "^29.7.0",
49
51
  "regenerator-runtime": "^0.14.1",
50
- "shelljs": "^0.8.5",
51
- "typescript": "^5.4.5",
52
- "typescript-eslint": "^8.4.0",
53
- "webpack": "^5.91.0",
54
- "webpack-cli": "^5.1.4"
52
+ "shelljs": "^0.9.1",
53
+ "typescript": "^5.8.2",
54
+ "typescript-eslint": "^8.26.1",
55
+ "webpack": "^5.98.0",
56
+ "webpack-cli": "^6.0.1"
55
57
  },
56
58
  "repository": {
57
59
  "type": "git",
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Parses a JSON object and extracts the StoryFormat, StoryTitle and StoryVersion.
3
+ * @param {object} obj Incoming JSON object.
4
+ * @returns {object} An object containing the extracted results.
5
+ */
6
+ export function parser(obj) {
7
+ // Check if the object is a valid JSON object.
8
+ if (typeof obj !== 'object' || obj === null) {
9
+ throw new Error('Error: Invalid JSON object');
10
+ }
11
+
12
+ // Extracted results.
13
+ let results = {
14
+ StoryFormat: null,
15
+ StoryTitle: null,
16
+ StoryVersion: null
17
+ };
18
+
19
+ // Does the object contain 'StoryFormat'?
20
+ if (Object.hasOwnProperty.call(obj, 'story-format')) {
21
+ results.StoryFormat = obj['story-format'];
22
+ }
23
+
24
+ // Does the object contain 'StoryTitle'?
25
+ if (Object.hasOwnProperty.call(obj, 'story-title')) {
26
+ results.StoryTitle = obj['story-title'];
27
+ }
28
+
29
+ // Does the object contain 'StoryVersion'?
30
+ if (Object.hasOwnProperty.call(obj, 'story-version')) {
31
+ results.StoryVersion = obj['story-version'];
32
+ }
33
+
34
+ // Return the extracted results.
35
+ return results;
36
+ }
@@ -0,0 +1,34 @@
1
+ import {readFileSync, existsSync} from 'node:fs';
2
+
3
+ /**
4
+ * Read a JSON file and return its contents.
5
+ * @param {string} path Path to the JSON file.
6
+ * @returns {object} Parsed JSON object.
7
+ * @throws {Error} If the file does not exist.
8
+ * @throws {Error} If the file is not a valid JSON file.
9
+ * @example
10
+ * const contents = reader('test/Config/files/valid.json');
11
+ * console.log(contents); // {"story-format": 'Harlowe', "story-title": "My Story"}
12
+ */
13
+ export function reader(path) {
14
+ // Does the file exist?
15
+ if (!existsSync(path)) {
16
+ throw new Error(`Error: File ${path} not found`);
17
+ }
18
+
19
+ // Read the file.
20
+ const contents = readFileSync(path, 'utf8');
21
+
22
+ // Parsed contents.
23
+ let parsedContents = null;
24
+
25
+ // Try to parse the contents into JSON object.
26
+ try {
27
+ parsedContents = JSON.parse(contents);
28
+ } catch (error) {
29
+ throw new Error(`Error: File ${path} is not a valid JSON file. ${error.message}`);
30
+ }
31
+
32
+ // Return the parsed contents.
33
+ return parsedContents;
34
+ }
package/src/Story.js CHANGED
@@ -5,7 +5,7 @@ import { encode } from 'html-entities';
5
5
  const creatorName = 'extwee';
6
6
 
7
7
  // Set the creator version.
8
- const creatorVersion = '2.2.4';
8
+ const creatorVersion = '2.2.6';
9
9
 
10
10
  /**
11
11
  * Story class.
@@ -781,6 +781,15 @@ class Story {
781
781
  PIDcounter++;
782
782
  });
783
783
 
784
+ // Generate <tw-tag> elements for each tag, if any.
785
+ const tagList = Object.keys(this.tagColors);
786
+
787
+ // For each tag, generate a <tw-tag> element.
788
+ tagList.forEach((tag) => {
789
+ // Add the <tw-tag> element.
790
+ storyData += `\t<tw-tag name="${tag}" color="${this.tagColors[tag]}"></tw-tag>\n`;
791
+ });
792
+
784
793
  // Close the HTML element.
785
794
  storyData += '</tw-storydata>';
786
795
 
@@ -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
  }
package/src/TWS/parse.js CHANGED
@@ -28,9 +28,8 @@ function parse (binaryFileContents) {
28
28
  // Try to parse the pickle data, assuming it is pickle data.
29
29
  pythonObject = parser.parse(binaryFileContents);
30
30
  } catch (error) {
31
- console.log(error);
32
31
  // This is a Buffer, but not pickle data.
33
- throw new TypeError('Error: Buffer does not contain Python pickle data!');
32
+ throw new TypeError(`Error: Buffer does not contain Python pickle data! ${error}`);
34
33
  }
35
34
 
36
35
  // Create Story object.
package/src/extwee.js CHANGED
@@ -53,12 +53,12 @@ const isFile = (path) => {
53
53
  program
54
54
  .name('extwee')
55
55
  .description('CLI for Extwee')
56
- .version('2.2.0', '-v, -V, --version', 'Output the current version')
56
+ .option('-v, --version', '2.2.4')
57
57
  .option('-c, --compile', 'Compile input into output')
58
58
  .option('-d, --decompile', 'De-compile input into output')
59
- .option('-t1, --twine1', 'Enable Twine 1 processing')
60
- .option('-name <storyFormatName>', 'Name of the Twine 1 story format (needed for `code.js` inclusion)')
61
- .option('-codejs <codeJSFile>', 'Twine 1 code.js file for use with Twine 1 HTML', (value) => {
59
+ .option('--twine1', 'Enable Twine 1 processing')
60
+ .option('--name <storyFormatName>', 'Name of the Twine 1 story format (needed for `code.js` inclusion)')
61
+ .option('--codejs <codeJSFile>', 'Twine 1 code.js file for use with Twine 1 HTML', (value) => {
62
62
  // Does the input file exist?
63
63
  if (isFile(value) === false) {
64
64
  // We cannot do anything without valid input.
@@ -67,7 +67,7 @@ program
67
67
 
68
68
  return value;
69
69
  })
70
- .option('-engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
70
+ .option('--engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
71
71
  // Does the input file exist?
72
72
  if (isFile(value) === false) {
73
73
  // We cannot do anything without valid input.
@@ -76,7 +76,7 @@ program
76
76
 
77
77
  return value;
78
78
  })
79
- .option('-header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
79
+ .option('--header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
80
80
  // Does the input file exist?
81
81
  if (isFile(value) === false) {
82
82
  // We cannot do anything without valid input.
@@ -27,12 +27,12 @@ describe('CLI', () => {
27
27
  });
28
28
 
29
29
  it('Twine 1 - compile: Twee 3 + Twine 1 engine.js + Twine 1 code.js + Twine 1 header.html', () => {
30
- shell.exec(`node ${currentPath}/src/extwee.js -t1 -c -i ${testFilePath}/example6.twee -o ${testFilePath}/output/test3.html -codejs ${testFilePath}/twine1/code.js -engine ${testFilePath}/twine1/engine.js -header ${testFilePath}/twine1/header.html -name Test`);
30
+ shell.exec(`node ${currentPath}/src/extwee.js --twine1 -c -i ${testFilePath}/example6.twee -o ${testFilePath}/output/test3.html --codejs ${testFilePath}/twine1/code.js --engine ${testFilePath}/twine1/engine.js --header ${testFilePath}/twine1/header.html --name Test`);
31
31
  expect(shell.test('-e', `${testFilePath}/output/test3.html`)).toBe(true);
32
32
  });
33
33
 
34
34
  it('Twine 1 - de-compile: Twine 1 HTML into Twee 3', () => {
35
- shell.exec(`node ${currentPath}/src/extwee.js -t1 -d -i ${testFilePath}/twine1Test.html -o ${testFilePath}/output/test.twee`);
35
+ shell.exec(`node ${currentPath}/src/extwee.js --twine1 -d -i ${testFilePath}/twine1Test.html -o ${testFilePath}/output/test.twee`);
36
36
  expect(shell.test('-e', `${testFilePath}/output/test.twee`)).toBe(true);
37
37
  });
38
38
 
@@ -0,0 +1,46 @@
1
+ import {reader as ConfigReader} from '../../src/Config/reader.js';
2
+ import {parser as ConfigParser} from '../../src/Config/parser.js';
3
+
4
+ describe('src/Config/reader.js', () => {
5
+ describe('reader()', () => {
6
+ it('should throw an error if the file does not exist', () => {
7
+ expect(() => ConfigReader('non-existent-file.json')).toThrow('Error: File non-existent-file.json not found');
8
+ });
9
+
10
+ it('should throw an error if the file is not a valid JSON file', () => {
11
+ expect(() => ConfigReader('test/Config/files/invalid.json')).toThrow();
12
+ });
13
+
14
+ it('should return the parsed JSON contents of the file', () => {
15
+ const contents = ConfigReader('test/Config/files/valid.json');
16
+ expect(contents).toEqual({
17
+ "story-format": 'Harlowe',
18
+ "story-title": "My Story",
19
+ "story-version": "2.0.1"
20
+ });
21
+ });
22
+ });
23
+
24
+ describe('parser()', () => {
25
+
26
+ it('should throw an error if the object is not a valid JSON object', () => {
27
+ expect(() => ConfigParser('{')).toThrow();
28
+ });
29
+
30
+ it('should extract the StoryFormat, StoryTitle, and StoryVersion from the JSON object', () => {
31
+ const jsonObject = ConfigReader('test/Config/files/valid.json');
32
+ const contents = ConfigParser(jsonObject);
33
+ expect(contents.StoryFormat).toEqual('Harlowe');
34
+ expect(contents.StoryTitle).toEqual('My Story');
35
+ expect(contents.StoryVersion).toEqual('2.0.1');
36
+ });
37
+
38
+ it('should not extract the StoryFormat, StoryTitle, and StoryVersion if they do not exist in the JSON object', () => {
39
+ const jsonObject = ConfigReader('test/Config/files/empty.json');
40
+ const contents = ConfigParser(jsonObject);
41
+ expect(contents.StoryFormat).toBeNull();
42
+ expect(contents.StoryTitle).toBeNull();
43
+ expect(contents.StoryVersion).toBeNull();
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "some": "value"
3
+ }
@@ -0,0 +1 @@
1
+ invalid
@@ -0,0 +1,5 @@
1
+ {
2
+ "story-format": "Harlowe",
3
+ "story-title": "My Story",
4
+ "story-version": "2.0.1"
5
+ }
@@ -819,6 +819,49 @@ describe('Story', () => {
819
819
  // Expect the creator to be encoded.
820
820
  expect(result.includes(`creator="${creatorName}"`)).not.toBe(true);
821
821
  });
822
+
823
+ it('Should not encode story tag colors if none are present', () => {
824
+ // Add passage.
825
+ s.addPassage(new Passage('Start', 'Word'));
826
+ // Set start.
827
+ s.start = 'Start';
828
+ // Create HTML.
829
+ const result = s.toTwine2HTML();
830
+ // Expect the tag colors to not be encoded if none are present.
831
+ expect(result.includes('<tw-tag')).toBe(false);
832
+ });
833
+
834
+ it('Should encode single story tag color', () => {
835
+ // Add passage.
836
+ s.addPassage(new Passage('Start', 'Word'));
837
+ // Set start.
838
+ s.start = 'Start';
839
+ // Add tag colors.
840
+ s.tagColors = {
841
+ bar: 'green'
842
+ };
843
+ // Create HTML.
844
+ const result = s.toTwine2HTML();
845
+ // Expect the tag colors to be encoded.
846
+ expect(result.includes('<tw-tag name="bar" color="green"></tw-tag>')).toBe(true);
847
+ });
848
+
849
+ it('Should encode multiple story tag colors', () => {
850
+ // Add passage.
851
+ s.addPassage(new Passage('Start', 'Word'));
852
+ // Set start.
853
+ s.start = 'Start';
854
+ // Add tag colors.
855
+ s.tagColors = {
856
+ bar: 'green',
857
+ foo: 'red'
858
+ };
859
+ // Create HTML.
860
+ const result = s.toTwine2HTML();
861
+ // Expect the tag colors to be encoded.
862
+ expect(result.includes('<tw-tag name="bar" color="green"></tw-tag>')).toBe(true);
863
+ expect(result.includes('<tw-tag name="foo" color="red"></tw-tag>')).toBe(true);
864
+ });
822
865
  });
823
866
 
824
867
  describe('toTwine1HTML()', function () {
@@ -15,6 +15,29 @@ describe('StoryFormat', () => {
15
15
  });
16
16
  });
17
17
 
18
+ describe('Constructor with default values', () => {
19
+ it('Should create an instance of StoryFormat', () => {
20
+ const sf = new StoryFormat();
21
+ expect(sf).toBeInstanceOf(StoryFormat);
22
+ });
23
+ });
24
+
25
+ describe('Constructor with parameters', () => {
26
+ it('Should create an instance of StoryFormat with parameters', () => {
27
+ const sf = new StoryFormat('name', 'version', 'description', 'author', 'image', 'url', 'license', true, 'source');
28
+ expect(sf).toBeInstanceOf(StoryFormat);
29
+ expect(sf.name).toBe('name');
30
+ expect(sf.version).toBe('version');
31
+ expect(sf.description).toBe('description');
32
+ expect(sf.author).toBe('author');
33
+ expect(sf.image).toBe('image');
34
+ expect(sf.url).toBe('url');
35
+ expect(sf.license).toBe('license');
36
+ expect(sf.proofing).toBe(true);
37
+ expect(sf.source).toBe('source');
38
+ });
39
+ });
40
+
18
41
  describe('name', () => {
19
42
  it('Set new String', () => {
20
43
  const sf = new StoryFormat();
@@ -153,7 +176,44 @@ describe('StoryFormat', () => {
153
176
  describe('toString', () => {
154
177
  it('Should return string representation', () => {
155
178
  const sf = new StoryFormat();
179
+ sf.version = '1.0.0';
156
180
  expect(sf.toString()).toBe(JSON.stringify(sf, null, '\t'));
157
181
  });
182
+
183
+ it('Should throw error if version is not valid', () => {
184
+ const sf = new StoryFormat();
185
+ expect(() => {
186
+ sf.toString();
187
+ }).toThrow();
188
+ });
189
+ });
190
+
191
+ describe('toJSON', () => {
192
+ it('Should return JSON representation with default name', () => {
193
+ const sf = new StoryFormat();
194
+ sf.version = '1.0.0';
195
+ expect(sf.toJSON().includes("Untitled Story Format")).toEqual(true);
196
+ });
197
+
198
+ it('Should return JSON representation with custom name', () => {
199
+ const sf = new StoryFormat();
200
+ sf.name = 'Custom Name';
201
+ sf.version = '1.0.0';
202
+ expect(sf.toJSON().includes("Custom Name")).toEqual(true);
203
+ });
204
+
205
+ it('Should overwrite with default name if name is empty string', () => {
206
+ const sf = new StoryFormat();
207
+ sf.name = '';
208
+ sf.version = '1.0.0';
209
+ expect(sf.toJSON().includes("Untitled Story Format")).toEqual(true);
210
+ });
211
+
212
+ it('Should throw error if version is not valid', () => {
213
+ const sf = new StoryFormat();
214
+ expect(() => {
215
+ sf.toJSON();
216
+ }).toThrow();
217
+ });
158
218
  });
159
219
  });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Parses a JSON object and extracts the StoryFormat, StoryTitle and StoryVersion.
3
+ * @param {object} obj Incoming JSON object.
4
+ * @returns {object} An object containing the extracted results.
5
+ */
6
+ export function parser(obj: object): object;