extwee 2.3.3 → 2.3.5

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.
Files changed (41) hide show
  1. package/build/extwee.core.min.js +1 -1
  2. package/build/extwee.twine1html.min.js +1 -1
  3. package/build/extwee.twine2archive.min.js +1 -1
  4. package/build/extwee.tws.min.js +1 -1
  5. package/docs/build/extwee.core.min.js +1 -0
  6. package/docs/build/extwee.twine1html.min.js +1 -0
  7. package/docs/build/extwee.twine2archive.min.js +1 -0
  8. package/docs/build/extwee.tws.min.js +1 -0
  9. package/docs/demos/compiler/extwee.core.min.js +1 -0
  10. package/docs/demos/compiler/index.css +105 -0
  11. package/docs/demos/compiler/index.html +359 -0
  12. package/package.json +19 -18
  13. package/src/CLI/CommandLineProcessing.js +148 -153
  14. package/src/Passage.js +6 -4
  15. package/src/Story.js +1 -1
  16. package/src/Twee/parse.js +117 -21
  17. package/src/Twine2HTML/parse-web.js +7 -1
  18. package/src/Web/web-core.js +22 -2
  19. package/src/Web/web-twine1html.js +25 -5
  20. package/src/Web/web-twine2archive.js +25 -5
  21. package/src/Web/web-tws.js +22 -4
  22. package/test/Objects/Passage.test.js +1 -1
  23. package/test/Twee/Twee.Escaping.test.js +200 -0
  24. package/test/Twine1HTML/Twine1HTML.Parse.Web.test.js +484 -0
  25. package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.Web.test.js +293 -0
  26. package/test/Twine2HTML/Twine2HTML.Parse.Web.test.js +329 -0
  27. package/test/Web/web-core-coverage.test.js +175 -0
  28. package/test/Web/web-core-global.test.js +93 -0
  29. package/test/Web/web-core.test.js +156 -0
  30. package/test/Web/web-twine1html.test.js +105 -0
  31. package/test/Web/web-twine2archive.test.js +96 -0
  32. package/test/Web/web-tws.test.js +77 -0
  33. package/test/Web/window.Extwee.test.js +7 -2
  34. package/types/src/Story.d.ts +1 -1
  35. package/types/src/Twee/parse.d.ts +21 -0
  36. package/types/src/Web/web-core.d.ts +23 -1
  37. package/types/src/Web/web-twine1html.d.ts +7 -0
  38. package/types/src/Web/web-twine2archive.d.ts +7 -0
  39. package/types/src/Web/web-tws.d.ts +5 -0
  40. package/webpack.config.js +2 -1
  41. package/src/Web/web-index.js +0 -31
