@rohal12/twee-ts 1.1.1
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/README.md +86 -0
- package/dist/bin/twee-ts.js +2247 -0
- package/dist/bin/twee-ts.js.map +1 -0
- package/dist/chunk-QPA7RLK5.js +1783 -0
- package/dist/chunk-QPA7RLK5.js.map +1 -0
- package/dist/index.cjs +2212 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +352 -0
- package/dist/index.d.ts +352 -0
- package/dist/index.js +2160 -0
- package/dist/index.js.map +1 -0
- package/dist/plugins/rollup.d.ts +23 -0
- package/dist/plugins/rollup.js +34 -0
- package/dist/plugins/rollup.js.map +1 -0
- package/dist/plugins/vite.d.ts +44 -0
- package/dist/plugins/vite.js +46 -0
- package/dist/plugins/vite.js.map +1 -0
- package/dist/types-B3qtPxs8.d.ts +43 -0
- package/package.json +70 -0
|
@@ -0,0 +1,1783 @@
|
|
|
1
|
+
// src/compiler.ts
|
|
2
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
3
|
+
|
|
4
|
+
// src/ifid.ts
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
function generateIFID() {
|
|
7
|
+
return randomUUID().toUpperCase();
|
|
8
|
+
}
|
|
9
|
+
function validateIFID(ifid) {
|
|
10
|
+
let uuid = ifid;
|
|
11
|
+
switch (ifid.length) {
|
|
12
|
+
case 36:
|
|
13
|
+
break;
|
|
14
|
+
case 45: {
|
|
15
|
+
if (ifid.slice(0, 7).toUpperCase() !== "UUID://" || ifid.slice(43) !== "//") {
|
|
16
|
+
return "invalid IFID UUID://...// format";
|
|
17
|
+
}
|
|
18
|
+
uuid = ifid.slice(7, 43);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
default:
|
|
22
|
+
return `invalid IFID length: ${ifid.length}`;
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < uuid.length; i++) {
|
|
25
|
+
const ch = uuid[i];
|
|
26
|
+
switch (i) {
|
|
27
|
+
case 8:
|
|
28
|
+
case 13:
|
|
29
|
+
case 18:
|
|
30
|
+
case 23:
|
|
31
|
+
if (ch !== "-") {
|
|
32
|
+
return `invalid IFID character '${ch}' at position ${i + 1}`;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 14:
|
|
36
|
+
if (ch < "1" || ch > "5") {
|
|
37
|
+
return `invalid version '${ch}' at position ${i + 1}`;
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case 19:
|
|
41
|
+
if (!["8", "9", "a", "A", "b", "B"].includes(ch)) {
|
|
42
|
+
return `invalid variant '${ch}' at position ${i + 1}`;
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
default:
|
|
46
|
+
if (!/^[0-9a-fA-F]$/.test(ch)) {
|
|
47
|
+
return `invalid IFID hex value '${ch}' at position ${i + 1}`;
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/escape.ts
|
|
56
|
+
function attrEscape(s) {
|
|
57
|
+
if (s.length === 0) return s;
|
|
58
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/'/g, "'");
|
|
59
|
+
}
|
|
60
|
+
function htmlEscape(s) {
|
|
61
|
+
if (s.length === 0) return s;
|
|
62
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
63
|
+
}
|
|
64
|
+
function tiddlerEscape(s) {
|
|
65
|
+
if (s.length === 0) return s;
|
|
66
|
+
return s.replace(/\\/g, "\\s").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/\t/g, "\\t").replace(/\n/g, "\\n");
|
|
67
|
+
}
|
|
68
|
+
function tweeEscape(s) {
|
|
69
|
+
if (s.length === 0) return s;
|
|
70
|
+
return s.replace(/[\\[\]{}]/g, (ch) => "\\" + ch);
|
|
71
|
+
}
|
|
72
|
+
function tweeUnescape(s) {
|
|
73
|
+
if (s.length === 0) return s;
|
|
74
|
+
let result = "";
|
|
75
|
+
for (let i = 0; i < s.length; i++) {
|
|
76
|
+
if (s[i] === "\\") {
|
|
77
|
+
i++;
|
|
78
|
+
if (i >= s.length) break;
|
|
79
|
+
}
|
|
80
|
+
result += s[i];
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/passage.ts
|
|
86
|
+
var INFO_PASSAGE_NAMES = /* @__PURE__ */ new Set([
|
|
87
|
+
"StoryAuthor",
|
|
88
|
+
"StoryInit",
|
|
89
|
+
"StoryMenu",
|
|
90
|
+
"StorySubtitle",
|
|
91
|
+
"StoryTitle",
|
|
92
|
+
"PassageReady",
|
|
93
|
+
"PassageDone",
|
|
94
|
+
"PassageHeader",
|
|
95
|
+
"PassageFooter",
|
|
96
|
+
"StoryBanner",
|
|
97
|
+
"StoryCaption",
|
|
98
|
+
"MenuOptions",
|
|
99
|
+
"MenuShare",
|
|
100
|
+
"MenuStory",
|
|
101
|
+
"StoryInterface",
|
|
102
|
+
"StoryShare",
|
|
103
|
+
"StorySettings",
|
|
104
|
+
"StoryData",
|
|
105
|
+
"StoryIncludes"
|
|
106
|
+
]);
|
|
107
|
+
var INFO_TAGS = ["annotation", "script", "stylesheet", "widget"];
|
|
108
|
+
function hasTag(p, tag) {
|
|
109
|
+
return p.tags.includes(tag);
|
|
110
|
+
}
|
|
111
|
+
function hasAnyTag(p, ...tags) {
|
|
112
|
+
return p.tags.some((t) => tags.includes(t));
|
|
113
|
+
}
|
|
114
|
+
function hasTagStartingWith(p, prefix) {
|
|
115
|
+
return p.tags.some((t) => t.startsWith(prefix));
|
|
116
|
+
}
|
|
117
|
+
function hasInfoTags(p) {
|
|
118
|
+
return hasAnyTag(p, ...INFO_TAGS) || hasTagStartingWith(p, "Twine.");
|
|
119
|
+
}
|
|
120
|
+
function hasInfoName(p) {
|
|
121
|
+
return INFO_PASSAGE_NAMES.has(p.name);
|
|
122
|
+
}
|
|
123
|
+
function isStoryPassage(p) {
|
|
124
|
+
return !hasInfoName(p) && !hasInfoTags(p);
|
|
125
|
+
}
|
|
126
|
+
function hasMetadataPosition(p) {
|
|
127
|
+
return p.metadata != null && p.metadata.position != null && p.metadata.position !== "";
|
|
128
|
+
}
|
|
129
|
+
function hasMetadataSize(p) {
|
|
130
|
+
return p.metadata != null && p.metadata.size != null && p.metadata.size !== "";
|
|
131
|
+
}
|
|
132
|
+
function hasAnyMetadata(p) {
|
|
133
|
+
return hasMetadataPosition(p) || hasMetadataSize(p);
|
|
134
|
+
}
|
|
135
|
+
function marshalMetadata(meta) {
|
|
136
|
+
const obj = {};
|
|
137
|
+
if (meta.position) obj.position = meta.position;
|
|
138
|
+
if (meta.size) obj.size = meta.size;
|
|
139
|
+
return JSON.stringify(obj);
|
|
140
|
+
}
|
|
141
|
+
function passageToTwee(p, outMode) {
|
|
142
|
+
let output;
|
|
143
|
+
if (outMode === "twee3") {
|
|
144
|
+
output = ":: " + tweeEscape(p.name);
|
|
145
|
+
if (p.tags.length > 0) {
|
|
146
|
+
output += " [" + tweeEscape(p.tags.join(" ")) + "]";
|
|
147
|
+
}
|
|
148
|
+
if (hasAnyMetadata(p) && p.metadata) {
|
|
149
|
+
output += " " + marshalMetadata(p.metadata);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
output = ":: " + p.name;
|
|
153
|
+
if (p.tags.length > 0) {
|
|
154
|
+
output += " [" + p.tags.join(" ") + "]";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
output += "\n";
|
|
158
|
+
if (p.text.length > 0) {
|
|
159
|
+
output += p.text + "\n";
|
|
160
|
+
}
|
|
161
|
+
output += "\n\n";
|
|
162
|
+
return output;
|
|
163
|
+
}
|
|
164
|
+
function passageToPassagedata(p, pid) {
|
|
165
|
+
let position;
|
|
166
|
+
let size;
|
|
167
|
+
if (hasMetadataPosition(p)) {
|
|
168
|
+
position = p.metadata.position;
|
|
169
|
+
} else {
|
|
170
|
+
const x = pid % 10;
|
|
171
|
+
let y = Math.floor(pid / 10);
|
|
172
|
+
const xp = x === 0 ? 10 : x;
|
|
173
|
+
const yp = x === 0 ? y : y + 1;
|
|
174
|
+
position = `${xp * 125 - 25},${yp * 125 - 25}`;
|
|
175
|
+
}
|
|
176
|
+
if (hasMetadataSize(p)) {
|
|
177
|
+
size = p.metadata.size;
|
|
178
|
+
} else {
|
|
179
|
+
size = "100,100";
|
|
180
|
+
}
|
|
181
|
+
return `<tw-passagedata pid="${pid}" name=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} position=${quote(attrEscape(position))} size=${quote(attrEscape(size))}>${htmlEscape(p.text)}</tw-passagedata>`;
|
|
182
|
+
}
|
|
183
|
+
function passageToTiddler(p, pid) {
|
|
184
|
+
let position;
|
|
185
|
+
if (hasMetadataPosition(p)) {
|
|
186
|
+
position = p.metadata.position;
|
|
187
|
+
} else {
|
|
188
|
+
const x = pid % 10;
|
|
189
|
+
let y = Math.floor(pid / 10);
|
|
190
|
+
const xp = x === 0 ? 10 : x;
|
|
191
|
+
const yp = x === 0 ? y : y + 1;
|
|
192
|
+
position = `${xp * 140 - 130},${yp * 140 - 130}`;
|
|
193
|
+
}
|
|
194
|
+
return `<div tiddler=${quote(attrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} twine-position=${quote(attrEscape(position))}>${tiddlerEscape(p.text)}</div>`;
|
|
195
|
+
}
|
|
196
|
+
function countWords(p) {
|
|
197
|
+
let text = p.text;
|
|
198
|
+
text = text.replace(/\n/g, "");
|
|
199
|
+
text = text.replace(/(?:\/%.+?%\/|\/\*.+?\*\/|<!--.+?-->)/gs, "");
|
|
200
|
+
const normalized = text.normalize("NFKD");
|
|
201
|
+
const count = [...normalized].length;
|
|
202
|
+
if (count === 0) return 0;
|
|
203
|
+
const words = Math.floor(count / 5);
|
|
204
|
+
return count % 5 > 0 ? words + 1 : words;
|
|
205
|
+
}
|
|
206
|
+
function applyTagAliases(passages, aliases) {
|
|
207
|
+
const entries = Object.entries(aliases);
|
|
208
|
+
if (entries.length === 0) return;
|
|
209
|
+
for (const p of passages) {
|
|
210
|
+
const original = [...p.tags];
|
|
211
|
+
for (const [alias, canonical] of entries) {
|
|
212
|
+
if (original.includes(alias) && !p.tags.includes(canonical)) {
|
|
213
|
+
p.tags.push(canonical);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function quote(s) {
|
|
219
|
+
return `"${s}"`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/story.ts
|
|
223
|
+
function createStory() {
|
|
224
|
+
return {
|
|
225
|
+
name: "",
|
|
226
|
+
ifid: "",
|
|
227
|
+
passages: [],
|
|
228
|
+
legacyIFID: "",
|
|
229
|
+
twine1: { settings: /* @__PURE__ */ new Map() },
|
|
230
|
+
twine2: {
|
|
231
|
+
format: "",
|
|
232
|
+
formatVersion: "",
|
|
233
|
+
options: /* @__PURE__ */ new Map(),
|
|
234
|
+
start: "",
|
|
235
|
+
tagColors: /* @__PURE__ */ new Map(),
|
|
236
|
+
zoom: 1
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function storyHas(story, name) {
|
|
241
|
+
return story.passages.some((p) => p.name === name);
|
|
242
|
+
}
|
|
243
|
+
function storyIndex(story, name) {
|
|
244
|
+
return story.passages.findIndex((p) => p.name === name);
|
|
245
|
+
}
|
|
246
|
+
function storyAppend(story, p, diagnostics) {
|
|
247
|
+
const i = storyIndex(story, p.name);
|
|
248
|
+
if (i === -1) {
|
|
249
|
+
story.passages.push(p);
|
|
250
|
+
} else {
|
|
251
|
+
diagnostics.push({
|
|
252
|
+
level: "warning",
|
|
253
|
+
message: `Replacing existing passage "${p.name}" with duplicate.`
|
|
254
|
+
});
|
|
255
|
+
story.passages[i] = p;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function storyPrepend(story, p, diagnostics) {
|
|
259
|
+
const i = storyIndex(story, p.name);
|
|
260
|
+
if (i === -1) {
|
|
261
|
+
story.passages.unshift(p);
|
|
262
|
+
} else {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
level: "warning",
|
|
265
|
+
message: `Replacing existing passage "${p.name}" with duplicate.`
|
|
266
|
+
});
|
|
267
|
+
story.passages[i] = p;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function marshalStoryData(story) {
|
|
271
|
+
const data = {};
|
|
272
|
+
if (story.ifid) data.ifid = story.ifid;
|
|
273
|
+
if (story.twine2.format) data.format = story.twine2.format;
|
|
274
|
+
if (story.twine2.formatVersion) data["format-version"] = story.twine2.formatVersion;
|
|
275
|
+
const options = [];
|
|
276
|
+
for (const [opt, val] of story.twine2.options) {
|
|
277
|
+
if (val) options.push(opt);
|
|
278
|
+
}
|
|
279
|
+
if (options.length > 0) data.options = options;
|
|
280
|
+
if (story.twine2.start) data.start = story.twine2.start;
|
|
281
|
+
if (story.twine2.tagColors.size > 0) {
|
|
282
|
+
data["tag-colors"] = Object.fromEntries(story.twine2.tagColors);
|
|
283
|
+
}
|
|
284
|
+
if (story.twine2.zoom !== 1) data.zoom = story.twine2.zoom;
|
|
285
|
+
return JSON.stringify(data, null, " ");
|
|
286
|
+
}
|
|
287
|
+
function unmarshalStoryData(story, json) {
|
|
288
|
+
let data;
|
|
289
|
+
try {
|
|
290
|
+
data = JSON.parse(json);
|
|
291
|
+
} catch (e) {
|
|
292
|
+
return `Cannot unmarshal "StoryData"; ${e instanceof Error ? e.message : String(e)}`;
|
|
293
|
+
}
|
|
294
|
+
if (data.ifid) story.ifid = data.ifid.toUpperCase();
|
|
295
|
+
if (data.format) story.twine2.format = data.format;
|
|
296
|
+
if (data["format-version"]) story.twine2.formatVersion = data["format-version"];
|
|
297
|
+
if (data.options) {
|
|
298
|
+
for (const opt of data.options) {
|
|
299
|
+
story.twine2.options.set(opt, true);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
if (data.start) story.twine2.start = data.start;
|
|
303
|
+
if (data["tag-colors"]) {
|
|
304
|
+
for (const [tag, color] of Object.entries(data["tag-colors"])) {
|
|
305
|
+
story.twine2.tagColors.set(tag, color);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (data.zoom != null && data.zoom !== 0) story.twine2.zoom = data.zoom;
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function unmarshalStorySettings(story, text, diagnostics) {
|
|
312
|
+
const obsolete = [];
|
|
313
|
+
for (const rawLine of text.split("\n")) {
|
|
314
|
+
const line = rawLine.trim();
|
|
315
|
+
if (line.length === 0) continue;
|
|
316
|
+
const colonIdx = line.indexOf(":");
|
|
317
|
+
if (colonIdx === -1) {
|
|
318
|
+
diagnostics.push({
|
|
319
|
+
level: "warning",
|
|
320
|
+
message: `Malformed "StorySettings" entry; skipping "${line}".`
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const key = line.slice(0, colonIdx).trim().toLowerCase();
|
|
325
|
+
const val = line.slice(colonIdx + 1).trim().toLowerCase();
|
|
326
|
+
switch (key) {
|
|
327
|
+
case "ifid": {
|
|
328
|
+
const err = validateIFID(val);
|
|
329
|
+
if (err === null) {
|
|
330
|
+
story.legacyIFID = val.toUpperCase();
|
|
331
|
+
}
|
|
332
|
+
obsolete.push('"ifid"');
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
case "zoom":
|
|
336
|
+
obsolete.push('"zoom"');
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
story.twine1.settings.set(key, val);
|
|
340
|
+
}
|
|
341
|
+
if (obsolete.length > 0) {
|
|
342
|
+
const entries = obsolete.length === 1 ? "entry" : "entries";
|
|
343
|
+
const pronoun = obsolete.length === 1 ? "it" : "them";
|
|
344
|
+
diagnostics.push({
|
|
345
|
+
level: "warning",
|
|
346
|
+
message: `Detected obsolete "StorySettings" ${entries}: ${obsolete.join(", ")}. Please remove ${pronoun} from the "StorySettings" special passage.`
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function storyAdd(story, p, diagnostics) {
|
|
351
|
+
switch (p.name) {
|
|
352
|
+
case "StoryIncludes":
|
|
353
|
+
diagnostics.push({
|
|
354
|
+
level: "warning",
|
|
355
|
+
message: 'Ignoring "StoryIncludes" compiler special passage; twee-ts allows you to specify project files and/or directories to recursively search.'
|
|
356
|
+
});
|
|
357
|
+
break;
|
|
358
|
+
case "StoryData": {
|
|
359
|
+
const err = unmarshalStoryData(story, p.text);
|
|
360
|
+
if (err === null) {
|
|
361
|
+
if (story.ifid.length > 0) {
|
|
362
|
+
const vErr = validateIFID(story.ifid);
|
|
363
|
+
if (vErr !== null) {
|
|
364
|
+
diagnostics.push({
|
|
365
|
+
level: "error",
|
|
366
|
+
message: `Cannot validate IFID; ${vErr}.`
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
p.text = marshalStoryData(story);
|
|
371
|
+
} else {
|
|
372
|
+
diagnostics.push({
|
|
373
|
+
level: "error",
|
|
374
|
+
message: `Cannot unmarshal "StoryData" compiler special passage; ${err}.`
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
case "StorySettings":
|
|
380
|
+
unmarshalStorySettings(story, p.text, diagnostics);
|
|
381
|
+
break;
|
|
382
|
+
case "StoryTitle":
|
|
383
|
+
p.text = p.text.trim();
|
|
384
|
+
story.name = p.text;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
storyAppend(story, p, diagnostics);
|
|
388
|
+
}
|
|
389
|
+
function getStoryStats(story) {
|
|
390
|
+
let storyPassages = 0;
|
|
391
|
+
let words = 0;
|
|
392
|
+
for (const p of story.passages) {
|
|
393
|
+
if (isStoryPassage(p)) {
|
|
394
|
+
storyPassages++;
|
|
395
|
+
words += countWords(p);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return { passages: story.passages.length, storyPassages, words };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/filesystem.ts
|
|
402
|
+
import { readdirSync, statSync, watch as fsWatch } from "fs";
|
|
403
|
+
import { resolve, relative, join } from "path";
|
|
404
|
+
|
|
405
|
+
// src/media-types.ts
|
|
406
|
+
import { extname } from "path";
|
|
407
|
+
var MEDIA_TYPE_MAP = {
|
|
408
|
+
// Audio
|
|
409
|
+
aac: "audio/aac",
|
|
410
|
+
flac: "audio/flac",
|
|
411
|
+
ogg: "audio/ogg",
|
|
412
|
+
wav: "audio/wav",
|
|
413
|
+
mp3: "audio/mpeg",
|
|
414
|
+
m4a: "audio/mp4",
|
|
415
|
+
oga: "audio/ogg",
|
|
416
|
+
opus: "audio/ogg",
|
|
417
|
+
wave: "audio/wav",
|
|
418
|
+
weba: "audio/webm",
|
|
419
|
+
// Fonts
|
|
420
|
+
otf: "font/otf",
|
|
421
|
+
ttf: "font/ttf",
|
|
422
|
+
woff: "font/woff",
|
|
423
|
+
woff2: "font/woff2",
|
|
424
|
+
// Images
|
|
425
|
+
gif: "image/gif",
|
|
426
|
+
jpeg: "image/jpeg",
|
|
427
|
+
jpg: "image/jpeg",
|
|
428
|
+
png: "image/png",
|
|
429
|
+
svg: "image/svg+xml",
|
|
430
|
+
tif: "image/tiff",
|
|
431
|
+
tiff: "image/tiff",
|
|
432
|
+
webp: "image/webp",
|
|
433
|
+
// Metadata
|
|
434
|
+
vtt: "text/vtt",
|
|
435
|
+
// Video
|
|
436
|
+
mp4: "video/mp4",
|
|
437
|
+
ogv: "video/ogg",
|
|
438
|
+
webm: "video/webm"
|
|
439
|
+
};
|
|
440
|
+
function mediaTypeFromExt(ext) {
|
|
441
|
+
return MEDIA_TYPE_MAP[ext] ?? "";
|
|
442
|
+
}
|
|
443
|
+
function mediaTypeFromFilename(filename) {
|
|
444
|
+
return mediaTypeFromExt(normalizedFileExt(filename));
|
|
445
|
+
}
|
|
446
|
+
function normalizedFileExt(filename) {
|
|
447
|
+
const ext = extname(filename);
|
|
448
|
+
if (!ext) return "";
|
|
449
|
+
return ext.slice(1).toLowerCase();
|
|
450
|
+
}
|
|
451
|
+
function slugify(original) {
|
|
452
|
+
return original.replace(/[\x00-\x20!-/:-@[-^`{-\x9f]+/g, "_");
|
|
453
|
+
}
|
|
454
|
+
function fontFormatHint(ext) {
|
|
455
|
+
switch (ext) {
|
|
456
|
+
case "ttf":
|
|
457
|
+
return "truetype";
|
|
458
|
+
case "otf":
|
|
459
|
+
return "opentype";
|
|
460
|
+
default:
|
|
461
|
+
return ext;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/filesystem.ts
|
|
466
|
+
function getFilenames(pathnames, outFilename) {
|
|
467
|
+
const filenames = [];
|
|
468
|
+
const absOutFile = outFilename ? resolve(outFilename) : "";
|
|
469
|
+
function walk(pathname) {
|
|
470
|
+
let stat;
|
|
471
|
+
try {
|
|
472
|
+
stat = statSync(pathname);
|
|
473
|
+
} catch {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (stat.isFile()) {
|
|
477
|
+
const abs = resolve(pathname);
|
|
478
|
+
if (abs === absOutFile) return;
|
|
479
|
+
const rel = relative(process.cwd(), abs);
|
|
480
|
+
filenames.push(rel || abs);
|
|
481
|
+
} else if (stat.isDirectory()) {
|
|
482
|
+
let entries;
|
|
483
|
+
try {
|
|
484
|
+
entries = readdirSync(pathname);
|
|
485
|
+
} catch {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (const entry of entries) {
|
|
489
|
+
walk(join(pathname, entry));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
for (const pathname of pathnames) {
|
|
494
|
+
walk(pathname);
|
|
495
|
+
}
|
|
496
|
+
return filenames;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/formats.ts
|
|
500
|
+
import { readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
501
|
+
import { join as join2 } from "path";
|
|
502
|
+
import { homedir } from "os";
|
|
503
|
+
function parseFormatJSON(source, formatId) {
|
|
504
|
+
const first = source.indexOf("{");
|
|
505
|
+
const last = source.lastIndexOf("}");
|
|
506
|
+
if (first === -1 || last === -1) return null;
|
|
507
|
+
let chunk = source.slice(first, last + 1);
|
|
508
|
+
try {
|
|
509
|
+
return JSON.parse(chunk);
|
|
510
|
+
} catch {
|
|
511
|
+
if (formatId.toLowerCase().startsWith("harlowe")) {
|
|
512
|
+
const setupIdx = chunk.lastIndexOf(',"setup": function');
|
|
513
|
+
if (setupIdx !== -1) {
|
|
514
|
+
chunk = chunk.slice(0, setupIdx) + "}";
|
|
515
|
+
try {
|
|
516
|
+
return JSON.parse(chunk);
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
function readUTF8(filename) {
|
|
525
|
+
let content = readFileSync(filename, "utf-8");
|
|
526
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
527
|
+
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
528
|
+
return content;
|
|
529
|
+
}
|
|
530
|
+
function discoverFormats(searchDirs) {
|
|
531
|
+
const formats = /* @__PURE__ */ new Map();
|
|
532
|
+
const baseFilenames = ["format.js", "header.html"];
|
|
533
|
+
for (const searchDir of searchDirs) {
|
|
534
|
+
try {
|
|
535
|
+
const stat = statSync2(searchDir);
|
|
536
|
+
if (!stat.isDirectory()) continue;
|
|
537
|
+
} catch {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
let entries;
|
|
541
|
+
try {
|
|
542
|
+
entries = readdirSync2(searchDir);
|
|
543
|
+
} catch {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
for (const entry of entries) {
|
|
547
|
+
const formatDir = join2(searchDir, entry);
|
|
548
|
+
try {
|
|
549
|
+
if (!statSync2(formatDir).isDirectory()) continue;
|
|
550
|
+
} catch {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
for (const baseFilename of baseFilenames) {
|
|
554
|
+
const formatFilename = join2(formatDir, baseFilename);
|
|
555
|
+
try {
|
|
556
|
+
const stat = statSync2(formatFilename);
|
|
557
|
+
if (!stat.isFile()) continue;
|
|
558
|
+
} catch {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const isTwine2 = baseFilename === "format.js";
|
|
562
|
+
const format = {
|
|
563
|
+
id: entry,
|
|
564
|
+
filename: formatFilename,
|
|
565
|
+
isTwine2,
|
|
566
|
+
name: "",
|
|
567
|
+
version: "",
|
|
568
|
+
proofing: false
|
|
569
|
+
};
|
|
570
|
+
if (isTwine2) {
|
|
571
|
+
try {
|
|
572
|
+
const source = readUTF8(formatFilename);
|
|
573
|
+
const data = parseFormatJSON(source, entry);
|
|
574
|
+
if (!data) continue;
|
|
575
|
+
format.name = data.name;
|
|
576
|
+
format.version = data.version;
|
|
577
|
+
format.proofing = data.proofing ?? false;
|
|
578
|
+
} catch {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
formats.set(entry, format);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return formats;
|
|
588
|
+
}
|
|
589
|
+
function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
|
|
590
|
+
const subdirNames = ["storyformats", ".storyformats", "story-formats", "storyFormats", "targets"];
|
|
591
|
+
const basePaths = /* @__PURE__ */ new Set();
|
|
592
|
+
try {
|
|
593
|
+
basePaths.add(homedir());
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
basePaths.add(process.cwd());
|
|
597
|
+
const dirs = [];
|
|
598
|
+
for (const base of basePaths) {
|
|
599
|
+
for (const sub of subdirNames) {
|
|
600
|
+
const dir = join2(base, sub);
|
|
601
|
+
try {
|
|
602
|
+
if (statSync2(dir).isDirectory()) {
|
|
603
|
+
dirs.push(dir);
|
|
604
|
+
}
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
dirs.push(...extraPaths);
|
|
610
|
+
if (useTweegoPath) {
|
|
611
|
+
const tweegoPath = process.env.TWEEGO_PATH;
|
|
612
|
+
if (tweegoPath) {
|
|
613
|
+
dirs.push(...tweegoPath.split(process.platform === "win32" ? ";" : ":"));
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return dirs;
|
|
617
|
+
}
|
|
618
|
+
function parseSemver(v) {
|
|
619
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
|
|
620
|
+
if (!m) return null;
|
|
621
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
622
|
+
}
|
|
623
|
+
function semverCompare(a, b) {
|
|
624
|
+
for (let i = 0; i < 3; i++) {
|
|
625
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
626
|
+
}
|
|
627
|
+
return 0;
|
|
628
|
+
}
|
|
629
|
+
function getFormatIdByNameAndVersion(formats, name, version) {
|
|
630
|
+
const wanted = parseSemver(version);
|
|
631
|
+
let bestVersion = null;
|
|
632
|
+
let bestId;
|
|
633
|
+
for (const [id, f] of formats) {
|
|
634
|
+
if (!f.isTwine2 || f.name !== name) continue;
|
|
635
|
+
const have = parseSemver(f.version);
|
|
636
|
+
if (!have) continue;
|
|
637
|
+
if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
|
|
638
|
+
if (!bestVersion || semverCompare(have, bestVersion) > 0) {
|
|
639
|
+
bestVersion = have;
|
|
640
|
+
bestId = id;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return bestId;
|
|
645
|
+
}
|
|
646
|
+
function readFormatSource(format) {
|
|
647
|
+
const source = readUTF8(format.filename);
|
|
648
|
+
if (format.isTwine2) {
|
|
649
|
+
const data = parseFormatJSON(source, format.id);
|
|
650
|
+
if (!data) throw new Error(`Cannot parse format ${format.id} JSON`);
|
|
651
|
+
return data.source;
|
|
652
|
+
}
|
|
653
|
+
return source;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/loader.ts
|
|
657
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
658
|
+
import { basename } from "path";
|
|
659
|
+
|
|
660
|
+
// src/lexer.ts
|
|
661
|
+
var EOF = -1;
|
|
662
|
+
var HEADER_DELIM = "::";
|
|
663
|
+
var NEWLINE_HEADER_DELIM = "\n::";
|
|
664
|
+
var LexerContext = class {
|
|
665
|
+
input;
|
|
666
|
+
line = 1;
|
|
667
|
+
start = 0;
|
|
668
|
+
pos = 0;
|
|
669
|
+
items = [];
|
|
670
|
+
constructor(input) {
|
|
671
|
+
this.input = input;
|
|
672
|
+
}
|
|
673
|
+
next() {
|
|
674
|
+
if (this.pos >= this.input.length) return EOF;
|
|
675
|
+
const ch = this.input.charCodeAt(this.pos);
|
|
676
|
+
this.pos++;
|
|
677
|
+
if (ch === 10) this.line++;
|
|
678
|
+
return ch;
|
|
679
|
+
}
|
|
680
|
+
peek() {
|
|
681
|
+
if (this.pos >= this.input.length) return EOF;
|
|
682
|
+
return this.input.charCodeAt(this.pos);
|
|
683
|
+
}
|
|
684
|
+
backup() {
|
|
685
|
+
if (this.pos > this.start) {
|
|
686
|
+
this.pos--;
|
|
687
|
+
if (this.input.charCodeAt(this.pos) === 10) this.line--;
|
|
688
|
+
} else {
|
|
689
|
+
throw new Error("backup would leave pos < start");
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
emit(type) {
|
|
693
|
+
const val = this.input.slice(this.start, this.pos);
|
|
694
|
+
this.items.push({ type, line: this.line, pos: this.start, val });
|
|
695
|
+
if (type === 6 /* Content */) {
|
|
696
|
+
for (let i = this.start; i < this.pos; i++) {
|
|
697
|
+
if (this.input.charCodeAt(i) === 10) this.line++;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
this.start = this.pos;
|
|
701
|
+
}
|
|
702
|
+
ignore() {
|
|
703
|
+
for (let i = this.start; i < this.pos; i++) {
|
|
704
|
+
if (this.input.charCodeAt(i) === 10) this.line++;
|
|
705
|
+
}
|
|
706
|
+
this.start = this.pos;
|
|
707
|
+
}
|
|
708
|
+
acceptRun(valid) {
|
|
709
|
+
let ch;
|
|
710
|
+
for (ch = this.next(); ch !== EOF && valid.includes(String.fromCharCode(ch)); ch = this.next()) {
|
|
711
|
+
}
|
|
712
|
+
if (ch !== EOF) this.backup();
|
|
713
|
+
}
|
|
714
|
+
errorf(message) {
|
|
715
|
+
this.items.push({
|
|
716
|
+
type: 0 /* Error */,
|
|
717
|
+
line: this.line,
|
|
718
|
+
pos: this.start,
|
|
719
|
+
val: message
|
|
720
|
+
});
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
/** Drain all pending items and return them. */
|
|
724
|
+
drainItems() {
|
|
725
|
+
const result = this.items;
|
|
726
|
+
this.items = [];
|
|
727
|
+
return result;
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
function acceptQuoted(ctx, quote2) {
|
|
731
|
+
for (; ; ) {
|
|
732
|
+
const ch = ctx.next();
|
|
733
|
+
switch (ch) {
|
|
734
|
+
case 92: {
|
|
735
|
+
const r = ctx.next();
|
|
736
|
+
if (r !== 10 && r !== EOF) break;
|
|
737
|
+
}
|
|
738
|
+
// falls through
|
|
739
|
+
case 10:
|
|
740
|
+
case EOF:
|
|
741
|
+
return "unterminated quoted string";
|
|
742
|
+
default:
|
|
743
|
+
if (ch === quote2) return null;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function lexProlog(ctx) {
|
|
749
|
+
if (ctx.input.startsWith(HEADER_DELIM, ctx.pos)) {
|
|
750
|
+
return lexHeaderDelim;
|
|
751
|
+
}
|
|
752
|
+
const i = ctx.input.indexOf(NEWLINE_HEADER_DELIM, ctx.pos);
|
|
753
|
+
if (i > -1) {
|
|
754
|
+
ctx.pos = i + 1;
|
|
755
|
+
ctx.ignore();
|
|
756
|
+
return lexHeaderDelim;
|
|
757
|
+
}
|
|
758
|
+
ctx.emit(1 /* EOF */);
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
function lexContent(ctx) {
|
|
762
|
+
if (ctx.input.startsWith(HEADER_DELIM, ctx.pos)) {
|
|
763
|
+
return lexHeaderDelim;
|
|
764
|
+
}
|
|
765
|
+
const i = ctx.input.indexOf(NEWLINE_HEADER_DELIM, ctx.pos);
|
|
766
|
+
if (i > -1) {
|
|
767
|
+
ctx.pos = i + 1;
|
|
768
|
+
ctx.emit(6 /* Content */);
|
|
769
|
+
return lexHeaderDelim;
|
|
770
|
+
}
|
|
771
|
+
ctx.pos = ctx.input.length;
|
|
772
|
+
if (ctx.pos > ctx.start) {
|
|
773
|
+
ctx.emit(6 /* Content */);
|
|
774
|
+
}
|
|
775
|
+
ctx.emit(1 /* EOF */);
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
function lexHeaderDelim(ctx) {
|
|
779
|
+
ctx.pos += HEADER_DELIM.length;
|
|
780
|
+
ctx.emit(2 /* Header */);
|
|
781
|
+
return lexName;
|
|
782
|
+
}
|
|
783
|
+
function lexName(ctx) {
|
|
784
|
+
let r;
|
|
785
|
+
outer: for (; ; ) {
|
|
786
|
+
r = ctx.next();
|
|
787
|
+
switch (r) {
|
|
788
|
+
case 92: {
|
|
789
|
+
const next = ctx.next();
|
|
790
|
+
if (next !== 10 && next !== EOF) break;
|
|
791
|
+
r = next;
|
|
792
|
+
}
|
|
793
|
+
// falls through
|
|
794
|
+
case 91:
|
|
795
|
+
// [
|
|
796
|
+
case 93:
|
|
797
|
+
// ]
|
|
798
|
+
case 123:
|
|
799
|
+
// {
|
|
800
|
+
case 125:
|
|
801
|
+
// }
|
|
802
|
+
case 10:
|
|
803
|
+
// \n
|
|
804
|
+
case EOF:
|
|
805
|
+
if (r !== EOF) ctx.backup();
|
|
806
|
+
break outer;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
ctx.emit(3 /* Name */);
|
|
810
|
+
switch (r) {
|
|
811
|
+
case 91:
|
|
812
|
+
return lexTags;
|
|
813
|
+
// [
|
|
814
|
+
case 93:
|
|
815
|
+
return ctx.errorf(`unexpected right square bracket ']'`);
|
|
816
|
+
case 123:
|
|
817
|
+
return lexMetadata;
|
|
818
|
+
// {
|
|
819
|
+
case 125:
|
|
820
|
+
return ctx.errorf(`unexpected right curly brace '}'`);
|
|
821
|
+
case 10:
|
|
822
|
+
ctx.pos++;
|
|
823
|
+
ctx.ignore();
|
|
824
|
+
return lexContent;
|
|
825
|
+
}
|
|
826
|
+
ctx.emit(1 /* EOF */);
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
function lexNextOptionalBlock(ctx) {
|
|
830
|
+
ctx.acceptRun(" ");
|
|
831
|
+
ctx.ignore();
|
|
832
|
+
const r = ctx.peek();
|
|
833
|
+
switch (r) {
|
|
834
|
+
case 91:
|
|
835
|
+
return lexTags;
|
|
836
|
+
// [
|
|
837
|
+
case 93:
|
|
838
|
+
return ctx.errorf(`unexpected right square bracket ']'`);
|
|
839
|
+
case 123:
|
|
840
|
+
return lexMetadata;
|
|
841
|
+
// {
|
|
842
|
+
case 125:
|
|
843
|
+
return ctx.errorf(`unexpected right curly brace '}'`);
|
|
844
|
+
case 10:
|
|
845
|
+
ctx.pos++;
|
|
846
|
+
ctx.ignore();
|
|
847
|
+
return lexContent;
|
|
848
|
+
case EOF:
|
|
849
|
+
ctx.emit(1 /* EOF */);
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
return ctx.errorf(`illegal character '${String.fromCharCode(r)}' amid the optional blocks`);
|
|
853
|
+
}
|
|
854
|
+
function lexTags(ctx) {
|
|
855
|
+
ctx.pos++;
|
|
856
|
+
for (; ; ) {
|
|
857
|
+
const r = ctx.next();
|
|
858
|
+
switch (r) {
|
|
859
|
+
case 92: {
|
|
860
|
+
const next = ctx.next();
|
|
861
|
+
if (next !== 10 && next !== EOF) break;
|
|
862
|
+
}
|
|
863
|
+
// falls through
|
|
864
|
+
case 10:
|
|
865
|
+
case EOF:
|
|
866
|
+
if (r === 10) ctx.backup();
|
|
867
|
+
return ctx.errorf("unterminated tag block");
|
|
868
|
+
case 93:
|
|
869
|
+
if (ctx.pos > ctx.start) ctx.emit(4 /* Tags */);
|
|
870
|
+
return lexNextOptionalBlock;
|
|
871
|
+
case 91:
|
|
872
|
+
return ctx.errorf(`unexpected left square bracket '['`);
|
|
873
|
+
case 123:
|
|
874
|
+
return ctx.errorf(`unexpected left curly brace '{'`);
|
|
875
|
+
case 125:
|
|
876
|
+
return ctx.errorf(`unexpected right curly brace '}'`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function lexMetadata(ctx) {
|
|
881
|
+
ctx.pos++;
|
|
882
|
+
let depth = 1;
|
|
883
|
+
for (; ; ) {
|
|
884
|
+
const r = ctx.next();
|
|
885
|
+
switch (r) {
|
|
886
|
+
case 34: {
|
|
887
|
+
const err = acceptQuoted(ctx, 34);
|
|
888
|
+
if (err) return ctx.errorf(err);
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
case 10:
|
|
892
|
+
ctx.backup();
|
|
893
|
+
// falls through
|
|
894
|
+
case EOF:
|
|
895
|
+
return ctx.errorf("unterminated metadata block");
|
|
896
|
+
case 123:
|
|
897
|
+
depth++;
|
|
898
|
+
break;
|
|
899
|
+
case 125:
|
|
900
|
+
depth--;
|
|
901
|
+
if (depth === 0) {
|
|
902
|
+
if (ctx.pos > ctx.start) ctx.emit(5 /* Metadata */);
|
|
903
|
+
return lexNextOptionalBlock;
|
|
904
|
+
}
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function* tweeLexer(input) {
|
|
910
|
+
const ctx = new LexerContext(input);
|
|
911
|
+
let state = lexProlog;
|
|
912
|
+
while (state !== null) {
|
|
913
|
+
state = state(ctx);
|
|
914
|
+
const items = ctx.drainItems();
|
|
915
|
+
for (const item of items) {
|
|
916
|
+
yield item;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const remaining = ctx.drainItems();
|
|
920
|
+
for (const item of remaining) {
|
|
921
|
+
yield item;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// src/twee2-compat.ts
|
|
926
|
+
var twee2DetectRe = /^:: *[^[]*?(?: *\[.*?\])? *<(.*?)> *$/m;
|
|
927
|
+
var twee2HeaderRe = /^(:: *[^[]*?)( *\[.*?\])?(?: *<(.*?)>)? *$/gm;
|
|
928
|
+
var twee2BadPosRe = /^(::.*?) *\{"position":" *"\}$/gm;
|
|
929
|
+
function hasTwee2Syntax(s) {
|
|
930
|
+
return twee2DetectRe.test(s);
|
|
931
|
+
}
|
|
932
|
+
function twee2ToV3(s) {
|
|
933
|
+
if (!hasTwee2Syntax(s)) return s;
|
|
934
|
+
s = s.replace(twee2HeaderRe, (_, p1, p2, p3) => {
|
|
935
|
+
const tags = p2 ?? "";
|
|
936
|
+
const pos = p3 ?? "";
|
|
937
|
+
return `${p1}${tags} {"position":"${pos}"}`;
|
|
938
|
+
});
|
|
939
|
+
s = s.replace(twee2BadPosRe, "$1");
|
|
940
|
+
return s;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// src/parser.ts
|
|
944
|
+
function parseTwee(source, options = {}) {
|
|
945
|
+
const { filename = "<inline>", trim = true, twee2Compat = false } = options;
|
|
946
|
+
const diagnostics = [];
|
|
947
|
+
if (twee2Compat) {
|
|
948
|
+
source = twee2ToV3(source);
|
|
949
|
+
}
|
|
950
|
+
const passages = [];
|
|
951
|
+
let current = null;
|
|
952
|
+
let pCount = 0;
|
|
953
|
+
let lastType = 1 /* EOF */;
|
|
954
|
+
for (const item of tweeLexer(source)) {
|
|
955
|
+
switch (item.type) {
|
|
956
|
+
case 0 /* Error */:
|
|
957
|
+
diagnostics.push({
|
|
958
|
+
level: "error",
|
|
959
|
+
message: `line ${item.line}: Malformed twee source; ${item.val}.`,
|
|
960
|
+
file: filename,
|
|
961
|
+
line: item.line
|
|
962
|
+
});
|
|
963
|
+
return { passages, diagnostics };
|
|
964
|
+
case 1 /* EOF */:
|
|
965
|
+
if (pCount > 0 && current) {
|
|
966
|
+
passages.push(current);
|
|
967
|
+
}
|
|
968
|
+
return { passages, diagnostics };
|
|
969
|
+
case 2 /* Header */:
|
|
970
|
+
pCount++;
|
|
971
|
+
if (pCount > 1 && current) {
|
|
972
|
+
passages.push(current);
|
|
973
|
+
}
|
|
974
|
+
current = { name: "", tags: [], text: "" };
|
|
975
|
+
break;
|
|
976
|
+
case 3 /* Name */: {
|
|
977
|
+
if (!current) break;
|
|
978
|
+
const name = tweeUnescape(item.val).trim();
|
|
979
|
+
if (name.length === 0) {
|
|
980
|
+
diagnostics.push({
|
|
981
|
+
level: "error",
|
|
982
|
+
message: `line ${item.line}: Malformed twee source; passage with no name.`,
|
|
983
|
+
file: filename,
|
|
984
|
+
line: item.line
|
|
985
|
+
});
|
|
986
|
+
return { passages, diagnostics };
|
|
987
|
+
}
|
|
988
|
+
current.name = name;
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
case 4 /* Tags */: {
|
|
992
|
+
if (!current) break;
|
|
993
|
+
if (lastType !== 3 /* Name */) {
|
|
994
|
+
diagnostics.push({
|
|
995
|
+
level: "error",
|
|
996
|
+
message: `line ${item.line}: Malformed twee source; optional tags block must immediately follow the passage name.`,
|
|
997
|
+
file: filename,
|
|
998
|
+
line: item.line
|
|
999
|
+
});
|
|
1000
|
+
return { passages, diagnostics };
|
|
1001
|
+
}
|
|
1002
|
+
const inner = item.val.slice(1, -1);
|
|
1003
|
+
current.tags = tweeUnescape(inner).split(/\s+/).filter(Boolean);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
case 5 /* Metadata */: {
|
|
1007
|
+
if (!current) break;
|
|
1008
|
+
if (lastType !== 3 /* Name */ && lastType !== 4 /* Tags */) {
|
|
1009
|
+
diagnostics.push({
|
|
1010
|
+
level: "error",
|
|
1011
|
+
message: `line ${item.line}: Malformed twee source; optional metadata block must immediately follow the passage name or tags block.`,
|
|
1012
|
+
file: filename,
|
|
1013
|
+
line: item.line
|
|
1014
|
+
});
|
|
1015
|
+
return { passages, diagnostics };
|
|
1016
|
+
}
|
|
1017
|
+
try {
|
|
1018
|
+
const parsed = JSON.parse(item.val);
|
|
1019
|
+
const meta = {};
|
|
1020
|
+
if (typeof parsed.position === "string") meta.position = parsed.position;
|
|
1021
|
+
if (typeof parsed.size === "string") meta.size = parsed.size;
|
|
1022
|
+
current.metadata = meta;
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
diagnostics.push({
|
|
1025
|
+
level: "warning",
|
|
1026
|
+
message: `load ${filename}: line ${item.line}: Malformed twee source; could not decode metadata (reason: ${e instanceof Error ? e.message : String(e)}).`,
|
|
1027
|
+
file: filename,
|
|
1028
|
+
line: item.line
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
case 6 /* Content */: {
|
|
1034
|
+
if (!current) break;
|
|
1035
|
+
current.text = trim ? item.val.trim() : item.val;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
lastType = item.type;
|
|
1040
|
+
}
|
|
1041
|
+
if (pCount > 0 && current) {
|
|
1042
|
+
passages.push(current);
|
|
1043
|
+
}
|
|
1044
|
+
return { passages, diagnostics };
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/loader.ts
|
|
1048
|
+
function readUTF82(filename) {
|
|
1049
|
+
let content = readFileSync2(filename, "utf-8");
|
|
1050
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1051
|
+
content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1052
|
+
return content;
|
|
1053
|
+
}
|
|
1054
|
+
function readBase64(filename) {
|
|
1055
|
+
return readFileSync2(filename).toString("base64");
|
|
1056
|
+
}
|
|
1057
|
+
function loadSources(story, filenames, opts, diagnostics, processedFiles) {
|
|
1058
|
+
for (const filename of filenames) {
|
|
1059
|
+
if (processedFiles.has(filename)) {
|
|
1060
|
+
diagnostics.push({ level: "warning", message: `load ${filename}: Skipping duplicate.` });
|
|
1061
|
+
continue;
|
|
1062
|
+
}
|
|
1063
|
+
const ext = normalizedFileExt(filename);
|
|
1064
|
+
try {
|
|
1065
|
+
switch (ext) {
|
|
1066
|
+
case "tw":
|
|
1067
|
+
case "twee":
|
|
1068
|
+
loadTwee(story, filename, opts, diagnostics);
|
|
1069
|
+
break;
|
|
1070
|
+
case "tw2":
|
|
1071
|
+
case "twee2":
|
|
1072
|
+
loadTwee(story, filename, { ...opts, twee2Compat: true }, diagnostics);
|
|
1073
|
+
break;
|
|
1074
|
+
case "css":
|
|
1075
|
+
loadTagged(story, "stylesheet", filename, diagnostics);
|
|
1076
|
+
break;
|
|
1077
|
+
case "js":
|
|
1078
|
+
loadTagged(story, "script", filename, diagnostics);
|
|
1079
|
+
break;
|
|
1080
|
+
case "otf":
|
|
1081
|
+
case "ttf":
|
|
1082
|
+
case "woff":
|
|
1083
|
+
case "woff2":
|
|
1084
|
+
loadFont(story, filename, diagnostics);
|
|
1085
|
+
break;
|
|
1086
|
+
case "gif":
|
|
1087
|
+
case "jpeg":
|
|
1088
|
+
case "jpg":
|
|
1089
|
+
case "png":
|
|
1090
|
+
case "svg":
|
|
1091
|
+
case "tif":
|
|
1092
|
+
case "tiff":
|
|
1093
|
+
case "webp":
|
|
1094
|
+
loadMedia(story, "Twine.image", filename, diagnostics);
|
|
1095
|
+
break;
|
|
1096
|
+
case "aac":
|
|
1097
|
+
case "flac":
|
|
1098
|
+
case "m4a":
|
|
1099
|
+
case "mp3":
|
|
1100
|
+
case "oga":
|
|
1101
|
+
case "ogg":
|
|
1102
|
+
case "opus":
|
|
1103
|
+
case "wav":
|
|
1104
|
+
case "wave":
|
|
1105
|
+
case "weba":
|
|
1106
|
+
loadMedia(story, "Twine.audio", filename, diagnostics);
|
|
1107
|
+
break;
|
|
1108
|
+
case "mp4":
|
|
1109
|
+
case "ogv":
|
|
1110
|
+
case "webm":
|
|
1111
|
+
loadMedia(story, "Twine.video", filename, diagnostics);
|
|
1112
|
+
break;
|
|
1113
|
+
case "vtt":
|
|
1114
|
+
loadMedia(story, "Twine.vtt", filename, diagnostics);
|
|
1115
|
+
break;
|
|
1116
|
+
default:
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
} catch (e) {
|
|
1120
|
+
diagnostics.push({
|
|
1121
|
+
level: "error",
|
|
1122
|
+
message: `load ${filename}: ${e instanceof Error ? e.message : String(e)}`,
|
|
1123
|
+
file: filename
|
|
1124
|
+
});
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
processedFiles.add(filename);
|
|
1128
|
+
}
|
|
1129
|
+
if (story.name !== "" && !story.passages.some((p) => p.name === "StoryTitle")) {
|
|
1130
|
+
storyPrepend(story, { name: "StoryTitle", tags: [], text: story.name }, diagnostics);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function loadInlineSources(story, sources, opts, diagnostics) {
|
|
1134
|
+
for (const source of sources) {
|
|
1135
|
+
if (typeof source === "string") {
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
const content = typeof source.content === "string" ? source.content : source.content.toString("utf-8");
|
|
1139
|
+
const ext = normalizedFileExt(source.filename);
|
|
1140
|
+
if (ext === "tw" || ext === "twee" || ext === "tw2" || ext === "twee2" || !ext) {
|
|
1141
|
+
const twee2 = ext === "tw2" || ext === "twee2" || opts.twee2Compat;
|
|
1142
|
+
const result = parseTwee(content, {
|
|
1143
|
+
filename: source.filename,
|
|
1144
|
+
trim: opts.trim ?? true,
|
|
1145
|
+
twee2Compat: twee2
|
|
1146
|
+
});
|
|
1147
|
+
diagnostics.push(...result.diagnostics);
|
|
1148
|
+
for (const p of result.passages) {
|
|
1149
|
+
storyAdd(story, p, diagnostics);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function loadTwee(story, filename, opts, diagnostics) {
|
|
1155
|
+
const source = readUTF82(filename);
|
|
1156
|
+
const result = parseTwee(source, {
|
|
1157
|
+
filename,
|
|
1158
|
+
trim: opts.trim ?? true,
|
|
1159
|
+
twee2Compat: opts.twee2Compat ?? false
|
|
1160
|
+
});
|
|
1161
|
+
diagnostics.push(...result.diagnostics);
|
|
1162
|
+
for (const p of result.passages) {
|
|
1163
|
+
storyAdd(story, p, diagnostics);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function loadTagged(story, tag, filename, diagnostics) {
|
|
1167
|
+
const source = readUTF82(filename);
|
|
1168
|
+
storyAdd(story, { name: basename(filename), tags: [tag], text: source }, diagnostics);
|
|
1169
|
+
}
|
|
1170
|
+
function loadMedia(story, tag, filename, diagnostics) {
|
|
1171
|
+
const source = readBase64(filename);
|
|
1172
|
+
const name = basename(filename).split(".")[0];
|
|
1173
|
+
storyAdd(story, {
|
|
1174
|
+
name,
|
|
1175
|
+
tags: [tag],
|
|
1176
|
+
text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
|
|
1177
|
+
}, diagnostics);
|
|
1178
|
+
}
|
|
1179
|
+
function loadFont(story, filename, diagnostics) {
|
|
1180
|
+
const source = readBase64(filename);
|
|
1181
|
+
const name = basename(filename);
|
|
1182
|
+
const family = name.split(".")[0];
|
|
1183
|
+
const ext = normalizedFileExt(filename);
|
|
1184
|
+
const mediaType = mediaTypeFromExt(ext);
|
|
1185
|
+
const hint = fontFormatHint(ext);
|
|
1186
|
+
storyAdd(story, {
|
|
1187
|
+
name,
|
|
1188
|
+
tags: ["stylesheet"],
|
|
1189
|
+
text: `@font-face {
|
|
1190
|
+
font-family: "${family}";
|
|
1191
|
+
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1192
|
+
}`
|
|
1193
|
+
}, diagnostics);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/output-twine2.ts
|
|
1197
|
+
var CREATOR_NAME = "Twee-ts";
|
|
1198
|
+
var CREATOR_VERSION = "0.1.0";
|
|
1199
|
+
function toTwine2Archive(story, startName, diagnostics) {
|
|
1200
|
+
return getTwine2DataChunk(story, startName, diagnostics) + "\n";
|
|
1201
|
+
}
|
|
1202
|
+
function toTwine2HTML(story, format, startName, diagnostics) {
|
|
1203
|
+
let template = readFormatSource(format);
|
|
1204
|
+
if (template.includes("{{STORY_NAME}}")) {
|
|
1205
|
+
template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
|
|
1206
|
+
}
|
|
1207
|
+
if (template.includes("{{STORY_DATA}}")) {
|
|
1208
|
+
template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics));
|
|
1209
|
+
}
|
|
1210
|
+
return template;
|
|
1211
|
+
}
|
|
1212
|
+
function getTwine2DataChunk(story, startName, diagnostics) {
|
|
1213
|
+
ensureIFID(story, diagnostics);
|
|
1214
|
+
const parts = [];
|
|
1215
|
+
let startID = "";
|
|
1216
|
+
let pid = 0;
|
|
1217
|
+
const scripts = [];
|
|
1218
|
+
const stylesheets = [];
|
|
1219
|
+
for (const p of story.passages) {
|
|
1220
|
+
if (hasTag(p, "Twine.private")) continue;
|
|
1221
|
+
if (hasTag(p, "script")) scripts.push(p);
|
|
1222
|
+
else if (hasTag(p, "stylesheet")) stylesheets.push(p);
|
|
1223
|
+
}
|
|
1224
|
+
let styleContent = "";
|
|
1225
|
+
if (stylesheets.length === 1) {
|
|
1226
|
+
styleContent = stylesheets[0].text;
|
|
1227
|
+
} else if (stylesheets.length > 1) {
|
|
1228
|
+
let pidS = 1;
|
|
1229
|
+
for (const p of stylesheets) {
|
|
1230
|
+
if (pidS > 1 && !styleContent.endsWith("\n")) styleContent += "\n";
|
|
1231
|
+
styleContent += `/* twine-user-stylesheet #${pidS}: "${p.name}" */
|
|
1232
|
+
`;
|
|
1233
|
+
styleContent += p.text;
|
|
1234
|
+
pidS++;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
parts.push(`<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">${styleContent}</style>`);
|
|
1238
|
+
let scriptContent = "";
|
|
1239
|
+
if (scripts.length === 1) {
|
|
1240
|
+
scriptContent = scripts[0].text;
|
|
1241
|
+
} else if (scripts.length > 1) {
|
|
1242
|
+
let pidS = 1;
|
|
1243
|
+
for (const p of scripts) {
|
|
1244
|
+
if (pidS > 1 && !scriptContent.endsWith("\n")) scriptContent += "\n";
|
|
1245
|
+
scriptContent += `/* twine-user-script #${pidS}: "${p.name}" */
|
|
1246
|
+
`;
|
|
1247
|
+
scriptContent += p.text;
|
|
1248
|
+
pidS++;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
parts.push(`<script role="script" id="twine-user-script" type="text/twine-javascript">${scriptContent}</script>`);
|
|
1252
|
+
for (const [tag, color] of story.twine2.tagColors) {
|
|
1253
|
+
parts.push(`<tw-tag name="${attrEscape(tag)}" color="${attrEscape(color)}"></tw-tag>`);
|
|
1254
|
+
}
|
|
1255
|
+
pid = 1;
|
|
1256
|
+
for (const p of story.passages) {
|
|
1257
|
+
if (p.name === "StoryTitle" || p.name === "StoryData" || hasAnyTag(p, "script", "stylesheet", "Twine.private")) {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
if (p.name === "StorySettings" && story.twine1.settings.size === 0) {
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
parts.push(passageToPassagedata(p, pid));
|
|
1264
|
+
if (startName === p.name) {
|
|
1265
|
+
startID = String(pid);
|
|
1266
|
+
}
|
|
1267
|
+
pid++;
|
|
1268
|
+
}
|
|
1269
|
+
const opts = [];
|
|
1270
|
+
for (const [opt, val] of story.twine2.options) {
|
|
1271
|
+
if (val) opts.push(opt);
|
|
1272
|
+
}
|
|
1273
|
+
const options = opts.join(" ");
|
|
1274
|
+
const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
|
|
1275
|
+
const wrapper = `<!-- UUID://${story.ifid}// --><tw-storydata name="${attrEscape(story.name)}" startnode="${startID}" creator="${attrEscape(CREATOR_NAME)}" creator-version="${attrEscape(CREATOR_VERSION)}" ifid="${attrEscape(story.ifid)}" zoom="${attrEscape(zoom)}" format="${attrEscape(story.twine2.format)}" format-version="${attrEscape(story.twine2.formatVersion)}" options="${attrEscape(options)}" hidden>`;
|
|
1276
|
+
return wrapper + parts.join("") + "</tw-storydata>";
|
|
1277
|
+
}
|
|
1278
|
+
function ensureIFID(story, diagnostics) {
|
|
1279
|
+
if (story.ifid !== "") return;
|
|
1280
|
+
if (story.legacyIFID !== "") {
|
|
1281
|
+
diagnostics.push({
|
|
1282
|
+
level: "warning",
|
|
1283
|
+
message: 'Story IFID not found; reusing "ifid" entry from the "StorySettings" special passage.'
|
|
1284
|
+
});
|
|
1285
|
+
story.ifid = story.legacyIFID;
|
|
1286
|
+
} else {
|
|
1287
|
+
story.ifid = generateIFID();
|
|
1288
|
+
diagnostics.push({
|
|
1289
|
+
level: "warning",
|
|
1290
|
+
message: `Story IFID not found; generated ${story.ifid} for your project. Add it to a StoryData passage.`
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/output-twine1.ts
|
|
1296
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1297
|
+
import { join as join3, dirname } from "path";
|
|
1298
|
+
var CREATOR_NAME2 = "twee-ts";
|
|
1299
|
+
var CREATOR_VERSION2 = "0.1.0";
|
|
1300
|
+
function toTwine1Archive(story, _startName) {
|
|
1301
|
+
const { data, count } = getTwine1PassageChunk(story);
|
|
1302
|
+
return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
|
|
1303
|
+
`;
|
|
1304
|
+
}
|
|
1305
|
+
function toTwine1HTML(story, format, startName, _diagnostics) {
|
|
1306
|
+
const formatDir = dirname(format.filename);
|
|
1307
|
+
const parentDir = dirname(formatDir);
|
|
1308
|
+
let template = readFormatSource(format);
|
|
1309
|
+
const { data, count } = getTwine1PassageChunk(story);
|
|
1310
|
+
template = tryReplaceComponent(template, '"USER_LIB"', join3(formatDir, "userlib.js"), false);
|
|
1311
|
+
template = tryReplaceComponent(template, '"ENGINE"', join3(parentDir, "engine.js"), true);
|
|
1312
|
+
template = tryReplaceComponent(template, '"SUGARCANE"', join3(formatDir, "code.js"), true);
|
|
1313
|
+
template = tryReplaceComponent(template, '"JONAH"', join3(formatDir, "code.js"), true);
|
|
1314
|
+
if (story.twine1.settings.get("jquery") === "on") {
|
|
1315
|
+
template = tryReplaceComponent(template, '"JQUERY"', join3(parentDir, "jquery.js"), true);
|
|
1316
|
+
}
|
|
1317
|
+
if (story.twine1.settings.get("modernizr") === "on") {
|
|
1318
|
+
template = tryReplaceComponent(template, '"MODERNIZR"', join3(parentDir, "modernizr.js"), true);
|
|
1319
|
+
}
|
|
1320
|
+
const displayStart = startName === "Start" ? "" : startName;
|
|
1321
|
+
template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${CREATOR_VERSION2}`);
|
|
1322
|
+
template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
|
|
1323
|
+
template = template.replace('"START_AT"', `"${displayStart}"`);
|
|
1324
|
+
template = template.replace('"STORY_SIZE"', `"${count}"`);
|
|
1325
|
+
if (template.includes('"STORY"')) {
|
|
1326
|
+
template = template.replace('"STORY"', data);
|
|
1327
|
+
} else {
|
|
1328
|
+
let footer;
|
|
1329
|
+
try {
|
|
1330
|
+
footer = readFileSync3(join3(formatDir, "footer.html"), "utf-8");
|
|
1331
|
+
} catch {
|
|
1332
|
+
footer = "</div>\n</body>\n</html>\n";
|
|
1333
|
+
}
|
|
1334
|
+
template += data + footer;
|
|
1335
|
+
}
|
|
1336
|
+
if (story.ifid) {
|
|
1337
|
+
if (template.includes('<div id="store-area"')) {
|
|
1338
|
+
template = template.replace('<div id="store-area"', `<!-- UUID://${story.ifid}// --><div id="store-area"`);
|
|
1339
|
+
} else {
|
|
1340
|
+
template = template.replace('<div id="storeArea"', `<!-- UUID://${story.ifid}// --><div id="storeArea"`);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return template;
|
|
1344
|
+
}
|
|
1345
|
+
function getTwine1PassageChunk(story) {
|
|
1346
|
+
let data = "";
|
|
1347
|
+
let count = 0;
|
|
1348
|
+
for (const p of story.passages) {
|
|
1349
|
+
if (hasTag(p, "Twine.private")) continue;
|
|
1350
|
+
count++;
|
|
1351
|
+
data += passageToTiddler(p, count);
|
|
1352
|
+
}
|
|
1353
|
+
return { data, count };
|
|
1354
|
+
}
|
|
1355
|
+
function tryReplaceComponent(template, placeholder, componentPath, _required) {
|
|
1356
|
+
if (!template.includes(placeholder)) return template;
|
|
1357
|
+
try {
|
|
1358
|
+
let content = readFileSync3(componentPath, "utf-8");
|
|
1359
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1360
|
+
return template.replace(placeholder, content);
|
|
1361
|
+
} catch {
|
|
1362
|
+
return template;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/output-twee.ts
|
|
1367
|
+
function toTwee(story, outMode) {
|
|
1368
|
+
let data = "";
|
|
1369
|
+
for (const p of story.passages) {
|
|
1370
|
+
data += passageToTwee(p, outMode);
|
|
1371
|
+
}
|
|
1372
|
+
return data;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/modules.ts
|
|
1376
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1377
|
+
import { basename as basename2 } from "path";
|
|
1378
|
+
function readUTF83(filename) {
|
|
1379
|
+
let content = readFileSync4(filename, "utf-8");
|
|
1380
|
+
if (content.charCodeAt(0) === 65279) content = content.slice(1);
|
|
1381
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
1382
|
+
}
|
|
1383
|
+
function readBase642(filename) {
|
|
1384
|
+
return readFileSync4(filename).toString("base64");
|
|
1385
|
+
}
|
|
1386
|
+
function loadModules(filenames) {
|
|
1387
|
+
const processed = /* @__PURE__ */ new Set();
|
|
1388
|
+
const headTags = [];
|
|
1389
|
+
for (const filename of filenames) {
|
|
1390
|
+
if (processed.has(filename)) continue;
|
|
1391
|
+
const ext = normalizedFileExt(filename);
|
|
1392
|
+
let tag = null;
|
|
1393
|
+
switch (ext) {
|
|
1394
|
+
case "css":
|
|
1395
|
+
tag = loadModuleTagged("style", filename);
|
|
1396
|
+
break;
|
|
1397
|
+
case "js":
|
|
1398
|
+
tag = loadModuleTagged("script", filename);
|
|
1399
|
+
break;
|
|
1400
|
+
case "otf":
|
|
1401
|
+
case "ttf":
|
|
1402
|
+
case "woff":
|
|
1403
|
+
case "woff2":
|
|
1404
|
+
tag = loadModuleFont(filename);
|
|
1405
|
+
break;
|
|
1406
|
+
default:
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (tag) headTags.push(tag);
|
|
1410
|
+
processed.add(filename);
|
|
1411
|
+
}
|
|
1412
|
+
return headTags.join("\n");
|
|
1413
|
+
}
|
|
1414
|
+
function loadModuleTagged(tag, filename) {
|
|
1415
|
+
const source = readUTF83(filename).trim();
|
|
1416
|
+
if (source.length === 0) return null;
|
|
1417
|
+
const family = basename2(filename).split(".")[0];
|
|
1418
|
+
const idSlug = `${tag}-module-${slugify(family)}`;
|
|
1419
|
+
const mimeType = tag === "script" ? "text/javascript" : "text/css";
|
|
1420
|
+
return `<${tag} id="${idSlug}" type="${mimeType}">${source}</${tag}>`;
|
|
1421
|
+
}
|
|
1422
|
+
function loadModuleFont(filename) {
|
|
1423
|
+
const source = readBase642(filename);
|
|
1424
|
+
const family = basename2(filename).split(".")[0];
|
|
1425
|
+
const idSlug = `style-module-${slugify(family)}`;
|
|
1426
|
+
const ext = normalizedFileExt(filename);
|
|
1427
|
+
const mediaType = mediaTypeFromExt(ext);
|
|
1428
|
+
const hint = fontFormatHint(ext);
|
|
1429
|
+
return `<style id="${idSlug}" type="text/css">@font-face {
|
|
1430
|
+
font-family: "${family}";
|
|
1431
|
+
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1432
|
+
}</style>`;
|
|
1433
|
+
}
|
|
1434
|
+
function modifyHead(html, modulePaths, headFile) {
|
|
1435
|
+
const parts = [];
|
|
1436
|
+
if (modulePaths.length > 0) {
|
|
1437
|
+
const modules = loadModules(modulePaths).trim();
|
|
1438
|
+
if (modules.length > 0) parts.push(modules);
|
|
1439
|
+
}
|
|
1440
|
+
if (headFile) {
|
|
1441
|
+
try {
|
|
1442
|
+
const source = readUTF83(headFile).trim();
|
|
1443
|
+
if (source.length > 0) parts.push(source);
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (parts.length > 0) {
|
|
1448
|
+
parts.push("</head>");
|
|
1449
|
+
return html.replace("</head>", parts.join("\n"));
|
|
1450
|
+
}
|
|
1451
|
+
return html;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/remote-formats.ts
|
|
1455
|
+
import { mkdirSync, readFileSync as readFileSync5, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
|
|
1456
|
+
import { join as join4 } from "path";
|
|
1457
|
+
import { homedir as homedir2 } from "os";
|
|
1458
|
+
var DEFAULT_SFA_INDICES = [
|
|
1459
|
+
"https://videlais.github.io/story-formats-archive/official/index.json",
|
|
1460
|
+
"https://videlais.github.io/story-formats-archive/unofficial/index.json"
|
|
1461
|
+
];
|
|
1462
|
+
function getCacheDir() {
|
|
1463
|
+
const xdg = process.env["XDG_CACHE_HOME"];
|
|
1464
|
+
const base = xdg || join4(homedir2(), ".cache");
|
|
1465
|
+
return join4(base, "twee-ts", "storyformats");
|
|
1466
|
+
}
|
|
1467
|
+
var indexCache = /* @__PURE__ */ new Map();
|
|
1468
|
+
function clearIndexCache() {
|
|
1469
|
+
indexCache.clear();
|
|
1470
|
+
}
|
|
1471
|
+
async function fetchIndex(url) {
|
|
1472
|
+
const cached = indexCache.get(url);
|
|
1473
|
+
if (cached) return cached;
|
|
1474
|
+
const res = await fetch(url);
|
|
1475
|
+
if (!res.ok) {
|
|
1476
|
+
throw new Error(`Failed to fetch format index from ${url}: ${res.status} ${res.statusText}`);
|
|
1477
|
+
}
|
|
1478
|
+
const data = await res.json();
|
|
1479
|
+
indexCache.set(url, data);
|
|
1480
|
+
return data;
|
|
1481
|
+
}
|
|
1482
|
+
function findEntry(index, name, version) {
|
|
1483
|
+
const wanted = parseSemver(version);
|
|
1484
|
+
const entries = [...index.twine2 ?? [], ...index.twine1 ?? []];
|
|
1485
|
+
let bestEntry;
|
|
1486
|
+
let bestVersion = null;
|
|
1487
|
+
for (const entry of entries) {
|
|
1488
|
+
if (entry.name.toLowerCase() !== name.toLowerCase()) continue;
|
|
1489
|
+
const have = parseSemver(entry.version);
|
|
1490
|
+
if (!have) continue;
|
|
1491
|
+
if (wanted && semverCompare(have, wanted) === 0) return entry;
|
|
1492
|
+
if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
|
|
1493
|
+
if (!bestVersion || semverCompare(have, bestVersion) > 0) {
|
|
1494
|
+
bestVersion = have;
|
|
1495
|
+
bestEntry = entry;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
return bestEntry;
|
|
1500
|
+
}
|
|
1501
|
+
async function verifySHA256(content, expectedHex) {
|
|
1502
|
+
const digest = await crypto.subtle.digest("SHA-256", content);
|
|
1503
|
+
const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1504
|
+
return hex === expectedHex.toLowerCase();
|
|
1505
|
+
}
|
|
1506
|
+
function getDownloadUrl(indexUrl, entry) {
|
|
1507
|
+
const base = indexUrl.replace(/\/index\.json$/, "");
|
|
1508
|
+
return `${base}/twine2/${entry.name}/${entry.version}/format.js`;
|
|
1509
|
+
}
|
|
1510
|
+
async function fetchAndCacheFormat(entry, downloadUrl) {
|
|
1511
|
+
const res = await fetch(downloadUrl);
|
|
1512
|
+
if (!res.ok) {
|
|
1513
|
+
throw new Error(`Failed to download format from ${downloadUrl}: ${res.status} ${res.statusText}`);
|
|
1514
|
+
}
|
|
1515
|
+
const text = await res.text();
|
|
1516
|
+
const checksumKey = Object.keys(entry.checksums ?? {}).find((k) => k.endsWith("format.js"));
|
|
1517
|
+
if (checksumKey) {
|
|
1518
|
+
const expected = entry.checksums[checksumKey];
|
|
1519
|
+
const encoder = new TextEncoder();
|
|
1520
|
+
const valid = await verifySHA256(encoder.encode(text), expected);
|
|
1521
|
+
if (!valid) {
|
|
1522
|
+
throw new Error(`Checksum verification failed for ${entry.name} ${entry.version}`);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
const id = `${entry.name.toLowerCase().replace(/\s+/g, "-")}-${parseSemver(entry.version)?.[0] ?? "0"}`;
|
|
1526
|
+
const data = parseFormatJSON(text, id);
|
|
1527
|
+
if (!data) {
|
|
1528
|
+
throw new Error(`Failed to parse format JSON from ${downloadUrl}`);
|
|
1529
|
+
}
|
|
1530
|
+
const cacheDir = getCacheDir();
|
|
1531
|
+
const formatDir = join4(cacheDir, entry.name, entry.version);
|
|
1532
|
+
mkdirSync(formatDir, { recursive: true });
|
|
1533
|
+
const formatPath = join4(formatDir, "format.js");
|
|
1534
|
+
writeFileSync(formatPath, text, "utf-8");
|
|
1535
|
+
return {
|
|
1536
|
+
id,
|
|
1537
|
+
filename: formatPath,
|
|
1538
|
+
isTwine2: true,
|
|
1539
|
+
name: data.name,
|
|
1540
|
+
version: data.version,
|
|
1541
|
+
proofing: data.proofing ?? false
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
async function fetchDirectFormat(url) {
|
|
1545
|
+
const res = await fetch(url);
|
|
1546
|
+
if (!res.ok) {
|
|
1547
|
+
throw new Error(`Failed to download format from ${url}: ${res.status} ${res.statusText}`);
|
|
1548
|
+
}
|
|
1549
|
+
const text = await res.text();
|
|
1550
|
+
const tempId = "direct-url";
|
|
1551
|
+
const data = parseFormatJSON(text, tempId);
|
|
1552
|
+
if (!data) {
|
|
1553
|
+
throw new Error(`Failed to parse format JSON from ${url}`);
|
|
1554
|
+
}
|
|
1555
|
+
const id = `${data.name.toLowerCase().replace(/\s+/g, "-")}-${parseSemver(data.version)?.[0] ?? "0"}`;
|
|
1556
|
+
const cacheDir = getCacheDir();
|
|
1557
|
+
const formatDir = join4(cacheDir, data.name, data.version);
|
|
1558
|
+
mkdirSync(formatDir, { recursive: true });
|
|
1559
|
+
const formatPath = join4(formatDir, "format.js");
|
|
1560
|
+
writeFileSync(formatPath, text, "utf-8");
|
|
1561
|
+
return {
|
|
1562
|
+
id,
|
|
1563
|
+
filename: formatPath,
|
|
1564
|
+
isTwine2: true,
|
|
1565
|
+
name: data.name,
|
|
1566
|
+
version: data.version,
|
|
1567
|
+
proofing: data.proofing ?? false
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
async function resolveRemoteFormat(name, version, indices, urls) {
|
|
1571
|
+
if (urls) {
|
|
1572
|
+
for (const url of urls) {
|
|
1573
|
+
try {
|
|
1574
|
+
const info = await fetchDirectFormat(url);
|
|
1575
|
+
if (info.name.toLowerCase() === name.toLowerCase()) {
|
|
1576
|
+
const wanted = parseSemver(version);
|
|
1577
|
+
const have = parseSemver(info.version);
|
|
1578
|
+
if (!wanted || have && have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
|
|
1579
|
+
return info;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
} catch {
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
const allIndices = [...indices ?? [], ...DEFAULT_SFA_INDICES];
|
|
1587
|
+
for (const indexUrl of allIndices) {
|
|
1588
|
+
try {
|
|
1589
|
+
const index = await fetchIndex(indexUrl);
|
|
1590
|
+
const entry = findEntry(index, name, version);
|
|
1591
|
+
if (entry) {
|
|
1592
|
+
const cached = getCachedFormat(entry.name, entry.version);
|
|
1593
|
+
if (cached) return cached;
|
|
1594
|
+
const downloadUrl = getDownloadUrl(indexUrl, entry);
|
|
1595
|
+
return await fetchAndCacheFormat(entry, downloadUrl);
|
|
1596
|
+
}
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
return void 0;
|
|
1601
|
+
}
|
|
1602
|
+
function getCachedFormat(name, version) {
|
|
1603
|
+
const formatPath = join4(getCacheDir(), name, version, "format.js");
|
|
1604
|
+
try {
|
|
1605
|
+
if (!existsSync(formatPath)) return void 0;
|
|
1606
|
+
const source = readFileSync5(formatPath, "utf-8");
|
|
1607
|
+
const id = `${name.toLowerCase().replace(/\s+/g, "-")}-${parseSemver(version)?.[0] ?? "0"}`;
|
|
1608
|
+
const data = parseFormatJSON(source, id);
|
|
1609
|
+
if (!data) return void 0;
|
|
1610
|
+
return {
|
|
1611
|
+
id,
|
|
1612
|
+
filename: formatPath,
|
|
1613
|
+
isTwine2: true,
|
|
1614
|
+
name: data.name,
|
|
1615
|
+
version: data.version,
|
|
1616
|
+
proofing: data.proofing ?? false
|
|
1617
|
+
};
|
|
1618
|
+
} catch {
|
|
1619
|
+
return void 0;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
// src/compiler.ts
|
|
1624
|
+
var DEFAULT_FORMAT_ID = "sugarcube-2";
|
|
1625
|
+
var DEFAULT_START_NAME = "Start";
|
|
1626
|
+
var TweeTsError = class extends Error {
|
|
1627
|
+
constructor(message, diagnostics = []) {
|
|
1628
|
+
super(message);
|
|
1629
|
+
this.diagnostics = diagnostics;
|
|
1630
|
+
this.name = "TweeTsError";
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
async function compile(options) {
|
|
1634
|
+
return buildOutput(options);
|
|
1635
|
+
}
|
|
1636
|
+
async function buildOutput(options) {
|
|
1637
|
+
const diagnostics = [];
|
|
1638
|
+
const outputMode = options.outputMode ?? "html";
|
|
1639
|
+
const trim = options.trim ?? true;
|
|
1640
|
+
const twee2Compat = options.twee2Compat ?? false;
|
|
1641
|
+
const testMode = options.testMode ?? false;
|
|
1642
|
+
const noRemote = options.noRemote ?? false;
|
|
1643
|
+
clearIndexCache();
|
|
1644
|
+
const filePaths = [];
|
|
1645
|
+
const inlineSources = [];
|
|
1646
|
+
for (const source of options.sources) {
|
|
1647
|
+
if (typeof source === "string") {
|
|
1648
|
+
filePaths.push(source);
|
|
1649
|
+
} else {
|
|
1650
|
+
inlineSources.push(source);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const sourceFilenames = getFilenames(filePaths);
|
|
1654
|
+
const story = createStory();
|
|
1655
|
+
const processedFiles = /* @__PURE__ */ new Set();
|
|
1656
|
+
loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
|
|
1657
|
+
loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
|
|
1658
|
+
if (options.tagAliases) {
|
|
1659
|
+
applyTagAliases(story.passages, options.tagAliases);
|
|
1660
|
+
}
|
|
1661
|
+
const fatalErrors = diagnostics.filter((d) => d.level === "error");
|
|
1662
|
+
if (fatalErrors.length > 0 && outputMode === "html") {
|
|
1663
|
+
}
|
|
1664
|
+
let format;
|
|
1665
|
+
let formatId = options.formatId ?? "";
|
|
1666
|
+
if (outputMode === "html") {
|
|
1667
|
+
const formatSearchDirs = getFormatSearchDirs(
|
|
1668
|
+
options.formatPaths,
|
|
1669
|
+
options.useTweegoPath ?? true
|
|
1670
|
+
);
|
|
1671
|
+
const formats = discoverFormats(formatSearchDirs);
|
|
1672
|
+
if (!formatId && story.twine2.format) {
|
|
1673
|
+
formatId = getFormatIdByNameAndVersion(formats, story.twine2.format, story.twine2.formatVersion) ?? "";
|
|
1674
|
+
}
|
|
1675
|
+
if (!formatId) {
|
|
1676
|
+
formatId = DEFAULT_FORMAT_ID;
|
|
1677
|
+
}
|
|
1678
|
+
format = formats.get(formatId);
|
|
1679
|
+
if (!format && !noRemote) {
|
|
1680
|
+
const remoteName = story.twine2.format || formatId;
|
|
1681
|
+
const remoteVersion = story.twine2.formatVersion || "";
|
|
1682
|
+
try {
|
|
1683
|
+
format = await resolveRemoteFormat(
|
|
1684
|
+
remoteName,
|
|
1685
|
+
remoteVersion,
|
|
1686
|
+
options.formatIndices,
|
|
1687
|
+
options.formatUrls
|
|
1688
|
+
);
|
|
1689
|
+
} catch {
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (!format) {
|
|
1693
|
+
const reason = noRemote ? " (remote fetching disabled)" : "";
|
|
1694
|
+
diagnostics.push({
|
|
1695
|
+
level: "error",
|
|
1696
|
+
message: `Story format "${formatId}" is not available${reason}. Found: ${[...formats.keys()].join(", ") || "none"}`
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
let startName;
|
|
1701
|
+
if (options.startPassage) {
|
|
1702
|
+
startName = options.startPassage;
|
|
1703
|
+
} else if (story.twine2.start) {
|
|
1704
|
+
startName = story.twine2.start;
|
|
1705
|
+
} else {
|
|
1706
|
+
startName = DEFAULT_START_NAME;
|
|
1707
|
+
}
|
|
1708
|
+
if (testMode) {
|
|
1709
|
+
story.twine2.options.set("debug", true);
|
|
1710
|
+
}
|
|
1711
|
+
let output;
|
|
1712
|
+
switch (outputMode) {
|
|
1713
|
+
case "twee3":
|
|
1714
|
+
case "twee1":
|
|
1715
|
+
output = toTwee(story, outputMode);
|
|
1716
|
+
break;
|
|
1717
|
+
case "twine2-archive":
|
|
1718
|
+
output = toTwine2Archive(story, startName, diagnostics);
|
|
1719
|
+
break;
|
|
1720
|
+
case "twine1-archive":
|
|
1721
|
+
output = toTwine1Archive(story, startName);
|
|
1722
|
+
break;
|
|
1723
|
+
case "json":
|
|
1724
|
+
output = storyToJSON(story);
|
|
1725
|
+
break;
|
|
1726
|
+
case "html":
|
|
1727
|
+
default: {
|
|
1728
|
+
if (!storyHas(story, startName)) {
|
|
1729
|
+
diagnostics.push({
|
|
1730
|
+
level: "error",
|
|
1731
|
+
message: `Starting passage "${startName}" not found.`
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
if (!format) {
|
|
1735
|
+
throw new TweeTsError("No story format available for HTML output.", diagnostics);
|
|
1736
|
+
}
|
|
1737
|
+
if (format.isTwine2) {
|
|
1738
|
+
output = toTwine2HTML(story, format, startName, diagnostics);
|
|
1739
|
+
} else {
|
|
1740
|
+
if (story.name === "" && !storyHas(story, "StoryTitle")) {
|
|
1741
|
+
diagnostics.push({
|
|
1742
|
+
level: "error",
|
|
1743
|
+
message: 'Special passage "StoryTitle" not found.'
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
output = toTwine1HTML(story, format, startName, diagnostics);
|
|
1747
|
+
}
|
|
1748
|
+
const modulePaths = options.modules ? getFilenames(options.modules) : [];
|
|
1749
|
+
output = modifyHead(output, modulePaths, options.headFile);
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
const stats = {
|
|
1754
|
+
...getStoryStats(story),
|
|
1755
|
+
files: [...processedFiles]
|
|
1756
|
+
};
|
|
1757
|
+
return { output, story, format, diagnostics, stats };
|
|
1758
|
+
}
|
|
1759
|
+
function storyToJSON(story) {
|
|
1760
|
+
return JSON.stringify({
|
|
1761
|
+
name: story.name,
|
|
1762
|
+
ifid: story.ifid,
|
|
1763
|
+
passages: story.passages.map((p) => ({
|
|
1764
|
+
name: p.name,
|
|
1765
|
+
tags: p.tags,
|
|
1766
|
+
text: p.text,
|
|
1767
|
+
metadata: p.metadata ?? null
|
|
1768
|
+
})),
|
|
1769
|
+
twine2: {
|
|
1770
|
+
format: story.twine2.format,
|
|
1771
|
+
formatVersion: story.twine2.formatVersion,
|
|
1772
|
+
start: story.twine2.start,
|
|
1773
|
+
tagColors: Object.fromEntries(story.twine2.tagColors),
|
|
1774
|
+
options: Object.fromEntries(story.twine2.options),
|
|
1775
|
+
zoom: story.twine2.zoom
|
|
1776
|
+
}
|
|
1777
|
+
}, null, 2);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
export {
|
|
1781
|
+
compile
|
|
1782
|
+
};
|
|
1783
|
+
//# sourceMappingURL=chunk-QPA7RLK5.js.map
|