extwee 2.3.14 → 2.3.16

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,40 @@
1
+ const jsdom = require('jsdom');
2
+ const { JSDOM } = jsdom;
3
+
4
+ // Test in a DOM context
5
+ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
6
+ const doc = dom.window.document;
7
+
8
+ // Create elements with both encodings
9
+ const div1 = doc.createElement('div');
10
+ div1.setAttribute('data-test', 'test&apos;value');
11
+ const div2 = doc.createElement('div');
12
+ div2.setAttribute('data-test', 'test&#39;value');
13
+
14
+ console.log('div1 getAttribute:', div1.getAttribute('data-test'));
15
+ console.log('div2 getAttribute:', div2.getAttribute('data-test'));
16
+ console.log('Are they equal?', div1.getAttribute('data-test') === div2.getAttribute('data-test'));
17
+
18
+ // Test with innerHTML
19
+ div1.innerHTML = 'Text with &apos;quote&apos;';
20
+ div2.innerHTML = 'Text with &#39;quote&#39;';
21
+ console.log('div1 textContent:', div1.textContent);
22
+ console.log('div2 textContent:', div2.textContent);
23
+ console.log('Text content equal?', div1.textContent === div2.textContent);
24
+
25
+ // Test parsing HTML with both
26
+ const html1 = '<tw-passagedata name="Test" data-value="It&apos;s">Content</tw-passagedata>';
27
+ const html2 = '<tw-passagedata name="Test" data-value="It&#39;s">Content</tw-passagedata>';
28
+
29
+ const container1 = doc.createElement('div');
30
+ const container2 = doc.createElement('div');
31
+ container1.innerHTML = html1;
32
+ container2.innerHTML = html2;
33
+
34
+ const passage1 = container1.querySelector('tw-passagedata');
35
+ const passage2 = container2.querySelector('tw-passagedata');
36
+
37
+ console.log('\nParsing tw-passagedata:');
38
+ console.log('passage1 data-value:', passage1.getAttribute('data-value'));
39
+ console.log('passage2 data-value:', passage2.getAttribute('data-value'));
40
+ console.log('Attributes equal?', passage1.getAttribute('data-value') === passage2.getAttribute('data-value'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.3.14",
3
+ "version": "2.3.16",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
@@ -17,11 +17,11 @@
17
17
  },
18
18
  "scripts": {
19
19
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
20
- "lint": "eslint ./src/**/*.js --fix",
21
- "lint:test": "eslint ./test/**/*.test.js --fix",
20
+ "lint": "eslint \"./src/**/*.js\" --fix",
21
+ "lint:test": "eslint \"./test/**/*.test.js\" --fix",
22
22
  "build:web": "webpack",
23
23
  "analyze:web": "webpack-bundle-analyzer build/extwee.web.min.js",
24
- "gen-types": "npx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types",
24
+ "gen-types": "npx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types --skipLibCheck",
25
25
  "copy:build": "cp build/*.js docs/build",
26
26
  "all": "npm run lint && npm run lint:test && npm run test && npm run build:web && npm run gen-types && npm run copy:build"
27
27
  },
@@ -36,34 +36,35 @@
36
36
  "commander": "^14.0.3",
37
37
  "graphemer": "^1.4.0",
38
38
  "html-entities": "^2.6.0",
39
- "node-html-parser": "^7.0.2",
39
+ "node-html-parser": "^7.1.0",
40
40
  "pickleparser": "^0.2.1",
41
- "semver": "^7.7.3",
42
- "shelljs": "^0.10.0"
41
+ "semver": "^7.7.4"
43
42
  },
