extwee 2.3.2 → 2.3.4
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 -0
- package/build/extwee.twine1html.min.js +1 -0
- package/build/extwee.twine2archive.min.js +1 -0
- package/build/extwee.tws.min.js +1 -0
- package/build/test-modular.html +126 -0
- 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/eslint.config.js +4 -1
- package/package.json +25 -22
- package/src/IFID/generate.js +2 -2
- package/src/Story.js +1 -1
- package/src/Twine1HTML/parse-web.js +255 -0
- package/src/Twine2ArchiveHTML/parse-web.js +134 -0
- package/src/Twine2HTML/parse-web.js +434 -0
- package/src/Web/web-core.js +51 -0
- package/src/Web/web-twine1html.js +35 -0
- package/src/Web/web-twine2archive.js +35 -0
- package/src/Web/web-tws.js +30 -0
- package/test/Config/Config.test.js +1 -1
- package/test/Config/isDirectory.test.js +15 -9
- package/test/Config/isFile.test.js +14 -11
- package/test/Config/loadStoryFormat.test.js +49 -33
- package/test/Config/readDirectories.test.js +25 -15
- package/test/Objects/Story.test.js +1 -0
- package/test/StoryFormat/StoryFormat.Parse.test.js +1 -0
- package/test/Twine1HTML/Twine1HTML.Parse.Web.test.js +484 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.Web.test.js +293 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +1 -0
- package/test/Twine2HTML/Twine2HTML.Parse.Web.test.js +329 -0
- package/test/Twine2HTML/Twine2HTML.Parse.test.js +1 -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/window.Extwee.test.js +25 -13
- package/types/src/Story.d.ts +1 -1
- package/types/src/Twine1HTML/parse-web.d.ts +10 -0
- package/types/src/Twine2ArchiveHTML/parse-web.d.ts +37 -0
- package/types/src/Twine2HTML/parse-web.d.ts +21 -0
- package/types/src/Web/html-entities-lite.d.ts +12 -0
- package/types/src/Web/semver-lite.d.ts +10 -0
- package/types/src/Web/uuid-lite.d.ts +6 -0
- package/types/src/Web/web-core.d.ts +23 -0
- package/types/src/Web/web-index.d.ts +1 -0
- package/types/src/Web/web-twine1html.d.ts +10 -0
- package/types/src/Web/web-twine2archive.d.ts +10 -0
- package/types/src/Web/web-tws.d.ts +7 -0
- package/webpack.config.js +23 -2
- package/build/extwee.web.min.js +0 -2
- package/build/extwee.web.min.js.LICENSE.txt +0 -1
- package/web-index.js +0 -31
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { Story } from '../Story.js';
|
|
2
|
+
import Passage from '../Passage.js';
|
|
3
|
+
import { decode } from 'html-entities';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Lightweight HTML parser for web builds - specifically for Twine 2 HTML parsing
|
|
7
|
+
* This replaces node-html-parser to reduce bundle size
|
|
8
|
+
*/
|
|
9
|
+
class LightweightTwine2Parser {
|
|
10
|
+
constructor(html) {
|
|
11
|
+
this.html = html;
|
|
12
|
+
this.doc = null;
|
|
13
|
+
|
|
14
|
+
// Parse HTML using browser's native DOMParser if available, otherwise fallback
|
|
15
|
+
if (typeof DOMParser !== 'undefined') {
|
|
16
|
+
const parser = new DOMParser();
|
|
17
|
+
this.doc = parser.parseFromString(html, 'text/html');
|
|
18
|
+
} else {
|
|
19
|
+
// Fallback for environments without DOMParser
|
|
20
|
+
this.doc = this.createSimpleDOM(html);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getElementsByTagName(tagName) {
|
|
25
|
+
if (this.doc && this.doc.getElementsByTagName) {
|
|
26
|
+
return Array.from(this.doc.getElementsByTagName(tagName));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Fallback implementation
|
|
30
|
+
if (tagName === 'tw-storydata') {
|
|
31
|
+
return this.extractStoryDataElements();
|
|
32
|
+
}
|
|
33
|
+
if (tagName === 'tw-passagedata') {
|
|
34
|
+
return this.extractPassageDataElements();
|
|
35
|
+
}
|
|
36
|
+
if (tagName === 'style') {
|
|
37
|
+
return this.extractStyleElements();
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
extractStoryDataElements() {
|
|
43
|
+
const storyDataRegex = /<tw-storydata[^>]*>([\s\S]*?)<\/tw-storydata>/gi;
|
|
44
|
+
const elements = [];
|
|
45
|
+
let match;
|
|
46
|
+
|
|
47
|
+
while ((match = storyDataRegex.exec(this.html)) !== null) {
|
|
48
|
+
const elementHtml = match[0];
|
|
49
|
+
const attributes = this.parseAttributes(elementHtml);
|
|
50
|
+
const innerHTML = match[1];
|
|
51
|
+
|
|
52
|
+
elements.push({
|
|
53
|
+
attributes,
|
|
54
|
+
innerHTML,
|
|
55
|
+
rawText: innerHTML
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return elements;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
extractPassageDataElements() {
|
|
63
|
+
const passageDataRegex = /<tw-passagedata[^>]*>([\s\S]*?)<\/tw-passagedata>/gi;
|
|
64
|
+
const elements = [];
|
|
65
|
+
let match;
|
|
66
|
+
|
|
67
|
+
while ((match = passageDataRegex.exec(this.html)) !== null) {
|
|
68
|
+
const elementHtml = match[0];
|
|
69
|
+
const attributes = this.parseAttributes(elementHtml);
|
|
70
|
+
const textContent = this.extractTextContent(match[1]);
|
|
71
|
+
|
|
72
|
+
elements.push({
|
|
73
|
+
attributes,
|
|
74
|
+
rawText: textContent
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return elements;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
extractStyleElements() {
|
|
82
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
83
|
+
const elements = [];
|
|
84
|
+
let match;
|
|
85
|
+
|
|
86
|
+
while ((match = styleRegex.exec(this.html)) !== null) {
|
|
87
|
+
const elementHtml = match[0];
|
|
88
|
+
const attributes = this.parseAttributes(elementHtml);
|
|
89
|
+
const textContent = match[1];
|
|
90
|
+
|
|
91
|
+
elements.push({
|
|
92
|
+
attributes,
|
|
93
|
+
rawText: textContent,
|
|
94
|
+
innerHTML: textContent
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return elements;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
parseAttributes(elementHtml) {
|
|
102
|
+
const attributes = {};
|
|
103
|
+
|
|
104
|
+
// Extract just the opening tag to avoid getting attributes from nested elements
|
|
105
|
+
const openingTagMatch = elementHtml.match(/^<[^>]*>/);
|
|
106
|
+
if (!openingTagMatch) return attributes;
|
|
107
|
+
|
|
108
|
+
const openingTag = openingTagMatch[0];
|
|
109
|
+
|
|
110
|
+
// Common attribute patterns
|
|
111
|
+
const attributeRegex = /(\w+(?:-\w+)*)=["']([^"']*)["']/g;
|
|
112
|
+
let match;
|
|
113
|
+
|
|
114
|
+
while ((match = attributeRegex.exec(openingTag)) !== null) {
|
|
115
|
+
attributes[match[1]] = match[2];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return attributes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
extractTextContent(html) {
|
|
122
|
+
// Remove HTML tags and decode basic entities
|
|
123
|
+
return html
|
|
124
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
125
|
+
.replace(/</g, '<')
|
|
126
|
+
.replace(/>/g, '>')
|
|
127
|
+
.replace(/"/g, '"')
|
|
128
|
+
.replace(/'/g, "'")
|
|
129
|
+
.replace(/&/g, '&') // This should be last
|
|
130
|
+
.trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// eslint-disable-next-line no-unused-vars
|
|
134
|
+
createSimpleDOM(_html) {
|
|
135
|
+
// Minimal DOM-like object for fallback
|
|
136
|
+
return {
|
|
137
|
+
getElementsByTagName: (tagName) => {
|
|
138
|
+
if (tagName === 'tw-storydata') {
|
|
139
|
+
return this.extractStoryDataElements();
|
|
140
|
+
}
|
|
141
|
+
if (tagName === 'tw-passagedata') {
|
|
142
|
+
return this.extractPassageDataElements();
|
|
143
|
+
}
|
|
144
|
+
if (tagName === 'style') {
|
|
145
|
+
return this.extractStyleElements();
|
|
146
|
+
}
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Web-optimized Twine 2 HTML parser with reduced dependencies
|
|
155
|
+
* Parse Twine 2 HTML into Story object using lightweight DOM parsing
|
|
156
|
+
*
|
|
157
|
+
* See: Twine 2 HTML Output Specification
|
|
158
|
+
* (https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-htmloutput-spec.md)
|
|
159
|
+
*
|
|
160
|
+
* Produces warnings for:
|
|
161
|
+
* - Missing name attribute on `<tw-storydata>` element.
|
|
162
|
+
* - Missing IFID attribute on `<tw-storydata>` element.
|
|
163
|
+
* - Malformed IFID attribute on `<tw-storydata>` element.
|
|
164
|
+
* @function parse
|
|
165
|
+
* @param {string} content - Twine 2 HTML content to parse.
|
|
166
|
+
* @returns {Story} Story object based on Twine 2 HTML content.
|
|
167
|
+
* @throws {TypeError} Content is not a string.
|
|
168
|
+
* @throws {Error} Not Twine 2 HTML content!
|
|
169
|
+
* @throws {Error} Cannot parse passage data without name!
|
|
170
|
+
* @throws {Error} Passages are required to have PID!
|
|
171
|
+
*/
|
|
172
|
+
function parse(content) {
|
|
173
|
+
// Create new story.
|
|
174
|
+
const story = new Story();
|
|
175
|
+
|
|
176
|
+
// Can only parse string values.
|
|
177
|
+
if (typeof content !== 'string') {
|
|
178
|
+
throw new TypeError('TypeError: Content is not a string!');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Set default start node.
|
|
182
|
+
let startNode = null;
|
|
183
|
+
|
|
184
|
+
// Use lightweight parser for web builds
|
|
185
|
+
const dom = new LightweightTwine2Parser(content);
|
|
186
|
+
|
|
187
|
+
// Pull out the `<tw-storydata>` element.
|
|
188
|
+
const storyDataElements = dom.getElementsByTagName('tw-storydata');
|
|
189
|
+
|
|
190
|
+
// Did we find any elements?
|
|
191
|
+
if (storyDataElements.length === 0) {
|
|
192
|
+
// If there is not a single `<tw-storydata>` element, this is not a Twine 2 story!
|
|
193
|
+
throw new TypeError('TypeError: Not Twine 2 HTML content!');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// We only parse the first element found.
|
|
197
|
+
const storyData = storyDataElements[0];
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* name: (string) Required.
|
|
201
|
+
* The name of the story.
|
|
202
|
+
*/
|
|
203
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
|
|
204
|
+
// Set the story name
|
|
205
|
+
story.name = storyData.attributes.name;
|
|
206
|
+
} else {
|
|
207
|
+
// Name is a required field. Warn user.
|
|
208
|
+
console.warn('Warning: The name attribute is missing from tw-storydata!');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* ifid: (string) Required.
|
|
213
|
+
* An IFID is a sequence of between 8 and 63 characters,
|
|
214
|
+
* each of which shall be a digit, a capital letter or a
|
|
215
|
+
* hyphen that uniquely identify a story (see Treaty of Babel).
|
|
216
|
+
*/
|
|
217
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'ifid')) {
|
|
218
|
+
// Update story IFID.
|
|
219
|
+
story.IFID = storyData.attributes.ifid;
|
|
220
|
+
} else {
|
|
221
|
+
// Name is a required filed. Warn user.
|
|
222
|
+
console.warn('Warning: The ifid attribute is missing from tw-storydata!');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check if the IFID has valid formatting.
|
|
226
|
+
if (story.IFID.match(/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/) === null) {
|
|
227
|
+
// IFID is not valid.
|
|
228
|
+
console.warn('Warning: The IFID is not in valid UUIDv4 formatting on tw-storydata!');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* creator: (string) Optional.
|
|
233
|
+
* The name of program used to create the file.
|
|
234
|
+
*/
|
|
235
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator')) {
|
|
236
|
+
// Update story creator
|
|
237
|
+
story.creator = storyData.attributes.creator;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* creator-version: (string) Optional.
|
|
242
|
+
* The version of program used to create the file.
|
|
243
|
+
*/
|
|
244
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'creator-version')) {
|
|
245
|
+
// Update story creator version
|
|
246
|
+
story.creatorVersion = storyData.attributes['creator-version'];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* format: (string) Optional.
|
|
251
|
+
* The story format used when publishing file.
|
|
252
|
+
*/
|
|
253
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format')) {
|
|
254
|
+
// Update story format
|
|
255
|
+
story.format = storyData.attributes.format;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* format-version: (string) Optional.
|
|
260
|
+
* The version of story format used when publishing file.
|
|
261
|
+
*/
|
|
262
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'format-version')) {
|
|
263
|
+
// Update story format version
|
|
264
|
+
story.formatVersion = storyData.attributes['format-version'];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* startnode: (string) Optional.
|
|
269
|
+
* The PID of the starting passage.
|
|
270
|
+
*/
|
|
271
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'startnode')) {
|
|
272
|
+
// Update start node
|
|
273
|
+
startNode = storyData.attributes.startnode;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* zoom: (string) Optional.
|
|
278
|
+
* Zoom level between 0.25 (25%) and 4.0 (400%).
|
|
279
|
+
*/
|
|
280
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'zoom')) {
|
|
281
|
+
// Convert to Number and save
|
|
282
|
+
story.zoom = Number(storyData.attributes.zoom);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* options: (string) Optional.
|
|
287
|
+
* String of comma-separated key-value pairs for story.
|
|
288
|
+
* Each pair is separated by a comma.
|
|
289
|
+
*/
|
|
290
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'options')) {
|
|
291
|
+
// Update metadata with options
|
|
292
|
+
const options = storyData.attributes.options;
|
|
293
|
+
|
|
294
|
+
// Split by comma and parse each key-value pair
|
|
295
|
+
if (options.length > 0) {
|
|
296
|
+
const pairs = options.split(',');
|
|
297
|
+
pairs.forEach(pair => {
|
|
298
|
+
const [key, value] = pair.split(':');
|
|
299
|
+
if (key && value) {
|
|
300
|
+
story.metadata[key.trim()] = value.trim();
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* hidden: (string) Optional.
|
|
308
|
+
* String of passage names that should not be included in the output.
|
|
309
|
+
*/
|
|
310
|
+
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'hidden')) {
|
|
311
|
+
// Update metadata with hidden passages
|
|
312
|
+
story.metadata.hidden = storyData.attributes.hidden;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Parse tag colors from style elements
|
|
316
|
+
const styleElements = dom.getElementsByTagName('style');
|
|
317
|
+
styleElements.forEach(styleElement => {
|
|
318
|
+
const styleContent = styleElement.innerHTML || styleElement.rawText || '';
|
|
319
|
+
|
|
320
|
+
// Look for tag color definitions
|
|
321
|
+
const tagColorRegex = /tw-story-tag-(.+?)\s*{\s*color:\s*(.+?)\s*}/g;
|
|
322
|
+
let match;
|
|
323
|
+
|
|
324
|
+
while ((match = tagColorRegex.exec(styleContent)) !== null) {
|
|
325
|
+
const tagName = match[1];
|
|
326
|
+
const color = match[2];
|
|
327
|
+
story.tagColors[tagName] = color;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Pull out the `<tw-passagedata>` elements.
|
|
332
|
+
const storyPassages = dom.getElementsByTagName('tw-passagedata');
|
|
333
|
+
|
|
334
|
+
// Move through the passages.
|
|
335
|
+
storyPassages.forEach(passage => {
|
|
336
|
+
// Get the passage attributes.
|
|
337
|
+
const attr = passage.attributes;
|
|
338
|
+
// Get the passage text and decode HTML entities.
|
|
339
|
+
const text = decode(passage.rawText);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* name: (string) Required.
|
|
343
|
+
* The name of the passage.
|
|
344
|
+
*/
|
|
345
|
+
if (!Object.prototype.hasOwnProperty.call(attr, 'name')) {
|
|
346
|
+
// Name is required! Warn user and skip passage.
|
|
347
|
+
console.warn('Warning: Cannot parse passage data without name!');
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Get passage name.
|
|
352
|
+
const name = attr.name;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* pid: (string) Required.
|
|
356
|
+
* The Passage ID (PID).
|
|
357
|
+
*/
|
|
358
|
+
if (!Object.prototype.hasOwnProperty.call(attr, 'pid')) {
|
|
359
|
+
// PID is required! Throw error.
|
|
360
|
+
throw new Error('Error: Passages are required to have PID!');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* position: (string) Optional.
|
|
365
|
+
* Comma-separated X and Y coordinates of the passage within Twine 2.
|
|
366
|
+
*/
|
|
367
|
+
let position = null;
|
|
368
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'position')) {
|
|
369
|
+
position = attr.position;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* size: (string) Optional.
|
|
374
|
+
* Comma-separated width and height of the passage within Twine 2.
|
|
375
|
+
*/
|
|
376
|
+
let size = null;
|
|
377
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'size')) {
|
|
378
|
+
size = attr.size;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* tags: (string) Optional.
|
|
383
|
+
* Space-separated list of passage tags, if any.
|
|
384
|
+
*/
|
|
385
|
+
let tags = [];
|
|
386
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
|
|
387
|
+
if (attr.tags.length > 0 && attr.tags !== '""') {
|
|
388
|
+
tags = attr.tags.split(' ').filter(tag => tag !== '');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* metadata: (object) Optional.
|
|
394
|
+
* An object containing additional metadata about the passage.
|
|
395
|
+
*/
|
|
396
|
+
const metadata = {};
|
|
397
|
+
|
|
398
|
+
// Does position exist?
|
|
399
|
+
if (position !== null) {
|
|
400
|
+
metadata.position = position;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Does size exist?
|
|
404
|
+
if (size !== null) {
|
|
405
|
+
metadata.size = size;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* pid: (string) Required.
|
|
410
|
+
* The Passage ID (PID).
|
|
411
|
+
*/
|
|
412
|
+
const pid = attr.pid;
|
|
413
|
+
|
|
414
|
+
// If the PID is the start node, update the story start.
|
|
415
|
+
if (startNode === pid) {
|
|
416
|
+
story.start = name;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Add the passage.
|
|
420
|
+
story.addPassage(
|
|
421
|
+
new Passage(
|
|
422
|
+
name,
|
|
423
|
+
text,
|
|
424
|
+
tags,
|
|
425
|
+
metadata
|
|
426
|
+
)
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Return story object.
|
|
431
|
+
return story;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export { parse };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Core web build - Common parsers and functionality
|
|
2
|
+
import { parse as parseTwee } from '../Twee/parse.js';
|
|
3
|
+
import { parse as parseJSON } from '../JSON/parse.js';
|
|
4
|
+
import { parse as parseStoryFormat } from '../StoryFormat/parse.js';
|
|
5
|
+
import { parse as parseTwine2HTML } from '../Twine2HTML/parse-web.js';
|
|
6
|
+
import { compile as compileTwine2HTML } from '../Twine2HTML/compile.js';
|
|
7
|
+
import { generate as generateIFID } from '../IFID/generate.js';
|
|
8
|
+
import { Story } from '../Story.js';
|
|
9
|
+
import Passage from '../Passage.js';
|
|
10
|
+
import StoryFormat from '../StoryFormat.js';
|
|
11
|
+
|
|
12
|
+
// Core functionality - most commonly used
|
|
13
|
+
const Extwee = {
|
|
14
|
+
// Core parsers (immediately available)
|
|
15
|
+
parseTwee,
|
|
16
|
+
parseJSON,
|
|
17
|
+
parseStoryFormat,
|
|
18
|
+
parseTwine2HTML,
|
|
19
|
+
|
|
20
|
+
// Core compiler
|
|
21
|
+
compileTwine2HTML,
|
|
22
|
+
|
|
23
|
+
// Core utilities
|
|
24
|
+
generateIFID,
|
|
25
|
+
Story,
|
|
26
|
+
Passage,
|
|
27
|
+
StoryFormat,
|
|
28
|
+
|
|
29
|
+
// Version info
|
|
30
|
+
version: '2.3.3'
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Export individual functions for ES6 module usage
|
|
34
|
+
export { parseTwee, parseJSON, parseStoryFormat, parseTwine2HTML, compileTwine2HTML, generateIFID, Story, Passage, StoryFormat };
|
|
35
|
+
|
|
36
|
+
// Export default for webpack UMD build
|
|
37
|
+
export default Extwee;
|
|
38
|
+
|
|
39
|
+
// For direct ES6 module usage, also assign to global object
|
|
40
|
+
// Use globalThis for cross-environment compatibility (browser, Node.js, Web Workers)
|
|
41
|
+
const globalObject = (function() {
|
|
42
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
43
|
+
if (typeof window !== 'undefined') return window;
|
|
44
|
+
if (typeof global !== 'undefined') return global;
|
|
45
|
+
if (typeof self !== 'undefined') return self;
|
|
46
|
+
return null;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
if (globalObject) {
|
|
50
|
+
globalObject.Extwee = Extwee;
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Twine1HTML parser module
|
|
2
|
+
import { parse as parseTwine1HTML } from '../Twine1HTML/parse-web.js';
|
|
3
|
+
import { compile as compileTwine1HTML } from '../Twine1HTML/compile.js';
|
|
4
|
+
|
|
5
|
+
// Create UMD-compatible export object
|
|
6
|
+
const Extwee = {
|
|
7
|
+
parseTwine1HTML,
|
|
8
|
+
compileTwine1HTML,
|
|
9
|
+
parse: parseTwine1HTML, // For module consistency
|
|
10
|
+
compile: compileTwine1HTML // For module consistency
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Export for webpack UMD build
|
|
14
|
+
export default Extwee;
|
|
15
|
+
|
|
16
|
+
// Also export individual functions for ES6 module usage
|
|
17
|
+
export {
|
|
18
|
+
parseTwine1HTML as parse,
|
|
19
|
+
compileTwine1HTML as compile
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Add to global Extwee object for direct usage
|
|
23
|
+
const globalObject = (function() {
|
|
24
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
25
|
+
if (typeof window !== 'undefined') return window;
|
|
26
|
+
if (typeof global !== 'undefined') return global;
|
|
27
|
+
if (typeof self !== 'undefined') return self;
|
|
28
|
+
return null;
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
if (globalObject) {
|
|
32
|
+
globalObject.Extwee = globalObject.Extwee || {};
|
|
33
|
+
globalObject.Extwee.parseTwine1HTML = parseTwine1HTML;
|
|
34
|
+
globalObject.Extwee.compileTwine1HTML = compileTwine1HTML;
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Twine2ArchiveHTML parser module
|
|
2
|
+
import { parse as parseTwine2ArchiveHTML } from '../Twine2ArchiveHTML/parse-web.js';
|
|
3
|
+
import { compile as compileTwine2ArchiveHTML } from '../Twine2ArchiveHTML/compile.js';
|
|
4
|
+
|
|
5
|
+
// Create UMD-compatible export object
|
|
6
|
+
const Extwee = {
|
|
7
|
+
parseTwine2ArchiveHTML,
|
|
8
|
+
compileTwine2ArchiveHTML,
|
|
9
|
+
parse: parseTwine2ArchiveHTML, // For module consistency
|
|
10
|
+
compile: compileTwine2ArchiveHTML // For module consistency
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Export for webpack UMD build
|
|
14
|
+
export default Extwee;
|
|
15
|
+
|
|
16
|
+
// Also export individual functions for ES6 module usage
|
|
17
|
+
export {
|
|
18
|
+
parseTwine2ArchiveHTML as parse,
|
|
19
|
+
compileTwine2ArchiveHTML as compile
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Add to global Extwee object for direct usage
|
|
23
|
+
const globalObject = (function() {
|
|
24
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
25
|
+
if (typeof window !== 'undefined') return window;
|
|
26
|
+
if (typeof global !== 'undefined') return global;
|
|
27
|
+
if (typeof self !== 'undefined') return self;
|
|
28
|
+
return null;
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
if (globalObject) {
|
|
32
|
+
globalObject.Extwee = globalObject.Extwee || {};
|
|
33
|
+
globalObject.Extwee.parseTwine2ArchiveHTML = parseTwine2ArchiveHTML;
|
|
34
|
+
globalObject.Extwee.compileTwine2ArchiveHTML = compileTwine2ArchiveHTML;
|
|
35
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// TWS parser module
|
|
2
|
+
import { parse as parseTWS } from '../TWS/parse.js';
|
|
3
|
+
|
|
4
|
+
// Create UMD-compatible export object
|
|
5
|
+
const Extwee = {
|
|
6
|
+
parseTWS,
|
|
7
|
+
parse: parseTWS // For module consistency
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Export for webpack UMD build
|
|
11
|
+
export default Extwee;
|
|
12
|
+
|
|
13
|
+
// Also export individual functions for ES6 module usage
|
|
14
|
+
export {
|
|
15
|
+
parseTWS as parse
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Add to global Extwee object for direct usage
|
|
19
|
+
const globalObject = (function() {
|
|
20
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
21
|
+
if (typeof window !== 'undefined') return window;
|
|
22
|
+
if (typeof global !== 'undefined') return global;
|
|
23
|
+
if (typeof self !== 'undefined') return self;
|
|
24
|
+
return null;
|
|
25
|
+
})();
|
|
26
|
+
|
|
27
|
+
if (globalObject) {
|
|
28
|
+
globalObject.Extwee = globalObject.Extwee || {};
|
|
29
|
+
globalObject.Extwee.parseTWS = parseTWS;
|
|
30
|
+
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { statSync } from 'node:fs';
|
|
1
|
+
import {jest} from '@jest/globals';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
// Mock the fs module before importing anything that uses it
|
|
4
|
+
const mockStatSync = jest.fn();
|
|
5
|
+
jest.unstable_mockModule('node:fs', () => ({
|
|
6
|
+
statSync: mockStatSync
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
// Now import the modules that depend on the mocked module
|
|
10
|
+
const { isDirectory } = await import('../../src/CLI/isDirectory.js');
|
|
5
11
|
|
|
6
12
|
describe('isDirectory', () => {
|
|
7
13
|
afterEach(() => {
|
|
@@ -10,32 +16,32 @@ describe('isDirectory', () => {
|
|
|
10
16
|
|
|
11
17
|
it('should return true if the path is a directory', () => {
|
|
12
18
|
const mockStats = { isDirectory: jest.fn(() => true) };
|
|
13
|
-
|
|
19
|
+
mockStatSync.mockReturnValue(mockStats);
|
|
14
20
|
|
|
15
21
|
const result = isDirectory('/valid/directory/path');
|
|
16
|
-
expect(
|
|
22
|
+
expect(mockStatSync).toHaveBeenCalledWith('/valid/directory/path');
|
|
17
23
|
expect(mockStats.isDirectory).toHaveBeenCalled();
|
|
18
24
|
expect(result).toBe(true);
|
|
19
25
|
});
|
|
20
26
|
|
|
21
27
|
it('should return false if the path is not a directory', () => {
|
|
22
28
|
const mockStats = { isDirectory: jest.fn(() => false) };
|
|
23
|
-
|
|
29
|
+
mockStatSync.mockReturnValue(mockStats);
|
|
24
30
|
|
|
25
31
|
const result = isDirectory('/valid/file/path');
|
|
26
|
-
expect(
|
|
32
|
+
expect(mockStatSync).toHaveBeenCalledWith('/valid/file/path');
|
|
27
33
|
expect(mockStats.isDirectory).toHaveBeenCalled();
|
|
28
34
|
expect(result).toBe(false);
|
|
29
35
|
});
|
|
30
36
|
|
|
31
37
|
it('should return false and log an error if statSync throws an error', () => {
|
|
32
38
|
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
33
|
-
|
|
39
|
+
mockStatSync.mockImplementation(() => {
|
|
34
40
|
throw new Error('Test error');
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
const result = isDirectory('/invalid/path');
|
|
38
|
-
expect(
|
|
44
|
+
expect(mockStatSync).toHaveBeenCalledWith('/invalid/path');
|
|
39
45
|
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Test error'));
|
|
40
46
|
expect(result).toBe(false);
|
|
41
47
|
|