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.
Files changed (57) hide show
  1. package/.github/workflows/dependabot-automerge.yml +23 -0
  2. package/.github/workflows/nodejs.yml +4 -1
  3. package/README.md +9 -0
  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/package.json +22 -22
  10. package/src/CLI/CommandLineProcessing.js +196 -0
  11. package/src/CLI/ProcessConfig/loadStoryFormat.js +102 -0
  12. package/src/CLI/ProcessConfig/readDirectories.js +46 -0
  13. package/src/CLI/ProcessConfig.js +175 -0
  14. package/src/CLI/isDirectory.js +27 -0
  15. package/src/CLI/isFile.js +28 -0
  16. package/src/Config/parser.js +30 -8
  17. package/src/Passage.js +17 -2
  18. package/src/Story.js +92 -1
  19. package/src/extwee.js +20 -195
  20. package/test/Config/Config.test.js +40 -10
  21. package/test/Config/files/full.json +8 -0
  22. package/test/Config/files/valid.json +4 -3
  23. package/test/Config/isDirectory.test.js +44 -0
  24. package/test/Config/isFile.test.js +50 -0
  25. package/test/Config/loadStoryFormat.test.js +101 -0
  26. package/test/Config/readDirectories.test.js +68 -0
  27. package/test/Objects/Passage.test.js +5 -0
  28. package/test/Objects/Story.test.js +131 -0
  29. package/test/TWS/Parse.test.js +0 -22
  30. package/test/Web/window.Extwee.test.js +85 -0
  31. package/types/Story.d.ts +25 -0
  32. package/types/index.d.ts +4 -2
  33. package/types/src/CLI/CommandLineProcessing.d.ts +8 -0
  34. package/types/src/CLI/ProcessConfig/loadStoryFormat.d.ts +20 -0
  35. package/types/src/CLI/ProcessConfig/readDirectories.d.ts +9 -0
  36. package/types/src/CLI/ProcessConfig.d.ts +12 -0
  37. package/types/src/CLI/isDirectory.d.ts +1 -0
  38. package/types/src/CLI/isFile.d.ts +1 -0
  39. package/types/src/Config/parser.d.ts +6 -0
  40. package/types/src/Config/reader.d.ts +11 -0
  41. package/types/src/IFID/generate.d.ts +14 -0
  42. package/types/src/JSON/parse.d.ts +44 -1
  43. package/types/src/Passage.d.ts +49 -4
  44. package/types/src/Story.d.ts +110 -16
  45. package/types/src/StoryFormat/compile.d.ts +8 -0
  46. package/types/src/StoryFormat/parse.d.ts +46 -3
  47. package/types/src/StoryFormat.d.ts +69 -38
  48. package/types/src/TWS/parse.d.ts +3 -3
  49. package/types/src/Twee/parse.d.ts +3 -4
  50. package/types/src/Twine1HTML/compile.d.ts +3 -1
  51. package/types/src/Twine1HTML/parse.d.ts +3 -4
  52. package/types/src/Twine2ArchiveHTML/compile.d.ts +8 -0
  53. package/types/src/Twine2ArchiveHTML/parse.d.ts +31 -1
  54. package/types/src/Twine2HTML/compile.d.ts +7 -2
  55. package/types/src/Twine2HTML/parse.d.ts +12 -9
  56. package/index.html +0 -22
  57. package/test/TWS/TWSParser/Example1.tws +0 -150
