extwee 2.3.6 → 2.3.8
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 -1
- package/docs/build/extwee.twine1html.min.js +1 -1
- package/docs/build/extwee.twine2archive.min.js +1 -1
- package/docs/build/extwee.tws.min.js +1 -1
- package/docs/demos/decompile/extwee.core.min.js +1 -0
- package/docs/demos/decompile/index.css +584 -0
- package/docs/demos/decompile/index.html +468 -0
- package/package.json +11 -13
- package/src/IFID/generate-web.js +20 -0
- package/src/IFID/generate.js +5 -4
- package/src/Story.js +1 -1
- package/src/Twine1HTML/parse-web.js +47 -8
- package/src/Twine2ArchiveHTML/parse-web.js +33 -7
- package/src/Twine2HTML/parse-web.js +105 -17
- package/test/Twine1HTML/Twine1HTML.Parse.Web.test.js +1 -1
- package/test/Twine2HTML/Twine2HTML.Parse.Web.test.js +2 -2
- package/types/src/IFID/generate-web.d.ts +14 -0
- package/types/src/IFID/generate.d.ts +2 -2
- package/types/src/Story.d.ts +1 -1
- package/webpack.config.js +14 -0
|
@@ -8,23 +8,48 @@ class LightweightTwine2ArchiveParser {
|
|
|
8
8
|
constructor(html) {
|
|
9
9
|
this.html = html;
|
|
10
10
|
this.doc = null;
|
|
11
|
+
this.usingDOMParser = false;
|
|
11
12
|
|
|
12
13
|
// Parse HTML using browser's native DOMParser if available, otherwise fallback
|
|
13
14
|
if (typeof DOMParser !== 'undefined') {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
try {
|
|
16
|
+
const parser = new DOMParser();
|
|
17
|
+
this.doc = parser.parseFromString(html, 'text/html');
|
|
18
|
+
this.usingDOMParser = true;
|
|
19
|
+
|
|
20
|
+
// Check if parsing was successful (DOMParser doesn't throw errors, but creates error documents)
|
|
21
|
+
const parserError = this.doc.querySelector('parsererror');
|
|
22
|
+
if (parserError) {
|
|
23
|
+
console.warn('DOMParser encountered an error, falling back to regex parsing:', parserError.textContent);
|
|
24
|
+
this.doc = this.createSimpleDOM(html);
|
|
25
|
+
this.usingDOMParser = false;
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn('DOMParser failed, falling back to regex parsing:', error.message);
|
|
29
|
+
this.doc = this.createSimpleDOM(html);
|
|
30
|
+
this.usingDOMParser = false;
|
|
31
|
+
}
|
|
16
32
|
} else {
|
|
17
33
|
// Fallback for environments without DOMParser
|
|
18
34
|
this.doc = this.createSimpleDOM(html);
|
|
35
|
+
this.usingDOMParser = false;
|
|
19
36
|
}
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
getElementsByTagName(tagName) {
|
|
23
|
-
if (this.doc && this.doc.getElementsByTagName) {
|
|
24
|
-
|
|
40
|
+
if (this.usingDOMParser && this.doc && this.doc.getElementsByTagName) {
|
|
41
|
+
// Use native DOM methods when DOMParser is available and working
|
|
42
|
+
const elements = Array.from(this.doc.getElementsByTagName(tagName));
|
|
43
|
+
|
|
44
|
+
// Convert DOM elements to expected format for compatibility
|
|
45
|
+
return elements.map(element => ({
|
|
46
|
+
outerHTML: element.outerHTML,
|
|
47
|
+
// For compatibility with the original parser interface
|
|
48
|
+
toString: () => element.outerHTML
|
|
49
|
+
}));
|
|
25
50
|
}
|
|
26
51
|
|
|
27
|
-
// Fallback implementation for
|
|
52
|
+
// Fallback implementation for environments without DOMParser
|
|
28
53
|
if (tagName === 'tw-storydata') {
|
|
29
54
|
return this.extractStoryDataElements();
|
|
30
55
|
}
|
|
@@ -52,11 +77,12 @@ class LightweightTwine2ArchiveParser {
|
|
|
52
77
|
|
|
53
78
|
// eslint-disable-next-line no-unused-vars
|
|
54
79
|
createSimpleDOM(_htmlContent) {
|
|
55
|
-
// Minimal DOM-like object for fallback
|
|
80
|
+
// Minimal DOM-like object for fallback when DOMParser is not available
|
|
81
|
+
// This should only be used in very limited environments
|
|
56
82
|
return {
|
|
57
83
|
getElementsByTagName: (tagName) => {
|
|
58
84
|
if (tagName === 'tw-storydata') {
|
|
59
|
-
return this.extractStoryDataElements(
|
|
85
|
+
return this.extractStoryDataElements();
|
|
60
86
|
}
|
|
61
87
|
return [];
|
|
62
88
|
}
|
|
@@ -10,23 +10,61 @@ class LightweightTwine2Parser {
|
|
|
10
10
|
constructor(html) {
|
|
11
11
|
this.html = html;
|
|
12
12
|
this.doc = null;
|
|
13
|
+
this.usingDOMParser = false;
|
|
13
14
|
|
|
14
15
|
// Parse HTML using browser's native DOMParser if available, otherwise fallback
|
|
15
16
|
if (typeof DOMParser !== 'undefined') {
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
try {
|
|
18
|
+
const parser = new DOMParser();
|
|
19
|
+
this.doc = parser.parseFromString(html, 'text/html');
|
|
20
|
+
this.usingDOMParser = true;
|
|
21
|
+
|
|
22
|
+
// Check if parsing was successful (DOMParser doesn't throw errors, but creates error documents)
|
|
23
|
+
const parserError = this.doc.querySelector('parsererror');
|
|
24
|
+
if (parserError) {
|
|
25
|
+
console.warn('DOMParser encountered an error, falling back to regex parsing:', parserError.textContent);
|
|
26
|
+
this.doc = this.createSimpleDOM(html);
|
|
27
|
+
this.usingDOMParser = false;
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.warn('DOMParser failed, falling back to regex parsing:', error.message);
|
|
31
|
+
this.doc = this.createSimpleDOM(html);
|
|
32
|
+
this.usingDOMParser = false;
|
|
33
|
+
}
|
|
18
34
|
} else {
|
|
19
35
|
// Fallback for environments without DOMParser
|
|
20
36
|
this.doc = this.createSimpleDOM(html);
|
|
37
|
+
this.usingDOMParser = false;
|
|
21
38
|
}
|
|
22
39
|
}
|
|
23
40
|
|
|
24
41
|
getElementsByTagName(tagName) {
|
|
25
|
-
if (this.doc && this.doc.getElementsByTagName) {
|
|
26
|
-
|
|
42
|
+
if (this.usingDOMParser && this.doc && this.doc.getElementsByTagName) {
|
|
43
|
+
// Use native DOM methods when DOMParser is available and working
|
|
44
|
+
const elements = Array.from(this.doc.getElementsByTagName(tagName));
|
|
45
|
+
|
|
46
|
+
// Convert DOM elements to our expected format
|
|
47
|
+
return elements.map(element => {
|
|
48
|
+
const attributes = {};
|
|
49
|
+
|
|
50
|
+
// Extract attributes using DOM methods - much more reliable than regex
|
|
51
|
+
if (element.attributes) {
|
|
52
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
53
|
+
const attr = element.attributes[i];
|
|
54
|
+
// DOM automatically handles HTML entity decoding
|
|
55
|
+
attributes[attr.name] = attr.value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
attributes,
|
|
61
|
+
innerHTML: element.innerHTML || '',
|
|
62
|
+
rawText: element.textContent || element.innerText || ''
|
|
63
|
+
};
|
|
64
|
+
});
|
|
27
65
|
}
|
|
28
66
|
|
|
29
|
-
// Fallback implementation
|
|
67
|
+
// Fallback implementation for environments without DOMParser or when DOM parsing fails
|
|
30
68
|
if (tagName === 'tw-storydata') {
|
|
31
69
|
return this.extractStoryDataElements();
|
|
32
70
|
}
|
|
@@ -107,12 +145,49 @@ class LightweightTwine2Parser {
|
|
|
107
145
|
|
|
108
146
|
const openingTag = openingTagMatch[0];
|
|
109
147
|
|
|
110
|
-
//
|
|
111
|
-
|
|
148
|
+
// Enhanced attribute parsing to handle multiple formats:
|
|
149
|
+
// 1. Quoted attributes: name="value" or name='value'
|
|
150
|
+
// 2. Unquoted attributes: name=value
|
|
151
|
+
// 3. Boolean attributes: hidden, selected, etc.
|
|
152
|
+
|
|
153
|
+
// First, handle quoted attributes (including those with escaped quotes)
|
|
154
|
+
const quotedAttributeRegex = /(\w+(?:-\w+)*)=["']([^"']*)["']/g;
|
|
112
155
|
let match;
|
|
113
156
|
|
|
114
|
-
while ((match =
|
|
115
|
-
|
|
157
|
+
while ((match = quotedAttributeRegex.exec(openingTag)) !== null) {
|
|
158
|
+
// Decode basic HTML entities in attribute values
|
|
159
|
+
const value = match[2]
|
|
160
|
+
.replace(/"/g, '"')
|
|
161
|
+
.replace(/'/g, "'")
|
|
162
|
+
.replace(/</g, '<')
|
|
163
|
+
.replace(/>/g, '>')
|
|
164
|
+
.replace(/&/g, '&'); // This should be last
|
|
165
|
+
|
|
166
|
+
attributes[match[1]] = value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Handle unquoted attributes (but avoid matching already processed quoted ones)
|
|
170
|
+
let tagWithoutQuoted = openingTag;
|
|
171
|
+
const quotedMatches = [...openingTag.matchAll(quotedAttributeRegex)];
|
|
172
|
+
quotedMatches.forEach(quotedMatch => {
|
|
173
|
+
tagWithoutQuoted = tagWithoutQuoted.replace(quotedMatch[0], '');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const unquotedAttributeRegex = /(\w+(?:-\w+)*)=([^\s>]+)/g;
|
|
177
|
+
while ((match = unquotedAttributeRegex.exec(tagWithoutQuoted)) !== null) {
|
|
178
|
+
if (!attributes[match[1]]) { // Don't overwrite quoted attributes
|
|
179
|
+
attributes[match[1]] = match[2];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Handle boolean attributes (attributes without values)
|
|
184
|
+
const booleanAttributeRegex = /\s(\w+(?:-\w+)*)(?=\s|>|$)/g;
|
|
185
|
+
while ((match = booleanAttributeRegex.exec(openingTag)) !== null) {
|
|
186
|
+
const attrName = match[1];
|
|
187
|
+
// Only add if it's not already parsed as a key=value attribute and not the tag name
|
|
188
|
+
if (!attributes[attrName] && !openingTag.includes(`${attrName}=`) && attrName !== openingTag.match(/<(\w+)/)?.[1]) {
|
|
189
|
+
attributes[attrName] = true;
|
|
190
|
+
}
|
|
116
191
|
}
|
|
117
192
|
|
|
118
193
|
return attributes;
|
|
@@ -132,9 +207,11 @@ class LightweightTwine2Parser {
|
|
|
132
207
|
|
|
133
208
|
// eslint-disable-next-line no-unused-vars
|
|
134
209
|
createSimpleDOM(_html) {
|
|
135
|
-
// Minimal DOM-like object for fallback
|
|
210
|
+
// Minimal DOM-like object for fallback when DOMParser is not available
|
|
211
|
+
// This should only be used in very limited environments (like some older Node.js versions)
|
|
136
212
|
return {
|
|
137
213
|
getElementsByTagName: (tagName) => {
|
|
214
|
+
// Use regex-based extraction as fallback
|
|
138
215
|
if (tagName === 'tw-storydata') {
|
|
139
216
|
return this.extractStoryDataElements();
|
|
140
217
|
}
|
|
@@ -201,8 +278,14 @@ function parse(content) {
|
|
|
201
278
|
* The name of the story.
|
|
202
279
|
*/
|
|
203
280
|
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'name')) {
|
|
204
|
-
//
|
|
205
|
-
|
|
281
|
+
// Validate that the name is a non-empty string before setting
|
|
282
|
+
const nameValue = storyData.attributes.name;
|
|
283
|
+
if (typeof nameValue === 'string' && nameValue.trim().length > 0) {
|
|
284
|
+
story.name = nameValue.trim();
|
|
285
|
+
} else {
|
|
286
|
+
console.warn('Warning: The name attribute is empty or invalid on tw-storydata!');
|
|
287
|
+
// Keep the default name from Story constructor
|
|
288
|
+
}
|
|
206
289
|
} else {
|
|
207
290
|
// Name is a required field. Warn user.
|
|
208
291
|
console.warn('Warning: The name attribute is missing from tw-storydata!');
|
|
@@ -215,15 +298,20 @@ function parse(content) {
|
|
|
215
298
|
* hyphen that uniquely identify a story (see Treaty of Babel).
|
|
216
299
|
*/
|
|
217
300
|
if (Object.prototype.hasOwnProperty.call(storyData.attributes, 'ifid')) {
|
|
218
|
-
//
|
|
219
|
-
|
|
301
|
+
// Validate that the IFID is a non-empty string before setting
|
|
302
|
+
const ifidValue = storyData.attributes.ifid;
|
|
303
|
+
if (typeof ifidValue === 'string' && ifidValue.trim().length > 0) {
|
|
304
|
+
story.IFID = ifidValue.trim();
|
|
305
|
+
} else {
|
|
306
|
+
console.warn('Warning: The ifid attribute is empty or invalid on tw-storydata!');
|
|
307
|
+
}
|
|
220
308
|
} else {
|
|
221
|
-
//
|
|
309
|
+
// IFID is a required field. Warn user.
|
|
222
310
|
console.warn('Warning: The ifid attribute is missing from tw-storydata!');
|
|
223
311
|
}
|
|
224
312
|
|
|
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) {
|
|
313
|
+
// Check if the IFID has valid formatting (only if IFID was set).
|
|
314
|
+
if (story.IFID && 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
315
|
// IFID is not valid.
|
|
228
316
|
console.warn('Warning: The IFID is not in valid UUIDv4 formatting on tw-storydata!');
|
|
229
317
|
}
|
|
@@ -312,7 +312,7 @@ describe('Twine1HTML', function () {
|
|
|
312
312
|
const el = '<div id="storeArea"><div tiddler="Test" modifier="twee">Content</div></div>';
|
|
313
313
|
const s = parseTwine1HTMLWeb(el);
|
|
314
314
|
expect(s.size()).toBe(1);
|
|
315
|
-
expect(s.creator).toBe('
|
|
315
|
+
expect(s.creator).toBe('extwee');
|
|
316
316
|
} finally {
|
|
317
317
|
global.DOMParser = originalDOMParser;
|
|
318
318
|
}
|
|
@@ -128,7 +128,7 @@ describe('Twine2HTML', function () {
|
|
|
128
128
|
|
|
129
129
|
const content = '<tw-storydata name="Test Story"><tw-passagedata pid="1" name="Start">Content</tw-passagedata></tw-storydata>';
|
|
130
130
|
parseTwine2HTMLWeb(content);
|
|
131
|
-
expect(warningMessage).toBe('Warning: The
|
|
131
|
+
expect(warningMessage).toBe('Warning: The ifid attribute is missing from tw-storydata!');
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
it('Should warn for malformed IFID', function () {
|
|
@@ -201,7 +201,7 @@ describe('Twine2HTML', function () {
|
|
|
201
201
|
});
|
|
202
202
|
|
|
203
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
|
|
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
205
|
|
|
206
206
|
const story = parseTwine2HTMLWeb(content);
|
|
207
207
|
const passage = story.getPassageByName('Start');
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates an Interactive Fiction Identification (IFID) based the Treaty of Babel.
|
|
3
|
+
*
|
|
4
|
+
* For Twine works, the IFID is a UUID (v4) in uppercase.
|
|
5
|
+
* @see Treaty of Babel ({@link https://babel.ifarchive.org/babel_rev11.html#the-ifid-for-an-html-story-file})
|
|
6
|
+
* @function generate
|
|
7
|
+
* @description Generates a new IFID using UUIDv4 (RFC 4122). Browser version using Web Crypto API.
|
|
8
|
+
* @returns {string} IFID - A UUIDv4 string in uppercase format
|
|
9
|
+
* @example
|
|
10
|
+
* const ifid = generate();
|
|
11
|
+
* console.log(ifid);
|
|
12
|
+
* // => 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6'
|
|
13
|
+
*/
|
|
14
|
+
export function generate(): string;
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* For Twine works, the IFID is a UUID (v4) in uppercase.
|
|
5
5
|
* @see Treaty of Babel ({@link https://babel.ifarchive.org/babel_rev11.html#the-ifid-for-an-html-story-file})
|
|
6
6
|
* @function generate
|
|
7
|
-
* @description Generates a new IFID.
|
|
8
|
-
* @returns {string} IFID
|
|
7
|
+
* @description Generates a new IFID using UUIDv4 (RFC 4122).
|
|
8
|
+
* @returns {string} IFID - A UUIDv4 string in uppercase format
|
|
9
9
|
* @example
|
|
10
10
|
* const ifid = generate();
|
|
11
11
|
* console.log(ifid);
|
package/types/src/Story.d.ts
CHANGED
package/webpack.config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import webpack from 'webpack';
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
mode: 'production',
|
|
@@ -20,6 +21,19 @@ export default {
|
|
|
20
21
|
},
|
|
21
22
|
globalObject: 'this'
|
|
22
23
|
},
|
|
24
|
+
plugins: [
|
|
25
|
+
// Replace Node.js IFID generator with browser version for web builds
|
|
26
|
+
new webpack.NormalModuleReplacementPlugin(
|
|
27
|
+
/src[\\/]IFID[\\/]generate\.js$/,
|
|
28
|
+
'./generate-web.js'
|
|
29
|
+
)
|
|
30
|
+
],
|
|
31
|
+
resolve: {
|
|
32
|
+
fallback: {
|
|
33
|
+
// Exclude Node.js core modules from browser builds
|
|
34
|
+
'crypto': false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
23
37
|
optimization: {
|
|
24
38
|
usedExports: true,
|
|
25
39
|
sideEffects: false,
|