@@ -0,0 +1,105 @@
1
+ body {
2
+ background-color: #f8f9fa;
3
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
4
+ }
5
+
6
+ .demo-container {
7
+ max-width: 1200px;
8
+ margin: 0 auto;
9
+ padding: 20px;
10
+ }
11
+
12
+ .demo-header {
13
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
14
+ color: white;
15
+ padding: 40px 20px;
16
+ border-radius: 10px;
17
+ margin-bottom: 30px;
18
+ text-align: center;
19
+ }
20
+
21
+ .demo-section {
22
+ background: white;
23
+ border-radius: 10px;
24
+ padding: 30px;
25
+ margin-bottom: 20px;
26
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
27
+ }
28
+
29
+ .form-control, .form-select {
30
+ border-radius: 8px;
31
+ border: 2px solid #e9ecef;
32
+ transition: border-color 0.3s ease;
33
+ }
34
+
35
+ .form-control:focus, .form-select:focus {
36
+ border-color: #667eea;
37
+ box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
38
+ }
39
+
40
+ .btn-primary {
41
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
42
+ border: none;
43
+ border-radius: 8px;
44
+ padding: 12px 30px;
45
+ font-weight: 600;
46
+ transition: all 0.3s ease;
47
+ }
48
+
49
+ .btn-primary:hover {
50
+ transform: translateY(-2px);
51
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
52
+ }
53
+
54
+ .output-container {
55
+ background-color: #f8f9fa;
56
+ border: 2px solid #e9ecef;
57
+ border-radius: 8px;
58
+ padding: 20px;
59
+ margin-top: 20px;
60
+ min-height: 200px;
61
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
62
+ font-size: 14px;
63
+ white-space: pre-wrap;
64
+ overflow-x: auto;
65
+ }
66
+
67
+ .loading {
68
+ text-align: center;
69
+ color: #6c757d;
70
+ font-style: italic;
71
+ }
72
+
73
+ .error {
74
+ color: #dc3545;
75
+ background-color: #f8d7da;
76
+ border: 1px solid #f5c6cb;
77
+ border-radius: 5px;
78
+ padding: 10px;
79
+ margin-top: 10px;
80
+ }
81
+
82
+ .success {
83
+ color: #155724;
84
+ background-color: #d4edda;
85
+ border: 1px solid #c3e6cb;
86
+ border-radius: 5px;
87
+ padding: 10px;
88
+ margin-top: 10px;
89
+ }
90
+
91
+ .example-twee {
92
+ background-color: #f1f3f4;
93
+ border-left: 4px solid #667eea;
94
+ padding: 15px;
95
+ margin: 15px 0;
96
+ border-radius: 0 8px 8px 0;
97
+ }
98
+
99
+ .format-info {
100
+ background-color: #e7f3ff;
101
+ border: 1px solid #b8daff;
102
+ border-radius: 8px;
103
+ padding: 15px;
104
+ margin-top: 15px;
105
+ }
@@ -0,0 +1,359 @@
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 Story Format Compiler Demo</title>
7
+
8
+ <!-- Bootstrap CSS for styling -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
+
11
+ <!-- Custom CSS -->
12
+ <link href="./index.css" rel="stylesheet">
13
+ </head>
14
+ <body>
15
+ <div class="demo-container">
16
+ <!-- Header -->
17
+ <div class="demo-header">
18
+ <h1 class="mb-3">Example Extwee Story Format Compiler</h1>
19
+ <p class="lead mb-0">Compile Twee code with different Twine story formats</p>
20
+ </div>
21
+
22
+ <!-- Instructions -->
23
+ <div class="demo-section">
24
+ <h3>📝 How to Use</h3>
25
+ <ol>
26
+ <li><strong>Select a Story Format:</strong> Choose from Harlowe, SugarCube, Snowman, or Chapbook</li>
27
+ <li><strong>Enter Twee Code:</strong> Write or paste your Twee story code in the text area</li>
28
+ <li><strong>Compile:</strong> Click the "Compile Story" button to generate the HTML output</li>
29
+ </ol>
30
+
31
+ <div class="example-twee">
32
+ <strong>Example Twee Code:</strong><br>
33
+ <code>:: Start<br>
34
+ This is the beginning of your story.<br>
35
+ <br>
36
+ [[Continue to the next passage->Next]]<br>
37
+ <br>
38
+ :: Next<br>
39
+ This is the second passage of your story.<br>
40
+ <br>
41
+ The End.
42
+ </code>
43
+ </div>
44
+ <div>
45
+ <p>Similar to Twine, this page will automatically assign an IFID to your story.</p>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Main Demo Interface -->
50
+ <div class="demo-section">
51
+ <div class="row">
52
+ <div class="col-md-4 mb-3">
53
+ <label for="storyFormat" class="form-label"><strong>Story Format</strong></label>
54
+ <select id="storyFormat" class="form-select">
55
+ <option value="">Select a story format...</option>
56
+ <option value="harlowe" data-version="3.3.9">Harlowe (3.3.9)</option>
57
+ <option value="sugarcube" data-version="2.37.3">SugarCube (2.37.3)</option>
58
+ <option value="snowman" data-version="2.0.2">Snowman (2.0.2)</option>
59
+ <option value="chapbook" data-version="2.3.0">Chapbook (2.3.0)</option>
60
+ </select>
61
+ </div>
62
+
63
+ <div class="col-md-8 mb-3">
64
+ <div class="d-flex justify-content-between align-items-center">
65
+ <label for="tweeCode" class="form-label"><strong>Twee Code</strong></label>
66
+ <button id="loadExample" class="btn btn-sm btn-outline-secondary">Load Example</button>
67
+ </div>
68
+ <textarea id="tweeCode" class="form-control" rows="15"
69
+ placeholder="Enter your Twee code here..."></textarea>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="row">
74
+ <div class="col-12 text-center">
75
+ <button id="compileBtn" class="btn btn-primary btn-lg" disabled>
76
+ <span id="compileSpinner" class="spinner-border spinner-border-sm me-2" style="display: none;"></span>
77
+ <span id="compileBtnText">Compile Story</span>
78
+ </button>
79
+ </div>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Output Section -->
84
+ <div class="demo-section">
85
+ <h4>📄 Compiled Output</h4>
86
+ <div id="outputContainer" class="output-container">
87
+ <div class="loading">Select a story format and enter Twee code, then click "Compile Story" to see the output here.</div>
88
+ </div>
89
+
90
+ <div class="row mt-3">
91
+ <div class="col-6">
92
+ <button id="downloadBtn" class="btn btn-success w-100" style="display: none;">
93
+ 📥 Download HTML File
94
+ </button>
95
+ </div>
96
+ <div class="col-6">
97
+ <button id="previewBtn" class="btn btn-info w-100" style="display: none;">
98
+ 👁️ Preview in New Tab
99
+ </button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Footer -->
105
+ <div class="demo-section">
106
+ <div class="row">
107
+ <div class="col-md-12">
108
+ <h5>About This Demo</h5>
109
+ <p>This demo uses the <strong>Extwee</strong> web build to compile Twee code with story formats loaded dynamically from the <a href="https://github.com/videlais/story-formats-archive" target="_blank">Story Formats Archive</a>.</p>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- Load Dependencies -->
116
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
117
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
118
+
119
+ <!-- Load Local Extwee build -->
120
+ <script src="./extwee.core.min.js"></script>
121
+
122
+ <script>
123
+ $(document).ready(function() {
124
+ let storyFormats = {};
125
+ let currentCompiledHTML = '';
126
+
127
+ // Load story formats index
128
+ async function loadStoryFormatsIndex() {
129
+ try {
130
+ const response = await fetch('https://raw.githubusercontent.com/videlais/story-formats-archive/docs/official/index.json');
131
+ // Convert to JSON.
132
+ const data = await response.json();
133
+ // Use Twine 2 formats only.
134
+ storyFormats = data.twine2;
135
+ // Show versions on console
136
+ console.log('Loaded story formats index:', storyFormats);
137
+
138
+ } catch (error) {
139
+ console.error('Failed to load story formats index:', error);
140
+ showError('Failed to load story formats. Please check your internet connection.');
141
+ }
142
+ }
143
+
144
+ // Update format info when selection changes
145
+ $('#storyFormat').on('change', function() {
146
+ const selectedFormat = $(this).val();
147
+ const $compileBtn = $('#compileBtn');
148
+ const $formatInfo = $('#formatInfo');
149
+
150
+ if (selectedFormat) {
151
+ $compileBtn.prop('disabled', false);
152
+ $formatInfo.show();
153
+ } else {
154
+ $compileBtn.prop('disabled', true);
155
+ $formatInfo.hide();
156
+ }
157
+ });
158
+
159
+ // Load example code
160
+ $('#loadExample').on('click', function() {
161
+ const exampleCode = `:: Start
162
+ Welcome to your interactive story!
163
+
164
+ You find yourself at a crossroads in a mysterious forest.
165
+
166
+ [[Take the left path->LeftPath]]
167
+ [[Take the right path->RightPath]]
168
+
169
+ :: LeftPath
170
+ You venture down the left path and discover a hidden cottage.
171
+
172
+ The door creaks open as you approach...
173
+
174
+ [[Enter the cottage->Cottage]]
175
+ [[Return to the crossroads->Start]]
176
+
177
+ :: RightPath
178
+ The right path leads to a sparkling stream.
179
+
180
+ The water is crystal clear and you can see fish swimming below.
181
+
182
+ [[Follow the stream->Stream]]
183
+ [[Return to the crossroads->Start]]
184
+
185
+ :: Cottage
186
+ Inside the cottage, you find an old book on a dusty table.
187
+
188
+ As you open it, magical words begin to glow on the pages...
189
+
190
+ *The End*
191
+
192
+ :: Stream
193
+ You follow the stream until you reach a beautiful waterfall.
194
+
195
+ Behind the waterfall, you discover a secret cave filled with treasure!
196
+
197
+ *The End*`;
198
+
199
+ $('#tweeCode').val(exampleCode);
200
+ });
201
+
202
+ // Show error message
203
+
204
+ // Escape HTML helper to prevent XSS
205
+ function escapeHtml(str) {
206
+ return String(str)
207
+ .replace(/&/g, "&amp;")
208
+ .replace(/</g, "&lt;")
209
+ .replace(/>/g, "&gt;")
210
+ .replace(/"/g, "&quot;")
211
+ .replace(/'/g, "&#39;");
212
+ }
213
+ function showError(message) {
214
+ // Escape the error message to prevent XSS
215
+ $('#outputContainer').html(`<div class="error">❌ Error: ${escapeHtml(message)}</div>`);
216
+ }
217
+
218
+ // Show success message
219
+ function showSuccess(message) {
220
+ $('#outputContainer').html(`<div class="success">✅ ${message}</div>`);
221
+ }
222
+
223
+ // Compile story
224
+ $('#compileBtn').on('click', async function() {
225
+ const selectedFormat = $('#storyFormat').val();
226
+ const formatVersion = $('#storyFormat option:selected').data('version');
227
+ let tweeCode = $('#tweeCode').val().trim();
228
+
229
+ // Check if the input contains the "StoryData" passage
230
+ const hasStoryData = /::\s*StoryData/i.test(tweeCode);
231
+ // If it doesn't, we will add a default one
232
+ // and generate a new IFID
233
+ if (hasStoryData == false) {
234
+
235
+ const ifid = Extwee.generateIFID();
236
+ const storyDataPassage = `:: StoryData
237
+ {"ifid": "${ifid}"}`;
238
+ // Append to the start of the Twee code
239
+ tweeCode = storyDataPassage + '\n\n' + tweeCode;
240
+ console.log('Added StoryData passage with IFID:', ifid);
241
+ }
242
+
243
+ if (!selectedFormat) {
244
+ showError('Please select a story format.');
245
+ return;
246
+ }
247
+
248
+ if (!tweeCode) {
249
+ showError('Please enter some Twee code.');
250
+ return;
251
+ }
252
+
253
+ // Show loading state
254
+ const $btn = $(this);
255
+ const $spinner = $('#compileSpinner');
256
+ const $btnText = $('#compileBtnText');
257
+
258
+ $btn.prop('disabled', true);
259
+ $spinner.show();
260
+ $btnText.text('Compiling...');
261
+ $('#outputContainer').html('<div class="loading">🔄 Loading story format and compiling...</div>');
262
+
263
+ try {
264
+ // Fetch the latest version of the selected format
265
+ const formatData = storyFormats[selectedFormat];
266
+ let formatUrl = `https://raw.githubusercontent.com/videlais/story-formats-archive/docs/official/twine2/${selectedFormat}/${formatVersion}/format.js`;
267
+
268
+
269
+ console.log('Fetching format from:', formatUrl);
270
+
271
+ // Fetch the story format
272
+ const formatResponse = await fetch(formatUrl);
273
+ if (!formatResponse.ok) {
274
+ throw new Error(`Failed to fetch story format: ${formatResponse.status}`);
275
+ }
276
+
277
+ const formatCode = await formatResponse.text();
278
+ console.log('Story format loaded, size:', formatCode.length);
279
+
280
+ // Parse the story format
281
+ const storyFormat = Extwee.parseStoryFormat(formatCode);
282
+ console.log('Story format parsed:', storyFormat.name, storyFormat.version);
283
+
284
+ // Parse the Twee code
285
+ const story = Extwee.parseTwee(tweeCode);
286
+ console.log('Story parsed, passages:', story.passages.length);
287
+
288
+ // Set story format info
289
+ story.format = storyFormat.name;
290
+ story.formatVersion = storyFormat.version;
291
+
292
+ // Compile to HTML
293
+ const compiledHTML = Extwee.compileTwine2HTML(story, storyFormat);
294
+ currentCompiledHTML = compiledHTML;
295
+
296
+ // Show the compiled output (truncated for display)
297
+ const displayHTML = compiledHTML.length > 2000
298
+ ? compiledHTML.substring(0, 2000) + '\n\n... (output truncated, full HTML available for download) ...'
299
+ : compiledHTML;
300
+
301
+ $('#outputContainer').html(`<div class="success">✅ Story compiled successfully!</div><pre>${escapeHtml(displayHTML)}</pre>`);
302
+
303
+ // Show download and preview buttons
304
+ $('#downloadBtn, #previewBtn').show();
305
+
306
+ } catch (error) {
307
+ console.error('Compilation error:', error);
308
+ showError(error.message || 'An unexpected error occurred during compilation.');
309
+ } finally {
310
+ // Reset button state
311
+ $btn.prop('disabled', false);
312
+ $spinner.hide();
313
+ $btnText.text('Compile Story');
314
+ }
315
+ });
316
+
317
+ // Download compiled HTML
318
+ $('#downloadBtn').on('click', function() {
319
+ if (!currentCompiledHTML) {
320
+ showError('No compiled HTML available for download.');
321
+ return;
322
+ }
323
+
324
+ const blob = new Blob([currentCompiledHTML], { type: 'text/html' });
325
+ const url = URL.createObjectURL(blob);
326
+ const a = document.createElement('a');
327
+ a.href = url;
328
+ a.download = 'compiled-story.html';
329
+ document.body.appendChild(a);
330
+ a.click();
331
+ document.body.removeChild(a);
332
+ URL.revokeObjectURL(url);
333
+ });
334
+
335
+ // Preview compiled HTML
336
+ $('#previewBtn').on('click', function() {
337
+ if (!currentCompiledHTML) {
338
+ showError('No compiled HTML available for preview.');
339
+ return;
340
+ }
341
+
342
+ const previewWindow = window.open('', '_blank');
343
+ previewWindow.document.write(currentCompiledHTML);
344
+ previewWindow.document.close();
345
+ });
346
+
347
+ // Utility function to escape HTML
348
+ function escapeHtml(text) {
349
+ const div = document.createElement('div');
350
+ div.textContent = text;
351
+ return div.innerHTML;
352
+ }
353
+
354
+ // Initialize
355
+ loadStoryFormatsIndex();
356
+ });
357
+ </script>
358
+ </body>
359
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "extwee",
3
- "version": "2.3.3",
3
+ "version": "2.3.5",
4
4
  "description": "A story compiler tool using Twine-compatible formats",
5
5
  "author": "Dan Cox",
6
6
  "main": "index.js",
@@ -14,7 +14,8 @@
14
14
  "build:web": "webpack",
15
15
  "analyze:web": "webpack-bundle-analyzer build/extwee.web.min.js",
16
16
  "gen-types": "npx -p typescript tsc src/**/*.js --declaration --allowJs --emitDeclarationOnly --outDir types",
17
- "all": "npm run lint && npm run lint:test && npm run test && npm run build:web && npm run gen-types"
17
+ "copy:build": "cp build/*.js docs/build",
18
+ "all": "npm run lint && npm run lint:test && npm run test && npm run build:web && npm run gen-types && npm run copy:build"
18
19
  },
19
20
  "keywords": [
20
21
  "twine",
@@ -24,37 +25,37 @@
24
25
  ],
25
26
  "license": "MIT",
26
27
  "dependencies": {
27
- "commander": "^14.0.0",
28
+ "commander": "^14.0.1",
28
29
  "graphemer": "^1.4.0",
29
30
  "html-entities": "^2.6.0",
30
31
  "node-html-parser": "^7.0.1",
31
32
  "pickleparser": "^0.2.1",
32
- "semver": "^7.7.2",
33
+ "semver": "^7.7.3",
33
34
  "shelljs": "^0.10.0",
34
- "uuid": "^12.0.0"
35
+ "uuid": "^13.0.0"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@babel/cli": "^7.28.3",
38
39
  "@babel/core": "^7.28.4",
39
40
  "@babel/preset-env": "^7.28.3",
40
- "@eslint/js": "^9.35.0",
41
- "@inquirer/prompts": "^7.8.4",
42
- "@types/node": "^24.3.1",
41
+ "@eslint/js": "^9.37.0",
42
+ "@inquirer/prompts": "^7.9.0",
43
+ "@types/node": "^24.7.2",
43
44
  "@types/semver": "^7.7.1",
44
- "@types/uuid": "^10.0.0",
45
+ "@types/uuid": "^11.0.0",
45
46
  "babel-loader": "^10.0.0",
46
47
  "clean-jsdoc-theme": "^4.3.0",
47
- "core-js": "^3.45.1",
48
- "eslint": "^9.35.0",
48
+ "core-js": "^3.46.0",
49
+ "eslint": "^9.37.0",
49
50
  "eslint-plugin-jest": "^29.0.1",
50
- "eslint-plugin-jsdoc": "^54.5.0",
51
- "globals": "^16.3.0",
52
- "jest": "^30.1.3",
53
- "jest-environment-jsdom": "^30.1.2",
51
+ "eslint-plugin-jsdoc": "^61.1.4",
52
+ "globals": "^16.4.0",
53
+ "jest": "^30.2.0",
54
+ "jest-environment-jsdom": "^30.2.0",
54
55
  "regenerator-runtime": "^0.14.1",
55
- "typescript": "^5.9.2",
56
- "typescript-eslint": "^8.42.0",
57
- "webpack": "^5.101.3",
56
+ "typescript": "^5.9.3",
57
+ "typescript-eslint": "^8.46.1",
58
+ "webpack": "^5.102.1",
58
59
  "webpack-bundle-analyzer": "^4.10.2",
59
60
  "webpack-cli": "^6.0.1"
60
61
  },