@@ -0,0 +1,50 @@
1
+ import { isFile } from '../../src/CLI/isFile.js';
2
+ import { statSync } from 'node:fs';
3
+
4
+ // Mock the statSync function from 'fs'.
5
+ jest.mock('node:fs', () => ({
6
+ statSync: jest.fn(),
7
+ }));
8
+
9
+ describe('isFile', () => {
10
+ afterEach(() => {
11
+ jest.clearAllMocks();
12
+ });
13
+
14
+ it('should return true if the path is a valid file', () => {
15
+ // Mock statSync to return an object with isFile() returning true.
16
+ statSync.mockReturnValue({
17
+ isFile: jest.fn(() => true),
18
+ });
19
+
20
+ const result = isFile('/path/to/file');
21
+ expect(result).toBe(true);
22
+ expect(statSync).toHaveBeenCalledWith('/path/to/file');
23
+ });
24
+
25
+ it('should return false if the path is not a valid file', () => {
26
+ // Mock statSync to return an object with isFile() returning false.
27
+ statSync.mockReturnValue({
28
+ isFile: jest.fn(() => false),
29
+ });
30
+
31
+ const result = isFile('/path/to/directory');
32
+ expect(result).toBe(false);
33
+ expect(statSync).toHaveBeenCalledWith('/path/to/directory');
34
+ });
35
+
36
+ it('should return false and log an error if statSync throws an error', () => {
37
+ // Mock statSync to throw an error.
38
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
39
+ statSync.mockImplementation(() => {
40
+ throw new Error('File not found');
41
+ });
42
+
43
+ const result = isFile('/invalid/path');
44
+ expect(result).toBe(false);
45
+ expect(statSync).toHaveBeenCalledWith('/invalid/path');
46
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Error: File not found'));
47
+
48
+ consoleErrorSpy.mockRestore();
49
+ });
50
+ });
@@ -0,0 +1,101 @@
1
+ import { loadStoryFormat } from "../../src/CLI/ProcessConfig/loadStoryFormat.js";
2
+ import { isDirectory } from "../../src/CLI/isDirectory.js";
3
+ import { isFile } from "../../src/CLI/isFile.js";
4
+ import { readDirectories } from "../../src/CLI/ProcessConfig/readDirectories.js";
5
+ import { readFileSync } from "node:fs";
6
+
7
+ jest.mock("../../src/CLI/isDirectory.js");
8
+ jest.mock("../../src/CLI/isFile.js");
9
+ jest.mock("../../src/CLI/ProcessConfig/readDirectories.js");
10
+ jest.mock("node:fs");
11
+
12
+ describe("loadStoryFormat", () => {
13
+ afterEach(() => {
14
+ jest.clearAllMocks();
15
+ });
16
+
17
+ it("should throw an error if the story-formats directory does not exist", () => {
18
+ isDirectory.mockReturnValueOnce(false);
19
+
20
+ expect(() => loadStoryFormat("Harlowe", "3.2.0")).toThrow(
21
+ "Error: story-formats directory does not exist. Consider running 'npx sfa-get' to download the latest story formats."
22
+ );
23
+ });
24
+
25
+ it("should throw an error if the named story format directory does not exist", () => {
26
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(false);
27
+
28
+ expect(() => loadStoryFormat("Harlowe", "3.2.0")).toThrow(
29
+ "Error: story format Harlowe does not exist in the story-formats directory."
30
+ );
31
+ });
32
+
33
+ it("should throw an error if the version directory does not exist", () => {
34
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true).mockReturnValueOnce(false);
35
+
36
+ expect(() => loadStoryFormat("Harlowe", "3.2.0")).toThrow(
37
+ "Error: story format Harlowe version 3.2.0 does not exist in the story-formats directory."
38
+ );
39
+ });
40
+
41
+ it("should throw an error if the format.js file does not exist in the version directory", () => {
42
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true).mockReturnValueOnce(true);
43
+ isFile.mockReturnValueOnce(false);
44
+
45
+ expect(() => loadStoryFormat("Harlowe", "3.2.0")).toThrow(
46
+ "Error: story format Harlowe version 3.2.0 does not have a format.js file."
47
+ );
48
+ });
49
+
50
+ it("should return the contents of the format.js file if all checks pass", () => {
51
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true).mockReturnValueOnce(true);
52
+ isFile.mockReturnValueOnce(true);
53
+ readFileSync.mockReturnValueOnce("format.js content");
54
+
55
+ const result = loadStoryFormat("Harlowe", "3.2.0");
56
+ expect(result).toBe("format.js content");
57
+ });
58
+
59
+ it("should handle 'latest' version and return the contents of the format.js file", () => {
60
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true);
61
+ isFile.mockReturnValueOnce(false).mockReturnValueOnce(true);
62
+ readDirectories.mockReturnValueOnce(["3.2.0", "3.1.0"]);
63
+ readFileSync.mockReturnValueOnce("latest format.js content");
64
+
65
+ const result = loadStoryFormat("Harlowe", "latest");
66
+ expect(result).toBe("latest format.js content");
67
+ expect(readDirectories).toHaveBeenCalledWith("story-formats/Harlowe");
68
+ });
69
+
70
+ it("should throw an error if 'latest' version has no format.js file", () => {
71
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true);
72
+ isFile.mockReturnValueOnce(false).mockReturnValueOnce(false);
73
+ readDirectories.mockReturnValueOnce(["3.2.0", "3.1.0"]);
74
+
75
+ expect(() => loadStoryFormat("Harlowe", "latest")).toThrow(
76
+ "Error: story format Harlowe version latest does not have a format.js file."
77
+ );
78
+ });
79
+
80
+ it("should read format.js file from the story format directory if it exists", () => {
81
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true);
82
+ isFile.mockReturnValueOnce(true);
83
+ readDirectories.mockReturnValueOnce([]);
84
+ readFileSync.mockReturnValueOnce("latest format.js content");
85
+
86
+ const result = loadStoryFormat("Harlowe", "latest");
87
+ expect(result).toBe("latest format.js content");
88
+ expect(readFileSync).toHaveBeenCalledWith("story-formats/Harlowe/format.js", "utf-8");
89
+ });
90
+
91
+ it("should throw an error if the story format version is not 'latest' and version directories do not exist", () => {
92
+ isDirectory.mockReturnValueOnce(true).mockReturnValueOnce(true);
93
+ isFile.mockReturnValueOnce(false);
94
+ readDirectories.mockReturnValueOnce([]);
95
+
96
+ expect(() => loadStoryFormat("Harlowe", "latest")).toThrow(
97
+ `Error: story format Harlowe does not have any version directories.`
98
+ );
99
+ }
100
+ );
101
+ });
@@ -0,0 +1,68 @@
1
+ import { readDirectories } from '../../src/CLI/ProcessConfig/readDirectories.js';
2
+ import { readdirSync } from 'node:fs';
3
+ import { isDirectory } from '../../src/CLI/isDirectory.js';
4
+
5
+ jest.mock('node:fs');
6
+ jest.mock('../../src/CLI/isDirectory.js');
7
+
8
+ describe('readDirectories', () => {
9
+ afterEach(() => {
10
+ jest.clearAllMocks();
11
+ });
12
+
13
+ it('should return an empty array and log an error if the directory does not exist', () => {
14
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
15
+ isDirectory.mockReturnValue(false);
16
+
17
+ const result = readDirectories('/nonexistent');
18
+
19
+ expect(result).toEqual([]);
20
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error: Directory /nonexistent does not exist.');
21
+ consoleErrorSpy.mockRestore();
22
+ });
23
+
24
+ it('should return an empty array and log an error if readdirSync throws an error', () => {
25
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
26
+ isDirectory.mockReturnValue(true);
27
+ readdirSync.mockImplementation(() => {
28
+ throw new Error('Permission denied');
29
+ });
30
+
31
+ const result = readDirectories('/restricted');
32
+
33
+ expect(result).toEqual([]);
34
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error reading directory /restricted:', expect.any(Error));
35
+ consoleErrorSpy.mockRestore();
36
+ });
37
+
38
+ it('should return an empty array if the directory is empty', () => {
39
+ isDirectory.mockReturnValue(true);
40
+ readdirSync.mockReturnValue([]);
41
+
42
+ const result = readDirectories('/empty');
43
+
44
+ expect(result).toEqual([]);
45
+ });
46
+
47
+ it('should return an array of directories', () => {
48
+ isDirectory.mockReturnValue(true);
49
+ readdirSync.mockReturnValue(['dir1', 'file1', 'dir2']);
50
+
51
+ isDirectory.mockImplementation((path) => {
52
+ return path === '/test/dir1' || path === '/test/dir2';
53
+ });
54
+
55
+ const result = readDirectories('/test');
56
+
57
+ expect(result).toEqual(['dir1', 'dir2']);
58
+ });
59
+
60
+ it('should return an empty array if the result is not an array', () => {
61
+ isDirectory.mockReturnValue(true);
62
+ readdirSync.mockReturnValue('not an array');
63
+
64
+ const result = readDirectories('/test');
65
+
66
+ expect(result).toEqual([]);
67
+ });
68
+ });
@@ -240,5 +240,10 @@ describe('Passage', () => {
240
240
  expect(t.querySelector('tw-passagedata').getAttribute('tags')).toBe('&tag "bad"');
241
241
  expect(t.querySelector('tw-passagedata').getAttribute('position')).toBe('100,100');
242
242
  });
