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,255 @@
|
|
|
1
|
+
import Passage from '../Passage.js';
|
|
2
|
+
import { Story } from '../Story.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lightweight HTML parser for web builds - specifically for Twine 1 HTML parsing
|
|
6
|
+
* This replaces node-html-parser to reduce bundle size
|
|
7
|
+
*/
|
|
8
|
+
class LightweightTwine1Parser {
|
|
9
|
+
constructor(html) {
|
|
10
|
+
this.html = html;
|
|
11
|
+
this.doc = null;
|
|
12
|
+
|
|
13
|
+
// Parse HTML using browser's native DOMParser if available, otherwise fallback
|
|
14
|
+
if (typeof DOMParser !== 'undefined') {
|
|
15
|
+
const parser = new DOMParser();
|
|
16
|
+
this.doc = parser.parseFromString(html, 'text/html');
|
|
17
|
+
} else {
|
|
18
|
+
// Fallback for environments without DOMParser
|
|
19
|
+
this.doc = this.createSimpleDOM(html);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
querySelector(selector) {
|
|
24
|
+
if (this.doc && this.doc.querySelector) {
|
|
25
|
+
return this.doc.querySelector(selector);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Simple fallback implementation
|
|
29
|
+
if (selector === '#storeArea') {
|
|
30
|
+
const match = this.html.match(/<div[^>]*id=["']storeArea["'][^>]*>/i);
|
|
31
|
+
return match ? { found: true } : null;
|
|
32
|
+
}
|
|
33
|
+
if (selector === '#store-area') {
|
|
34
|
+
const match = this.html.match(/<div[^>]*id=["']store-area["'][^>]*>/i);
|
|
35
|
+
return match ? { found: true } : null;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
querySelectorAll(selector) {
|
|
41
|
+
if (this.doc && this.doc.querySelectorAll) {
|
|
42
|
+
return Array.from(this.doc.querySelectorAll(selector));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Fallback implementation for [tiddler] elements
|
|
46
|
+
if (selector === '[tiddler]') {
|
|
47
|
+
return this.extractTiddlerElements();
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
extractTiddlerElements() {
|
|
53
|
+
const tiddlerRegex = /<div[^>]*tiddler=["']([^"']+)["'][^>]*>([\s\S]*?)<\/div>/gi;
|
|
54
|
+
const elements = [];
|
|
55
|
+
let match;
|
|
56
|
+
|
|
57
|
+
while ((match = tiddlerRegex.exec(this.html)) !== null) {
|
|
58
|
+
const elementHtml = match[0];
|
|
59
|
+
const attributes = this.parseAttributes(elementHtml);
|
|
60
|
+
const textContent = this.extractTextContent(match[2]);
|
|
61
|
+
|
|
62
|
+
elements.push({
|
|
63
|
+
attributes,
|
|
64
|
+
rawText: textContent
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return elements;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
parseAttributes(elementHtml) {
|
|
72
|
+
const attributes = {};
|
|
73
|
+
|
|
74
|
+
// Extract tiddler attribute
|
|
75
|
+
const tiddlerMatch = elementHtml.match(/tiddler=["']([^"']+)["']/i);
|
|
76
|
+
if (tiddlerMatch) {
|
|
77
|
+
attributes.tiddler = tiddlerMatch[1];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract tags attribute
|
|
81
|
+
const tagsMatch = elementHtml.match(/tags=["']([^"']*)["']/i);
|
|
82
|
+
if (tagsMatch) {
|
|
83
|
+
attributes.tags = tagsMatch[1];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Extract twine-position attribute
|
|
87
|
+
const positionMatch = elementHtml.match(/twine-position=["']([^"']+)["']/i);
|
|
88
|
+
if (positionMatch) {
|
|
89
|
+
attributes['twine-position'] = positionMatch[1];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Extract modifier attribute
|
|
93
|
+
const modifierMatch = elementHtml.match(/modifier=["']([^"']+)["']/i);
|
|
94
|
+
if (modifierMatch) {
|
|
95
|
+
attributes.modifier = modifierMatch[1];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return attributes;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
extractTextContent(html) {
|
|
102
|
+
// Remove HTML tags and decode basic entities
|
|
103
|
+
return html
|
|
104
|
+
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
|
105
|
+
.replace(/</g, '<')
|
|
106
|
+
.replace(/>/g, '>')
|
|
107
|
+
.replace(/"/g, '"')
|
|
108
|
+
.replace(/'/g, "'")
|
|
109
|
+
.replace(/&/g, '&') // This should be last
|
|
110
|
+
.trim();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
createSimpleDOM(html) {
|
|
114
|
+
// Minimal DOM-like object for fallback
|
|
115
|
+
return {
|
|
116
|
+
querySelector: (selector) => {
|
|
117
|
+
if (selector === '#storeArea' && html.includes('id="storeArea"')) {
|
|
118
|
+
return { found: true };
|
|
119
|
+
}
|
|
120
|
+
if (selector === '#store-area' && html.includes('id="store-area"')) {
|
|
121
|
+
return { found: true };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
},
|
|
125
|
+
querySelectorAll: (selector) => {
|
|
126
|
+
if (selector === '[tiddler]') {
|
|
127
|
+
return this.extractTiddlerElements();
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Web-optimized Twine 1 HTML parser with reduced dependencies
|
|
137
|
+
* Parses Twine 1 HTML into a Story object using lightweight DOM parsing
|
|
138
|
+
* @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-1-htmloutput-doc.md Twine 1 HTML Documentation}
|
|
139
|
+
* @function parse
|
|
140
|
+
* @param {string} content - Twine 1 HTML content to parse.
|
|
141
|
+
* @returns {Story} Story object
|
|
142
|
+
*/
|
|
143
|
+
function parse(content) {
|
|
144
|
+
// Create a default Story.
|
|
145
|
+
const s = new Story();
|
|
146
|
+
|
|
147
|
+
// Use lightweight parser for web builds
|
|
148
|
+
const dom = new LightweightTwine1Parser(content);
|
|
149
|
+
|
|
150
|
+
// Look for `<div id="storeArea">`.
|
|
151
|
+
let storyData = dom.querySelector('#storeArea');
|
|
152
|
+
|
|
153
|
+
// Does the `<div id="storeArea">` element exist?
|
|
154
|
+
if (storyData === null) {
|
|
155
|
+
// Look for `<div id="store-area">`.
|
|
156
|
+
storyData = dom.querySelector('#store-area');
|
|
157
|
+
// Check for null
|
|
158
|
+
if (storyData == null) {
|
|
159
|
+
// Can't find any story data.
|
|
160
|
+
throw new Error('Cannot find #storeArea or #store-area!');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Pull out the `[tiddler]` elements.
|
|
165
|
+
const storyPassages = dom.querySelectorAll('[tiddler]');
|
|
166
|
+
|
|
167
|
+
// Move through the passages.
|
|
168
|
+
for (const passage of storyPassages) {
|
|
169
|
+
// Get the passage attributes.
|
|
170
|
+
const attr = passage.attributes;
|
|
171
|
+
// Get the passage text.
|
|
172
|
+
const text = passage.rawText;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* twine-position: (string) Required.
|
|
176
|
+
* Comma-separated X and Y coordinates of the passage within Twine 1.
|
|
177
|
+
*/
|
|
178
|
+
// Set a default position.
|
|
179
|
+
let position = null;
|
|
180
|
+
// Does position exist?
|
|
181
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'twine-position')) {
|
|
182
|
+
// Update position.
|
|
183
|
+
position = attr['twine-position'];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* tiddler: (string) Required.
|
|
188
|
+
* The name of the passage.
|
|
189
|
+
*/
|
|
190
|
+
// Create a default value.
|
|
191
|
+
const name = attr.tiddler;
|
|
192
|
+
// Is this `StoryTitle`?
|
|
193
|
+
if (name === 'StoryTitle') {
|
|
194
|
+
// If StoryTitle exists, we accept the story name.
|
|
195
|
+
s.name = text;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* tags: (string) Required.
|
|
200
|
+
* Space-separated list of passages tags, if any.
|
|
201
|
+
*/
|
|
202
|
+
// Create empty tag array.
|
|
203
|
+
let tags = [];
|
|
204
|
+
// Does the tags attribute exist?
|
|
205
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'tags')) {
|
|
206
|
+
// Escape any tags
|
|
207
|
+
// (Attributes can, themselves, be empty strings.)
|
|
208
|
+
if (attr.tags.length > 0 && attr.tags !== '""') {
|
|
209
|
+
// Escape the tags.
|
|
210
|
+
tags = attr.tags;
|
|
211
|
+
// Split by spaces into an array.
|
|
212
|
+
tags = tags.split(' ');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Remove any empty strings.
|
|
216
|
+
tags = tags.filter(tag => tag !== '');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Create metadata for passage.
|
|
220
|
+
// We translate Twine 1 attribute into Twine 2 metadata.
|
|
221
|
+
const metadata = {};
|
|
222
|
+
|
|
223
|
+
// Does position exist?
|
|
224
|
+
if (position !== null) {
|
|
225
|
+
// Add the property to metadata
|
|
226
|
+
metadata.position = position;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* modifier: (string) Optional.
|
|
231
|
+
* Name of the tool that last edited the passage.
|
|
232
|
+
* Generally, for versions of Twine 1, this value will be "twee".
|
|
233
|
+
* Twee compilers may place their own name (e.g. "tweego" for Tweego).
|
|
234
|
+
*/
|
|
235
|
+
if (Object.prototype.hasOwnProperty.call(attr, 'modifier')) {
|
|
236
|
+
// In Twine 2, `creator` maps to Twine 1's `modifier`.
|
|
237
|
+
s.creator = attr.modifier;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Add the passage.
|
|
241
|
+
s.addPassage(
|
|
242
|
+
new Passage(
|
|
243
|
+
name,
|
|
244
|
+
text,
|
|
245
|
+
tags,
|
|
246
|
+
metadata
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Return story object.
|
|
252
|
+
return s;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export { parse };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { parse as parseTwine2HTML } from '../Twine2HTML/parse-web.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight HTML parser for web builds - specifically for Twine 2 Archive HTML parsing
|
|
5
|
+
* This replaces node-html-parser to reduce bundle size and uses browser DOM APIs
|
|
6
|
+
*/
|
|
7
|
+
class LightweightTwine2ArchiveParser {
|
|
8
|
+
constructor(html) {
|
|
9
|
+
this.html = html;
|
|
10
|
+
this.doc = null;
|
|
11
|
+
|
|
12
|
+
// Parse HTML using browser's native DOMParser if available, otherwise fallback
|
|
13
|
+
if (typeof DOMParser !== 'undefined') {
|
|
14
|
+
const parser = new DOMParser();
|
|
15
|
+
this.doc = parser.parseFromString(html, 'text/html');
|
|
16
|
+
} else {
|
|
17
|
+
// Fallback for environments without DOMParser
|
|
18
|
+
this.doc = this.createSimpleDOM(html);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
getElementsByTagName(tagName) {
|
|
23
|
+
if (this.doc && this.doc.getElementsByTagName) {
|
|
24
|
+
return Array.from(this.doc.getElementsByTagName(tagName));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Fallback implementation for tw-storydata elements
|
|
28
|
+
if (tagName === 'tw-storydata') {
|
|
29
|
+
return this.extractStoryDataElements();
|
|
30
|
+
}
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
extractStoryDataElements() {
|
|
35
|
+
// Match tw-storydata elements with their complete content
|
|
36
|
+
const storyDataRegex = /<tw-storydata[^>]*>[\s\S]*?<\/tw-storydata>/gi;
|
|
37
|
+
const elements = [];
|
|
38
|
+
let match;
|
|
39
|
+
|
|
40
|
+
while ((match = storyDataRegex.exec(this.html)) !== null) {
|
|
41
|
+
const outerHTML = match[0];
|
|
42
|
+
|
|
43
|
+
elements.push({
|
|
44
|
+
outerHTML: outerHTML,
|
|
45
|
+
// For compatibility with the original parser interface
|
|
46
|
+
toString: () => outerHTML
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return elements;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line no-unused-vars
|
|
54
|
+
createSimpleDOM(_htmlContent) {
|
|
55
|
+
// Minimal DOM-like object for fallback
|
|
56
|
+
return {
|
|
57
|
+
getElementsByTagName: (tagName) => {
|
|
58
|
+
if (tagName === 'tw-storydata') {
|
|
59
|
+
return this.extractStoryDataElements(this.htmlContent);
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Web-optimized Twine 2 Archive HTML parser with reduced dependencies
|
|
69
|
+
* Parse Twine 2 Archive HTML and returns an array of story objects using browser DOM APIs.
|
|
70
|
+
* @see {@link https://github.com/iftechfoundation/twine-specs/blob/master/twine-2-archive-spec.md Twine 2 Archive Specification}
|
|
71
|
+
* @function parse
|
|
72
|
+
* @param {string} content - Content to parse for Twine 2 HTML elements.
|
|
73
|
+
* @throws {TypeError} - Content is not a string!
|
|
74
|
+
* @returns {Array} Array of stories found in content.
|
|
75
|
+
* @example
|
|
76
|
+
* const content = '<tw-storydata name="Untitled" startnode="1" creator="Twine" creator-version="2.3.9" ifid="A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6" zoom="1" format="Harlowe" format-version="3.1.0" options="" hidden><style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css"></style><script role="script" id="twine-user-script" type="text/twine-javascript"></script><tw-passagedata pid="1" name="Untitled Passage" tags="" position="0,0" size="100,100"></tw-passagedata></tw-storydata>';
|
|
77
|
+
* console.log(parse(content));
|
|
78
|
+
* // => [
|
|
79
|
+
* // Story {
|
|
80
|
+
* // name: 'Untitled',
|
|
81
|
+
* // startnode: '1',
|
|
82
|
+
* // creator: 'Twine',
|
|
83
|
+
* // creatorVersion: '2.3.9',
|
|
84
|
+
* // ifid: 'A1B2C3D4-E5F6-G7H8-I9J0-K1L2M3N4O5P6',
|
|
85
|
+
* // zoom: '1',
|
|
86
|
+
* // format: 'Harlowe',
|
|
87
|
+
* // formatVersion: '3.1.0',
|
|
88
|
+
* // options: '',
|
|
89
|
+
* // hidden: '',
|
|
90
|
+
* // passages: [
|
|
91
|
+
* // Passage {
|
|
92
|
+
* // pid: '1',
|
|
93
|
+
* // name: 'Untitled Passage',
|
|
94
|
+
* // tags: '',
|
|
95
|
+
* // position: '0,0',
|
|
96
|
+
* // size: '100,100',
|
|
97
|
+
* // text: ''
|
|
98
|
+
* // }
|
|
99
|
+
* // ]
|
|
100
|
+
* // }
|
|
101
|
+
* // ]
|
|
102
|
+
*/
|
|
103
|
+
function parse(content) {
|
|
104
|
+
// Can only parse string values.
|
|
105
|
+
if (typeof content !== 'string') {
|
|
106
|
+
throw new TypeError('Content is not a string!');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Use lightweight parser for web builds
|
|
110
|
+
const dom = new LightweightTwine2ArchiveParser(content);
|
|
111
|
+
|
|
112
|
+
// Array of possible story elements.
|
|
113
|
+
const outputArray = [];
|
|
114
|
+
|
|
115
|
+
// Pull out the `<tw-storydata>` element.
|
|
116
|
+
const storyDataElements = dom.getElementsByTagName('tw-storydata');
|
|
117
|
+
|
|
118
|
+
// Did we find any elements?
|
|
119
|
+
if (storyDataElements.length === 0) {
|
|
120
|
+
// Produce a warning if no Twine 2 HTML content is found.
|
|
121
|
+
console.warn('Warning: No Twine 2 HTML content found!');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Iterate through all `<tw-storydata>` elements.
|
|
125
|
+
for (const storyElement of storyDataElements) {
|
|
126
|
+
// Convert element back into HTML text and parse using web-optimized parser.
|
|
127
|
+
outputArray.push(parseTwine2HTML(storyElement.outerHTML));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Return array.
|
|
131
|
+
return outputArray;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export { parse };
|