44
43
  "devDependencies": {
45
44
  "@babel/cli": "^7.28.6",
46
45
  "@babel/core": "^7.29.0",
47
- "@babel/preset-env": "^7.29.0",
48
- "@eslint/js": "^9.39.2",
49
- "@inquirer/prompts": "^8.2.0",
50
- "@types/node": "^25.2.0",
46
+ "@babel/preset-env": "^7.29.3",
47
+ "@eslint/js": "^10.0.1",
48
+ "@inquirer/prompts": "^8.4.2",
49
+ "@types/node": "^25.6.0",
51
50
  "@types/semver": "^7.7.1",
52
- "babel-loader": "^10.0.0",
53
- "clean-jsdoc-theme": "^4.3.0",
54
- "core-js": "^3.48.0",
55
- "eslint": "^9.39.2",
56
- "eslint-plugin-jest": "^29.12.1",
57
- "eslint-plugin-jsdoc": "^62.5.0",
58
- "globals": "^17.3.0",
59
- "jest": "^30.2.0",
60
- "jest-environment-jsdom": "^30.2.0",
51
+ "babel-loader": "^10.1.1",
52
+ "clean-jsdoc-theme": "^4.3.2",
53
+ "core-js": "^3.49.0",
54
+ "eslint": "^10.3.0",
55
+ "eslint-plugin-jest": "^29.15.2",
56
+ "eslint-plugin-jsdoc": "^62.9.0",
57
+ "globals": "^17.6.0",
58
+ "jest": "^30.3.0",
59
+ "jest-environment-jsdom": "^30.3.0",
61
60
  "regenerator-runtime": "^0.14.1",
62
- "typescript": "^5.9.3",
63
- "typescript-eslint": "^8.54.0",
64
- "webpack": "^5.104.1",
65
- "webpack-bundle-analyzer": "^5.2.0",
66
- "webpack-cli": "^6.0.1"
61
+ "semgrep": "^0.0.1",
62
+ "shelljs": "^0.10.0",
63
+ "typescript": "^6.0.3",
64
+ "typescript-eslint": "^8.59.1",
65
+ "webpack": "^5.106.2",
66
+ "webpack-bundle-analyzer": "^5.3.0",
67
+ "webpack-cli": "^7.0.2"
67
68
  },