243
+
244
+ it('Should escape double-colon at start of text when exporting to Twee', function () {
245
+ const p = new Passage('test', ':: nefarious');
246
+ expect(p.toTwee()).toBe(':: test\n\\:: nefarious\n\n');
247
+ });
243
248
  });
244
249
  });
@@ -229,6 +229,45 @@ describe('Story', () => {
229
229
  });
230
230
  });
231
231
 
232
+ describe('storyStylesheet', () => {
233
+ let s = null;
234
+
235
+ beforeEach(() => {
236
+ s = new Story();
237
+ });
238
+
239
+ it('Set storyStylesheet', () => {
240
+ s.storyStylesheet = 'Test';
241
+ expect(s.storyStylesheet).not.toBe('');
242
+ });
243
+
244
+ it('Should throw error if not String', () => {
245
+ expect(() => {
246
+ s.storyStylesheet = 1;
247
+ }).toThrow();
248
+ }
249
+ );
250
+ });
251
+
252
+ describe('storyJavaScript', () => {
253
+ let s = null;
254
+
255
+ beforeEach(() => {
256
+ s = new Story();
257
+ });
258
+
259
+ it('Set storyJavaScript', () => {
260
+ s.storyJavaScript = 'Test';
261
+ expect(s.storyJavaScript).not.toBe('');
262
+ });
263
+
264
+ it('Should throw error if not String', () => {
265
+ expect(() => {
266
+ s.storyJavaScript = 1;
267
+ }).toThrow();
268
+ });
269
+ });
270
+
232
271
  describe('passages', () => {
233
272
  let s = null;
234
273
 
@@ -442,6 +481,30 @@ describe('Story', () => {
442
481
  // Should have a single passage.
443
482
  expect(result.passages.length).toBe(1);
444
483
  });
484
+
485
+ it('Should have style data', function () {
486
+ // Create default Story.
487
+ const s = new Story();
488
+ // Add a stylesheet.
489
+ s.storyStylesheet = 'Test';
490
+ // Convert to JSON and then back to object.
491
+ const result = JSON.parse(s.toJSON());
492
+ // Should have a stylesheet.
493
+ expect(result.style).toBe('Test');
494
+ }
495
+ );
496
+
497
+ it('Should have script data', function () {
498
+ // Create default Story.
499
+ const s = new Story();
500
+ // Add a script.
501
+ s.storyJavaScript = 'Test';
502
+ // Convert to JSON and then back to object.
503
+ const result = JSON.parse(s.toJSON());
504
+ // Should have a script.
505
+ expect(result.script).toBe('Test');
506
+ }
507
+ );
445
508
  });
