extwee 2.3.3 → 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.
- package/build/extwee.core.min.js +1 -1
- package/build/extwee.twine1html.min.js +1 -1
- package/build/extwee.twine2archive.min.js +1 -1
- package/build/extwee.tws.min.js +1 -1
- package/docs/build/extwee.core.min.js +1 -0
- package/docs/build/extwee.twine1html.min.js +1 -0
- package/docs/build/extwee.twine2archive.min.js +1 -0
- package/docs/build/extwee.tws.min.js +1 -0
- package/docs/demos/compiler/extwee.core.min.js +1 -0
- package/docs/demos/compiler/index.css +105 -0
- package/docs/demos/compiler/index.html +359 -0
- package/package.json +19 -18
- package/src/CLI/CommandLineProcessing.js +148 -153
- package/src/Passage.js +6 -4
- package/src/Story.js +1 -1
- package/src/Twee/parse.js +117 -21
- package/src/Twine2HTML/parse-web.js +7 -1
- package/src/Web/web-core.js +22 -2
- package/src/Web/web-twine1html.js +25 -5
- package/src/Web/web-twine2archive.js +25 -5
- package/src/Web/web-tws.js +22 -4
- package/test/Objects/Passage.test.js +1 -1
- package/test/Twee/Twee.Escaping.test.js +200 -0
- package/test/Twine1HTML/Twine1HTML.Parse.Web.test.js +484 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.Web.test.js +293 -0
- package/test/Twine2HTML/Twine2HTML.Parse.Web.test.js +329 -0
- package/test/Web/web-core-coverage.test.js +175 -0
- package/test/Web/web-core-global.test.js +93 -0
- package/test/Web/web-core.test.js +156 -0
- package/test/Web/web-twine1html.test.js +105 -0
- package/test/Web/web-twine2archive.test.js +96 -0
- package/test/Web/web-tws.test.js +77 -0
- package/test/Web/window.Extwee.test.js +7 -2
- package/types/src/Story.d.ts +1 -1
- package/types/src/Twee/parse.d.ts +21 -0
- package/types/src/Web/web-core.d.ts +23 -1
- package/types/src/Web/web-twine1html.d.ts +7 -0
- package/types/src/Web/web-twine2archive.d.ts +7 -0
- package/types/src/Web/web-tws.d.ts +5 -0
- package/webpack.config.js +2 -1
- package/src/Web/web-index.js +0 -31
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { parse } from '../../src/Twine2ArchiveHTML/parse-web.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mock environment to force fallback parsing since jsdom doesn't behave like browser DOMParser
|
|
8
|
+
* @param {string} content Content to parse
|
|
9
|
+
* @returns {Array} Array of Story objects
|
|
10
|
+
*/
|
|
11
|
+
function parseTwine2ArchiveHTMLWeb(content) {
|
|
12
|
+
// Force fallback mode by temporarily hiding DOMParser
|
|
13
|
+
const originalDOMParser = global.DOMParser;
|
|
14
|
+
delete global.DOMParser;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return parse(content);
|
|
18
|
+
} finally {
|
|
19
|
+
// Restore DOMParser
|
|
20
|
+
if (originalDOMParser) {
|
|
21
|
+
global.DOMParser = originalDOMParser;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('Twine2ArchiveHTML', function () {
|
|
27
|
+
describe('parse-web()', function () {
|
|
28
|
+
describe('Error handling', function () {
|
|
29
|
+
it('Should throw TypeError for non-string content', function () {
|
|
30
|
+
expect(() => { parseTwine2ArchiveHTMLWeb(null); }).toThrow('Content is not a string!');
|
|
31
|
+
expect(() => { parseTwine2ArchiveHTMLWeb(undefined); }).toThrow('Content is not a string!');
|
|
32
|
+
expect(() => { parseTwine2ArchiveHTMLWeb(123); }).toThrow('Content is not a string!');
|
|
33
|
+
expect(() => { parseTwine2ArchiveHTMLWeb({}); }).toThrow('Content is not a string!');
|
|
34
|
+
expect(() => { parseTwine2ArchiveHTMLWeb([]); }).toThrow('Content is not a string!');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Warning generation', function () {
|
|
39
|
+
let originalConsoleWarn;
|
|
40
|
+
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
originalConsoleWarn = console.warn;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
console.warn = originalConsoleWarn;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('Should warn when no Twine 2 HTML content is found', function () {
|
|
50
|
+
let warningMessage = '';
|
|
51
|
+
console.warn = (msg) => { warningMessage = msg; };
|
|
52
|
+
|
|
53
|
+
const result = parseTwine2ArchiveHTMLWeb('<div>no twine content here</div>');
|
|
54
|
+
expect(warningMessage).toBe('Warning: No Twine 2 HTML content found!');
|
|
55
|
+
expect(result).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('Should warn for empty string content', function () {
|
|
59
|
+
let warningMessage = '';
|
|
60
|
+
console.warn = (msg) => { warningMessage = msg; };
|
|
61
|
+
|
|
62
|
+
const result = parseTwine2ArchiveHTMLWeb('');
|
|
63
|
+
expect(warningMessage).toBe('Warning: No Twine 2 HTML content found!');
|
|
64
|
+
expect(result).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Basic parsing functionality', function () {
|
|
69
|
+
it('Should parse single story from archive', function () {
|
|
70
|
+
const content = '<tw-storydata name="Test Story" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">Hello World</tw-passagedata></tw-storydata>';
|
|
71
|
+
|
|
72
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
73
|
+
|
|
74
|
+
expect(stories.length).toBe(1);
|
|
75
|
+
expect(stories[0].name).toBe('Test Story');
|
|
76
|
+
expect(stories[0].IFID).toBe('12345678-1234-1234-1234-123456789012');
|
|
77
|
+
expect(stories[0].size()).toBe(1);
|
|
78
|
+
|
|
79
|
+
const passage = stories[0].getPassageByName('Start');
|
|
80
|
+
expect(passage.name).toBe('Start');
|
|
81
|
+
expect(passage.text).toBe('Hello World');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('Should parse multiple stories from archive', function () {
|
|
85
|
+
const content = `
|
|
86
|
+
<tw-storydata name="First Story" ifid="11111111-1111-1111-1111-111111111111">
|
|
87
|
+
<tw-passagedata pid="1" name="Start">First story content</tw-passagedata>
|
|
88
|
+
</tw-storydata>
|
|
89
|
+
<tw-storydata name="Second Story" ifid="22222222-2222-2222-2222-222222222222">
|
|
90
|
+
<tw-passagedata pid="1" name="Begin">Second story content</tw-passagedata>
|
|
91
|
+
<tw-passagedata pid="2" name="Next">More content</tw-passagedata>
|
|
92
|
+
</tw-storydata>
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
96
|
+
|
|
97
|
+
expect(stories.length).toBe(2);
|
|
98
|
+
|
|
99
|
+
// First story
|
|
100
|
+
expect(stories[0].name).toBe('First Story');
|
|
101
|
+
expect(stories[0].IFID).toBe('11111111-1111-1111-1111-111111111111');
|
|
102
|
+
expect(stories[0].size()).toBe(1);
|
|
103
|
+
expect(stories[0].getPassageByName('Start').text).toBe('First story content');
|
|
104
|
+
|
|
105
|
+
// Second story
|
|
106
|
+
expect(stories[1].name).toBe('Second Story');
|
|
107
|
+
expect(stories[1].IFID).toBe('22222222-2222-2222-2222-222222222222');
|
|
108
|
+
expect(stories[1].size()).toBe(2);
|
|
109
|
+
expect(stories[1].getPassageByName('Begin').text).toBe('Second story content');
|
|
110
|
+
expect(stories[1].getPassageByName('Next').text).toBe('More content');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('Should parse story with complex attributes', function () {
|
|
114
|
+
const content = '<tw-storydata name="Complex Story" ifid="12345678-1234-1234-1234-123456789012" creator="Twine" creator-version="2.3.9" format="Harlowe" format-version="3.1.0" startnode="2" zoom="1.5"><tw-passagedata pid="2" name="Start" position="100,200" size="150,100" tags="start important">Welcome to the story</tw-passagedata></tw-storydata>';
|
|
115
|
+
|
|
116
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
117
|
+
|
|
118
|
+
expect(stories.length).toBe(1);
|
|
119
|
+
const story = stories[0];
|
|
120
|
+
|
|
121
|
+
expect(story.name).toBe('Complex Story');
|
|
122
|
+
expect(story.creator).toBe('Twine');
|
|
123
|
+
expect(story.creatorVersion).toBe('2.3.9');
|
|
124
|
+
expect(story.format).toBe('Harlowe');
|
|
125
|
+
expect(story.formatVersion).toBe('3.1.0');
|
|
126
|
+
expect(story.start).toBe('Start');
|
|
127
|
+
expect(story.zoom).toBe(1.5);
|
|
128
|
+
|
|
129
|
+
const passage = story.getPassageByName('Start');
|
|
130
|
+
expect(passage.tags).toEqual(['start', 'important']);
|
|
131
|
+
expect(passage.metadata.position).toBe('100,200');
|
|
132
|
+
expect(passage.metadata.size).toBe('150,100');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('HTML content handling', function () {
|
|
137
|
+
it('Should handle nested HTML content in stories', function () {
|
|
138
|
+
const content = `
|
|
139
|
+
<div class="wrapper">
|
|
140
|
+
<tw-storydata name="Nested Story" ifid="12345678-1234-1234-1234-123456789012">
|
|
141
|
+
<tw-passagedata pid="1" name="Start">Story content</tw-passagedata>
|
|
142
|
+
</tw-storydata>
|
|
143
|
+
</div>
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
147
|
+
|
|
148
|
+
expect(stories.length).toBe(1);
|
|
149
|
+
expect(stories[0].name).toBe('Nested Story');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('Should handle stories with style elements', function () {
|
|
153
|
+
const content = '<tw-storydata name="Styled Story" ifid="12345678-1234-1234-1234-123456789012"><style>tw-story-tag-important { color: red; }</style><tw-passagedata pid="1" name="Start" tags="important">Styled content</tw-passagedata></tw-storydata>';
|
|
154
|
+
|
|
155
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
156
|
+
|
|
157
|
+
expect(stories.length).toBe(1);
|
|
158
|
+
const story = stories[0];
|
|
159
|
+
expect(story.tagColors.important).toBe('red;');
|
|
160
|
+
|
|
161
|
+
const passage = story.getPassageByName('Start');
|
|
162
|
+
expect(passage.tags).toEqual(['important']);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('Should handle stories with script elements', function () {
|
|
166
|
+
const content = '<tw-storydata name="Scripted Story" ifid="12345678-1234-1234-1234-123456789012"><script>console.log("test");</script><tw-passagedata pid="1" name="Start">Script content</tw-passagedata></tw-storydata>';
|
|
167
|
+
|
|
168
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
169
|
+
|
|
170
|
+
expect(stories.length).toBe(1);
|
|
171
|
+
expect(stories[0].name).toBe('Scripted Story');
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('Malformed content handling', function () {
|
|
176
|
+
it('Should handle incomplete tw-storydata elements', function () {
|
|
177
|
+
const content = '<tw-storydata name="Incomplete" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">Content';
|
|
178
|
+
|
|
179
|
+
// Should not throw, but should handle gracefully
|
|
180
|
+
expect(() => parseTwine2ArchiveHTMLWeb(content)).not.toThrow();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('Should handle mixed valid and invalid content', function () {
|
|
184
|
+
const content = `
|
|
185
|
+
<div>Random content</div>
|
|
186
|
+
<tw-storydata name="Valid Story" ifid="12345678-1234-1234-1234-123456789012">
|
|
187
|
+
<tw-passagedata pid="1" name="Start">Valid content</tw-passagedata>
|
|
188
|
+
</tw-storydata>
|
|
189
|
+
<div>More random content</div>
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
193
|
+
|
|
194
|
+
expect(stories.length).toBe(1);
|
|
195
|
+
expect(stories[0].name).toBe('Valid Story');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Fallback DOM parsing', function () {
|
|
200
|
+
it('Should work without DOMParser (fallback mode)', function () {
|
|
201
|
+
const originalDOMParser = global.DOMParser;
|
|
202
|
+
global.DOMParser = undefined;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const content = '<tw-storydata name="Fallback Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">Fallback content</tw-passagedata></tw-storydata>';
|
|
206
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
207
|
+
|
|
208
|
+
expect(stories.length).toBe(1);
|
|
209
|
+
expect(stories[0].name).toBe('Fallback Test');
|
|
210
|
+
expect(stories[0].getPassageByName('Start').text).toBe('Fallback content');
|
|
211
|
+
} finally {
|
|
212
|
+
global.DOMParser = originalDOMParser;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('Should handle multiple stories in fallback mode', function () {
|
|
217
|
+
const originalDOMParser = global.DOMParser;
|
|
218
|
+
global.DOMParser = undefined;
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const content = `
|
|
222
|
+
<tw-storydata name="First" ifid="11111111-1111-1111-1111-111111111111">
|
|
223
|
+
<tw-passagedata pid="1" name="Start">First</tw-passagedata>
|
|
224
|
+
</tw-storydata>
|
|
225
|
+
<tw-storydata name="Second" ifid="22222222-2222-2222-2222-222222222222">
|
|
226
|
+
<tw-passagedata pid="1" name="Begin">Second</tw-passagedata>
|
|
227
|
+
</tw-storydata>
|
|
228
|
+
`;
|
|
229
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
230
|
+
|
|
231
|
+
expect(stories.length).toBe(2);
|
|
232
|
+
expect(stories[0].name).toBe('First');
|
|
233
|
+
expect(stories[1].name).toBe('Second');
|
|
234
|
+
} finally {
|
|
235
|
+
global.DOMParser = originalDOMParser;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('Edge cases', function () {
|
|
241
|
+
it('Should handle empty archive content', function () {
|
|
242
|
+
const result = parseTwine2ArchiveHTMLWeb('');
|
|
243
|
+
expect(result).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('Should handle whitespace-only content', function () {
|
|
247
|
+
const result = parseTwine2ArchiveHTMLWeb(' \n\t ');
|
|
248
|
+
expect(result).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('Should handle stories with unusual character encodings', function () {
|
|
252
|
+
const content = '<tw-storydata name="Unicode Story 📚" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">Content with emojis 🎮 and unicode ü ñ</tw-passagedata></tw-storydata>';
|
|
253
|
+
|
|
254
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
255
|
+
|
|
256
|
+
expect(stories.length).toBe(1);
|
|
257
|
+
expect(stories[0].name).toBe('Unicode Story 📚');
|
|
258
|
+
expect(stories[0].getPassageByName('Start').text).toBe('Content with emojis 🎮 and unicode ü ñ');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('Should handle stories with very long content', function () {
|
|
262
|
+
const longContent = 'A'.repeat(10000);
|
|
263
|
+
const content = `<tw-storydata name="Long Story" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">${longContent}</tw-passagedata></tw-storydata>`;
|
|
264
|
+
|
|
265
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
266
|
+
|
|
267
|
+
expect(stories.length).toBe(1);
|
|
268
|
+
expect(stories[0].getPassageByName('Start').text).toBe(longContent);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('Should handle archive with multiple identical story names but different IFIDs', function () {
|
|
272
|
+
const content = `
|
|
273
|
+
<tw-storydata name="Same Name" ifid="11111111-1111-1111-1111-111111111111">
|
|
274
|
+
<tw-passagedata pid="1" name="Start">First version</tw-passagedata>
|
|
275
|
+
</tw-storydata>
|
|
276
|
+
<tw-storydata name="Same Name" ifid="22222222-2222-2222-2222-222222222222">
|
|
277
|
+
<tw-passagedata pid="1" name="Start">Second version</tw-passagedata>
|
|
278
|
+
</tw-storydata>
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
const stories = parseTwine2ArchiveHTMLWeb(content);
|
|
282
|
+
|
|
283
|
+
expect(stories.length).toBe(2);
|
|
284
|
+
expect(stories[0].name).toBe('Same Name');
|
|
285
|
+
expect(stories[1].name).toBe('Same Name');
|
|
286
|
+
expect(stories[0].IFID).toBe('11111111-1111-1111-1111-111111111111');
|
|
287
|
+
expect(stories[1].IFID).toBe('22222222-2222-2222-2222-222222222222');
|
|
288
|
+
expect(stories[0].getPassageByName('Start').text).toBe('First version');
|
|
289
|
+
expect(stories[1].getPassageByName('Start').text).toBe('Second version');
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { parse } from '../../src/Twine2HTML/parse-web.js';
|
|
5
|
+
import { Story } from '../../src/Story.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mock environment to force fallback parsing since jsdom doesn't behave like browser DOMParser
|
|
9
|
+
* @param {string} content Content to parse
|
|
10
|
+
* @returns {Story} Story object
|
|
11
|
+
*/
|
|
12
|
+
function parseTwine2HTMLWeb(content) {
|
|
13
|
+
// Force fallback mode by temporarily hiding DOMParser
|
|
14
|
+
const originalDOMParser = global.DOMParser;
|
|
15
|
+
delete global.DOMParser;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return parse(content);
|
|
19
|
+
} finally {
|
|
20
|
+
// Restore DOMParser
|
|
21
|
+
if (originalDOMParser) {
|
|
22
|
+
global.DOMParser = originalDOMParser;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('Twine2HTML', function () {
|
|
28
|
+
describe('parse-web()', function () {
|
|
29
|
+
describe('Error handling', function () {
|
|
30
|
+
it('Should throw TypeError for non-string content', function () {
|
|
31
|
+
expect(() => { parseTwine2HTMLWeb(null); }).toThrow('TypeError: Content is not a string!');
|
|
32
|
+
expect(() => { parseTwine2HTMLWeb(undefined); }).toThrow('TypeError: Content is not a string!');
|
|
33
|
+
expect(() => { parseTwine2HTMLWeb(123); }).toThrow('TypeError: Content is not a string!');
|
|
34
|
+
expect(() => { parseTwine2HTMLWeb({}); }).toThrow('TypeError: Content is not a string!');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('Should throw TypeError for non-Twine2 HTML content', function () {
|
|
38
|
+
expect(() => { parseTwine2HTMLWeb('<div>not twine content</div>'); }).toThrow('TypeError: Not Twine 2 HTML content!');
|
|
39
|
+
expect(() => { parseTwine2HTMLWeb(''); }).toThrow('TypeError: Not Twine 2 HTML content!');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('Should throw Error for passages without PID', function () {
|
|
43
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata name="Test Passage">Content</tw-passagedata></tw-storydata>';
|
|
44
|
+
expect(() => { parseTwine2HTMLWeb(content); }).toThrow('Error: Passages are required to have PID!');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('Basic parsing functionality', function () {
|
|
49
|
+
it('Should parse basic Twine2 HTML with required attributes', function () {
|
|
50
|
+
const content = '<tw-storydata name="Test Story" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">Hello World</tw-passagedata></tw-storydata>';
|
|
51
|
+
|
|
52
|
+
const story = parseTwine2HTMLWeb(content);
|
|
53
|
+
|
|
54
|
+
expect(story.name).toBe('Test Story');
|
|
55
|
+
expect(story.IFID).toBe('12345678-1234-1234-1234-123456789012');
|
|
56
|
+
expect(story.size()).toBe(1);
|
|
57
|
+
|
|
58
|
+
const passage = story.getPassageByName('Start');
|
|
59
|
+
expect(passage.name).toBe('Start');
|
|
60
|
+
expect(passage.text).toBe('Hello World');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('Should parse story with all optional attributes', function () {
|
|
64
|
+
const content = '<tw-storydata name="Complex Story" ifid="12345678-1234-1234-1234-123456789012" creator="Twine" creator-version="2.3.9" format="Harlowe" format-version="3.1.0" startnode="2" zoom="1.5" options="key1:value1,key2:value2" hidden="passage1,passage2"><tw-passagedata pid="2" name="Start" position="100,200" size="150,100" tags="start important">Welcome to the story</tw-passagedata></tw-storydata>';
|
|
65
|
+
|
|
66
|
+
const story = parseTwine2HTMLWeb(content);
|
|
67
|
+
|
|
68
|
+
expect(story.name).toBe('Complex Story');
|
|
69
|
+
expect(story.IFID).toBe('12345678-1234-1234-1234-123456789012');
|
|
70
|
+
expect(story.creator).toBe('Twine');
|
|
71
|
+
expect(story.creatorVersion).toBe('2.3.9');
|
|
72
|
+
expect(story.format).toBe('Harlowe');
|
|
73
|
+
expect(story.formatVersion).toBe('3.1.0');
|
|
74
|
+
expect(story.start).toBe('Start');
|
|
75
|
+
expect(story.zoom).toBe(1.5);
|
|
76
|
+
expect(story.metadata.key1).toBe('value1');
|
|
77
|
+
expect(story.metadata.key2).toBe('value2');
|
|
78
|
+
expect(story.metadata.hidden).toBe('passage1,passage2');
|
|
79
|
+
|
|
80
|
+
const passage = story.getPassageByName('Start');
|
|
81
|
+
expect(passage.name).toBe('Start');
|
|
82
|
+
expect(passage.text).toBe('Welcome to the story');
|
|
83
|
+
expect(passage.tags).toEqual(['start', 'important']);
|
|
84
|
+
expect(passage.metadata.position).toBe('100,200');
|
|
85
|
+
expect(passage.metadata.size).toBe('150,100');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('Should parse story with multiple passages', function () {
|
|
89
|
+
const content = '<tw-storydata name="Multi Story" ifid="12345678-1234-1234-1234-123456789012" startnode="1"><tw-passagedata pid="1" name="Start">Start content</tw-passagedata><tw-passagedata pid="2" name="Second" tags="special">Second content</tw-passagedata></tw-storydata>';
|
|
90
|
+
|
|
91
|
+
const story = parseTwine2HTMLWeb(content);
|
|
92
|
+
|
|
93
|
+
expect(story.size()).toBe(2);
|
|
94
|
+
expect(story.start).toBe('Start');
|
|
95
|
+
|
|
96
|
+
const startPassage = story.getPassageByName('Start');
|
|
97
|
+
expect(startPassage.text).toBe('Start content');
|
|
98
|
+
|
|
99
|
+
const secondPassage = story.getPassageByName('Second');
|
|
100
|
+
expect(secondPassage.text).toBe('Second content');
|
|
101
|
+
expect(secondPassage.tags).toEqual(['special']);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Warning generation', function () {
|
|
106
|
+
let originalConsoleWarn;
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
originalConsoleWarn = console.warn;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
afterEach(() => {
|
|
113
|
+
console.warn = originalConsoleWarn;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('Should warn for missing name attribute', function () {
|
|
117
|
+
let warningMessage = '';
|
|
118
|
+
console.warn = (msg) => { warningMessage = msg; };
|
|
119
|
+
|
|
120
|
+
const content = '<tw-storydata ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
121
|
+
parseTwine2HTMLWeb(content);
|
|
122
|
+
expect(warningMessage).toBe('Warning: The name attribute is missing from tw-storydata!');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('Should warn for missing IFID attribute', function () {
|
|
126
|
+
let warningMessage = '';
|
|
127
|
+
console.warn = (msg) => { warningMessage = msg; };
|
|
128
|
+
|
|
129
|
+
const content = '<tw-storydata name="Test Story"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
130
|
+
parseTwine2HTMLWeb(content);
|
|
131
|
+
expect(warningMessage).toBe('Warning: The IFID is not in valid UUIDv4 formatting on tw-storydata!');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('Should warn for malformed IFID', function () {
|
|
135
|
+
let warningMessage = '';
|
|
136
|
+
console.warn = (msg) => { warningMessage = msg; };
|
|
137
|
+
|
|
138
|
+
const content = '<tw-storydata name="Test Story" ifid="invalid-ifid"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
139
|
+
parseTwine2HTMLWeb(content);
|
|
140
|
+
expect(warningMessage).toBe('Warning: The IFID is not in valid UUIDv4 formatting on tw-storydata!');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('Should warn for passage without name', function () {
|
|
144
|
+
let warningMessage = '';
|
|
145
|
+
console.warn = (msg) => { warningMessage = msg; };
|
|
146
|
+
|
|
147
|
+
const content = '<tw-storydata name="Test Story" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1">Content</tw-passagedata></tw-storydata>';
|
|
148
|
+
parseTwine2HTMLWeb(content);
|
|
149
|
+
expect(warningMessage).toBe('Warning: Cannot parse passage data without name!');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Tag color parsing from styles', function () {
|
|
154
|
+
it('Should parse tag colors from style elements', function () {
|
|
155
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><style>tw-story-tag-important { color: red; } tw-story-tag-special { color: #00ff00; }</style><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
156
|
+
|
|
157
|
+
const story = parseTwine2HTMLWeb(content);
|
|
158
|
+
|
|
159
|
+
expect(story.tagColors.important).toBe('red;');
|
|
160
|
+
expect(story.tagColors.special).toBe('#00ff00;');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('Should handle multiple style elements', function () {
|
|
164
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><style>tw-story-tag-red { color: red; }</style><style>tw-story-tag-blue { color: blue; }</style><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
165
|
+
|
|
166
|
+
const story = parseTwine2HTMLWeb(content);
|
|
167
|
+
|
|
168
|
+
expect(story.tagColors.red).toBe('red;');
|
|
169
|
+
expect(story.tagColors.blue).toBe('blue;');
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('HTML entity decoding', function () {
|
|
174
|
+
it('Should decode HTML entities in passage text', function () {
|
|
175
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start"><p>Hello & welcome</p></tw-passagedata></tw-storydata>';
|
|
176
|
+
|
|
177
|
+
const story = parseTwine2HTMLWeb(content);
|
|
178
|
+
const passage = story.getPassageByName('Start');
|
|
179
|
+
|
|
180
|
+
expect(passage.text).toBe('<p>Hello & welcome</p>');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('Should handle complex HTML entities', function () {
|
|
184
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start">"Quote" 'Apostrophe' © & more</tw-passagedata></tw-storydata>';
|
|
185
|
+
|
|
186
|
+
const story = parseTwine2HTMLWeb(content);
|
|
187
|
+
const passage = story.getPassageByName('Start');
|
|
188
|
+
|
|
189
|
+
expect(passage.text).toBe('"Quote" \'Apostrophe\' © & more');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('Tag handling', function () {
|
|
194
|
+
it('Should handle empty tags attribute', function () {
|
|
195
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start" tags="">Content</tw-passagedata></tw-storydata>';
|
|
196
|
+
|
|
197
|
+
const story = parseTwine2HTMLWeb(content);
|
|
198
|
+
const passage = story.getPassageByName('Start');
|
|
199
|
+
|
|
200
|
+
expect(passage.tags.length).toBe(0);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('Should handle quoted empty tags', function () {
|
|
204
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start" tags=\\"\\"\\" >Content</tw-passagedata></tw-storydata>';
|
|
205
|
+
|
|
206
|
+
const story = parseTwine2HTMLWeb(content);
|
|
207
|
+
const passage = story.getPassageByName('Start');
|
|
208
|
+
|
|
209
|
+
expect(passage.tags.length).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('Should handle multiple tags', function () {
|
|
213
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start" tags="tag1 tag2 tag3">Content</tw-passagedata></tw-storydata>';
|
|
214
|
+
|
|
215
|
+
const story = parseTwine2HTMLWeb(content);
|
|
216
|
+
const passage = story.getPassageByName('Start');
|
|
217
|
+
|
|
218
|
+
expect(passage.tags).toEqual(['tag1', 'tag2', 'tag3']);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('Should filter out empty tag strings', function () {
|
|
222
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start" tags="tag1 tag2 tag3">Content</tw-passagedata></tw-storydata>';
|
|
223
|
+
|
|
224
|
+
const story = parseTwine2HTMLWeb(content);
|
|
225
|
+
const passage = story.getPassageByName('Start');
|
|
226
|
+
|
|
227
|
+
expect(passage.tags).toEqual(['tag1', 'tag2', 'tag3']);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('Options parsing', function () {
|
|
232
|
+
it('Should parse empty options', function () {
|
|
233
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012" options=""><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
234
|
+
|
|
235
|
+
const story = parseTwine2HTMLWeb(content);
|
|
236
|
+
|
|
237
|
+
expect(Object.keys(story.metadata).length).toBe(0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('Should parse single option', function () {
|
|
241
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012" options="debug:true"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
242
|
+
|
|
243
|
+
const story = parseTwine2HTMLWeb(content);
|
|
244
|
+
|
|
245
|
+
expect(story.metadata.debug).toBe('true');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('Should parse multiple options', function () {
|
|
249
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012" options="debug:true,undo:false,jquery:disabled"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
250
|
+
|
|
251
|
+
const story = parseTwine2HTMLWeb(content);
|
|
252
|
+
|
|
253
|
+
expect(story.metadata.debug).toBe('true');
|
|
254
|
+
expect(story.metadata.undo).toBe('false');
|
|
255
|
+
expect(story.metadata.jquery).toBe('disabled');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('Should handle malformed options gracefully', function () {
|
|
259
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012" options="debug:true,malformed,another:value"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
260
|
+
|
|
261
|
+
const story = parseTwine2HTMLWeb(content);
|
|
262
|
+
|
|
263
|
+
expect(story.metadata.debug).toBe('true');
|
|
264
|
+
expect(story.metadata.another).toBe('value');
|
|
265
|
+
expect(story.metadata.malformed).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('Fallback DOM parsing', function () {
|
|
270
|
+
it('Should work without DOMParser (fallback mode)', function () {
|
|
271
|
+
const originalDOMParser = global.DOMParser;
|
|
272
|
+
global.DOMParser = undefined;
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const content = '<tw-storydata name="Fallback Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Start" tags="test" position="100,200">Fallback content</tw-passagedata></tw-storydata>';
|
|
276
|
+
const story = parseTwine2HTMLWeb(content);
|
|
277
|
+
|
|
278
|
+
expect(story.name).toBe('Fallback Test');
|
|
279
|
+
expect(story.size()).toBe(1);
|
|
280
|
+
|
|
281
|
+
const passage = story.getPassageByName('Start');
|
|
282
|
+
expect(passage.text).toBe('Fallback content');
|
|
283
|
+
expect(passage.tags).toEqual(['test']);
|
|
284
|
+
expect(passage.metadata.position).toBe('100,200');
|
|
285
|
+
} finally {
|
|
286
|
+
global.DOMParser = originalDOMParser;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Edge cases', function () {
|
|
292
|
+
it('Should handle empty passage content', function () {
|
|
293
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Empty"></tw-passagedata></tw-storydata>';
|
|
294
|
+
|
|
295
|
+
const story = parseTwine2HTMLWeb(content);
|
|
296
|
+
const passage = story.getPassageByName('Empty');
|
|
297
|
+
|
|
298
|
+
expect(passage.text).toBe('');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('Should handle special characters in passage names', function () {
|
|
302
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Passage with special: chars & symbols!">Content</tw-passagedata></tw-storydata>';
|
|
303
|
+
|
|
304
|
+
const story = parseTwine2HTMLWeb(content);
|
|
305
|
+
const passage = story.getPassageByName('Passage with special: chars & symbols!');
|
|
306
|
+
|
|
307
|
+
expect(passage.text).toBe('Content');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('Should handle whitespace in passage content', function () {
|
|
311
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012"><tw-passagedata pid="1" name="Whitespace"> \\n\\t Content with whitespace \\n\\t </tw-passagedata></tw-storydata>';
|
|
312
|
+
|
|
313
|
+
const story = parseTwine2HTMLWeb(content);
|
|
314
|
+
const passage = story.getPassageByName('Whitespace');
|
|
315
|
+
|
|
316
|
+
expect(passage.text).toBe('\\n\\t Content with whitespace \\n\\t');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('Should handle numeric zoom values', function () {
|
|
320
|
+
const content = '<tw-storydata name="Test" ifid="12345678-1234-1234-1234-123456789012" zoom="2.5"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
321
|
+
|
|
322
|
+
const story = parseTwine2HTMLWeb(content);
|
|
323
|
+
|
|
324
|
+
expect(story.zoom).toBe(2.5);
|
|
325
|
+
expect(typeof story.zoom).toBe('number');
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|