68
69
  "repository": {
69
70
  "type": "git",
@@ -23,6 +23,14 @@ import { readFileSync } from "node:fs";
23
23
  * // Output: The contents of the format.js file.
24
24
  */
25
25
  export function loadStoryFormat(storyFormatName, storyFormatVersion) {
26
+ // Validate path components to prevent path traversal.
27
+ if (/[/\\]|\.\./.test(storyFormatName)) {
28
+ throw new Error('Error: story format name contains invalid characters.');
29
+ }
30
+ if (/[/\\]|\.\./.test(storyFormatVersion)) {
31
+ throw new Error('Error: story format version contains invalid characters.');
32
+ }
33
+
26
34
  // If the story-formats directory does not exist, throw error.
27
35
  if (isDirectory('story-formats') === false) {
28
36
  throw new Error(`Error: story-formats directory does not exist. Consider running 'npx sfa-get' to download the latest story formats.`);
@@ -49,8 +57,6 @@ export function loadStoryFormat(storyFormatName, storyFormatVersion) {
49
57
  // The directories are expected to be version directories.
50
58
  let directories = readDirectories(`story-formats/${storyFormatName}`);
51
59
 
52
- console.log("!!! directories", directories);
53
-
54
60
  // Check if there are any version directories.
55
61
  if (directories.length === 0) {
56
62
  // If there are no version directories, throw error.
@@ -63,7 +69,6 @@ export function loadStoryFormat(storyFormatName, storyFormatVersion) {
63
69
  });
64
70
 
65
71
  // Get the latest version directory.
66
- // The latest version is the last directory in the sorted list.
67
72
  const latestVersion = directories[0];
68
73
 
69
74
  // Set the filepath to the latest version directory.
@@ -11,9 +11,6 @@ import { isDirectory } from '../isDirectory.js';
11
11
  */
12
12
  export function readDirectories(directory) {
13
13
 
14
- // Create default response.
15
- let results = [];
16
-
17
14
  // Check if the directory exists.
18
15
  const isDir = isDirectory(directory);
19
16
  // If the directory does not exist, return an empty array
@@ -23,6 +20,7 @@ export function readDirectories(directory) {
23
20
  }
24
21
 
25
22
  // Read the directory and return the list of files.
23
+ let results;
26
24
  try {
27
25
  results = readdirSync(directory);
28
26
  } catch (error) {
@@ -20,34 +20,34 @@ export function parser(obj) {
20
20
  };
21
21
 
22
22
  // Does the object contain 'StoryFormat'?
23
- if (Object.hasOwnProperty.call(obj, 'story-format')) {
23
+ if (Object.prototype.hasOwnProperty.call(obj, 'story-format')) {
24
24
  results.StoryFormat = obj['story-format'];
25
25
  }
26
26
 
27
27
  // Does the object contain 'StoryFormatVersion'?
28
- if (Object.hasOwnProperty.call(obj, 'story-format-version')) {
28
+ if (Object.prototype.hasOwnProperty.call(obj, 'story-format-version')) {
29
29
  results.StoryFormatVersion = obj['story-format-version'];
30
30
  } else {
31
31
  results.StoryFormatVersion = "latest";
32
32
  }
33
33
 
34
34
  // Does the object contain 'mode'?
35
- if (Object.hasOwnProperty.call(obj, 'mode')) {
35
+ if (Object.prototype.hasOwnProperty.call(obj, 'mode')) {
36
36
  results.Mode = obj['mode'];
37
37
  }
38
38
 
39
39
  // Does the object contain 'input'?
40
- if (Object.hasOwnProperty.call(obj, 'input')) {
40
+ if (Object.prototype.hasOwnProperty.call(obj, 'input')) {
41
41
  results.Input = obj['input'];
42
42
  }
43
43
 
44
44
  // Does the object contain 'output'?
45
- if (Object.hasOwnProperty.call(obj, 'output')) {
45
+ if (Object.prototype.hasOwnProperty.call(obj, 'output')) {
46
46
  results.Output = obj['output'];
47
47
  }
48
48
 
49
49
  // Does the object contain 'twine1-project'?
50
- if (Object.hasOwnProperty.call(obj, 'twine1-project')) {
50
+ if (Object.prototype.hasOwnProperty.call(obj, 'twine1-project')) {
51
51
  results.Twine1Project = obj['twine1-project'];
52
52
  } else {
53
53
  results.Twine1Project = false;
@@ -20,13 +20,13 @@ export function reader(path) {
20
20
  const contents = readFileSync(path, 'utf8');
21
21
 
22
22
  // Parsed contents.
23
- let parsedContents = null;
23
+ let parsedContents;
24
24
 
25
25
  // Try to parse the contents into JSON object.
26
26
  try {
27
27
  parsedContents = JSON.parse(contents);
28
28
  } catch (error) {
29
- throw new Error(`Error: File ${path} is not a valid JSON file. ${error.message}`);
29
+ throw new Error(`Error: File ${path} is not a valid JSON file. ${error.message}`, { cause: error });
30
30
  }
31
31
 
32
32
  // Return the parsed contents.
package/src/JSON/parse.js CHANGED
@@ -52,7 +52,7 @@ import Passage from '../Passage.js';
52
52
  */
53
53
  function parse (jsonString) {
54
54
  // Create future object.
55
- let result = {};
55
+ let result;
56
56
 
57
57
  // Create Story.
58
58
  const s = new Story();
@@ -61,7 +61,7 @@ function parse (jsonString) {
61
61
  try {
62
62
  result = JSON.parse(jsonString);
63
63
  } catch (error) {
64
- throw new Error(`Error: JSON could not be parsed! ${error.message}`);
64
+ throw new Error(`Error: JSON could not be parsed! ${error.message}`, { cause: error });
65
65
  }
66
66
 
67
67
  // Name
@@ -142,8 +142,12 @@ function parse (jsonString) {
142
142
 
143
143
  // Does s have tags?
144
144
  if (Object.prototype.hasOwnProperty.call(p, 'tags')) {
145
- // Set tags.
146
- newP.tags = p.tags;
145
+ // Tags must be an array; coerce space-separated strings for compatibility.
146
+ if (Array.isArray(p.tags)) {
147
+ newP.tags = p.tags;
148
+ } else if (typeof p.tags === 'string') {
149
+ newP.tags = p.tags.length > 0 ? p.tags.split(' ').filter(t => t !== '') : [];
150
+ }
147
151
  }
148
152
 
149
153
  // Does s have metadata?
package/src/Passage.js CHANGED
@@ -2,42 +2,42 @@ import { encode } from 'html-entities';
2
2
  import { escapeTweeMetacharacters } from './Twee/parse.js';
3
3
 
4
4
  /**
5
- * Passage class.
6
- * @class
7
- * @classdesc Represents a passage in a Twine story.
8
- * @property {string} name - Name of the passage.
9
- * @property {Array} tags - Tags for the passage.
10
- * @property {object} metadata - Metadata for the passage.
11
- * @property {string} text - Text content of the passage.
12
- * @method {string} toTwee - Return a Twee representation.
13
- * @method {string} toJSON - Return JSON representation.
14
- * @method {string} toTwine2HTML - Return Twine 2 HTML representation.
15
- * @method {string} toTwine1HTML - Return Twine 1 HTML representation.
16
- * @example
17
- * const p = new Passage('Start', 'This is the start of the story.');
18
- * console.log(p.toTwee());
19
- * // :: Start
20
- * // This is the start of the story.
21
- * //
22
- * console.log(p.toJSON());
23
- * // {"name":"Start","tags":[],"metadata":{},"text":"This is the start of the story."}
24
- * console.log(p.toTwine2HTML());
25
- * // <tw-passagedata pid="1" name="Start" tags="" >This is the start of the story.</tw-passagedata>
26
- * console.log(p.toTwine1HTML());
27
- * // <div tiddler="Start" tags="" modifier="extwee" twine-position="10,10">This is the start of the story.</div>
28
- * @example
29
- * const p = new Passage('Start', 'This is the start of the story.', ['start', 'beginning'], {position: '10,10', size: '100,100'});
30
- * console.log(p.toTwee());
31
- * // :: Start [start beginning] {"position":"10,10","size":"100,100"}
32
- * // This is the start of the story.
33
- * //
34
- * console.log(p.toJSON());
35
- * // {"name":"Start","tags":["start","beginning"],"metadata":{"position":"10,10","size":"100,100"},"text":"This is the start of the story."}
36
- * console.log(p.toTwine2HTML());
37
- * // <tw-passagedata pid="1" name="Start" tags="start beginning" position="10,10" size="100,100">This is the start of the story.</tw-passagedata>
38
- * console.log(p.toTwine1HTML());
39
- * // <div tiddler="Start" tags="start beginning" modifier="extwee" twine-position="10,10">This is the start of the story.</div>
40
- */
5
+ * Passage class.
6
+ * @class
7
+ * @classdesc Represents a passage in a Twine story.
8
+ * @property {string} name - Name of the passage.
9
+ * @property {Array} tags - Tags for the passage.
10
+ * @property {object} metadata - Metadata for the passage.
11
+ * @property {string} text - Text content of the passage.
12
+ * @function toTwee - Return a Twee representation.
13
+ * @function toJSON - Return JSON representation.
14
+ * @function toTwine2HTML - Return Twine 2 HTML representation.
15
+ * @function toTwine1HTML - Return Twine 1 HTML representation.
16
+ * @example
17
+ * const p = new Passage('Start', 'This is the start of the story.');
18
+ * console.log(p.toTwee());
19
+ * // :: Start
20
+ * // This is the start of the story.
21
+ * //
22
+ * console.log(p.toJSON());
23
+ * // {"name":"Start","tags":[],"metadata":{},"text":"This is the start of the story."}
24
+ * console.log(p.toTwine2HTML());
25
+ * // <tw-passagedata pid="1" name="Start" tags="" >This is the start of the story.</tw-passagedata>
26
+ * console.log(p.toTwine1HTML());
27
+ * // <div tiddler="Start" tags="" modifier="extwee" twine-position="10,10">This is the start of the story.</div>
28
+ * @example
29
+ * const p = new Passage('Start', 'This is the start of the story.', ['start', 'beginning'], {position: '10,10', size: '100,100'});
30
+ * console.log(p.toTwee());
31
+ * // :: Start [start beginning] {"position":"10,10","size":"100,100"}
32
+ * // This is the start of the story.
33
+ * //
34
+ * console.log(p.toJSON());
35
+ * // {"name":"Start","tags":["start","beginning"],"metadata":{"position":"10,10","size":"100,100"},"text":"This is the start of the story."}
36
+ * console.log(p.toTwine2HTML());
37
+ * // <tw-passagedata pid="1" name="Start" tags="start beginning" position="10,10" size="100,100">This is the start of the story.</tw-passagedata>
38
+ * console.log(p.toTwine1HTML());
39
+ * // <div tiddler="Start" tags="start beginning" modifier="extwee" twine-position="10,10">This is the start of the story.</div>
40
+ */
41
41
  export default class Passage {
42
42
  /**
43
43
  * Name of the Passage
@@ -95,6 +95,7 @@ export default class Passage {
95
95
  get name () { return this.#_name; }
96
96
 
97
97
  /**
98
+ * Set passage name.
98
99
  * @param {string} s - Name to replace
99
100
  * @throws {Error} Name must be a String!
100
101
  */
@@ -113,6 +114,7 @@ export default class Passage {
113
114
  get tags () { return this.#_tags; }
114
115
 
115
116
  /**
117
+ * Set passage tags.
116
118
  * @param {Array} t - Replacement array
117
119
  * @throws {Error} Tags must be an array!
118
120
  */
@@ -133,6 +135,7 @@ export default class Passage {
133
135
  get metadata () { return this.#_metadata; }
134
136
 
135
137
  /**
138
+ * Set passage metadata.
136
139
  * @param {object} m - Replacement object
137
140
  * @throws {Error} Metadata must be an object literal!
138
141
  */
@@ -152,6 +155,7 @@ export default class Passage {
152
155
  get text () { return this.#_text; }
153
156
 
154
157
  /**
158
+ * Set passage text.
155
159
  * @param {string} t - Replacement text
156
160
  * @throws {Error} Text should be a String!
157
161
  */
@@ -168,8 +172,7 @@ export default class Passage {
168
172
  * Return a Twee representation.
169
173
  *
170
174
  * See: https://github.com/iftechfoundation/twine-specs/blob/master/twee-3-specification.md
171
- *
172
- * @method toTwee
175
+ * @function toTwee
173
176
  * @returns {string} String form of passage.
174
177
  */
175
178
  toTwee () {
@@ -216,7 +219,7 @@ export default class Passage {
216
219
 
217
220
  /**
218
221
  * Return JSON representation.
219
- * @method toJSON
222
+ * @function toJSON
220
223
  * @returns {string} JSON string.
221
224
  */
222
225
  toJSON () {
@@ -235,7 +238,7 @@ export default class Passage {
235
238
  /**
236
239
  * Return Twine 2 HTML representation.
237
240
  * (Default Passage ID is 1.)
238
- * @method toTwine2HTML
241
+ * @function toTwine2HTML
239
242
  * @param {number} pid - Passage ID (PID) to record in HTML.
240
243
  * @returns {string} Twine 2 HTML string.
241
244
  */
@@ -292,7 +295,7 @@ export default class Passage {
292
295
 
293
296
  /**
294
297
  * Return Twine 1 HTML representation.
295
- * @method toTwine1HTML
298
+ * @function toTwine1HTML
296
299
  * @returns {string} Twine 1 HTML string.
297
300
  */
298
301
  toTwine1HTML () {