446
509
 
447
510
  describe('toTwee()', function () {
@@ -647,6 +710,48 @@ describe('Story', () => {
647
710
  // Test for passage text.
648
711
  expect(p[0].text).toBe('Test');
649
712
  });
713
+
714
+ it('Should encode story stylesheet as passage with "stylesheet" tag', () => {
715
+ // Add passages.
716
+ s.storyStylesheet = 'Test';
717
+
718
+ // Set IFID.
719
+ s.IFID = 'DE7DF8AD-E4CD-499E-A4E7-C5B98B73449A';
720
+
721
+ // Convert into Twee.
722
+ const t = s.toTwee();
723
+
724
+ // Convert back into Story.
725
+ const story = parseTwee(t);
726
+
727
+ // Search for 'stylesheet'.
728
+ const p = story.getPassagesByTag('stylesheet');
729
+
730
+ // Test for passage text.
731
+ expect(p[0].text).toBe('Test');
732
+ }
733
+ );
734
+
735
+ it('Should encode story JavaScript as passage with "script" tag', () => {
736
+ // Add passages.
737
+ s.storyJavaScript = 'Test';
738
+
739
+ // Set IFID.
740
+ s.IFID = 'DE7DF8AD-E4CD-499E-A4E7-C5B98B73449A';
741
+
742
+ // Convert into Twee.
743
+ const t = s.toTwee();
744
+
745
+ // Convert back into Story.
746
+ const story = parseTwee(t);
747
+
748
+ // Search for 'stylesheet'.
749
+ const p = story.getPassagesByTag('script');
750
+
751
+ // Test for passage text.
752
+ expect(p[0].text).toBe('Test');
753
+ }
754
+ );
650
755
  });
