extwee 2.3.6 → 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.
@@ -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
- const parser = new DOMParser();
17
- this.doc = parser.parseFromString(html, 'text/html');
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
- return Array.from(this.doc.getElementsByTagName(tagName));
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
- // Common attribute patterns
111
- const attributeRegex = /(\w+(?:-\w+)*)=["']([^"']*)["']/g;
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 = attributeRegex.exec(openingTag)) !== null) {
115
- attributes[match[1]] = match[2];
157
+ while ((match = quotedAttributeRegex.exec(openingTag)) !== null) {
158
+ // Decode basic HTML entities in attribute values
159
+ const value = match[2]
160
+ .replace(/&quot;/g, '"')
161
+ .replace(/&#39;/g, "'")
162
+ .replace(/&lt;/g, '<')
163
+ .replace(/&gt;/g, '>')
164
+ .replace(/&amp;/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
- // Set the story name
205
- story.name = storyData.attributes.name;
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
- // Update story IFID.
219
- story.IFID = storyData.attributes.ifid;
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
- // Name is a required filed. Warn user.
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('twee');
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 IFID is not in valid UUIDv4 formatting on tw-storydata!');
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=\\"\\"\\" >Content</tw-passagedata></tw-storydata>';
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');
@@ -251,5 +251,5 @@ export class Story {
251
251
  #private;
252
252
  }
253
253
  export const creatorName: "extwee";
254
- export const creatorVersion: "2.3.6";
254
+ export const creatorVersion: "2.3.7";
255
255
  import Passage from './Passage.js';