extwee 2.3.4 → 2.3.5

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.
@@ -0,0 +1,200 @@
1
+ import { parse as parseTwee, escapeTweeMetacharacters, unescapeTweeMetacharacters } from '../../src/Twee/parse.js';
2
+ import Passage from '../../src/Passage.js';
3
+
4
+ describe('Twee Escaping', () => {
5
+ describe('unescapeTweeMetacharacters()', () => {
6
+ it('Should unescape square brackets', () => {
7
+ expect(unescapeTweeMetacharacters('Test\\[Name\\]')).toBe('Test[Name]');
8
+ });
9
+
10
+ it('Should unescape curly braces', () => {
11
+ expect(unescapeTweeMetacharacters('Test\\{Name\\}')).toBe('Test{Name}');
12
+ });
13
+
14
+ it('Should unescape backslashes', () => {
15
+ expect(unescapeTweeMetacharacters('Test\\\\Name')).toBe('Test\\Name');
16
+ });
17
+
18
+ it('Should unescape any character after backslash (robust decoding)', () => {
19
+ expect(unescapeTweeMetacharacters('\\q')).toBe('q');
20
+ expect(unescapeTweeMetacharacters('\\x')).toBe('x');
21
+ expect(unescapeTweeMetacharacters('\\5')).toBe('5');
22
+ });
23
+
24
+ it('Should handle complex combinations', () => {
25
+ expect(unescapeTweeMetacharacters('\\[Hello\\] \\{world\\} \\\\\\\\')).toBe('[Hello] {world} \\\\');
26
+ });
27
+
28
+ it('Should handle non-string input gracefully', () => {
29
+ expect(unescapeTweeMetacharacters(null)).toBe(null);
30
+ expect(unescapeTweeMetacharacters(undefined)).toBe(undefined);
31
+ expect(unescapeTweeMetacharacters(123)).toBe(123);
32
+ });
33
+
34
+ it('Should handle empty strings', () => {
35
+ expect(unescapeTweeMetacharacters('')).toBe('');
36
+ });
37
+
38
+ it('Should handle strings without escape sequences', () => {
39
+ expect(unescapeTweeMetacharacters('Normal Text')).toBe('Normal Text');
40
+ });
41
+ });
42
+
43
+ describe('escapeTweeMetacharacters()', () => {
44
+ it('Should escape square brackets', () => {
45
+ expect(escapeTweeMetacharacters('Test[Name]')).toBe('Test\\[Name\\]');
46
+ });
47
+
48
+ it('Should escape curly braces', () => {
49
+ expect(escapeTweeMetacharacters('Test{Name}')).toBe('Test\\{Name\\}');
50
+ });
51
+
52
+ it('Should escape backslashes', () => {
53
+ expect(escapeTweeMetacharacters('Test\\Name')).toBe('Test\\\\Name');
54
+ });
55
+
56
+ it('Should escape complex combinations', () => {
57
+ expect(escapeTweeMetacharacters('[Hello] {world} \\\\')).toBe('\\[Hello\\] \\{world\\} \\\\\\\\');
58
+ });
59
+
60
+ it('Should handle non-string input gracefully', () => {
61
+ expect(escapeTweeMetacharacters(null)).toBe(null);
62
+ expect(escapeTweeMetacharacters(undefined)).toBe(undefined);
63
+ expect(escapeTweeMetacharacters(123)).toBe(123);
64
+ });
65
+
66
+ it('Should handle empty strings', () => {
67
+ expect(escapeTweeMetacharacters('')).toBe('');
68
+ });
69
+
70
+ it('Should handle strings without metacharacters', () => {
71
+ expect(escapeTweeMetacharacters('Normal Text')).toBe('Normal Text');
72
+ });
73
+ });
74
+
75
+ describe('Round-trip escaping', () => {
76
+ it('Should preserve text through escape -> unescape cycle', () => {
77
+ const original = 'Test[Name]{Value}\\Path';
78
+ const escaped = escapeTweeMetacharacters(original);
79
+ const unescaped = unescapeTweeMetacharacters(escaped);
80
+ expect(unescaped).toBe(original);
81
+ });
82
+
83
+ it('Should handle complex nested scenarios', () => {
84
+ const original = '[{\\}]{[\\]}';
85
+ const escaped = escapeTweeMetacharacters(original);
86
+ const unescaped = unescapeTweeMetacharacters(escaped);
87
+ expect(unescaped).toBe(original);
88
+ });
89
+ });
90
+
91
+ describe('Twee parsing with escaping', () => {
92
+ it('Should correctly parse escaped passage names', () => {
93
+ const content = ':: Test\\[Name\\] \\{with\\} \\\\brackets\nContent here';
94
+ const story = parseTwee(content);
95
+ expect(story.passages[0].name).toBe('Test[Name] {with} \\brackets');
96
+ });
97
+
98
+ it('Should correctly parse escaped tags', () => {
99
+ const content = ':: TestPassage [tag\\[1\\] tag\\{2\\}]\nContent here';
100
+ const story = parseTwee(content);
101
+ expect(story.passages[0].tags).toEqual(['tag[1]', 'tag{2}']);
102
+ });
103
+
104
+ it('Should handle mixed escaped and non-escaped content', () => {
105
+ const content = ':: Test\\[Passage [normal escaped\\]tag]\nContent here';
106
+ const story = parseTwee(content);
107
+ expect(story.passages[0].name).toBe('Test[Passage');
108
+ expect(story.passages[0].tags).toEqual(['normal', 'escaped]tag']);
109
+ });
110
+
111
+ it('Should handle the cursed.twee test case correctly', () => {
112
+ const content = `:: StoryData
113
+ {
114
+ "ifid": "22F25A58-7062-4927-95B6-F424DDB2EC65",
115
+ "format": "Harlowe",
116
+ "format-version": "3.3.8",
117
+ "start": "[Hello] {world} \\\\\\\\",
118
+ "zoom": 1
119
+ }
120
+
121
+ :: \\[Hello\\] \\{world\\} \\\\\\\\
122
+ Content here`;
123
+
124
+ const story = parseTwee(content);
125
+ expect(story.start).toBe('[Hello] {world} \\\\');
126
+
127
+ // Find the passage by the unescaped name
128
+ const passage = story.getPassageByName('[Hello] {world} \\\\');
129
+ expect(passage).not.toBeNull();
130
+ expect(passage.name).toBe('[Hello] {world} \\\\');
131
+ });
132
+
133
+ it('Should handle complex metadata with escaped keys (if supported)', () => {
134
+ const content = ':: TestPassage {"position": "100,200", "size": "150,100"}\nContent here';
135
+ const story = parseTwee(content);
136
+ expect(story.passages[0].metadata.position).toBe('100,200');
137
+ expect(story.passages[0].metadata.size).toBe('150,100');
138
+ });
139
+ });
140
+
141
+ describe('Twee generation with escaping', () => {
142
+ it('Should escape passage names when generating Twee', () => {
143
+ const passage = new Passage('Test[Name]', 'Content', [], {});
144
+ const tweeOutput = passage.toTwee();
145
+ expect(tweeOutput).toContain(':: Test\\[Name\\]');
146
+ });
147
+
148
+ it('Should escape tag names when generating Twee', () => {
149
+ const passage = new Passage('TestPassage', 'Content', ['tag[1]', 'tag{2}'], {});
150
+ const tweeOutput = passage.toTwee();
151
+ expect(tweeOutput).toContain('[tag\\[1\\] tag\\{2\\}]');
152
+ });
153
+
154
+ it('Should handle complex escaping scenarios', () => {
155
+ const passage = new Passage('Test[Name]{Value}\\Path', 'Content', ['complex[tag]'], {});
156
+ const tweeOutput = passage.toTwee();
157
+ expect(tweeOutput).toContain(':: Test\\[Name\\]\\{Value\\}\\\\Path');
158
+ expect(tweeOutput).toContain('[complex\\[tag\\]]');
159
+ });
160
+
161
+ it('Should preserve normal names and tags without escaping', () => {
162
+ const passage = new Passage('NormalName', 'Content', ['normaltag'], {});
163
+ const tweeOutput = passage.toTwee();
164
+ expect(tweeOutput).toContain(':: NormalName');
165
+ expect(tweeOutput).toContain('[normaltag]');
166
+ });
167
+ });
168
+
169
+ describe('Edge cases', () => {
170
+ it('Should handle empty passage names gracefully', () => {
171
+ const content = ':: \nContent here';
172
+ expect(() => parseTwee(content)).toThrow('Malformed passage header!');
173
+ });
174
+
175
+ it('Should handle passages with only escaped characters as names', () => {
176
+ const content = ':: \\[\\]\nContent here';
177
+ const story = parseTwee(content);
178
+ expect(story.passages[0].name).toBe('[]');
179
+ });
180
+
181
+ it('Should handle multiple consecutive escapes', () => {
182
+ const content = ':: Test\\\\\\\\Name\nContent here';
183
+ const story = parseTwee(content);
184
+ expect(story.passages[0].name).toBe('Test\\\\Name');
185
+ });
186
+
187
+ it('Should handle tags with only metacharacters', () => {
188
+ const content = ':: TestPassage [\\[\\] \\{\\}]\nContent here';
189
+ const story = parseTwee(content);
190
+ expect(story.passages[0].tags).toEqual(['[]', '{}']);
191
+ });
192
+
193
+ it('Should handle mixed whitespace and escaping', () => {
194
+ const content = ':: Test\\[Name\\] [ tag\\[1\\] tag2 ]\nContent here';
195
+ const story = parseTwee(content);
196
+ expect(story.passages[0].name).toBe('Test[Name]');
197
+ expect(story.passages[0].tags).toEqual(['tag[1]', 'tag2']);
198
+ });
199
+ });
200
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ /**
6
+ * Tests for web-twine1html.js module
7
+ * Tests module exports and functionality
8
+ */
9
+
10
+ import { describe, expect, it } from '@jest/globals';
11
+
12
+ // Import to test basic functionality
13
+ import { parse, compile } from '../../src/Web/web-twine1html.js';
14
+ import Extwee from '../../src/Web/web-twine1html.js';
15
+
16
+ describe('web-twine1html.js module tests', () => {
17
+
18
+ describe('ES6 module exports', () => {
19
+ it('should export parse and compile functions', () => {
20
+ expect(parse).toBeDefined();
21
+ expect(compile).toBeDefined();
22
+ expect(typeof parse).toBe('function');
23
+ expect(typeof compile).toBe('function');
24
+ });
25
+
26
+ it('should export default object with parseTwine1HTML and compileTwine1HTML', () => {
27
+ expect(Extwee.parseTwine1HTML).toBeDefined();
28
+ expect(Extwee.compileTwine1HTML).toBeDefined();
29
+ expect(Extwee.parse).toBeDefined();
30
+ expect(Extwee.compile).toBeDefined();
31
+ expect(typeof Extwee.parseTwine1HTML).toBe('function');
32
+ expect(typeof Extwee.compileTwine1HTML).toBe('function');
33
+ });
34
+ });
35
+
36
+ describe('Global object assignment', () => {
37
+ it('should assign functions to global object when available', () => {
38
+ // In Node.js environment, should assign to globalThis
39
+ expect(globalThis.Extwee).toBeDefined();
40
+ expect(globalThis.Extwee.parseTwine1HTML).toBeDefined();
41
+ expect(globalThis.Extwee.compileTwine1HTML).toBeDefined();
42
+ expect(typeof globalThis.Extwee.parseTwine1HTML).toBe('function');
43
+ expect(typeof globalThis.Extwee.compileTwine1HTML).toBe('function');
44
+ });
45
+
46
+ it('should preserve existing Extwee properties', () => {
47
+ // Should not overwrite the entire object, just add properties
48
+ if (globalThis.Extwee && globalThis.Extwee.version) {
49
+ expect(globalThis.Extwee.version).toBeDefined();
50
+ }
51
+ expect(globalThis.Extwee.parseTwine1HTML).toBeDefined();
52
+ expect(globalThis.Extwee.compileTwine1HTML).toBeDefined();
53
+ });
54
+ });
55
+
56
+ describe('Functional integration tests', () => {
57
+ it('should have working parseTwine1HTML function', () => {
58
+ // Test with valid Twine 1 HTML
59
+ const sampleHtml = `
60
+ <html>
61
+ <head><title>Test</title></head>
62
+ <body>
63
+ <div id="storeArea" data-size="2">
64
+ <div tiddler="Start" tags="" twine-position="100,100">Start passage</div>
65
+ </div>
66
+ </body>
67
+ </html>
68
+ `;
69
+
70
+ expect(() => {
71
+ const result = parse(sampleHtml);
72
+ expect(result).toBeDefined();
73
+ expect(result.passages).toBeDefined();
74
+ }).not.toThrow();
75
+ });
76
+
77
+ it('should have working compileTwine1HTML function', async () => {
78
+ // Import required classes dynamically to avoid circular imports
79
+ const { Story } = await import('../../src/Story.js');
80
+ const { default: Passage } = await import('../../src/Passage.js');
81
+ const { default: StoryFormat } = await import('../../src/StoryFormat.js');
82
+
83
+ const story = new Story();
84
+ story.name = "Test Story";
85
+ story.addPassage(new Passage("Start", "This is the start", [], {}));
86
+
87
+ const storyFormat = new StoryFormat();
88
+ storyFormat.source = "window.story = STORY;";
89
+ storyFormat.version = "1.0.0";
90
+
91
+ expect(() => {
92
+ const result = compile(story, storyFormat, '', '', '');
93
+ expect(typeof result).toBe('string');
94
+ }).not.toThrow();
95
+ });
96
+
97
+ it('should have same functions in exports and global', () => {
98
+ // Test that parse and compile are the same functions
99
+ expect(parse).toBe(Extwee.parse);
100
+ expect(compile).toBe(Extwee.compile);
101
+ expect(parse).toBe(Extwee.parseTwine1HTML);
102
+ expect(compile).toBe(Extwee.compileTwine1HTML);
103
+ });
104
+ });
105
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ /**
6
+ * Tests for web-twine2archive.js module
7
+ * Tests module exports and functionality
8
+ */
9
+
10
+ import { describe, expect, it } from '@jest/globals';
11
+
12
+ // Import to test basic functionality
13
+ import { parse, compile } from '../../src/Web/web-twine2archive.js';
14
+ import Extwee from '../../src/Web/web-twine2archive.js';
15
+
16
+ describe('web-twine2archive.js module tests', () => {
17
+
18
+ describe('ES6 module exports', () => {
19
+ it('should export parse and compile functions', () => {
20
+ expect(parse).toBeDefined();
21
+ expect(compile).toBeDefined();
22
+ expect(typeof parse).toBe('function');
23
+ expect(typeof compile).toBe('function');
24
+ });
25
+
26
+ it('should export default object with parseTwine2ArchiveHTML and compileTwine2ArchiveHTML', () => {
27
+ expect(Extwee.parseTwine2ArchiveHTML).toBeDefined();
28
+ expect(Extwee.compileTwine2ArchiveHTML).toBeDefined();
29
+ expect(Extwee.parse).toBeDefined();
30
+ expect(Extwee.compile).toBeDefined();
31
+ expect(typeof Extwee.parseTwine2ArchiveHTML).toBe('function');
32
+ expect(typeof Extwee.compileTwine2ArchiveHTML).toBe('function');
33
+ });
34
+ });
35
+
36
+ describe('Global object assignment', () => {
37
+ it('should assign functions to global object when available', () => {
38
+ // In Node.js environment, should assign to globalThis
39
+ expect(globalThis.Extwee).toBeDefined();
40
+ expect(globalThis.Extwee.parseTwine2ArchiveHTML).toBeDefined();
41
+ expect(globalThis.Extwee.compileTwine2ArchiveHTML).toBeDefined();
42
+ expect(typeof globalThis.Extwee.parseTwine2ArchiveHTML).toBe('function');
43
+ expect(typeof globalThis.Extwee.compileTwine2ArchiveHTML).toBe('function');
44
+ });
45
+
46
+ it('should preserve existing Extwee properties', () => {
47
+ // Should not overwrite the entire object, just add properties
48
+ if (globalThis.Extwee && globalThis.Extwee.version) {
49
+ expect(globalThis.Extwee.version).toBeDefined();
50
+ }
51
+ expect(globalThis.Extwee.parseTwine2ArchiveHTML).toBeDefined();
52
+ expect(globalThis.Extwee.compileTwine2ArchiveHTML).toBeDefined();
53
+ });
54
+ });
55
+
56
+ describe('Functional integration tests', () => {
57
+ it('should have working parseTwine2ArchiveHTML function', () => {
58
+ // Test with valid Twine 2 Archive HTML
59
+ const sampleHtml = `
60
+ <tw-storydata name="Test" startnode="1" creator="Twine" creator-version="2.3.5">
61
+ <tw-passagedata pid="1" name="Start" tags="">Start passage</tw-passagedata>
62
+ </tw-storydata>
63
+ `;
64
+
65
+ expect(() => {
66
+ const result = parse(sampleHtml);
67
+ expect(result).toBeDefined();
68
+ expect(Array.isArray(result)).toBe(true);
69
+ }).not.toThrow();
70
+ });
71
+
72
+ it('should have working compileTwine2ArchiveHTML function', async () => {
73
+ // Import required classes
74
+ const { Story } = await import('../../src/Story.js');
75
+ const { default: Passage } = await import('../../src/Passage.js');
76
+
77
+ const story = new Story();
78
+ story.name = "Test Story";
79
+ story.IFID = "12345678-1234-5678-9012-123456789012";
80
+ story.addPassage(new Passage("Start", "This is the start", [], {}));
81
+
82
+ expect(() => {
83
+ const result = compile([story]);
84
+ expect(typeof result).toBe('string');
85
+ }).not.toThrow();
86
+ });
87
+
88
+ it('should have same functions in exports and global', () => {
89
+ // Test that parse and compile are the same functions
90
+ expect(parse).toBe(Extwee.parse);
91
+ expect(compile).toBe(Extwee.compile);
92
+ expect(parse).toBe(Extwee.parseTwine2ArchiveHTML);
93
+ expect(compile).toBe(Extwee.compileTwine2ArchiveHTML);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+
5
+ /**
6
+ * Tests for web-tws.js module
7
+ * Tests module exports and functionality
8
+ */
9
+
10
+ import { describe, expect, it } from '@jest/globals';
11
+
12
+ // Import to test basic functionality
13
+ import { parse } from '../../src/Web/web-tws.js';
14
+ import Extwee from '../../src/Web/web-tws.js';
15
+
16
+ describe('web-tws.js module tests', () => {
17
+
18
+ describe('ES6 module exports', () => {
19
+ it('should export parse function', () => {
20
+ expect(parse).toBeDefined();
21
+ expect(typeof parse).toBe('function');
22
+ });
23
+
24
+ it('should export default object with parseTWS', () => {
25
+ expect(Extwee.parseTWS).toBeDefined();
26
+ expect(Extwee.parse).toBeDefined();
27
+ expect(typeof Extwee.parseTWS).toBe('function');
28
+ expect(typeof Extwee.parse).toBe('function');
29
+ });
30
+ });
31
+
32
+ describe('Global object assignment', () => {
33
+ it('should assign functions to global object when available', () => {
34
+ // In Node.js environment, should assign to globalThis
35
+ expect(globalThis.Extwee).toBeDefined();
36
+ expect(globalThis.Extwee.parseTWS).toBeDefined();
37
+ expect(typeof globalThis.Extwee.parseTWS).toBe('function');
38
+ });
39
+
40
+ it('should preserve existing Extwee properties', () => {
41
+ // Should not overwrite the entire object, just add properties
42
+ if (globalThis.Extwee && globalThis.Extwee.version) {
43
+ expect(globalThis.Extwee.version).toBeDefined();
44
+ }
45
+ expect(globalThis.Extwee.parseTWS).toBeDefined();
46
+ });
47
+ });
48
+
49
+ describe('Functional integration tests', () => {
50
+ it('should have working parseTWS function', () => {
51
+ // Create a minimal valid TWS buffer (pickled data)
52
+ // This is a very basic test - TWS parsing is complex
53
+ const validBuffer = Buffer.from([
54
+ 0x80, 0x02, // Python pickle protocol version 2
55
+ 0x7d, 0x71, 0x00, // Empty dict
56
+ 0x2e // STOP
57
+ ]);
58
+
59
+ expect(() => {
60
+ const result = parse(validBuffer);
61
+ expect(result).toBeDefined();
62
+ }).not.toThrow();
63
+ });
64
+
65
+ it('should throw error for invalid input', () => {
66
+ expect(() => {
67
+ parse("not a buffer");
68
+ }).toThrow();
69
+ });
70
+
71
+ it('should have same functions in exports and global', () => {
72
+ // Test that parse is the same function
73
+ expect(parse).toBe(Extwee.parse);
74
+ expect(parse).toBe(Extwee.parseTWS);
75
+ });
76
+ });
77
+ });
@@ -251,5 +251,5 @@ export class Story {
251
251
  #private;
252
252
  }
253
253
  export const creatorName: "extwee";
254
- export const creatorVersion: "2.3.4";
254
+ export const creatorVersion: "2.3.5";
255
255
  import Passage from './Passage.js';
@@ -6,4 +6,25 @@
6
6
  * @returns {Story} story
7
7
  */
8
8
  export function parse(fileContents: string): Story;
9
+ /**
10
+ * Escapes Twee 3 metacharacters according to the specification.
11
+ * This is used when writing Twee files to ensure special characters are properly escaped.
12
+ * @function escapeTweeMetacharacters
13
+ * @param {string} text - Text to escape
14
+ * @returns {string} Escaped text
15
+ */
16
+ export function escapeTweeMetacharacters(text: string): string;
17
+ /**
18
+ * Unescapes Twee 3 metacharacters according to the specification.
19
+ *
20
+ * From the Twee 3 specification:
21
+ * - Encoding: To avoid ambiguity, non-escape backslashes must also be escaped via
22
+ * the same mechanism (i.e. `foo\bar` must become `foo\\bar`).
23
+ * - Decoding: To make decoding more robust, any escaped character within a chunk of
24
+ * encoded text must yield the character minus the backslash (i.e. `\q` must yield `q`).
25
+ * @function unescapeTweeMetacharacters
26
+ * @param {string} text - Text to unescape
27
+ * @returns {string} Unescaped text
28
+ */
29
+ export function unescapeTweeMetacharacters(text: string): string;
9
30
  import { Story } from '../Story.js';