extwee 2.3.5 → 2.3.7

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.
@@ -0,0 +1,462 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Extwee Decompiler Demo</title>
7
+ <link rel="stylesheet" href="index.css">
8
+ <script src="./extwee.core.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <div class="container">
12
+ <header>
13
+ <h1>Extwee Decompiler Demo</h1>
14
+ <p>Upload a Twine 2 HTML file to analyze its structure and extract statistics.</p>
15
+ </header>
16
+
17
+ <main>
18
+ <section class="upload-section">
19
+ <div class="file-input-container">
20
+ <input type="file" id="fileInput" accept=".html,.htm" />
21
+ <label for="fileInput" class="file-input-label">
22
+ <span class="file-input-text">Choose Twine 2 HTML File</span>
23
+ <span class="file-input-icon">📁</span>
24
+ </label>
25
+ </div>
26
+ <div class="file-info" id="fileInfo"></div>
27
+ </section>
28
+
29
+ <section class="results-section" id="resultsSection" style="display: none;">
30
+ <div class="tabs">
31
+ <button class="tab-button active" data-tab="overview">Overview</button>
32
+ <button class="tab-button" data-tab="passages">Passages</button>
33
+ <button class="tab-button" data-tab="tags">Tags</button>
34
+ <button class="tab-button" data-tab="metadata">Metadata</button>
35
+ </div>
36
+
37
+ <div class="tab-content active" id="overview">
38
+ <div class="stats-grid">
39
+ <div class="stat-card">
40
+ <div class="stat-number" id="passageCount">0</div>
41
+ <div class="stat-label">Passages</div>
42
+ </div>
43
+ <div class="stat-card">
44
+ <div class="stat-number" id="wordCount">0</div>
45
+ <div class="stat-label">Total Words</div>
46
+ </div>
47
+ <div class="stat-card">
48
+ <div class="stat-number" id="characterCount">0</div>
49
+ <div class="stat-label">Characters</div>
50
+ </div>
51
+ <div class="stat-card">
52
+ <div class="stat-number" id="tagCount">0</div>
53
+ <div class="stat-label">Unique Tags</div>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="story-info">
58
+ <h3>Story Information</h3>
59
+ <div class="info-grid">
60
+ <div class="info-item">
61
+ <label>Title:</label>
62
+ <span id="storyTitle">-</span>
63
+ </div>
64
+ <div class="info-item">
65
+ <label>IFID:</label>
66
+ <span id="storyIFID">-</span>
67
+ </div>
68
+ <div class="info-item">
69
+ <label>Format:</label>
70
+ <span id="storyFormat">-</span>
71
+ </div>
72
+ <div class="info-item">
73
+ <label>Format Version:</label>
74
+ <span id="storyFormatVersion">-</span>
75
+ </div>
76
+ <div class="info-item">
77
+ <label>Start Passage:</label>
78
+ <span id="startPassage">-</span>
79
+ </div>
80
+ <div class="info-item">
81
+ <label>Creator:</label>
82
+ <span id="storyCreator">-</span>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="tab-content" id="passages">
89
+ <div class="passages-header">
90
+ <h3>Passages (<span id="passageListCount">0</span>)</h3>
91
+ <div class="search-container">
92
+ <input type="text" id="passageSearch" placeholder="Search passages...">
93
+ </div>
94
+ </div>
95
+ <div class="passages-list" id="passagesList"></div>
96
+ </div>
97
+
98
+ <div class="tab-content" id="tags">
99
+ <div class="tags-header">
100
+ <h3>Tags Overview</h3>
101
+ </div>
102
+ <div class="tags-container" id="tagsContainer"></div>
103
+ </div>
104
+
105
+ <div class="tab-content" id="metadata">
106
+ <div class="metadata-container">
107
+ <h3>Technical Details</h3>
108
+ <div class="metadata-content" id="metadataContent"></div>
109
+ </div>
110
+ </div>
111
+ </section>
112
+
113
+ <section class="error-section" id="errorSection" style="display: none;">
114
+ <div class="error-message">
115
+ <h3>Error</h3>
116
+ <p id="errorText"></p>
117
+ </div>
118
+ </section>
119
+ </main>
120
+ </div>
121
+
122
+ <script>
123
+ class TwineDecompiler {
124
+ constructor() {
125
+ this.story = null;
126
+ this.initializeEventListeners();
127
+ }
128
+
129
+ initializeEventListeners() {
130
+ const fileInput = document.getElementById('fileInput');
131
+ const tabButtons = document.querySelectorAll('.tab-button');
132
+ const passageSearch = document.getElementById('passageSearch');
133
+
134
+ fileInput.addEventListener('change', (e) => this.handleFileUpload(e));
135
+
136
+ tabButtons.forEach(button => {
137
+ button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
138
+ });
139
+
140
+ passageSearch.addEventListener('input', (e) => this.filterPassages(e.target.value));
141
+ }
142
+
143
+ async handleFileUpload(event) {
144
+ const file = event.target.files[0];
145
+ if (!file) return;
146
+
147
+ this.showFileInfo(file);
148
+ this.hideError();
149
+ this.hideResults();
150
+
151
+ try {
152
+ const content = await this.readFile(file);
153
+
154
+ // First, let's validate that this looks like Twine 2 HTML
155
+ if (!content.includes('<tw-storydata')) {
156
+ throw new Error('Not Twine 2 HTML content');
157
+ }
158
+
159
+ this.story = Extwee.parseTwine2HTML(content);
160
+ this.displayResults();
161
+ } catch (error) {
162
+ console.error('Parsing error:', error);
163
+
164
+ // Provide more helpful error messages for common issues
165
+ let errorMessage = error.message;
166
+ if (error.message.includes('Story name must be a string')) {
167
+ errorMessage = 'The Twine 2 HTML file has an invalid story name attribute. This usually means the file is corrupted or not a standard Twine 2 HTML export.';
168
+ } else if (error.message.includes('Not Twine 2 HTML content')) {
169
+ errorMessage = 'This file does not appear to be a valid Twine 2 HTML export. Please make sure you uploaded a file exported from Twine 2 using "Publish to File".';
170
+ } else if (error.message.includes('Content is not a string')) {
171
+ errorMessage = 'Unable to read the file content. Please make sure the file is not corrupted and is a text-based HTML file.';
172
+ } else if (error.message.includes('Passages are required to have PID')) {
173
+ errorMessage = 'The Twine 2 HTML file contains passages without proper IDs. This suggests the file may be corrupted or manually edited.';
174
+ }
175
+
176
+ this.showError(errorMessage);
177
+ }
178
+ }
179
+
180
+ readFile(file) {
181
+ return new Promise((resolve, reject) => {
182
+ const reader = new FileReader();
183
+ reader.onload = (e) => resolve(e.target.result);
184
+ reader.onerror = (e) => reject(new Error('Failed to read file'));
185
+ reader.readAsText(file);
186
+ });
187
+ }
188
+
189
+ showFileInfo(file) {
190
+ const fileInfo = document.getElementById('fileInfo');
191
+ const fileSize = (file.size / 1024).toFixed(2);
192
+ fileInfo.innerHTML = `
193
+ <strong>File:</strong> ${file.name} (${fileSize} KB)<br>
194
+ <strong>Last Modified:</strong> ${new Date(file.lastModified).toLocaleString()}
195
+ `;
196
+ }
197
+
198
+ displayResults() {
199
+ this.showResults();
200
+ this.populateOverview();
201
+ this.populatePassages();
202
+ this.populateTags();
203
+ this.populateMetadata();
204
+ }
205
+
206
+ populateOverview() {
207
+ const passages = this.story.passages;
208
+ const allText = passages.map(p => p.text).join(' ');
209
+ const wordCount = this.countWords(allText);
210
+ const characterCount = allText.length;
211
+ const uniqueTags = this.getUniqueTags(passages);
212
+
213
+ document.getElementById('passageCount').textContent = passages.length;
214
+ document.getElementById('wordCount').textContent = wordCount.toLocaleString();
215
+ document.getElementById('characterCount').textContent = characterCount.toLocaleString();
216
+ document.getElementById('tagCount').textContent = uniqueTags.length;
217
+
218
+ document.getElementById('storyTitle').textContent = this.story.name || 'Untitled';
219
+ document.getElementById('storyIFID').textContent = this.story.IFID || 'None';
220
+ document.getElementById('storyFormat').textContent = this.story.format || 'Unknown';
221
+ document.getElementById('storyFormatVersion').textContent = this.story.formatVersion || 'Unknown';
222
+ document.getElementById('startPassage').textContent = this.story.start || 'None';
223
+ document.getElementById('storyCreator').textContent = this.story.creator || 'Unknown';
224
+ }
225
+
226
+ populatePassages() {
227
+ const passagesList = document.getElementById('passagesList');
228
+ const passageListCount = document.getElementById('passageListCount');
229
+
230
+ passageListCount.textContent = this.story.passages.length;
231
+ passagesList.innerHTML = '';
232
+
233
+ this.story.passages.forEach((passage, index) => {
234
+ const passageElement = this.createPassageElement(passage, index);
235
+ passagesList.appendChild(passageElement);
236
+ });
237
+ }
238
+
239
+ createPassageElement(passage, index) {
240
+ const div = document.createElement('div');
241
+ div.className = 'passage-item';
242
+
243
+ const wordCount = this.countWords(passage.text);
244
+ const characterCount = passage.text.length;
245
+ const tagsHtml = passage.tags.length > 0
246
+ ? passage.tags.map(tag => `<span class="tag">${tag}</span>`).join('')
247
+ : '<span class="no-tags">No tags</span>';
248
+
249
+ div.innerHTML = `
250
+ <div class="passage-header">
251
+ <h4 class="passage-name">${this.escapeHtml(passage.name)}</h4>
252
+ <div class="passage-stats">
253
+ <span class="stat">${wordCount} words</span>
254
+ <span class="stat">${characterCount} chars</span>
255
+ </div>
256
+ </div>
257
+ <div class="passage-tags">${tagsHtml}</div>
258
+ <div class="passage-preview">
259
+ ${this.createTextPreview(passage.text)}
260
+ </div>
261
+ `;
262
+
263
+ return div;
264
+ }
265
+
266
+ populateTags() {
267
+ const tagsContainer = document.getElementById('tagsContainer');
268
+ const tagStats = this.getTagStatistics();
269
+
270
+ if (tagStats.length === 0) {
271
+ tagsContainer.innerHTML = '<p class="no-data">No tags found in this story.</p>';
272
+ return;
273
+ }
274
+
275
+ tagsContainer.innerHTML = '';
276
+
277
+ tagStats.forEach(tagStat => {
278
+ const tagElement = document.createElement('div');
279
+ tagElement.className = 'tag-stat';
280
+
281
+ const color = this.story.tagColors[tagStat.name] || '#666';
282
+
283
+ tagElement.innerHTML = `
284
+ <div class="tag-stat-header">
285
+ <span class="tag-name" style="border-left: 4px solid ${color};">${tagStat.name}</span>
286
+ <span class="tag-count">${tagStat.count} passage${tagStat.count !== 1 ? 's' : ''}</span>
287
+ </div>
288
+ <div class="tag-passages">
289
+ ${tagStat.passages.map(p => `<span class="passage-link">${this.escapeHtml(p)}</span>`).join(', ')}
290
+ </div>
291
+ `;
292
+
293
+ tagsContainer.appendChild(tagElement);
294
+ });
295
+ }
296
+
297
+ populateMetadata() {
298
+ const metadataContent = document.getElementById('metadataContent');
299
+
300
+ const metadata = {
301
+ 'Story Properties': {
302
+ 'Name': this.story.name,
303
+ 'IFID': this.story.IFID,
304
+ 'Start Passage': this.story.start,
305
+ 'Format': this.story.format,
306
+ 'Format Version': this.story.formatVersion,
307
+ 'Creator': this.story.creator,
308
+ 'Creator Version': this.story.creatorVersion,
309
+ 'Zoom Level': this.story.zoom
310
+ },
311
+ 'Statistics': {
312
+ 'Total Passages': this.story.passages.length,
313
+ 'Total Words': this.countWords(this.story.passages.map(p => p.text).join(' ')),
314
+ 'Total Characters': this.story.passages.map(p => p.text).join('').length,
315
+ 'Unique Tags': new Set(this.story.passages.flatMap(p => p.tags)).size,
316
+ 'Average Words per Passage': Math.round(this.countWords(this.story.passages.map(p => p.text).join(' ')) / this.story.passages.length)
317
+ }
318
+ };
319
+
320
+ if (Object.keys(this.story.metadata).length > 0) {
321
+ metadata['Additional Metadata'] = this.story.metadata;
322
+ }
323
+
324
+ metadataContent.innerHTML = '';
325
+
326
+ Object.entries(metadata).forEach(([section, data]) => {
327
+ const sectionDiv = document.createElement('div');
328
+ sectionDiv.className = 'metadata-section';
329
+
330
+ const sectionTitle = document.createElement('h4');
331
+ sectionTitle.textContent = section;
332
+ sectionDiv.appendChild(sectionTitle);
333
+
334
+ const dataList = document.createElement('dl');
335
+ dataList.className = 'metadata-list';
336
+
337
+ Object.entries(data).forEach(([key, value]) => {
338
+ const dt = document.createElement('dt');
339
+ dt.textContent = key;
340
+ const dd = document.createElement('dd');
341
+ dd.textContent = value || 'N/A';
342
+
343
+ dataList.appendChild(dt);
344
+ dataList.appendChild(dd);
345
+ });
346
+
347
+ sectionDiv.appendChild(dataList);
348
+ metadataContent.appendChild(sectionDiv);
349
+ });
350
+ }
351
+
352
+ getTagStatistics() {
353
+ const tagMap = new Map();
354
+
355
+ this.story.passages.forEach(passage => {
356
+ passage.tags.forEach(tag => {
357
+ if (!tagMap.has(tag)) {
358
+ tagMap.set(tag, []);
359
+ }
360
+ tagMap.get(tag).push(passage.name);
361
+ });
362
+ });
363
+
364
+ return Array.from(tagMap.entries())
365
+ .map(([name, passages]) => ({
366
+ name,
367
+ count: passages.length,
368
+ passages: passages.sort()
369
+ }))
370
+ .sort((a, b) => b.count - a.count);
371
+ }
372
+
373
+ countWords(text) {
374
+ if (!text) return 0;
375
+ // Remove HTML tags and count words
376
+ const cleanText = text.replace(/<[^>]*>/g, '').trim();
377
+ return cleanText ? cleanText.split(/\s+/).length : 0;
378
+ }
379
+
380
+ getUniqueTags(passages) {
381
+ const tags = new Set();
382
+ passages.forEach(passage => {
383
+ passage.tags.forEach(tag => tags.add(tag));
384
+ });
385
+ return Array.from(tags);
386
+ }
387
+
388
+ createTextPreview(text, maxLength = 200) {
389
+ if (!text) return '<em>Empty passage</em>';
390
+
391
+ // Remove HTML tags for preview
392
+ const cleanText = text.replace(/<[^>]*>/g, '').trim();
393
+
394
+ if (cleanText.length <= maxLength) {
395
+ return this.escapeHtml(cleanText);
396
+ }
397
+
398
+ return this.escapeHtml(cleanText.substring(0, maxLength)) + '...';
399
+ }
400
+
401
+ filterPassages(searchTerm) {
402
+ const passageItems = document.querySelectorAll('.passage-item');
403
+ const term = searchTerm.toLowerCase();
404
+
405
+ passageItems.forEach(item => {
406
+ const passageName = item.querySelector('.passage-name').textContent.toLowerCase();
407
+ const passageText = item.querySelector('.passage-preview').textContent.toLowerCase();
408
+ const passageTags = Array.from(item.querySelectorAll('.tag')).map(tag => tag.textContent.toLowerCase());
409
+
410
+ const matches = passageName.includes(term) ||
411
+ passageText.includes(term) ||
412
+ passageTags.some(tag => tag.includes(term));
413
+
414
+ item.style.display = matches ? 'block' : 'none';
415
+ });
416
+ }
417
+
418
+ switchTab(tabName) {
419
+ // Update tab buttons
420
+ document.querySelectorAll('.tab-button').forEach(btn => {
421
+ btn.classList.remove('active');
422
+ });
423
+ document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
424
+
425
+ // Update tab content
426
+ document.querySelectorAll('.tab-content').forEach(content => {
427
+ content.classList.remove('active');
428
+ });
429
+ document.getElementById(tabName).classList.add('active');
430
+ }
431
+
432
+ showResults() {
433
+ document.getElementById('resultsSection').style.display = 'block';
434
+ }
435
+
436
+ hideResults() {
437
+ document.getElementById('resultsSection').style.display = 'none';
438
+ }
439
+
440
+ showError(message) {
441
+ document.getElementById('errorText').textContent = message;
442
+ document.getElementById('errorSection').style.display = 'block';
443
+ }
444
+
445
+ hideError() {
446
+ document.getElementById('errorSection').style.display = 'none';
447
+ }
448
+
449
+ escapeHtml(text) {
450
+ const div = document.createElement('div');
451
+ div.textContent = text;
452
+ return div.innerHTML;
453
+ }
454
+ }
455
+
456
+ // Initialize the decompiler when the page loads
457
+ document.addEventListener('DOMContentLoaded', () => {
458
+ new TwineDecompiler();
459
+ });
460
+ </script>
461
+ </body>
462
+ </html>
package/package.json CHANGED
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.3.5",
3
+ "version": "2.3.7",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./web": "./build/extwee.core.min.js",
10
+ "./web/core": "./build/extwee.core.min.js",
11
+ "./web/twine1html": "./build/extwee.twine1html.min.js",
12
+ "./web/twine2archive": "./build/extwee.twine2archive.min.js",
13
+ "./web/tws": "./build/extwee.tws.min.js"
14
+ },
7
15
  "bin": {
8
16
  "extwee": "src/extwee.js"
9
17
  },
