@rohal12/twee-ts 1.1.1 → 1.2.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.
package/dist/index.cjs CHANGED
@@ -50,7 +50,7 @@ __export(src_exports, {
50
50
  module.exports = __toCommonJS(src_exports);
51
51
 
52
52
  // src/compiler.ts
53
- var import_node_fs7 = require("fs");
53
+ var import_node_fs6 = require("fs");
54
54
 
55
55
  // src/ifid.ts
56
56
  var import_node_crypto = require("crypto");
@@ -120,13 +120,26 @@ function tweeEscape(s) {
120
120
  if (s.length === 0) return s;
121
121
  return s.replace(/[\\[\]{}]/g, (ch) => "\\" + ch);
122
122
  }
123
+ function jsStringEscape(s) {
124
+ if (s.length === 0) return s;
125
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/'/g, "\\'").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
126
+ }
127
+ function commentSanitize(s) {
128
+ return s.replace(/\*\//g, "* /");
129
+ }
130
+ function htmlCommentSanitize(s) {
131
+ return s.replace(/-->/g, "-- >");
132
+ }
123
133
  function tweeUnescape(s) {
124
134
  if (s.length === 0) return s;
125
135
  let result = "";
126
136
  for (let i = 0; i < s.length; i++) {
127
137
  if (s[i] === "\\") {
128
138
  i++;
129
- if (i >= s.length) break;
139
+ if (i >= s.length) {
140
+ result += "\\";
141
+ break;
142
+ }
130
143
  }
131
144
  result += s[i];
132
145
  }
@@ -219,16 +232,16 @@ function passageToPassagedata(p, pid) {
219
232
  let position;
220
233
  let size;
221
234
  if (hasMetadataPosition(p)) {
222
- position = p.metadata.position;
235
+ position = p.metadata?.position ?? "";
223
236
  } else {
224
237
  const x = pid % 10;
225
- let y = Math.floor(pid / 10);
238
+ const y = Math.floor(pid / 10);
226
239
  const xp = x === 0 ? 10 : x;
227
240
  const yp = x === 0 ? y : y + 1;
228
241
  position = `${xp * 125 - 25},${yp * 125 - 25}`;
229
242
  }
230
243
  if (hasMetadataSize(p)) {
231
- size = p.metadata.size;
244
+ size = p.metadata?.size ?? "";
232
245
  } else {
233
246
  size = "100,100";
234
247
  }
@@ -237,10 +250,10 @@ function passageToPassagedata(p, pid) {
237
250
  function passageToTiddler(p, pid) {
238
251
  let position;
239
252
  if (hasMetadataPosition(p)) {
240
- position = p.metadata.position;
253
+ position = p.metadata?.position ?? "";
241
254
  } else {
242
255
  const x = pid % 10;
243
- let y = Math.floor(pid / 10);
256
+ const y = Math.floor(pid / 10);
244
257
  const xp = x === 0 ? 10 : x;
245
258
  const yp = x === 0 ? y : y + 1;
246
259
  position = `${xp * 140 - 130},${yp * 140 - 130}`;
@@ -259,15 +272,17 @@ function countWords(p) {
259
272
  }
260
273
  function applyTagAliases(passages, aliases) {
261
274
  const entries = Object.entries(aliases);
262
- if (entries.length === 0) return;
263
- for (const p of passages) {
264
- const original = [...p.tags];
275
+ if (entries.length === 0) return [...passages];
276
+ return passages.map((p) => {
277
+ const original = p.tags;
278
+ const added = [];
265
279
  for (const [alias, canonical] of entries) {
266
- if (original.includes(alias) && !p.tags.includes(canonical)) {
267
- p.tags.push(canonical);
280
+ if (original.includes(alias) && !original.includes(canonical) && !added.includes(canonical)) {
281
+ added.push(canonical);
268
282
  }
269
283
  }
270
- }
284
+ return added.length > 0 ? { ...p, tags: [...original, ...added] } : p;
285
+ });
271
286
  }
272
287
  function quote(s) {
273
288
  return `"${s}"`;
@@ -339,27 +354,32 @@ function marshalStoryData(story) {
339
354
  return JSON.stringify(data, null, " ");
340
355
  }
341
356
  function unmarshalStoryData(story, json) {
342
- let data;
357
+ let raw;
343
358
  try {
344
- data = JSON.parse(json);
359
+ raw = JSON.parse(json);
345
360
  } catch (e) {
346
361
  return `Cannot unmarshal "StoryData"; ${e instanceof Error ? e.message : String(e)}`;
347
362
  }
348
- if (data.ifid) story.ifid = data.ifid.toUpperCase();
349
- if (data.format) story.twine2.format = data.format;
350
- if (data["format-version"]) story.twine2.formatVersion = data["format-version"];
351
- if (data.options) {
363
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
364
+ return 'Cannot unmarshal "StoryData"; expected a JSON object';
365
+ }
366
+ const data = raw;
367
+ if (typeof data.ifid === "string" && data.ifid) story.ifid = data.ifid.toUpperCase();
368
+ if (typeof data.format === "string" && data.format) story.twine2.format = data.format;
369
+ if (typeof data["format-version"] === "string" && data["format-version"])
370
+ story.twine2.formatVersion = data["format-version"];
371
+ if (Array.isArray(data.options)) {
352
372
  for (const opt of data.options) {
353
- story.twine2.options.set(opt, true);
373
+ if (typeof opt === "string") story.twine2.options.set(opt, true);
354
374
  }
355
375
  }
356
- if (data.start) story.twine2.start = data.start;
357
- if (data["tag-colors"]) {
376
+ if (typeof data.start === "string" && data.start) story.twine2.start = data.start;
377
+ if (typeof data["tag-colors"] === "object" && data["tag-colors"] !== null && !Array.isArray(data["tag-colors"])) {
358
378
  for (const [tag, color] of Object.entries(data["tag-colors"])) {
359
- story.twine2.tagColors.set(tag, color);
379
+ if (typeof color === "string") story.twine2.tagColors.set(tag, color);
360
380
  }
361
381
  }
362
- if (data.zoom != null && data.zoom !== 0) story.twine2.zoom = data.zoom;
382
+ if (typeof data.zoom === "number" && data.zoom !== 0) story.twine2.zoom = data.zoom;
363
383
  return null;
364
384
  }
365
385
  function unmarshalStorySettings(story, text, diagnostics) {
@@ -402,6 +422,7 @@ function unmarshalStorySettings(story, text, diagnostics) {
402
422
  }
403
423
  }
404
424
  function storyAdd(story, p, diagnostics) {
425
+ let processed = p;
405
426
  switch (p.name) {
406
427
  case "StoryIncludes":
407
428
  diagnostics.push({
@@ -421,7 +442,7 @@ function storyAdd(story, p, diagnostics) {
421
442
  });
422
443
  }
423
444
  }
424
- p.text = marshalStoryData(story);
445
+ processed = { ...p, text: marshalStoryData(story) };
425
446
  } else {
426
447
  diagnostics.push({
427
448
  level: "error",
@@ -433,12 +454,14 @@ function storyAdd(story, p, diagnostics) {
433
454
  case "StorySettings":
434
455
  unmarshalStorySettings(story, p.text, diagnostics);
435
456
  break;
436
- case "StoryTitle":
437
- p.text = p.text.trim();
438
- story.name = p.text;
457
+ case "StoryTitle": {
458
+ const trimmed = p.text.trim();
459
+ processed = { ...p, text: trimmed };
460
+ story.name = trimmed;
439
461
  break;
462
+ }
440
463
  }
441
- storyAppend(story, p, diagnostics);
464
+ storyAppend(story, processed, diagnostics);
442
465
  }
443
466
  function getStoryStats(story) {
444
467
  let storyPassages = 0;
@@ -492,7 +515,7 @@ var MEDIA_TYPE_MAP = {
492
515
  webm: "video/webm"
493
516
  };
494
517
  function mediaTypeFromExt(ext) {
495
- return MEDIA_TYPE_MAP[ext] ?? "";
518
+ return MEDIA_TYPE_MAP[ext] ?? "application/octet-stream";
496
519
  }
497
520
  function mediaTypeFromFilename(filename) {
498
521
  return mediaTypeFromExt(normalizedFileExt(filename));
@@ -514,8 +537,6 @@ var KNOWN_EXTENSIONS = /* @__PURE__ */ new Set([
514
537
  "twee",
515
538
  "tw2",
516
539
  "twee2",
517
- "htm",
518
- "html",
519
540
  "css",
520
541
  "js",
521
542
  "otf",
@@ -625,23 +646,49 @@ function watchFilesystem(pathnames, outFilename, callback) {
625
646
  }
626
647
 
627
648
  // src/formats.ts
649
+ var import_node_fs3 = require("fs");
650
+ var import_node_path4 = require("path");
651
+ var import_node_os = require("os");
652
+
653
+ // src/util.ts
628
654
  var import_node_fs2 = require("fs");
629
655
  var import_node_path3 = require("path");
630
- var import_node_os = require("os");
656
+ function readUTF8(filename) {
657
+ let content = (0, import_node_fs2.readFileSync)(filename, "utf-8");
658
+ if (content.charCodeAt(0) === 65279) content = content.slice(1);
659
+ content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
660
+ return content;
661
+ }
662
+ function readBase64(filename) {
663
+ return (0, import_node_fs2.readFileSync)(filename).toString("base64");
664
+ }
665
+ function baseNameWithoutExt(filename) {
666
+ const { name, ext } = (0, import_node_path3.parse)(filename);
667
+ return name || ext;
668
+ }
669
+
670
+ // src/formats.ts
631
671
  function parseFormatJSON(source, formatId) {
632
672
  const first = source.indexOf("{");
633
673
  const last = source.lastIndexOf("}");
634
674
  if (first === -1 || last === -1) return null;
635
675
  let chunk = source.slice(first, last + 1);
676
+ const parse = (json) => {
677
+ const raw = JSON.parse(json);
678
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
679
+ const obj = raw;
680
+ if (typeof obj.name !== "string" || typeof obj.version !== "string" || typeof obj.source !== "string") return null;
681
+ return { name: obj.name, version: obj.version, source: obj.source, proofing: obj.proofing === true };
682
+ };
636
683
  try {
637
- return JSON.parse(chunk);
684
+ return parse(chunk);
638
685
  } catch {
639
686
  if (formatId.toLowerCase().startsWith("harlowe")) {
640
687
  const setupIdx = chunk.lastIndexOf(',"setup": function');
641
688
  if (setupIdx !== -1) {
642
689
  chunk = chunk.slice(0, setupIdx) + "}";
643
690
  try {
644
- return JSON.parse(chunk);
691
+ return parse(chunk);
645
692
  } catch {
646
693
  }
647
694
  }
@@ -649,39 +696,33 @@ function parseFormatJSON(source, formatId) {
649
696
  return null;
650
697
  }
651
698
  }
652
- function readUTF8(filename) {
653
- let content = (0, import_node_fs2.readFileSync)(filename, "utf-8");
654
- if (content.charCodeAt(0) === 65279) content = content.slice(1);
655
- content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
656
- return content;
657
- }
658
699
  function discoverFormats(searchDirs) {
659
700
  const formats = /* @__PURE__ */ new Map();
660
701
  const baseFilenames = ["format.js", "header.html"];
661
702
  for (const searchDir of searchDirs) {
662
703
  try {
663
- const stat = (0, import_node_fs2.statSync)(searchDir);
704
+ const stat = (0, import_node_fs3.statSync)(searchDir);
664
705
  if (!stat.isDirectory()) continue;
665
706
  } catch {
666
707
  continue;
667
708
  }
668
709
  let entries;
669
710
  try {
670
- entries = (0, import_node_fs2.readdirSync)(searchDir);
711
+ entries = (0, import_node_fs3.readdirSync)(searchDir);
671
712
  } catch {
672
713
  continue;
673
714
  }
674
715
  for (const entry of entries) {
675
- const formatDir = (0, import_node_path3.join)(searchDir, entry);
716
+ const formatDir = (0, import_node_path4.join)(searchDir, entry);
676
717
  try {
677
- if (!(0, import_node_fs2.statSync)(formatDir).isDirectory()) continue;
718
+ if (!(0, import_node_fs3.statSync)(formatDir).isDirectory()) continue;
678
719
  } catch {
679
720
  continue;
680
721
  }
681
722
  for (const baseFilename of baseFilenames) {
682
- const formatFilename = (0, import_node_path3.join)(formatDir, baseFilename);
723
+ const formatFilename = (0, import_node_path4.join)(formatDir, baseFilename);
683
724
  try {
684
- const stat = (0, import_node_fs2.statSync)(formatFilename);
725
+ const stat = (0, import_node_fs3.statSync)(formatFilename);
685
726
  if (!stat.isFile()) continue;
686
727
  } catch {
687
728
  continue;
@@ -725,9 +766,9 @@ function getFormatSearchDirs(extraPaths = [], useTweegoPath = true) {
725
766
  const dirs = [];
726
767
  for (const base of basePaths) {
727
768
  for (const sub of subdirNames) {
728
- const dir = (0, import_node_path3.join)(base, sub);
769
+ const dir = (0, import_node_path4.join)(base, sub);
729
770
  try {
730
- if ((0, import_node_fs2.statSync)(dir).isDirectory()) {
771
+ if ((0, import_node_fs3.statSync)(dir).isDirectory()) {
731
772
  dirs.push(dir);
732
773
  }
733
774
  } catch {
@@ -749,9 +790,11 @@ function parseSemver(v) {
749
790
  return [Number(m[1]), Number(m[2]), Number(m[3])];
750
791
  }
751
792
  function semverCompare(a, b) {
752
- for (let i = 0; i < 3; i++) {
753
- if (a[i] !== b[i]) return a[i] - b[i];
754
- }
793
+ const [aMajor, aMinor, aPatch] = a;
794
+ const [bMajor, bMinor, bPatch] = b;
795
+ if (aMajor !== bMajor) return aMajor - bMajor;
796
+ if (aMinor !== bMinor) return aMinor - bMinor;
797
+ if (aPatch !== bPatch) return aPatch - bPatch;
755
798
  return 0;
756
799
  }
757
800
  function getFormatIdByNameAndVersion(formats, name, version) {
@@ -782,8 +825,7 @@ function readFormatSource(format) {
782
825
  }
783
826
 
784
827
  // src/loader.ts
785
- var import_node_fs3 = require("fs");
786
- var import_node_path4 = require("path");
828
+ var import_node_path5 = require("path");
787
829
 
788
830
  // src/lexer.ts
789
831
  var EOF = -1;
@@ -1075,13 +1117,13 @@ function hasTwee2Syntax(s) {
1075
1117
  }
1076
1118
  function twee2ToV3(s) {
1077
1119
  if (!hasTwee2Syntax(s)) return s;
1078
- s = s.replace(twee2HeaderRe, (_, p1, p2, p3) => {
1120
+ let result = s.replace(twee2HeaderRe, (_, p1, p2, p3) => {
1079
1121
  const tags = p2 ?? "";
1080
1122
  const pos = p3 ?? "";
1081
1123
  return `${p1}${tags} {"position":"${pos}"}`;
1082
1124
  });
1083
- s = s.replace(twee2BadPosRe, "$1");
1084
- return s;
1125
+ result = result.replace(twee2BadPosRe, "$1");
1126
+ return result;
1085
1127
  }
1086
1128
 
1087
1129
  // src/parser.ts
@@ -1159,7 +1201,11 @@ function parseTwee(source, options = {}) {
1159
1201
  return { passages, diagnostics };
1160
1202
  }
1161
1203
  try {
1162
- const parsed = JSON.parse(item.val);
1204
+ const raw = JSON.parse(item.val);
1205
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
1206
+ throw new Error("expected a JSON object");
1207
+ }
1208
+ const parsed = raw;
1163
1209
  const meta = {};
1164
1210
  if (typeof parsed.position === "string") meta.position = parsed.position;
1165
1211
  if (typeof parsed.size === "string") meta.size = parsed.size;
@@ -1179,6 +1225,16 @@ function parseTwee(source, options = {}) {
1179
1225
  current.text = trim ? item.val.trim() : item.val;
1180
1226
  break;
1181
1227
  }
1228
+ default: {
1229
+ const _exhaustive = item.type;
1230
+ diagnostics.push({
1231
+ level: "error",
1232
+ message: `line ${item.line}: Unhandled lexer item type: ${_exhaustive}.`,
1233
+ file: filename,
1234
+ line: item.line
1235
+ });
1236
+ return { passages, diagnostics };
1237
+ }
1182
1238
  }
1183
1239
  lastType = item.type;
1184
1240
  }
@@ -1189,15 +1245,6 @@ function parseTwee(source, options = {}) {
1189
1245
  }
1190
1246
 
1191
1247
  // src/loader.ts
1192
- function readUTF82(filename) {
1193
- let content = (0, import_node_fs3.readFileSync)(filename, "utf-8");
1194
- if (content.charCodeAt(0) === 65279) content = content.slice(1);
1195
- content = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1196
- return content;
1197
- }
1198
- function readBase64(filename) {
1199
- return (0, import_node_fs3.readFileSync)(filename).toString("base64");
1200
- }
1201
1248
  function loadSources(story, filenames, opts, diagnostics, processedFiles) {
1202
1249
  for (const filename of filenames) {
1203
1250
  if (processedFiles.has(filename)) {
@@ -1296,7 +1343,7 @@ function loadInlineSources(story, sources, opts, diagnostics) {
1296
1343
  }
1297
1344
  }
1298
1345
  function loadTwee(story, filename, opts, diagnostics) {
1299
- const source = readUTF82(filename);
1346
+ const source = readUTF8(filename);
1300
1347
  const result = parseTwee(source, {
1301
1348
  filename,
1302
1349
  trim: opts.trim ?? true,
@@ -1308,38 +1355,48 @@ function loadTwee(story, filename, opts, diagnostics) {
1308
1355
  }
1309
1356
  }
1310
1357
  function loadTagged(story, tag, filename, diagnostics) {
1311
- const source = readUTF82(filename);
1312
- storyAdd(story, { name: (0, import_node_path4.basename)(filename), tags: [tag], text: source }, diagnostics);
1358
+ const source = readUTF8(filename);
1359
+ storyAdd(story, { name: (0, import_node_path5.basename)(filename), tags: [tag], text: source }, diagnostics);
1313
1360
  }
1314
1361
  function loadMedia(story, tag, filename, diagnostics) {
1315
1362
  const source = readBase64(filename);
1316
- const name = (0, import_node_path4.basename)(filename).split(".")[0];
1317
- storyAdd(story, {
1318
- name,
1319
- tags: [tag],
1320
- text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1321
- }, diagnostics);
1363
+ const name = baseNameWithoutExt(filename);
1364
+ storyAdd(
1365
+ story,
1366
+ {
1367
+ name,
1368
+ tags: [tag],
1369
+ text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1370
+ },
1371
+ diagnostics
1372
+ );
1322
1373
  }
1323
1374
  function loadFont(story, filename, diagnostics) {
1324
1375
  const source = readBase64(filename);
1325
- const name = (0, import_node_path4.basename)(filename);
1326
- const family = name.split(".")[0];
1376
+ const name = (0, import_node_path5.basename)(filename);
1377
+ const family = baseNameWithoutExt(filename);
1327
1378
  const ext = normalizedFileExt(filename);
1328
1379
  const mediaType = mediaTypeFromExt(ext);
1329
1380
  const hint = fontFormatHint(ext);
1330
- storyAdd(story, {
1331
- name,
1332
- tags: ["stylesheet"],
1333
- text: `@font-face {
1381
+ storyAdd(
1382
+ story,
1383
+ {
1384
+ name,
1385
+ tags: ["stylesheet"],
1386
+ text: `@font-face {
1334
1387
  font-family: "${family}";
1335
1388
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1336
1389
  }`
1337
- }, diagnostics);
1390
+ },
1391
+ diagnostics
1392
+ );
1338
1393
  }
1339
1394
 
1395
+ // src/version.ts
1396
+ var VERSION = "1.2.0";
1397
+
1340
1398
  // src/output-twine2.ts
1341
1399
  var CREATOR_NAME = "Twee-ts";
1342
- var CREATOR_VERSION = "0.1.0";
1343
1400
  function toTwine2Archive(story, startName, diagnostics) {
1344
1401
  return getTwine2DataChunk(story, startName, diagnostics) + "\n";
1345
1402
  }
@@ -1367,12 +1424,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1367
1424
  }
1368
1425
  let styleContent = "";
1369
1426
  if (stylesheets.length === 1) {
1370
- styleContent = stylesheets[0].text;
1427
+ styleContent = stylesheets[0]?.text ?? "";
1371
1428
  } else if (stylesheets.length > 1) {
1372
1429
  let pidS = 1;
1373
1430
  for (const p of stylesheets) {
1374
1431
  if (pidS > 1 && !styleContent.endsWith("\n")) styleContent += "\n";
1375
- styleContent += `/* twine-user-stylesheet #${pidS}: "${p.name}" */
1432
+ styleContent += `/* twine-user-stylesheet #${pidS}: "${commentSanitize(p.name)}" */
1376
1433
  `;
1377
1434
  styleContent += p.text;
1378
1435
  pidS++;
@@ -1381,12 +1438,12 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1381
1438
  parts.push(`<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css">${styleContent}</style>`);
1382
1439
  let scriptContent = "";
1383
1440
  if (scripts.length === 1) {
1384
- scriptContent = scripts[0].text;
1441
+ scriptContent = scripts[0]?.text ?? "";
1385
1442
  } else if (scripts.length > 1) {
1386
1443
  let pidS = 1;
1387
1444
  for (const p of scripts) {
1388
1445
  if (pidS > 1 && !scriptContent.endsWith("\n")) scriptContent += "\n";
1389
- scriptContent += `/* twine-user-script #${pidS}: "${p.name}" */
1446
+ scriptContent += `/* twine-user-script #${pidS}: "${commentSanitize(p.name)}" */
1390
1447
  `;
1391
1448
  scriptContent += p.text;
1392
1449
  pidS++;
@@ -1416,7 +1473,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1416
1473
  }
1417
1474
  const options = opts.join(" ");
1418
1475
  const zoom = story.twine2.zoom === Math.floor(story.twine2.zoom) ? story.twine2.zoom.toString() : story.twine2.zoom.toFixed(1);
1419
- 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>`;
1476
+ 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(options)}" hidden>`;
1420
1477
  return wrapper + parts.join("") + "</tw-storydata>";
1421
1478
  }
1422
1479
  function ensureIFID(story, diagnostics) {
@@ -1438,50 +1495,50 @@ function ensureIFID(story, diagnostics) {
1438
1495
 
1439
1496
  // src/output-twine1.ts
1440
1497
  var import_node_fs4 = require("fs");
1441
- var import_node_path5 = require("path");
1498
+ var import_node_path6 = require("path");
1442
1499
  var CREATOR_NAME2 = "twee-ts";
1443
- var CREATOR_VERSION2 = "0.1.0";
1444
1500
  function toTwine1Archive(story, _startName) {
1445
1501
  const { data, count } = getTwine1PassageChunk(story);
1446
1502
  return `<div id="storeArea" data-size="${count}" hidden>${data}</div>
1447
1503
  `;
1448
1504
  }
1449
1505
  function toTwine1HTML(story, format, startName, _diagnostics) {
1450
- const formatDir = (0, import_node_path5.dirname)(format.filename);
1451
- const parentDir = (0, import_node_path5.dirname)(formatDir);
1506
+ const formatDir = (0, import_node_path6.dirname)(format.filename);
1507
+ const parentDir = (0, import_node_path6.dirname)(formatDir);
1452
1508
  let template = readFormatSource(format);
1453
1509
  const { data, count } = getTwine1PassageChunk(story);
1454
- template = tryReplaceComponent(template, '"USER_LIB"', (0, import_node_path5.join)(formatDir, "userlib.js"), false);
1455
- template = tryReplaceComponent(template, '"ENGINE"', (0, import_node_path5.join)(parentDir, "engine.js"), true);
1456
- template = tryReplaceComponent(template, '"SUGARCANE"', (0, import_node_path5.join)(formatDir, "code.js"), true);
1457
- template = tryReplaceComponent(template, '"JONAH"', (0, import_node_path5.join)(formatDir, "code.js"), true);
1510
+ template = tryReplaceComponent(template, '"USER_LIB"', (0, import_node_path6.join)(formatDir, "userlib.js"), false);
1511
+ template = tryReplaceComponent(template, '"ENGINE"', (0, import_node_path6.join)(parentDir, "engine.js"), true);
1512
+ template = tryReplaceComponent(template, '"SUGARCANE"', (0, import_node_path6.join)(formatDir, "code.js"), true);
1513
+ template = tryReplaceComponent(template, '"JONAH"', (0, import_node_path6.join)(formatDir, "code.js"), true);
1458
1514
  if (story.twine1.settings.get("jquery") === "on") {
1459
- template = tryReplaceComponent(template, '"JQUERY"', (0, import_node_path5.join)(parentDir, "jquery.js"), true);
1515
+ template = tryReplaceComponent(template, '"JQUERY"', (0, import_node_path6.join)(parentDir, "jquery.js"), true);
1460
1516
  }
1461
1517
  if (story.twine1.settings.get("modernizr") === "on") {
1462
- template = tryReplaceComponent(template, '"MODERNIZR"', (0, import_node_path5.join)(parentDir, "modernizr.js"), true);
1518
+ template = tryReplaceComponent(template, '"MODERNIZR"', (0, import_node_path6.join)(parentDir, "modernizr.js"), true);
1463
1519
  }
1464
1520
  const displayStart = startName === "Start" ? "" : startName;
1465
- template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${CREATOR_VERSION2}`);
1521
+ template = template.replace('"VERSION"', `Compiled with ${CREATOR_NAME2}, ${VERSION}`);
1466
1522
  template = template.replace('"TIME"', `Built on ${(/* @__PURE__ */ new Date()).toUTCString()}`);
1467
- template = template.replace('"START_AT"', `"${displayStart}"`);
1523
+ template = template.replace('"START_AT"', `"${jsStringEscape(displayStart)}"`);
1468
1524
  template = template.replace('"STORY_SIZE"', `"${count}"`);
1469
1525
  if (template.includes('"STORY"')) {
1470
1526
  template = template.replace('"STORY"', data);
1471
1527
  } else {
1472
1528
  let footer;
1473
1529
  try {
1474
- footer = (0, import_node_fs4.readFileSync)((0, import_node_path5.join)(formatDir, "footer.html"), "utf-8");
1530
+ footer = (0, import_node_fs4.readFileSync)((0, import_node_path6.join)(formatDir, "footer.html"), "utf-8");
1475
1531
  } catch {
1476
1532
  footer = "</div>\n</body>\n</html>\n";
1477
1533
  }
1478
1534
  template += data + footer;
1479
1535
  }
1480
1536
  if (story.ifid) {
1537
+ const safeIfid = htmlCommentSanitize(story.ifid);
1481
1538
  if (template.includes('<div id="store-area"')) {
1482
- template = template.replace('<div id="store-area"', `<!-- UUID://${story.ifid}// --><div id="store-area"`);
1539
+ template = template.replace('<div id="store-area"', `<!-- UUID://${safeIfid}// --><div id="store-area"`);
1483
1540
  } else {
1484
- template = template.replace('<div id="storeArea"', `<!-- UUID://${story.ifid}// --><div id="storeArea"`);
1541
+ template = template.replace('<div id="storeArea"', `<!-- UUID://${safeIfid}// --><div id="storeArea"`);
1485
1542
  }
1486
1543
  }
1487
1544
  return template;
@@ -1496,13 +1553,18 @@ function getTwine1PassageChunk(story) {
1496
1553
  }
1497
1554
  return { data, count };
1498
1555
  }
1499
- function tryReplaceComponent(template, placeholder, componentPath, _required) {
1556
+ function tryReplaceComponent(template, placeholder, componentPath, required) {
1500
1557
  if (!template.includes(placeholder)) return template;
1501
1558
  try {
1502
1559
  let content = (0, import_node_fs4.readFileSync)(componentPath, "utf-8");
1503
1560
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
1504
1561
  return template.replace(placeholder, content);
1505
- } catch {
1562
+ } catch (e) {
1563
+ if (required) {
1564
+ throw new Error(
1565
+ `Required format component not found: ${componentPath}: ${e instanceof Error ? e.message : String(e)}`
1566
+ );
1567
+ }
1506
1568
  return template;
1507
1569
  }
1508
1570
  }
@@ -1517,16 +1579,6 @@ function toTwee(story, outMode) {
1517
1579
  }
1518
1580
 
1519
1581
  // src/modules.ts
1520
- var import_node_fs5 = require("fs");
1521
- var import_node_path6 = require("path");
1522
- function readUTF83(filename) {
1523
- let content = (0, import_node_fs5.readFileSync)(filename, "utf-8");
1524
- if (content.charCodeAt(0) === 65279) content = content.slice(1);
1525
- return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1526
- }
1527
- function readBase642(filename) {
1528
- return (0, import_node_fs5.readFileSync)(filename).toString("base64");
1529
- }
1530
1582
  function loadModules(filenames) {
1531
1583
  const processed = /* @__PURE__ */ new Set();
1532
1584
  const headTags = [];
@@ -1556,16 +1608,16 @@ function loadModules(filenames) {
1556
1608
  return headTags.join("\n");
1557
1609
  }
1558
1610
  function loadModuleTagged(tag, filename) {
1559
- const source = readUTF83(filename).trim();
1611
+ const source = readUTF8(filename).trim();
1560
1612
  if (source.length === 0) return null;
1561
- const family = (0, import_node_path6.basename)(filename).split(".")[0];
1613
+ const family = baseNameWithoutExt(filename);
1562
1614
  const idSlug = `${tag}-module-${slugify(family)}`;
1563
1615
  const mimeType = tag === "script" ? "text/javascript" : "text/css";
1564
1616
  return `<${tag} id="${idSlug}" type="${mimeType}">${source}</${tag}>`;
1565
1617
  }
1566
1618
  function loadModuleFont(filename) {
1567
- const source = readBase642(filename);
1568
- const family = (0, import_node_path6.basename)(filename).split(".")[0];
1619
+ const source = readBase64(filename);
1620
+ const family = baseNameWithoutExt(filename);
1569
1621
  const idSlug = `style-module-${slugify(family)}`;
1570
1622
  const ext = normalizedFileExt(filename);
1571
1623
  const mediaType = mediaTypeFromExt(ext);
@@ -1575,7 +1627,7 @@ function loadModuleFont(filename) {
1575
1627
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1576
1628
  }</style>`;
1577
1629
  }
1578
- function modifyHead(html, modulePaths, headFile) {
1630
+ function modifyHead(html, modulePaths, headFile, diagnostics) {
1579
1631
  const parts = [];
1580
1632
  if (modulePaths.length > 0) {
1581
1633
  const modules = loadModules(modulePaths).trim();
@@ -1583,9 +1635,13 @@ function modifyHead(html, modulePaths, headFile) {
1583
1635
  }
1584
1636
  if (headFile) {
1585
1637
  try {
1586
- const source = readUTF83(headFile).trim();
1638
+ const source = readUTF8(headFile).trim();
1587
1639
  if (source.length > 0) parts.push(source);
1588
- } catch {
1640
+ } catch (e) {
1641
+ diagnostics?.push({
1642
+ level: "warning",
1643
+ message: `Failed to read head file "${headFile}": ${e instanceof Error ? e.message : String(e)}`
1644
+ });
1589
1645
  }
1590
1646
  }
1591
1647
  if (parts.length > 0) {
@@ -1596,7 +1652,7 @@ function modifyHead(html, modulePaths, headFile) {
1596
1652
  }
1597
1653
 
1598
1654
  // src/remote-formats.ts
1599
- var import_node_fs6 = require("fs");
1655
+ var import_node_fs5 = require("fs");
1600
1656
  var import_node_path7 = require("path");
1601
1657
  var import_node_os2 = require("os");
1602
1658
  var DEFAULT_SFA_INDICES = [
@@ -1612,6 +1668,24 @@ var indexCache = /* @__PURE__ */ new Map();
1612
1668
  function clearIndexCache() {
1613
1669
  indexCache.clear();
1614
1670
  }
1671
+ function isValidEntry(val) {
1672
+ if (typeof val !== "object" || val === null || Array.isArray(val)) return false;
1673
+ const obj = val;
1674
+ return typeof obj.name === "string" && typeof obj.version === "string" && typeof obj.checksums === "object" && obj.checksums !== null && !Array.isArray(obj.checksums);
1675
+ }
1676
+ function validateSFAIndex(json) {
1677
+ if (typeof json !== "object" || json === null) {
1678
+ throw new Error("SFA index is not an object");
1679
+ }
1680
+ const obj = json;
1681
+ const twine1 = Array.isArray(obj["twine1"]) ? obj["twine1"].filter(isValidEntry) : [];
1682
+ const twine2 = Array.isArray(obj["twine2"]) ? obj["twine2"].filter(isValidEntry) : [];
1683
+ return { twine1, twine2 };
1684
+ }
1685
+ function makeFormatId(name, version) {
1686
+ const major = parseSemver(version)?.[0] ?? "0";
1687
+ return `${name.toLowerCase().replace(/\s+/g, "-")}-${major}`;
1688
+ }
1615
1689
  async function fetchIndex(url) {
1616
1690
  const cached = indexCache.get(url);
1617
1691
  if (cached) return cached;
@@ -1619,37 +1693,42 @@ async function fetchIndex(url) {
1619
1693
  if (!res.ok) {
1620
1694
  throw new Error(`Failed to fetch format index from ${url}: ${res.status} ${res.statusText}`);
1621
1695
  }
1622
- const data = await res.json();
1696
+ const data = validateSFAIndex(await res.json());
1623
1697
  indexCache.set(url, data);
1624
1698
  return data;
1625
1699
  }
1626
1700
  function findEntry(index, name, version) {
1627
1701
  const wanted = parseSemver(version);
1628
- const entries = [...index.twine2 ?? [], ...index.twine1 ?? []];
1629
- let bestEntry;
1702
+ const searchArrays = [
1703
+ { entries: index.twine2 ?? [], formatType: "twine2" },
1704
+ { entries: index.twine1 ?? [], formatType: "twine1" }
1705
+ ];
1706
+ let bestResult;
1630
1707
  let bestVersion = null;
1631
- for (const entry of entries) {
1632
- if (entry.name.toLowerCase() !== name.toLowerCase()) continue;
1633
- const have = parseSemver(entry.version);
1634
- if (!have) continue;
1635
- if (wanted && semverCompare(have, wanted) === 0) return entry;
1636
- if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
1637
- if (!bestVersion || semverCompare(have, bestVersion) > 0) {
1638
- bestVersion = have;
1639
- bestEntry = entry;
1708
+ for (const { entries, formatType } of searchArrays) {
1709
+ for (const entry of entries) {
1710
+ if (entry.name.toLowerCase() !== name.toLowerCase()) continue;
1711
+ const have = parseSemver(entry.version);
1712
+ if (!have) continue;
1713
+ if (wanted && semverCompare(have, wanted) === 0) return { entry, formatType };
1714
+ if (wanted === null || have[0] === wanted[0] && semverCompare(have, wanted) >= 0) {
1715
+ if (!bestVersion || semverCompare(have, bestVersion) > 0) {
1716
+ bestVersion = have;
1717
+ bestResult = { entry, formatType };
1718
+ }
1640
1719
  }
1641
1720
  }
1642
1721
  }
1643
- return bestEntry;
1722
+ return bestResult;
1644
1723
  }
1645
1724
  async function verifySHA256(content, expectedHex) {
1646
1725
  const digest = await crypto.subtle.digest("SHA-256", content);
1647
1726
  const hex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
1648
1727
  return hex === expectedHex.toLowerCase();
1649
1728
  }
1650
- function getDownloadUrl(indexUrl, entry) {
1729
+ function getDownloadUrl(indexUrl, entry, formatType) {
1651
1730
  const base = indexUrl.replace(/\/index\.json$/, "");
1652
- return `${base}/twine2/${entry.name}/${entry.version}/format.js`;
1731
+ return `${base}/${formatType}/${entry.name}/${entry.version}/format.js`;
1653
1732
  }
1654
1733
  async function fetchAndCacheFormat(entry, downloadUrl) {
1655
1734
  const res = await fetch(downloadUrl);
@@ -1660,22 +1739,23 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
1660
1739
  const checksumKey = Object.keys(entry.checksums ?? {}).find((k) => k.endsWith("format.js"));
1661
1740
  if (checksumKey) {
1662
1741
  const expected = entry.checksums[checksumKey];
1742
+ if (!expected) throw new Error(`Missing checksum value for key "${checksumKey}"`);
1663
1743
  const encoder = new TextEncoder();
1664
1744
  const valid = await verifySHA256(encoder.encode(text), expected);
1665
1745
  if (!valid) {
1666
1746
  throw new Error(`Checksum verification failed for ${entry.name} ${entry.version}`);
1667
1747
  }
1668
1748
  }
1669
- const id = `${entry.name.toLowerCase().replace(/\s+/g, "-")}-${parseSemver(entry.version)?.[0] ?? "0"}`;
1749
+ const id = makeFormatId(entry.name, entry.version);
1670
1750
  const data = parseFormatJSON(text, id);
1671
1751
  if (!data) {
1672
1752
  throw new Error(`Failed to parse format JSON from ${downloadUrl}`);
1673
1753
  }
1674
1754
  const cacheDir = getCacheDir();
1675
1755
  const formatDir = (0, import_node_path7.join)(cacheDir, entry.name, entry.version);
1676
- (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1756
+ (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
1677
1757
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1678
- (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1758
+ (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
1679
1759
  return {
1680
1760
  id,
1681
1761
  filename: formatPath,
@@ -1696,12 +1776,12 @@ async function fetchDirectFormat(url) {
1696
1776
  if (!data) {
1697
1777
  throw new Error(`Failed to parse format JSON from ${url}`);
1698
1778
  }
1699
- const id = `${data.name.toLowerCase().replace(/\s+/g, "-")}-${parseSemver(data.version)?.[0] ?? "0"}`;
1779
+ const id = makeFormatId(data.name, data.version);
1700
1780
  const cacheDir = getCacheDir();
1701
1781
  const formatDir = (0, import_node_path7.join)(cacheDir, data.name, data.version);
1702
- (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1782
+ (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
1703
1783
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1704
- (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1784
+ (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
1705
1785
  return {
1706
1786
  id,
1707
1787
  filename: formatPath,
@@ -1712,6 +1792,7 @@ async function fetchDirectFormat(url) {
1712
1792
  };
1713
1793
  }
1714
1794
  async function resolveRemoteFormat(name, version, indices, urls) {
1795
+ let lastError;
1715
1796
  if (urls) {
1716
1797
  for (const url of urls) {
1717
1798
  try {
@@ -1723,7 +1804,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
1723
1804
  return info;
1724
1805
  }
1725
1806
  }
1726
- } catch {
1807
+ } catch (e) {
1808
+ lastError = e instanceof Error ? e : new Error(String(e));
1727
1809
  }
1728
1810
  }
1729
1811
  }
@@ -1731,24 +1813,26 @@ async function resolveRemoteFormat(name, version, indices, urls) {
1731
1813
  for (const indexUrl of allIndices) {
1732
1814
  try {
1733
1815
  const index = await fetchIndex(indexUrl);
1734
- const entry = findEntry(index, name, version);
1735
- if (entry) {
1736
- const cached = getCachedFormat(entry.name, entry.version);
1816
+ const result = findEntry(index, name, version);
1817
+ if (result) {
1818
+ const cached = getCachedFormat(result.entry.name, result.entry.version);
1737
1819
  if (cached) return cached;
1738
- const downloadUrl = getDownloadUrl(indexUrl, entry);
1739
- return await fetchAndCacheFormat(entry, downloadUrl);
1820
+ const downloadUrl = getDownloadUrl(indexUrl, result.entry, result.formatType);
1821
+ return await fetchAndCacheFormat(result.entry, downloadUrl);
1740
1822
  }
1741
- } catch {
1823
+ } catch (e) {
1824
+ lastError = e instanceof Error ? e : new Error(String(e));
1742
1825
  }
1743
1826
  }
1827
+ if (lastError) throw lastError;
1744
1828
  return void 0;
1745
1829
  }
1746
1830
  function getCachedFormat(name, version) {
1747
1831
  const formatPath = (0, import_node_path7.join)(getCacheDir(), name, version, "format.js");
1748
1832
  try {
1749
- if (!(0, import_node_fs6.existsSync)(formatPath)) return void 0;
1750
- const source = (0, import_node_fs6.readFileSync)(formatPath, "utf-8");
1751
- const id = `${name.toLowerCase().replace(/\s+/g, "-")}-${parseSemver(version)?.[0] ?? "0"}`;
1833
+ if (!(0, import_node_fs5.existsSync)(formatPath)) return void 0;
1834
+ const source = (0, import_node_fs5.readFileSync)(formatPath, "utf-8");
1835
+ const id = makeFormatId(name, version);
1752
1836
  const data = parseFormatJSON(source, id);
1753
1837
  if (!data) return void 0;
1754
1838
  return {
@@ -1767,26 +1851,26 @@ function discoverCachedFormats() {
1767
1851
  const formats = /* @__PURE__ */ new Map();
1768
1852
  const cacheDir = getCacheDir();
1769
1853
  try {
1770
- if (!(0, import_node_fs6.existsSync)(cacheDir)) return formats;
1854
+ if (!(0, import_node_fs5.existsSync)(cacheDir)) return formats;
1771
1855
  } catch {
1772
1856
  return formats;
1773
1857
  }
1774
1858
  let names;
1775
1859
  try {
1776
- names = (0, import_node_fs6.readdirSync)(cacheDir);
1860
+ names = (0, import_node_fs5.readdirSync)(cacheDir);
1777
1861
  } catch {
1778
1862
  return formats;
1779
1863
  }
1780
1864
  for (const name of names) {
1781
1865
  const nameDir = (0, import_node_path7.join)(cacheDir, name);
1782
1866
  try {
1783
- if (!(0, import_node_fs6.statSync)(nameDir).isDirectory()) continue;
1867
+ if (!(0, import_node_fs5.statSync)(nameDir).isDirectory()) continue;
1784
1868
  } catch {
1785
1869
  continue;
1786
1870
  }
1787
1871
  let versions;
1788
1872
  try {
1789
- versions = (0, import_node_fs6.readdirSync)(nameDir);
1873
+ versions = (0, import_node_fs5.readdirSync)(nameDir);
1790
1874
  } catch {
1791
1875
  continue;
1792
1876
  }
@@ -1815,7 +1899,7 @@ async function compile(options) {
1815
1899
  }
1816
1900
  async function compileToFile(options) {
1817
1901
  const result = await compile(options);
1818
- (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
1902
+ (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
1819
1903
  return result;
1820
1904
  }
1821
1905
  async function watch(options) {
@@ -1825,7 +1909,7 @@ async function watch(options) {
1825
1909
  const allPaths = [...filePaths, ...modulePaths];
1826
1910
  const handle = watchFilesystem(allPaths, options.outFile, () => {
1827
1911
  buildOutput(options).then((result) => {
1828
- (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
1912
+ (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
1829
1913
  options.onBuild?.(result);
1830
1914
  }).catch((e) => {
1831
1915
  options.onError?.(e instanceof Error ? e : new Error(String(e)));
@@ -1857,18 +1941,12 @@ async function buildOutput(options) {
1857
1941
  loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
1858
1942
  loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
1859
1943
  if (options.tagAliases) {
1860
- applyTagAliases(story.passages, options.tagAliases);
1861
- }
1862
- const fatalErrors = diagnostics.filter((d) => d.level === "error");
1863
- if (fatalErrors.length > 0 && outputMode === "html") {
1944
+ story.passages = applyTagAliases(story.passages, options.tagAliases);
1864
1945
  }
1865
1946
  let format;
1866
1947
  let formatId = options.formatId ?? "";
1867
1948
  if (outputMode === "html") {
1868
- const formatSearchDirs = getFormatSearchDirs(
1869
- options.formatPaths,
1870
- options.useTweegoPath ?? true
1871
- );
1949
+ const formatSearchDirs = getFormatSearchDirs(options.formatPaths, options.useTweegoPath ?? true);
1872
1950
  const formats = discoverFormats(formatSearchDirs);
1873
1951
  if (!formatId && story.twine2.format) {
1874
1952
  formatId = getFormatIdByNameAndVersion(formats, story.twine2.format, story.twine2.formatVersion) ?? "";
@@ -1881,13 +1959,12 @@ async function buildOutput(options) {
1881
1959
  const remoteName = story.twine2.format || formatId;
1882
1960
  const remoteVersion = story.twine2.formatVersion || "";
1883
1961
  try {
1884
- format = await resolveRemoteFormat(
1885
- remoteName,
1886
- remoteVersion,
1887
- options.formatIndices,
1888
- options.formatUrls
1889
- );
1890
- } catch {
1962
+ format = await resolveRemoteFormat(remoteName, remoteVersion, options.formatIndices, options.formatUrls);
1963
+ } catch (e) {
1964
+ diagnostics.push({
1965
+ level: "warning",
1966
+ message: `Remote format fetch failed for "${remoteName}": ${e instanceof Error ? e.message : String(e)}`
1967
+ });
1891
1968
  }
1892
1969
  }
1893
1970
  if (!format) {
@@ -1898,14 +1975,7 @@ async function buildOutput(options) {
1898
1975
  });
1899
1976
  }
1900
1977
  }
1901
- let startName;
1902
- if (options.startPassage) {
1903
- startName = options.startPassage;
1904
- } else if (story.twine2.start) {
1905
- startName = story.twine2.start;
1906
- } else {
1907
- startName = DEFAULT_START_NAME;
1908
- }
1978
+ const startName = options.startPassage || story.twine2.start || DEFAULT_START_NAME;
1909
1979
  if (testMode) {
1910
1980
  story.twine2.options.set("debug", true);
1911
1981
  }
@@ -1924,8 +1994,7 @@ async function buildOutput(options) {
1924
1994
  case "json":
1925
1995
  output = storyToJSON(story);
1926
1996
  break;
1927
- case "html":
1928
- default: {
1997
+ case "html": {
1929
1998
  if (!storyHas(story, startName)) {
1930
1999
  diagnostics.push({
1931
2000
  level: "error",
@@ -1947,9 +2016,13 @@ async function buildOutput(options) {
1947
2016
  output = toTwine1HTML(story, format, startName, diagnostics);
1948
2017
  }
1949
2018
  const modulePaths = options.modules ? getFilenames(options.modules) : [];
1950
- output = modifyHead(output, modulePaths, options.headFile);
2019
+ output = modifyHead(output, modulePaths, options.headFile, diagnostics);
1951
2020
  break;
1952
2021
  }
2022
+ default: {
2023
+ const _exhaustive = outputMode;
2024
+ throw new TweeTsError(`Unhandled output mode: ${_exhaustive}`, diagnostics);
2025
+ }
1953
2026
  }
1954
2027
  const stats = {
1955
2028
  ...getStoryStats(story),
@@ -1958,24 +2031,28 @@ async function buildOutput(options) {
1958
2031
  return { output, story, format, diagnostics, stats };
1959
2032
  }
1960
2033
  function storyToJSON(story) {
1961
- return JSON.stringify({
1962
- name: story.name,
1963
- ifid: story.ifid,
1964
- passages: story.passages.map((p) => ({
1965
- name: p.name,
1966
- tags: p.tags,
1967
- text: p.text,
1968
- metadata: p.metadata ?? null
1969
- })),
1970
- twine2: {
1971
- format: story.twine2.format,
1972
- formatVersion: story.twine2.formatVersion,
1973
- start: story.twine2.start,
1974
- tagColors: Object.fromEntries(story.twine2.tagColors),
1975
- options: Object.fromEntries(story.twine2.options),
1976
- zoom: story.twine2.zoom
1977
- }
1978
- }, null, 2);
2034
+ return JSON.stringify(
2035
+ {
2036
+ name: story.name,
2037
+ ifid: story.ifid,
2038
+ passages: story.passages.map((p) => ({
2039
+ name: p.name,
2040
+ tags: p.tags,
2041
+ text: p.text,
2042
+ metadata: p.metadata ?? null
2043
+ })),
2044
+ twine2: {
2045
+ format: story.twine2.format,
2046
+ formatVersion: story.twine2.formatVersion,
2047
+ start: story.twine2.start,
2048
+ tagColors: Object.fromEntries(story.twine2.tagColors),
2049
+ options: Object.fromEntries(story.twine2.options),
2050
+ zoom: story.twine2.zoom
2051
+ }
2052
+ },
2053
+ null,
2054
+ 2
2055
+ );
1979
2056
  }
1980
2057
 
1981
2058
  // src/inspect.ts
@@ -2081,17 +2158,17 @@ function storyInspect(story) {
2081
2158
  }
2082
2159
 
2083
2160
  // src/config.ts
2084
- var import_node_fs8 = require("fs");
2161
+ var import_node_fs7 = require("fs");
2085
2162
  var import_node_path8 = require("path");
2086
2163
  var CONFIG_FILENAME = "twee-ts.config.json";
2087
2164
  var VALID_OUTPUT_MODES = ["html", "twee3", "twee1", "twine2-archive", "twine1-archive", "json"];
2088
2165
  function loadConfig(dir) {
2089
2166
  const base = dir ?? process.cwd();
2090
2167
  const configPath = (0, import_node_path8.join)(base, CONFIG_FILENAME);
2091
- if (!(0, import_node_fs8.existsSync)(configPath)) return null;
2168
+ if (!(0, import_node_fs7.existsSync)(configPath)) return null;
2092
2169
  let raw;
2093
2170
  try {
2094
- raw = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
2171
+ raw = (0, import_node_fs7.readFileSync)(configPath, "utf-8");
2095
2172
  } catch {
2096
2173
  return null;
2097
2174
  }
@@ -2111,7 +2188,7 @@ function loadConfig(dir) {
2111
2188
  function loadConfigFile(filePath) {
2112
2189
  let raw;
2113
2190
  try {
2114
- raw = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
2191
+ raw = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
2115
2192
  } catch (e) {
2116
2193
  throw new Error(`Cannot read config file ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
2117
2194
  }