@rohal12/twee-ts 1.11.0 → 1.12.0

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