@@ -25,7 +33,7 @@
25
33
  ],
26
34
  "license": "MIT",
27
35
  "dependencies": {
28
- "commander": "^14.0.1",
36
+ "commander": "^14.0.2",
29
37
  "graphemer": "^1.4.0",
30
38
  "html-entities": "^2.6.0",
31
39
  "node-html-parser": "^7.0.1",
@@ -36,25 +44,24 @@
36
44
  },
37
45
  "devDependencies": {
38
46
  "@babel/cli": "^7.28.3",
39
- "@babel/core": "^7.28.4",
40
- "@babel/preset-env": "^7.28.3",
41
- "@eslint/js": "^9.37.0",
47
+ "@babel/core": "^7.28.5",
48
+ "@babel/preset-env": "^7.28.5",
49
+ "@eslint/js": "^9.38.0",
42
50
  "@inquirer/prompts": "^7.9.0",
43
- "@types/node": "^24.7.2",
51
+ "@types/node": "^24.9.1",
44
52
  "@types/semver": "^7.7.1",
45
- "@types/uuid": "^11.0.0",
46
53
  "babel-loader": "^10.0.0",
47
54
  "clean-jsdoc-theme": "^4.3.0",
48
55
  "core-js": "^3.46.0",
49
- "eslint": "^9.37.0",
56
+ "eslint": "^9.38.0",
50
57
  "eslint-plugin-jest": "^29.0.1",
51
- "eslint-plugin-jsdoc": "^61.1.4",
58
+ "eslint-plugin-jsdoc": "^61.1.8",
52
59
  "globals": "^16.4.0",
53
60
  "jest": "^30.2.0",
54
61
  "jest-environment-jsdom": "^30.2.0",
55
62
  "regenerator-runtime": "^0.14.1",
56
63
  "typescript": "^5.9.3",
57
- "typescript-eslint": "^8.46.1",
64
+ "typescript-eslint": "^8.46.2",
58
65
  "webpack": "^5.102.1",
59
66
  "webpack-bundle-analyzer": "^4.10.2",
60
67
  "webpack-cli": "^6.0.1"
package/src/Story.js CHANGED
@@ -7,7 +7,7 @@ import { encode } from 'html-entities';
7
7
  const creatorName = 'extwee';
8
8
 
9
9
  // Set the creator version.
10
- const creatorVersion = '2.3.5';
10
+ const creatorVersion = '2.3.7';
11
11
 
12
12
  /**
13
13
  * Story class.
@@ -9,23 +9,41 @@ class LightweightTwine1Parser {
9
9
  constructor(html) {
10
10
  this.html = html;
11
11
  this.doc = null;
12
+ this.usingDOMParser = false;
12
13
 
13
14
  // Parse HTML using browser's native DOMParser if available, otherwise fallback
14
15
  if (typeof DOMParser !== 'undefined') {
15
- const parser = new DOMParser();
16
- this.doc = parser.parseFromString(html, 'text/html');
16
+ try {
17
+ const parser = new DOMParser();
18
+ this.doc = parser.parseFromString(html, 'text/html');
19
+ this.usingDOMParser = true;
20
+
21
+ // Check if parsing was successful (DOMParser doesn't throw errors, but creates error documents)
22
+ const parserError = this.doc.querySelector('parsererror');
23
+ if (parserError) {
24
+ console.warn('DOMParser encountered an error, falling back to regex parsing:', parserError.textContent);
25
+ this.doc = this.createSimpleDOM(html);
26
+ this.usingDOMParser = false;
27
+ }
28
+ } catch (error) {
29
+ console.warn('DOMParser failed, falling back to regex parsing:', error.message);
30
+ this.doc = this.createSimpleDOM(html);
31
+ this.usingDOMParser = false;
32
+ }
17
33
  } else {
18
34
  // Fallback for environments without DOMParser
19
35
  this.doc = this.createSimpleDOM(html);
36
+ this.usingDOMParser = false;
20
37
  }
21
38
  }
22
39
 
23
40
  querySelector(selector) {
24
- if (this.doc && this.doc.querySelector) {
41
+ if (this.usingDOMParser && this.doc && this.doc.querySelector) {
42
+ // Use native DOM methods when DOMParser is available and working
25
43
  return this.doc.querySelector(selector);
26
44
  }
27
45
 
28
- // Simple fallback implementation
46
+ // Fallback implementation for environments without DOMParser
29
47
  if (selector === '#storeArea') {
30
48
  const match = this.html.match(/<div[^>]*id=["']storeArea["'][^>]*>/i);
31
49
  return match ? { found: true } : null;
@@ -38,11 +56,31 @@ class LightweightTwine1Parser {
38
56
  }
39
57
 
40
58
  querySelectorAll(selector) {
41
- if (this.doc && this.doc.querySelectorAll) {
42
- return Array.from(this.doc.querySelectorAll(selector));
59
+ if (this.usingDOMParser && this.doc && this.doc.querySelectorAll) {
60
+ // Use native DOM methods when DOMParser is available and working
61
+ const elements = Array.from(this.doc.querySelectorAll(selector));
62
+
63
+ // Convert DOM elements to expected format for compatibility
64
+ return elements.map(element => {
65
+ const attributes = {};
66
+
67
+ // Extract attributes using DOM methods - much more reliable than regex
68
+ if (element.attributes) {
69
+ for (let i = 0; i < element.attributes.length; i++) {
70
+ const attr = element.attributes[i];
71
+ // DOM automatically handles HTML entity decoding
72
+ attributes[attr.name] = attr.value;
73
+ }
74
+ }
75
+
76
+ return {
77
+ attributes,
78
+ rawText: element.textContent || element.innerText || ''
79
+ };
80
+ });
43
81
  }
44
82
 
45
- // Fallback implementation for [tiddler] elements
83
+ // Fallback implementation for environments without DOMParser
46
84
  if (selector === '[tiddler]') {
47
85
  return this.extractTiddlerElements();
48
86
  }
@@ -111,7 +149,8 @@ class LightweightTwine1Parser {
111
149
  }
112
150
 
113
151
  createSimpleDOM(html) {
114
- // Minimal DOM-like object for fallback
152
+ // Minimal DOM-like object for fallback when DOMParser is not available
153
+ // This should only be used in very limited environments
115
154
  return {
116
155
  querySelector: (selector) => {
117
156
  if (selector === '#storeArea' && html.includes('id="storeArea"')) {