651
756
 
652
757
  describe('toTwine2HTML()', () => {
@@ -695,6 +800,19 @@ describe('Story', () => {
695
800
  expect(result.includes('<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">Word')).toBe(true);
696
801
  });
697
802
 
803
+ it('Should encode stylesheet property', () => {
804
+ // Add passage.
805
+ s.addPassage(new Passage('Start', 'Word'));
806
+ // Set start.
807
+ s.start = 'Start';
808
+ // Set stylesheet.
809
+ s.storyStylesheet = 'Test';
810
+ // Create HTML.
811
+ const result = s.toTwine2HTML();
812
+ // Expect the stylesheet passage text to be encoded.
813
+ expect(result.includes('<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">Test')).toBe(true);
814
+ });
815
+
698
816
  it('Should encode script passages', () => {
699
817
  // Add passage.
700
818
  s.addPassage(new Passage('Start', 'Word'));
@@ -708,6 +826,19 @@ describe('Story', () => {
708
826
  expect(result.includes('<script role="script" id="twine-user-script" type="text/twine-javascript">Word')).toBe(true);
709
827
  });
710
828
 
829
+ it('Should encode script property', () => {
830
+ // Add passage.
831
+ s.addPassage(new Passage('Start', 'Word'));
832
+ // Set start.
833
+ s.start = 'Start';
834
+ // Set script.
835
+ s.storyJavaScript = 'Test';
836
+ // Create HTML.
837
+ const result = s.toTwine2HTML();
838
+ // Expect the script passage text to be encoded.
839
+ expect(result.includes('<script role="script" id="twine-user-script" type="text/twine-javascript">Test')).toBe(true);
840
+ });
841
+
711
842
  it('Should encode format', () => {
712
843
  // Add passage.
713
844
  s.addPassage(new Passage('Start', 'Word'));
@@ -7,28 +7,6 @@ describe('TWSParser', () => {
7
7
  expect(() => { parseTWS(0); }).toThrow();
8
8
  });
9
9
 
10
- describe('Story parsing', function () {
11
- let r = null;
12
-
13
- beforeAll(() => {
14
- const contents = fs.readFileSync('test/TWS/TWSParser/Example1.tws');
15
- //const b = Buffer.from(contents, 'binary');
16
- r = parseTWS(contents);
17
- });
18
-
19
- it('Should parse StoryTitle', function () {
20
- expect(r.name).toBe('Untitled Story');
21
- });
22
-
23
- it('Should parse zoom', function () {
24
- expect(r.zoom).toBe(1);
25
- });
26
-
27
- it('Should parse start passage', function () {
28
- expect(r.start).toBe('Start');
29
- });
30
- });
31
-
32
10
  describe('Passage parsing', function () {
33
11
  let r = null;
34
12
 
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+
5
+ import '../../web-index.js';
6
+
7
+ describe('Extwee', () => {
8
+ it('should have all the expected properties', () => {
9
+ expect(window.Extwee).toHaveProperty('parseTwee');
10
+ expect(window.Extwee).toHaveProperty('parseJSON');
11
+ expect(window.Extwee).toHaveProperty('parseTWS');
12
+ expect(window.Extwee).toHaveProperty('parseStoryFormat');
13
+ expect(window.Extwee).toHaveProperty('parseTwine1HTML');
14
+ expect(window.Extwee).toHaveProperty('parseTwine2HTML');
15
+ expect(window.Extwee).toHaveProperty('parseTwine2ArchiveHTML');
16
+ expect(window.Extwee).toHaveProperty('compileTwine1HTML');
17
+ expect(window.Extwee).toHaveProperty('compileTwine2HTML');
18
+ expect(window.Extwee).toHaveProperty('compileTwine2ArchiveHTML');
19
+ expect(window.Extwee).toHaveProperty('generateIFID');
20
+ expect(window.Extwee).toHaveProperty('Story');
21
+ expect(window.Extwee).toHaveProperty('Passage');
22
+ expect(window.Extwee).toHaveProperty('StoryFormat');
23
+ });
24
+
25
+ it('should have the expected types', () => {
26
+ expect(typeof window.Extwee.parseTwee).toBe('function');
27
+ expect(typeof window.Extwee.parseJSON).toBe('function');
28
+ expect(typeof window.Extwee.parseTWS).toBe('function');
29
+ expect(typeof window.Extwee.parseStoryFormat).toBe('function');
30
+ expect(typeof window.Extwee.parseTwine1HTML).toBe('function');
31
+ expect(typeof window.Extwee.parseTwine2HTML).toBe('function');
32
+ expect(typeof window.Extwee.parseTwine2ArchiveHTML).toBe('function');
33
+ expect(typeof window.Extwee.compileTwine1HTML).toBe('function');
34
+ expect(typeof window.Extwee.compileTwine2HTML).toBe('function');
35
+ expect(typeof window.Extwee.compileTwine2ArchiveHTML).toBe('function');
36
+ expect(typeof window.Extwee.generateIFID).toBe('function');
37
+ expect(typeof window.Extwee.Story).toBe('function');
38
+ expect(typeof window.Extwee.Passage).toBe('function');
39
+ expect(typeof window.Extwee.StoryFormat).toBe('function');
40
+ });
41
+
42
+ it('should have the expected properties in StoryFormat', () => {
43
+ const storyFormat = new window.Extwee.StoryFormat();
44
+ expect(storyFormat).toHaveProperty('name');
45
+ expect(storyFormat).toHaveProperty('version');
46
+ expect(storyFormat).toHaveProperty('description');
47
+ expect(storyFormat).toHaveProperty('author');
48
+ expect(storyFormat).toHaveProperty('image');
49
+ expect(storyFormat).toHaveProperty('url');
50
+ expect(storyFormat).toHaveProperty('license');
51
+ expect(storyFormat).toHaveProperty('proofing');
52
+ expect(storyFormat).toHaveProperty('source');
53
+ });
54
+
55
+ it('should have the expected properties in Story', () => {
56
+ const story = new window.Extwee.Story();
57
+ expect(story).toHaveProperty('name');
58
+ expect(story).toHaveProperty('IFID');
59
+ expect(story).toHaveProperty('start');
60
+ expect(story).toHaveProperty('format');
61
+ expect(story).toHaveProperty('formatVersion');
62
+ expect(story).toHaveProperty('zoom');
63
+ expect(story).toHaveProperty('passages');
64
+ expect(story).toHaveProperty('creator');
65
+ expect(story).toHaveProperty('creatorVersion');
66
+ expect(story).toHaveProperty('metadata');
67
+ expect(story).toHaveProperty('tagColors');
68
+ expect(story).toHaveProperty('storyJavaScript');
69
+ expect(story).toHaveProperty('storyStylesheet');
70
+ });
71
+
72
+ it('should have the expected types in Passage', () => {
73
+ const passage = new window.Extwee.Passage();
74
+ expect(passage).toHaveProperty('name');
75
+ expect(passage).toHaveProperty('text');
76
+ expect(passage).toHaveProperty('tags');
77
+ expect(passage).toHaveProperty('metadata');
78
+ });
79
+
80
+ it('should generate a valid IFID', () => {
81
+ const ifid = window.Extwee.generateIFID();
82
+ expect(ifid).toMatch(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/);
83
+ });
84
+
85
+ });
package/types/Story.d.ts CHANGED
@@ -13,6 +13,8 @@
13
13
  * @property {string} creatorVersion - Version used to create Story.
14
14
  * @property {object} metadata - Metadata of Story.
15
15
  * @property {object} tagColors - Tag Colors
16
+ * @property {string} storyJavaScript - Story JavaScript
17
+ * @property {string} storyStylesheet - Story Stylesheet
16
18
  * @method {number} addPassage - Add a passage to the story and returns the new length of the passages array.
17
19
  * @method {number} removePassageByName - Remove a passage from the story by name and returns the new length of the passages array.
18
20
  * @method {Array} getPassagesByTag - Find passages by tag.
@@ -142,6 +144,25 @@ export class Story {
142
144
  * @property {Array} passages - Passages
143
145
  */
144
146
  get passages(): any[];
147
+ /**
148
+ * @param {string} s - Replacement story stylesheet
149
+ */
150
+ set storyStylesheet(s: string);
151
+ /**
152
+ * Story stylesheet data can be set as a passage, property value, or both.
153
+ * @returns {string} storyStylesheet
154
+ */
155
+ get storyStylesheet(): string;
156
+ /**
157
+ * Set story JavaScript.
158
+ * @param {string} s - Replacement story JavaScript
159
+ */
160
+ set storyJavaScript(s: string);
161
+ /**
162
+ * Get story JavaScript.
163
+ * @returns {string} storyJavaScript
164
+ */
165
+ get storyJavaScript(): string;
145
166
  /**
146
167
  * Add a passage to the story.
147
168
  * Passing `StoryData` will override story metadata and `StoryTitle` will override story name.
@@ -209,6 +230,10 @@ export class Story {
209
230
  * - `format`: (string) Optional. The format of the story.
210
231
  * - `format-version`: (string) Optional. The version of the format of the story.
211
232
  *
233
+ * Because story stylesheet data can be represented as a passage, property value, or both, all approaches are encoded.
234
+ *
235
+ * Because story JavaScript can be represented as a passage, property value, or both, all approaches are encoded.
236
+ *
212
237
  * @method toTwine2HTML
213
238
  * @returns {string} Twine 2 HTML string
214
239
  */
package/types/index.d.ts CHANGED
@@ -8,7 +8,9 @@ import { parse as parseTwine2ArchiveHTML } from './src/Twine2ArchiveHTML/parse.j
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 Story from './src/Story.js';
11
+ import { compile as compileStoryFormat } from './src/StoryFormat/compile.js';
12
+ import { generate as generateIFID } from './src/IFID/generate.js';
13
+ import { Story } from './src/Story.js';
12
14
  import Passage from './src/Passage.js';
13
15
  import StoryFormat from './src/StoryFormat.js';
14
- export { parseTwee, parseJSON, parseTWS, parseStoryFormat, parseTwine1HTML, parseTwine2HTML, parseTwine2ArchiveHTML, compileTwine1HTML, compileTwine2HTML, compileTwine2ArchiveHTML, Story, Passage, StoryFormat };
16
+ export { parseTwee, parseJSON, parseTWS, parseStoryFormat, parseTwine1HTML, parseTwine2HTML, parseTwine2ArchiveHTML, compileTwine1HTML, compileTwine2HTML, compileTwine2ArchiveHTML, compileStoryFormat, generateIFID, Story, Passage, StoryFormat };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Process command line arguments.
3
+ * @function CommandLineProcessing
4
+ * @description This function processes the command line arguments passed to the Extwee CLI.
5
+ * @module CLI/commandLineProcessing
6
+ * @param {Array} argv - The command line arguments passed to the CLI.
7
+ */
8
+ export function CommandLineProcessing(argv: any[]): void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Load the story format from the story-formats directory.
3
+ * @function loadStoryFormat
4
+ * @description This function loads the story format from the story-formats directory.
5
+ * It checks if the story-formats directory exists, if the named story format exists,
6
+ * if the version directory exists, and if the format.js file exists.
7
+ * If any of these checks fail, the function will exit the process with an error message.
8
+ * If all checks pass, the function will return the contents of the format.js file.
9
+ * @param {string} storyFormatName - The name of the story format.
10
+ * @param {string} storyFormatVersion - The version of the story format.
11
+ * @returns {string} - The contents of the format.js file.
12
+ * @throws {Error} - If the story-formats directory does not exist, if the named story format does not exist,
13
+ * if the version directory does not exist, or if the format.js file does not exist.
14
+ * @example
15
+ * // Load the story format from the story-formats directory.
16
+ * const storyFormat = loadStoryFormat('Harlowe', '3.2.0');
17
+ * console.log(storyFormat);
18
+ * // Output: The contents of the format.js file.
19
+ */
20
+ export function loadStoryFormat(storyFormatName: string, storyFormatVersion: string): string;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Read the contents of a directory and returns all directories.
3
+ * @function readDirectories
4
+ * @description This function reads the contents of a directory and returns a list of directories.
5
+ * @param {string} directory - The path to the directory to read.
6
+ * @returns {Array<string>} - An array of directories in the directory.
7
+ * @throws {Error} - If the directory does not exist or if there is an error reading the directory.
8
+ */
9
+ export function readDirectories(directory: string): Array<string>;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Processes the config file, if present.
3
+ * @function ConfigFileProcessing
4
+ * @description This function processes the config file.
5
+ * It checks if the config file exists and if it does, it reads the config file.
6
+ * If the config file does not exist, the function will exit the process with an error message.
7
+ * The config file is used to store configuration options for the Extwee CLI.
8
+ * @returns {void}
9
+ * @throws {Error} - If the config file does not exist or if there is an error parsing the config file.
10
+ */
11
+ export function ConfigFileProcessing(): void;
12
+ export function ConfigFilePresent(): boolean;
@@ -0,0 +1 @@
1
+ export function isDirectory(path: any): boolean;
@@ -0,0 +1 @@
1
+ export function isFile(path: any): boolean;
@@ -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;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Read a JSON file and return its contents.
3
+ * @param {string} path Path to the JSON file.
4
+ * @returns {object} Parsed JSON object.
5
+ * @throws {Error} If the file does not exist.
6
+ * @throws {Error} If the file is not a valid JSON file.
7
+ * @example
8
+ * const contents = reader('test/Config/files/valid.json');
9
+ * console.log(contents); // {"story-format": 'Harlowe', "story-title": "My Story"}
10
+ */
11
+ export function reader(path: string): object;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Generates an Interactive Fiction Identification (IFID) based the Treaty of Babel.
3
+ *
4
+ * For Twine works, the IFID is a UUID (v4) in uppercase.
5
+ * @see Treaty of Babel ({@link https://babel.ifarchive.org/babel_rev11.html#the-ifid-for-an-html-story-file})
6
+ * @function generate
7
+ * @description Generates a new IFID.
8
+ * @returns {string} IFID
9
+ * @example
10
+ * const ifid = generate();
11
+ * console.log(ifid);
12
+ * // => 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6'
13
+ */
14
+ export function generate(): string;