extwee 2.3.3 → 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 -1
- package/build/extwee.twine1html.min.js +1 -1
- package/build/extwee.twine2archive.min.js +1 -1
- package/build/extwee.tws.min.js +1 -1
- 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/package.json +17 -16
- package/src/Story.js +1 -1
- package/src/Twine2HTML/parse-web.js +7 -1
- package/src/Web/web-core.js +22 -2
- package/src/Web/web-twine1html.js +25 -5
- package/src/Web/web-twine2archive.js +25 -5
- package/src/Web/web-tws.js +22 -4
- package/test/Twine1HTML/Twine1HTML.Parse.Web.test.js +484 -0
- package/test/Twine2ArchiveHTML/Twine2ArchiveHTML.Parse.Web.test.js +293 -0
- package/test/Twine2HTML/Twine2HTML.Parse.Web.test.js +329 -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 +7 -2
- package/types/src/Story.d.ts +1 -1
- package/types/src/Web/web-core.d.ts +23 -1
- package/types/src/Web/web-twine1html.d.ts +7 -0
- package/types/src/Web/web-twine2archive.d.ts +7 -0
- package/types/src/Web/web-tws.d.ts +5 -0
- package/webpack.config.js +2 -1
- 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, "&")
|
|
208
|
+
.replace(/</g, "<")
|
|
209
|
+
.replace(/>/g, ">")
|
|
210
|
+
.replace(/"/g, """)
|
|
211
|
+
.replace(/'/g, "'");
|
|
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
|
+
"version": "2.3.4",
|
|
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
|
-
"
|
|
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.
|
|
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
33
|
"semver": "^7.7.2",
|
|
33
34
|
"shelljs": "^0.10.0",
|
|
34
|
-
"uuid": "^
|
|
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.
|
|
41
|
-
"@inquirer/prompts": "^7.8.
|
|
42
|
-
"@types/node": "^24.
|
|
41
|
+
"@eslint/js": "^9.36.0",
|
|
42
|
+
"@inquirer/prompts": "^7.8.6",
|
|
43
|
+
"@types/node": "^24.6.1",
|
|
43
44
|
"@types/semver": "^7.7.1",
|
|
44
|
-
"@types/uuid": "^
|
|
45
|
+
"@types/uuid": "^11.0.0",
|
|
45
46
|
"babel-loader": "^10.0.0",
|
|
46
47
|
"clean-jsdoc-theme": "^4.3.0",
|
|
47
48
|
"core-js": "^3.45.1",
|
|
48
|
-
"eslint": "^9.
|
|
49
|
+
"eslint": "^9.36.0",
|
|
49
50
|
"eslint-plugin-jest": "^29.0.1",
|
|
50
|
-
"eslint-plugin-jsdoc": "^
|
|
51
|
-
"globals": "^16.
|
|
52
|
-
"jest": "^30.
|
|
53
|
-
"jest-environment-jsdom": "^30.
|
|
51
|
+
"eslint-plugin-jsdoc": "^60.7.0",
|
|
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.
|
|
56
|
-
"typescript-eslint": "^8.
|
|
57
|
-
"webpack": "^5.
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"typescript-eslint": "^8.45.0",
|
|
58
|
+
"webpack": "^5.102.0",
|
|
58
59
|
"webpack-bundle-analyzer": "^4.10.2",
|
|
59
60
|
"webpack-cli": "^6.0.1"
|
|
60
61
|
},
|
package/src/Story.js
CHANGED
|
@@ -101,11 +101,17 @@ class LightweightTwine2Parser {
|
|
|
101
101
|
parseAttributes(elementHtml) {
|
|
102
102
|
const attributes = {};
|
|
103
103
|
|
|
104
|
+
// Extract just the opening tag to avoid getting attributes from nested elements
|
|
105
|
+
const openingTagMatch = elementHtml.match(/^<[^>]*>/);
|
|
106
|
+
if (!openingTagMatch) return attributes;
|
|
107
|
+
|
|
108
|
+
const openingTag = openingTagMatch[0];
|
|
109
|
+
|
|
104
110
|
// Common attribute patterns
|
|
105
111
|
const attributeRegex = /(\w+(?:-\w+)*)=["']([^"']*)["']/g;
|
|
106
112
|
let match;
|
|
107
113
|
|
|
108
|
-
while ((match = attributeRegex.exec(
|
|
114
|
+
while ((match = attributeRegex.exec(openingTag)) !== null) {
|
|
109
115
|
attributes[match[1]] = match[2];
|
|
110
116
|
}
|
|
111
117
|
|
package/src/Web/web-core.js
CHANGED
|
@@ -10,7 +10,7 @@ import Passage from '../Passage.js';
|
|
|
10
10
|
import StoryFormat from '../StoryFormat.js';
|
|
11
11
|
|
|
12
12
|
// Core functionality - most commonly used
|
|
13
|
-
|
|
13
|
+
const Extwee = {
|
|
14
14
|
// Core parsers (immediately available)
|
|
15
15
|
parseTwee,
|
|
16
16
|
parseJSON,
|
|
@@ -27,5 +27,25 @@ window.Extwee = {
|
|
|
27
27
|
StoryFormat,
|
|
28
28
|
|
|
29
29
|
// Version info
|
|
30
|
-
version: '2.3.
|
|
30
|
+
version: '2.3.3'
|
|
31
31
|
};
|
|
32
|
+
|
|
33
|
+
// Export individual functions for ES6 module usage
|
|
34
|
+
export { parseTwee, parseJSON, parseStoryFormat, parseTwine2HTML, compileTwine2HTML, generateIFID, Story, Passage, StoryFormat };
|
|
35
|
+
|
|
36
|
+
// Export default for webpack UMD build
|
|
37
|
+
export default Extwee;
|
|
38
|
+
|
|
39
|
+
// For direct ES6 module usage, also assign to global object
|
|
40
|
+
// Use globalThis for cross-environment compatibility (browser, Node.js, Web Workers)
|
|
41
|
+
const globalObject = (function() {
|
|
42
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
43
|
+
if (typeof window !== 'undefined') return window;
|
|
44
|
+
if (typeof global !== 'undefined') return global;
|
|
45
|
+
if (typeof self !== 'undefined') return self;
|
|
46
|
+
return null;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
if (globalObject) {
|
|
50
|
+
globalObject.Extwee = Extwee;
|
|
51
|
+
}
|
|
@@ -2,14 +2,34 @@
|
|
|
2
2
|
import { parse as parseTwine1HTML } from '../Twine1HTML/parse-web.js';
|
|
3
3
|
import { compile as compileTwine1HTML } from '../Twine1HTML/compile.js';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Create UMD-compatible export object
|
|
6
|
+
const Extwee = {
|
|
7
|
+
parseTwine1HTML,
|
|
8
|
+
compileTwine1HTML,
|
|
9
|
+
parse: parseTwine1HTML, // For module consistency
|
|
10
|
+
compile: compileTwine1HTML // For module consistency
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Export for webpack UMD build
|
|
14
|
+
export default Extwee;
|
|
15
|
+
|
|
16
|
+
// Also export individual functions for ES6 module usage
|
|
6
17
|
export {
|
|
7
18
|
parseTwine1HTML as parse,
|
|
8
19
|
compileTwine1HTML as compile
|
|
9
20
|
};
|
|
10
21
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
window
|
|
22
|
+
// Add to global Extwee object for direct usage
|
|
23
|
+
const globalObject = (function() {
|
|
24
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
25
|
+
if (typeof window !== 'undefined') return window;
|
|
26
|
+
if (typeof global !== 'undefined') return global;
|
|
27
|
+
if (typeof self !== 'undefined') return self;
|
|
28
|
+
return null;
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
if (globalObject) {
|
|
32
|
+
globalObject.Extwee = globalObject.Extwee || {};
|
|
33
|
+
globalObject.Extwee.parseTwine1HTML = parseTwine1HTML;
|
|
34
|
+
globalObject.Extwee.compileTwine1HTML = compileTwine1HTML;
|
|
15
35
|
}
|
|
@@ -2,14 +2,34 @@
|
|
|
2
2
|
import { parse as parseTwine2ArchiveHTML } from '../Twine2ArchiveHTML/parse-web.js';
|
|
3
3
|
import { compile as compileTwine2ArchiveHTML } from '../Twine2ArchiveHTML/compile.js';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Create UMD-compatible export object
|
|
6
|
+
const Extwee = {
|
|
7
|
+
parseTwine2ArchiveHTML,
|
|
8
|
+
compileTwine2ArchiveHTML,
|
|
9
|
+
parse: parseTwine2ArchiveHTML, // For module consistency
|
|
10
|
+
compile: compileTwine2ArchiveHTML // For module consistency
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Export for webpack UMD build
|
|
14
|
+
export default Extwee;
|
|
15
|
+
|
|
16
|
+
// Also export individual functions for ES6 module usage
|
|
6
17
|
export {
|
|
7
18
|
parseTwine2ArchiveHTML as parse,
|
|
8
19
|
compileTwine2ArchiveHTML as compile
|
|
9
20
|
};
|
|
10
21
|
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
window
|
|
22
|
+
// Add to global Extwee object for direct usage
|
|
23
|
+
const globalObject = (function() {
|
|
24
|
+
if (typeof globalThis !== 'undefined') return globalThis;
|
|
25
|
+
if (typeof window !== 'undefined') return window;
|
|
26
|
+
if (typeof global !== 'undefined') return global;
|
|
27
|
+
if (typeof self !== 'undefined') return self;
|
|
28
|
+
return null;
|
|
29
|
+
})();
|
|
30
|
+
|
|
31
|
+
if (globalObject) {
|
|
32
|
+
globalObject.Extwee = globalObject.Extwee || {};
|
|
33
|
+
globalObject.Extwee.parseTwine2ArchiveHTML = parseTwine2ArchiveHTML;
|
|
34
|
+
globalObject.Extwee.compileTwine2ArchiveHTML = compileTwine2ArchiveHTML;
|
|
15
35
|
}
|