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.
Files changed (55) hide show
  1. package/build/extwee.core.min.js +1 -0
  2. package/build/extwee.twine1html.min.js +1 -0
  3. package/build/extwee.twine2archive.min.js +1 -0
  4. package/build/extwee.tws.min.js +1 -0
  5. package/build/test-modular.html +126 -0
  6. package/docs/build/extwee.core.min.js +1 -0
  7. package/docs/build/extwee.twine1html.min.js +1 -0
  8. package/docs/build/extwee.twine2archive.min.js +1 -0
  9. package/docs/build/extwee.tws.min.js +1 -0
  10. package/docs/demos/compiler/extwee.core.min.js +1 -0
  11. package/docs/demos/compiler/index.css +105 -0
  12. package/docs/demos/compiler/index.html +359 -0
  13. package/eslint.config.js +4 -1
  14. package/package.json +25 -22
  15. package/src/IFID/generate.js +2 -2
  16. package/src/Story.js +1 -1
  17. package/src/Twine1HTML/parse-web.js +255 -0
  18. package/src/Twine2ArchiveHTML/parse-web.js +134 -0
  19. package/src/Twine2HTML/parse-web.js +434 -0
  20. package/src/Web/web-core.js +51 -0
  21. package/src/Web/web-twine1html.js +35 -0
  22. package/src/Web/web-twine2archive.js +35 -0
  23. package/src/Web/web-tws.js +30 -0
  24. package/test/Config/Config.test.js +1 -1
  25. package/test/Config/isDirectory.test.js +15 -9
  26. package/test/Config/isFile.test.js +14 -11
  27. package/test/Config/loadStoryFormat.test.js +49 -33
  28. package/test/Config/readDirectories.test.js +25 -15
  29. package/test/Objects/Story.test.js +1 -0
  30. package/test/StoryFormat/StoryFormat.Parse.test.js +1 -0
  31. package/test/Twine1HTML/Twine1HTML.Parse.Web.test.js +484 -0
  32. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.Web.test.js +293 -0
  33. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.test.js +1 -0
  34. package/test/Twine2HTML/Twine2HTML.Parse.Web.test.js +329 -0
  35. package/test/Twine2HTML/Twine2HTML.Parse.test.js +1 -0
  36. package/test/Web/web-core-coverage.test.js +175 -0
  37. package/test/Web/web-core-global.test.js +93 -0
  38. package/test/Web/web-core.test.js +156 -0
  39. package/test/Web/window.Extwee.test.js +25 -13
  40. package/types/src/Story.d.ts +1 -1
  41. package/types/src/Twine1HTML/parse-web.d.ts +10 -0
  42. package/types/src/Twine2ArchiveHTML/parse-web.d.ts +37 -0
  43. package/types/src/Twine2HTML/parse-web.d.ts +21 -0
  44. package/types/src/Web/html-entities-lite.d.ts +12 -0
  45. package/types/src/Web/semver-lite.d.ts +10 -0
  46. package/types/src/Web/uuid-lite.d.ts +6 -0
  47. package/types/src/Web/web-core.d.ts +23 -0
  48. package/types/src/Web/web-index.d.ts +1 -0
  49. package/types/src/Web/web-twine1html.d.ts +10 -0
  50. package/types/src/Web/web-twine2archive.d.ts +10 -0
  51. package/types/src/Web/web-tws.d.ts +7 -0
  52. package/webpack.config.js +23 -2
  53. package/build/extwee.web.min.js +0 -2
  54. package/build/extwee.web.min.js.LICENSE.txt +0 -1
  55. 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(/&lt;/g, '<')
126
+ .replace(/&gt;/g, '>')
127
+ .replace(/&quot;/g, '"')
128
+ .replace(/&#39;/g, "'")
129
+ .replace(/&amp;/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,4 +1,4 @@
1
- import {reader as ConfigReader} from '../../src/Config/reader.js';
1
+ import { reader as ConfigReader } from '../../src/Config/reader.js';
2
2
  import {parser as ConfigParser} from '../../src/Config/parser.js';
3
3
 
4
4
  describe('src/Config/reader.js', () => {
@@ -1,7 +1,13 @@
1
- import { isDirectory } from '../../src/CLI/isDirectory';
2
- import { statSync } from 'node:fs';
1
+ import {jest} from '@jest/globals';
3
2
 
4
- jest.mock('node:fs');
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
- statSync.mockReturnValue(mockStats);
19
+ mockStatSync.mockReturnValue(mockStats);
14
20
 
15
21
  const result = isDirectory('/valid/directory/path');
16
- expect(statSync).toHaveBeenCalledWith('/valid/directory/path');
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
- statSync.mockReturnValue(mockStats);
29
+ mockStatSync.mockReturnValue(mockStats);
24
30
 
25
31
  const result = isDirectory('/valid/file/path');
26
- expect(statSync).toHaveBeenCalledWith('/valid/file/path');
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
- statSync.mockImplementation(() => {
39
+ mockStatSync.mockImplementation(() => {
34
40
  throw new Error('Test error');
35
41
  });
36
42
 
37
43
  const result = isDirectory('/invalid/path');
38
- expect(statSync).toHaveBeenCalledWith('/invalid/path');
44
+ expect(mockStatSync).toHaveBeenCalledWith('/invalid/path');
39
45
  expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Error: Test error'));
40
46
  expect(result).toBe(false);
41
47