extwee 2.2.6 → 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.
- package/.github/workflows/dependabot-automerge.yml +23 -0
- package/.github/workflows/nodejs.yml +4 -1
- package/README.md +9 -0
- package/SECURITY.md +1 -1
- package/build/extwee.web.min.js +2 -0
- package/build/extwee.web.min.js.LICENSE.txt +1 -0
- package/extwee.config.json +6 -0
- package/extwee.config.md +67 -0
- package/package.json +22 -22
- package/src/CLI/CommandLineProcessing.js +196 -0
- package/src/CLI/ProcessConfig/loadStoryFormat.js +102 -0
- package/src/CLI/ProcessConfig/readDirectories.js +46 -0
- package/src/CLI/ProcessConfig.js +175 -0
- package/src/CLI/isDirectory.js +27 -0
- package/src/CLI/isFile.js +28 -0
- package/src/Config/parser.js +30 -8
- package/src/Passage.js +17 -2
- package/src/Story.js +92 -1
- package/src/extwee.js +20 -195
- package/test/Config/Config.test.js +40 -10
- package/test/Config/files/full.json +8 -0
- package/test/Config/files/valid.json +4 -3
- package/test/Config/isDirectory.test.js +44 -0
- package/test/Config/isFile.test.js +50 -0
- package/test/Config/loadStoryFormat.test.js +101 -0
- package/test/Config/readDirectories.test.js +68 -0
- package/test/Objects/Passage.test.js +5 -0
- package/test/Objects/Story.test.js +131 -0
- package/test/TWS/Parse.test.js +0 -22
- package/test/Web/window.Extwee.test.js +85 -0
- package/types/Story.d.ts +25 -0
- package/types/index.d.ts +4 -2
- package/types/src/CLI/CommandLineProcessing.d.ts +8 -0
- package/types/src/CLI/ProcessConfig/loadStoryFormat.d.ts +20 -0
- package/types/src/CLI/ProcessConfig/readDirectories.d.ts +9 -0
- package/types/src/CLI/ProcessConfig.d.ts +12 -0
- package/types/src/CLI/isDirectory.d.ts +1 -0
- package/types/src/CLI/isFile.d.ts +1 -0
- package/types/src/Config/parser.d.ts +6 -0
- package/types/src/Config/reader.d.ts +11 -0
- package/types/src/IFID/generate.d.ts +14 -0
- package/types/src/JSON/parse.d.ts +44 -1
- package/types/src/Passage.d.ts +49 -4
- package/types/src/Story.d.ts +110 -16
- package/types/src/StoryFormat/compile.d.ts +8 -0
- package/types/src/StoryFormat/parse.d.ts +46 -3
- package/types/src/StoryFormat.d.ts +69 -38
- package/types/src/TWS/parse.d.ts +3 -3
- package/types/src/Twee/parse.d.ts +3 -4
- package/types/src/Twine1HTML/compile.d.ts +3 -1
- package/types/src/Twine1HTML/parse.d.ts +3 -4
- package/types/src/Twine2ArchiveHTML/compile.d.ts +8 -0
- package/types/src/Twine2ArchiveHTML/parse.d.ts +31 -1
- package/types/src/Twine2HTML/compile.d.ts +7 -2
- package/types/src/Twine2HTML/parse.d.ts +12 -9
- package/index.html +0 -22
- package/test/TWS/TWSParser/Example1.tws +0 -150
|
@@ -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
|
+
};
|
package/src/Config/parser.js
CHANGED
|
@@ -12,8 +12,11 @@ export function parser(obj) {
|
|
|
12
12
|
// Extracted results.
|
|
13
13
|
let results = {
|
|
14
14
|
StoryFormat: null,
|
|
15
|
-
|
|
16
|
-
|
|
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 '
|
|
25
|
-
if (Object.hasOwnProperty.call(obj, 'story-
|
|
26
|
-
results.
|
|
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 '
|
|
30
|
-
if (Object.hasOwnProperty.call(obj, '
|
|
31
|
-
results.
|
|
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
|
-
//
|
|
194
|
-
|
|
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.
|
|
10
|
+
const creatorVersion = '2.3.0';
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Story class.
|
|
@@ -22,6 +24,8 @@ const creatorVersion = '2.2.6';
|
|
|
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
|
|
package/src/extwee.js
CHANGED
|
@@ -1,206 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
#! /usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @file CLI for Extwee
|
|
5
5
|
* @author Dan Cox
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
parseTwine2HTML,
|
|
11
|
-
parseTwee,
|
|
12
|
-
parseStoryFormat,
|
|
13
|
-
parseTwine1HTML,
|
|
14
|
-
compileTwine2HTML,
|
|
15
|
-
compileTwine1HTML
|
|
16
|
-
} from '../index.js';
|
|
8
|
+
import { CommandLineProcessing } from "./CLI/CommandLineProcessing.js";
|
|
9
|
+
import { ConfigFilePresent, ConfigFileProcessing } from "./CLI/ProcessConfig.js";
|
|
17
10
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import { Command, InvalidArgumentError } from 'commander';
|
|
23
|
-
|
|
24
|
-
// Create a new Command.
|
|
25
|
-
const program = new Command();
|
|
26
|
-
|
|
27
|
-
/*
|
|
28
|
-
* Check if a passed option is a valid file.
|
|
29
|
-
* @function isFile
|
|
30
|
-
* @description Check if a file exists.
|
|
31
|
-
* @param {string} path - Path to file.
|
|
32
|
-
* @returns {boolean} True if file exists, false if not.
|
|
33
|
-
*/
|
|
34
|
-
const isFile = (path) => {
|
|
35
|
-
// set default.
|
|
36
|
-
let result = false;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
// Attempt tp get stats.
|
|
40
|
-
const stats = statSync(path);
|
|
41
|
-
|
|
42
|
-
// Return if path is a file.
|
|
43
|
-
result = stats.isFile();
|
|
44
|
-
} catch (e) {
|
|
45
|
-
// There was an error, so return false.
|
|
46
|
-
result = false;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Return either the default (false) or the result (true).
|
|
50
|
-
return result;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
program
|
|
54
|
-
.name('extwee')
|
|
55
|
-
.description('CLI for Extwee')
|
|
56
|
-
.option('-v, --version', '2.2.4')
|
|
57
|
-
.option('-c, --compile', 'Compile input into output')
|
|
58
|
-
.option('-d, --decompile', 'De-compile input into output')
|
|
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
|
-
// Does the input file exist?
|
|
63
|
-
if (isFile(value) === false) {
|
|
64
|
-
// We cannot do anything without valid input.
|
|
65
|
-
throw new InvalidArgumentError(`Twine 1 code.js ${value} does not exist.`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return value;
|
|
69
|
-
})
|
|
70
|
-
.option('--engine <engineFile>', 'Twine 1 engine.js file for use with Twine 1 HTML', (value) => {
|
|
71
|
-
// Does the input file exist?
|
|
72
|
-
if (isFile(value) === false) {
|
|
73
|
-
// We cannot do anything without valid input.
|
|
74
|
-
throw new InvalidArgumentError(`Twine 1 engine.js ${value} does not exist.`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return value;
|
|
78
|
-
})
|
|
79
|
-
.option('--header <headerFile>', 'Twine 1 header.html file for use with Twine 1 HTML', (value) => {
|
|
80
|
-
// Does the input file exist?
|
|
81
|
-
if (isFile(value) === false) {
|
|
82
|
-
// We cannot do anything without valid input.
|
|
83
|
-
throw new InvalidArgumentError(`Twine 1 header.html ${value} does not exist.`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return value;
|
|
87
|
-
})
|
|
88
|
-
.option('-s <storyformat>, --storyformat <storyformat>', 'Path to story format file for Twine 2', (value) => {
|
|
89
|
-
// Does the input file exist?
|
|
90
|
-
if (isFile(value) === false) {
|
|
91
|
-
// We cannot do anything without valid input.
|
|
92
|
-
throw new InvalidArgumentError(`Story format ${value} does not exist.`);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return value;
|
|
96
|
-
})
|
|
97
|
-
.option('-i <inputFile>, --input <inputFile>', 'Path to input file', (value) => {
|
|
98
|
-
// Does the input file exist?
|
|
99
|
-
if (isFile(value) === false) {
|
|
100
|
-
// We cannot do anything without valid input.
|
|
101
|
-
throw new InvalidArgumentError(`Input file ${value} does not exist.`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return value;
|
|
105
|
-
})
|
|
106
|
-
.option('-o <outputFile>, --output <outputFile>', 'Path to output file');
|
|
107
|
-
|
|
108
|
-
// Parse the passed arguments.
|
|
109
|
-
program.parse(process.argv);
|
|
110
|
-
|
|
111
|
-
// Create object of passed arguments parsed by Commander.
|
|
112
|
-
const options = program.opts();
|
|
113
|
-
|
|
114
|
-
/*
|
|
115
|
-
* Prepare some (soon to be) global variables.
|
|
11
|
+
/**
|
|
12
|
+
* As a command-line tool, Extwee can be invoked multiple ways.
|
|
13
|
+
* (1) Via NPX with command-line arguments. (process.argv.length > 2)
|
|
14
|
+
* (2) Via NPX in the presence of a `extwee.config.json` file.
|
|
116
15
|
*/
|
|
117
|
-
// Check if Twine 1 is enabled.
|
|
118
|
-
const isTwine1Mode = (options.twine1 === true);
|
|
119
|
-
|
|
120
|
-
// Check if Twine 2 is enabled.
|
|
121
|
-
const isTwine2Mode = (isTwine1Mode === false);
|
|
122
|
-
|
|
123
|
-
// Check if de-compile mode is enabled.
|
|
124
|
-
const isDecompileMode = (options.decompile === true);
|
|
125
|
-
|
|
126
|
-
// Check if compile mode is enabled.
|
|
127
|
-
const isCompileMode = (options.compile === true);
|
|
128
|
-
|
|
129
|
-
// De-compile Twine 2 HTML into Twee 3 branch.
|
|
130
|
-
// If -d is passed, -i and -o are required.
|
|
131
|
-
if (isTwine2Mode === true && isDecompileMode === true) {
|
|
132
|
-
// Read the input HTML file.
|
|
133
|
-
const inputHTML = readFileSync(options.i, 'utf-8');
|
|
134
16
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Exit the process.
|
|
142
|
-
process.exit();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Compile Twee 3 into Twine 2 HTML branch.
|
|
146
|
-
// If -c is passed, -i, -o, and -s are required.
|
|
147
|
-
if (isTwine2Mode === true && isCompileMode === true) {
|
|
148
|
-
// Read the input file.
|
|
149
|
-
const inputTwee = readFileSync(options.i, 'utf-8');
|
|
150
|
-
|
|
151
|
-
// Parse the input file.
|
|
152
|
-
const story = parseTwee(inputTwee);
|
|
153
|
-
|
|
154
|
-
// Read the story format file.
|
|
155
|
-
const inputStoryFormat = readFileSync(options.s, 'utf-8');
|
|
156
|
-
|
|
157
|
-
// Parse the story format file.
|
|
158
|
-
const parsedStoryFormat = parseStoryFormat(inputStoryFormat);
|
|
159
|
-
|
|
160
|
-
// Compile the story.
|
|
161
|
-
const Twine2HTML = compileTwine2HTML(story, parsedStoryFormat);
|
|
162
|
-
|
|
163
|
-
// Write the output file.
|
|
164
|
-
writeFileSync(options.o, Twine2HTML);
|
|
165
|
-
|
|
166
|
-
// Exit the process.
|
|
167
|
-
process.exit();
|
|
17
|
+
// Check if the command line arguments are present.
|
|
18
|
+
if(process.argv.length > 2) {
|
|
19
|
+
// Process the command line arguments.
|
|
20
|
+
CommandLineProcessing(process.argv);
|
|
21
|
+
// Exit the process.
|
|
22
|
+
process.exit(0);
|
|
168
23
|
}
|
|
169
24
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// Read the input file.
|
|
178
|
-
const inputTwee = readFileSync(options.i, 'utf-8');
|
|
179
|
-
|
|
180
|
-
// Parse the input file.
|
|
181
|
-
const story = parseTwee(inputTwee);
|
|
182
|
-
|
|
183
|
-
// Does the engine file exist?
|
|
184
|
-
const Twine1HTML = compileTwine1HTML(story, options.engine, options.header, options.name, options.codejs);
|
|
185
|
-
|
|
186
|
-
// Write the output file.
|
|
187
|
-
writeFileSync(options.o, Twine1HTML);
|
|
188
|
-
|
|
189
|
-
// Exit the process.
|
|
190
|
-
process.exit();
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// De-compile Twine 1 HTML into Twee 3 branch.
|
|
194
|
-
if (isTwine1Mode === true && isDecompileMode === true) {
|
|
195
|
-
// Read the input HTML file.
|
|
196
|
-
const inputHTML = readFileSync(options.i, 'utf-8');
|
|
197
|
-
|
|
198
|
-
// Parse the input HTML file into Story object.
|
|
199
|
-
const storyObject = parseTwine1HTML(inputHTML);
|
|
200
|
-
|
|
201
|
-
// Write the output file from Story as Twee 3.
|
|
202
|
-
writeFileSync(options.o, storyObject.toTwee());
|
|
203
|
-
|
|
204
|
-
// Exit the process.
|
|
205
|
-
process.exit();
|
|
206
|
-
}
|
|
25
|
+
// Check if the config file exists.
|
|
26
|
+
if(ConfigFilePresent()) {
|
|
27
|
+
// Process the config file.
|
|
28
|
+
ConfigFileProcessing();
|
|
29
|
+
// Exit the process.
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
@@ -14,9 +14,10 @@ describe('src/Config/reader.js', () => {
|
|
|
14
14
|
it('should return the parsed JSON contents of the file', () => {
|
|
15
15
|
const contents = ConfigReader('test/Config/files/valid.json');
|
|
16
16
|
expect(contents).toEqual({
|
|
17
|
-
"story-format": '
|
|
18
|
-
"
|
|
19
|
-
"
|
|
17
|
+
"story-format": 'harlowe',
|
|
18
|
+
"mode": "decompile",
|
|
19
|
+
"input": "index.html",
|
|
20
|
+
"output": "index.twee"
|
|
20
21
|
});
|
|
21
22
|
});
|
|
22
23
|
});
|
|
@@ -27,20 +28,49 @@ describe('src/Config/reader.js', () => {
|
|
|
27
28
|
expect(() => ConfigParser('{')).toThrow();
|
|
28
29
|
});
|
|
29
30
|
|
|
30
|
-
it('should extract the StoryFormat
|
|
31
|
+
it('should extract the StoryFormat and StoryFormatVersion from the JSON object', () => {
|
|
31
32
|
const jsonObject = ConfigReader('test/Config/files/valid.json');
|
|
32
33
|
const contents = ConfigParser(jsonObject);
|
|
33
|
-
expect(contents.StoryFormat).toEqual('
|
|
34
|
-
expect(contents.
|
|
35
|
-
expect(contents.
|
|
34
|
+
expect(contents.StoryFormat).toEqual('harlowe');
|
|
35
|
+
expect(contents.StoryFormatVersion).toEqual('latest');
|
|
36
|
+
expect(contents.Input).toEqual('index.html');
|
|
37
|
+
expect(contents.Output).toEqual('index.twee');
|
|
38
|
+
expect(contents.Mode).toEqual('decompile');
|
|
39
|
+
expect(contents.Twine1Project).toEqual(false);
|
|
36
40
|
});
|
|
37
41
|
|
|
38
|
-
it('should not extract
|
|
42
|
+
it('should not extract options if they do not exist in the JSON object', () => {
|
|
39
43
|
const jsonObject = ConfigReader('test/Config/files/empty.json');
|
|
40
44
|
const contents = ConfigParser(jsonObject);
|
|
41
45
|
expect(contents.StoryFormat).toBeNull();
|
|
42
|
-
expect(contents.
|
|
43
|
-
expect(contents.
|
|
46
|
+
expect(contents.StoryFormatVersion).toBe('latest');
|
|
47
|
+
expect(contents.Input).toBeNull();
|
|
48
|
+
expect(contents.Output).toBeNull();
|
|
49
|
+
expect(contents.Mode).toBeNull();
|
|
50
|
+
expect(contents.Twine1Project).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should set StoryFormatVersion to "latest" if it is not present in the JSON object', () => {
|
|
54
|
+
const jsonObject = ConfigReader('test/Config/files/valid.json');
|
|
55
|
+
const contents = ConfigParser(jsonObject);
|
|
56
|
+
expect(contents.StoryFormatVersion).toEqual('latest');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should set Twine1Project to false if it is not present in the JSON object', () => {
|
|
60
|
+
const jsonObject = ConfigReader('test/Config/files/valid.json');
|
|
61
|
+
const contents = ConfigParser(jsonObject);
|
|
62
|
+
expect(contents.Twine1Project).toEqual(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('Should read story-format, story-format-version, and twine1-project if present', () => {
|
|
66
|
+
const jsonObject = ConfigReader('test/Config/files/full.json');
|
|
67
|
+
const contents = ConfigParser(jsonObject);
|
|
68
|
+
expect(contents.StoryFormat).toEqual('harlowe');
|
|
69
|
+
expect(contents.StoryFormatVersion).toEqual('3.2.0');
|
|
70
|
+
expect(contents.Input).toEqual('index.twee');
|
|
71
|
+
expect(contents.Output).toEqual('index.html');
|
|
72
|
+
expect(contents.Mode).toEqual('compile');
|
|
73
|
+
expect(contents.Twine1Project).toEqual(false);
|
|
44
74
|
});
|
|
45
75
|
});
|
|
46
76
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { isDirectory } from '../../src/CLI/isDirectory';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
jest.mock('node:fs');
|
|
5
|
+
|
|
6
|
+
describe('isDirectory', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should return true if the path is a directory', () => {
|
|
12
|
+
const mockStats = { isDirectory: jest.fn(() => true) };
|
|
13
|
+
statSync.mockReturnValue(mockStats);
|
|
14
|
+
|
|
15
|
+
const result = isDirectory('/valid/directory/path');
|
|
16
|
+
expect(statSync).toHaveBeenCalledWith('/valid/directory/path');
|
|
17
|
+
expect(mockStats.isDirectory).toHaveBeenCalled();
|
|
18
|
+
expect(result).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should return false if the path is not a directory', () => {
|
|
22
|
+
const mockStats = { isDirectory: jest.fn(() => false) };
|
|
23
|
+
statSync.mockReturnValue(mockStats);
|
|
24
|
+
|
|
25
|
+
const result = isDirectory('/valid/file/path');
|
|
26
|
+
expect(statSync).toHaveBeenCalledWith('/valid/file/path');
|
|
27
|
+
expect(mockStats.isDirectory).toHaveBeenCalled();
|
|
28
|
+
expect(result).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return false and log an error if statSync throws an error', () => {
|
|
32
|
+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
33
|
+
statSync.mockImplementation(() => {
|
|
34
|
+
throw new Error('Test error');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = isDirectory('/invalid/path');
|
|
38
|
+
expect(statSync).toHaveBeenCalledWith('/invalid/path');
|
|
39
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Test error'));
|
|
40
|
+
expect(result).toBe(false);
|
|
41
|
+
|
|
42
|
+
consoleErrorSpy.mockRestore();
|
|
43
|
+
});
|
|
44
|
+
});
|