@rohal12/twee-ts 1.4.0 → 1.6.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_fs6 = require("fs");
53
+ var import_node_fs7 = require("fs");
54
54
 
55
55
  // src/ifid.ts
56
56
  var import_node_crypto = require("crypto");
@@ -241,7 +241,7 @@ function passageToTwee(p, outMode) {
241
241
  output += "\n\n";
242
242
  return output;
243
243
  }
244
- function passageToPassagedata(p, pid) {
244
+ function passageToPassagedata(p, pid, options) {
245
245
  let position;
246
246
  let size;
247
247
  if (hasMetadataPosition(p)) {
@@ -258,7 +258,11 @@ function passageToPassagedata(p, pid) {
258
258
  } else {
259
259
  size = "100,100";
260
260
  }
261
- return `<tw-passagedata pid="${pid}" name=${quote(fullAttrEscape(p.name))} tags=${quote(attrEscape(p.tags.join(" ")))} position=${quote(attrEscape(position))} size=${quote(attrEscape(size))}>${htmlEscape(p.text)}</tw-passagedata>`;
261
+ 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))}`;
262
+ if (options?.sourceInfo && p.source) {
263
+ attrs += ` data-source-file=${quote(attrEscape(p.source.file))} data-source-line="${p.source.line}"`;
264
+ }
265
+ return `${attrs}>${htmlEscape(p.text)}</tw-passagedata>`;
262
266
  }
263
267
  function passageToTiddler(p, pid, obfuscateRot13 = false) {
264
268
  let position;
@@ -305,8 +309,21 @@ function quote(s) {
305
309
  }
306
310
 
307
311
  // src/story.ts
312
+ var passageIndex = /* @__PURE__ */ new WeakMap();
313
+ function getIndex(story) {
314
+ let index = passageIndex.get(story);
315
+ if (!index) {
316
+ index = /* @__PURE__ */ new Map();
317
+ for (let i = 0; i < story.passages.length; i++) {
318
+ const passage = story.passages[i];
319
+ if (passage) index.set(passage.name, i);
320
+ }
321
+ passageIndex.set(story, index);
322
+ }
323
+ return index;
324
+ }
308
325
  function createStory() {
309
- return {
326
+ const story = {
310
327
  name: "",
311
328
  ifid: "",
312
329
  passages: [],
@@ -322,16 +339,17 @@ function createStory() {
322
339
  zoom: 1
323
340
  }
324
341
  };
342
+ passageIndex.set(story, /* @__PURE__ */ new Map());
343
+ return story;
325
344
  }
326
345
  function storyHas(story, name) {
327
- return story.passages.some((p) => p.name === name);
328
- }
329
- function storyIndex(story, name) {
330
- return story.passages.findIndex((p) => p.name === name);
346
+ return getIndex(story).has(name);
331
347
  }
332
348
  function storyAppend(story, p, diagnostics) {
333
- const i = storyIndex(story, p.name);
349
+ const index = getIndex(story);
350
+ const i = index.get(p.name) ?? -1;
334
351
  if (i === -1) {
352
+ index.set(p.name, story.passages.length);
335
353
  story.passages.push(p);
336
354
  } else {
337
355
  diagnostics.push({
@@ -342,9 +360,15 @@ function storyAppend(story, p, diagnostics) {
342
360
  }
343
361
  }
344
362
  function storyPrepend(story, p, diagnostics) {
345
- const i = storyIndex(story, p.name);
363
+ const index = getIndex(story);
364
+ const i = index.get(p.name) ?? -1;
346
365
  if (i === -1) {
347
366
  story.passages.unshift(p);
367
+ index.clear();
368
+ for (let j = 0; j < story.passages.length; j++) {
369
+ const passage = story.passages[j];
370
+ if (passage) index.set(passage.name, j);
371
+ }
348
372
  } else {
349
373
  diagnostics.push({
350
374
  level: "warning",
@@ -634,11 +658,15 @@ function watchFilesystem(pathnames, outFilename, callback) {
634
658
  const watchers = [];
635
659
  let buildTimer = null;
636
660
  const BUILD_DEBOUNCE = 500;
637
- function scheduleBuild() {
661
+ const pendingFiles = /* @__PURE__ */ new Set();
662
+ function scheduleBuild(changedFile) {
663
+ if (changedFile) pendingFiles.add(changedFile);
638
664
  if (buildTimer) clearTimeout(buildTimer);
639
665
  buildTimer = setTimeout(() => {
640
666
  buildTimer = null;
641
- callback();
667
+ const files = pendingFiles.size > 0 ? new Set(pendingFiles) : void 0;
668
+ pendingFiles.clear();
669
+ callback(files);
642
670
  }, BUILD_DEBOUNCE);
643
671
  }
644
672
  for (const pathname of pathnames) {
@@ -648,7 +676,8 @@ function watchFilesystem(pathnames, outFilename, callback) {
648
676
  const abs = (0, import_node_path2.resolve)(pathname, filename);
649
677
  if (abs === absOutFile) return;
650
678
  if (isKnownFileType(filename)) {
651
- scheduleBuild();
679
+ const rel = (0, import_node_path2.relative)(process.cwd(), abs);
680
+ scheduleBuild(rel || abs);
652
681
  }
653
682
  });
654
683
  watchers.push(watcher);
@@ -907,6 +936,7 @@ function readFormatSource(format) {
907
936
 
908
937
  // src/loader.ts
909
938
  var import_node_path5 = require("path");
939
+ var import_node_fs4 = require("fs");
910
940
 
911
941
  // src/lexer.ts
912
942
  var EOF = -1;
@@ -1238,7 +1268,7 @@ function parseTwee(source, options = {}) {
1238
1268
  if (pCount > 1 && current) {
1239
1269
  passages.push(current);
1240
1270
  }
1241
- current = { name: "", tags: [], text: "" };
1271
+ current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1242
1272
  break;
1243
1273
  case 3 /* Name */: {
1244
1274
  if (!current) break;
@@ -1430,54 +1460,182 @@ function loadInlineSources(story, sources, opts, diagnostics) {
1430
1460
  }
1431
1461
  }
1432
1462
  }
1433
- function loadTwee(story, filename, opts, diagnostics) {
1463
+ function parseTweeFile(filename, opts) {
1434
1464
  const source = readUTF8(filename);
1435
1465
  const result = parseTwee(source, {
1436
1466
  filename,
1437
1467
  trim: opts.trim ?? true,
1438
1468
  twee2Compat: opts.twee2Compat ?? false
1439
1469
  });
1440
- diagnostics.push(...result.diagnostics);
1441
- for (const p of result.passages) {
1442
- storyAdd(story, p, diagnostics);
1443
- }
1470
+ return { passages: result.passages, diagnostics: [...result.diagnostics] };
1444
1471
  }
1445
- function loadTagged(story, tag, filename, diagnostics) {
1472
+ function parseTaggedFile(tag, filename) {
1446
1473
  const source = readUTF8(filename);
1447
- storyAdd(story, { name: (0, import_node_path5.basename)(filename), tags: [tag], text: source }, diagnostics);
1474
+ return { passages: [{ name: (0, import_node_path5.basename)(filename), tags: [tag], text: source }], diagnostics: [] };
1448
1475
  }
1449
- function loadMedia(story, tag, filename, diagnostics) {
1476
+ function parseMediaFile(tag, filename) {
1450
1477
  const source = readBase64(filename);
1451
1478
  const name = baseNameWithoutExt(filename);
1452
- storyAdd(
1453
- story,
1454
- {
1455
- name,
1456
- tags: [tag],
1457
- text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1458
- },
1459
- diagnostics
1460
- );
1479
+ return {
1480
+ passages: [{ name, tags: [tag], text: `data:${mediaTypeFromFilename(filename)};base64,${source}` }],
1481
+ diagnostics: []
1482
+ };
1461
1483
  }
1462
- function loadFont(story, filename, diagnostics) {
1484
+ function parseFontFile(filename) {
1463
1485
  const source = readBase64(filename);
1464
1486
  const name = (0, import_node_path5.basename)(filename);
1465
1487
  const family = baseNameWithoutExt(filename);
1466
1488
  const ext = normalizedFileExt(filename);
1467
1489
  const mediaType = mediaTypeFromExt(ext);
1468
1490
  const hint = fontFormatHint(ext);
1469
- storyAdd(
1470
- story,
1471
- {
1472
- name,
1473
- tags: ["stylesheet"],
1474
- text: `@font-face {
1491
+ return {
1492
+ passages: [
1493
+ {
1494
+ name,
1495
+ tags: ["stylesheet"],
1496
+ text: `@font-face {
1475
1497
  font-family: "${family}";
1476
1498
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1477
1499
  }`
1478
- },
1479
- diagnostics
1480
- );
1500
+ }
1501
+ ],
1502
+ diagnostics: []
1503
+ };
1504
+ }
1505
+ function parseFile(filename, opts) {
1506
+ const ext = normalizedFileExt(filename);
1507
+ switch (ext) {
1508
+ case "tw":
1509
+ case "twee":
1510
+ return parseTweeFile(filename, opts);
1511
+ case "tw2":
1512
+ case "twee2":
1513
+ return parseTweeFile(filename, { ...opts, twee2Compat: true });
1514
+ case "css":
1515
+ return parseTaggedFile("stylesheet", filename);
1516
+ case "js":
1517
+ return parseTaggedFile("script", filename);
1518
+ case "otf":
1519
+ case "ttf":
1520
+ case "woff":
1521
+ case "woff2":
1522
+ return parseFontFile(filename);
1523
+ case "gif":
1524
+ case "jpeg":
1525
+ case "jpg":
1526
+ case "png":
1527
+ case "svg":
1528
+ case "tif":
1529
+ case "tiff":
1530
+ case "webp":
1531
+ return parseMediaFile("Twine.image", filename);
1532
+ case "aac":
1533
+ case "flac":
1534
+ case "m4a":
1535
+ case "mp3":
1536
+ case "oga":
1537
+ case "ogg":
1538
+ case "opus":
1539
+ case "wav":
1540
+ case "wave":
1541
+ case "weba":
1542
+ return parseMediaFile("Twine.audio", filename);
1543
+ case "mp4":
1544
+ case "ogv":
1545
+ case "webm":
1546
+ return parseMediaFile("Twine.video", filename);
1547
+ case "vtt":
1548
+ return parseMediaFile("Twine.vtt", filename);
1549
+ default:
1550
+ return void 0;
1551
+ }
1552
+ }
1553
+ function loadTwee(story, filename, opts, diagnostics) {
1554
+ const result = parseTweeFile(filename, opts);
1555
+ diagnostics.push(...result.diagnostics);
1556
+ for (const p of result.passages) {
1557
+ storyAdd(story, p, diagnostics);
1558
+ }
1559
+ }
1560
+ function loadTagged(story, tag, filename, diagnostics) {
1561
+ const result = parseTaggedFile(tag, filename);
1562
+ for (const p of result.passages) {
1563
+ storyAdd(story, p, diagnostics);
1564
+ }
1565
+ }
1566
+ function loadMedia(story, tag, filename, diagnostics) {
1567
+ const result = parseMediaFile(tag, filename);
1568
+ for (const p of result.passages) {
1569
+ storyAdd(story, p, diagnostics);
1570
+ }
1571
+ }
1572
+ function loadFont(story, filename, diagnostics) {
1573
+ const result = parseFontFile(filename);
1574
+ for (const p of result.passages) {
1575
+ storyAdd(story, p, diagnostics);
1576
+ }
1577
+ }
1578
+ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles) {
1579
+ const currentFiles = /* @__PURE__ */ new Set();
1580
+ for (const filename of filenames) {
1581
+ if (processedFiles.has(filename)) {
1582
+ diagnostics.push({ level: "warning", message: `load ${filename}: Skipping duplicate.` });
1583
+ continue;
1584
+ }
1585
+ currentFiles.add(filename);
1586
+ const cached = cache.get(filename);
1587
+ if (changedFiles && cached && !changedFiles.has(filename)) {
1588
+ diagnostics.push(...cached.diagnostics);
1589
+ for (const p of cached.passages) {
1590
+ storyAdd(story, p, diagnostics);
1591
+ }
1592
+ processedFiles.add(filename);
1593
+ continue;
1594
+ }
1595
+ let mtimeMs;
1596
+ try {
1597
+ mtimeMs = (0, import_node_fs4.statSync)(filename).mtimeMs;
1598
+ } catch {
1599
+ continue;
1600
+ }
1601
+ if (cached && cached.mtimeMs === mtimeMs) {
1602
+ diagnostics.push(...cached.diagnostics);
1603
+ for (const p of cached.passages) {
1604
+ storyAdd(story, p, diagnostics);
1605
+ }
1606
+ processedFiles.add(filename);
1607
+ continue;
1608
+ }
1609
+ try {
1610
+ const result = parseFile(filename, opts);
1611
+ if (!result) continue;
1612
+ cache.set(filename, {
1613
+ mtimeMs,
1614
+ passages: result.passages,
1615
+ diagnostics: result.diagnostics
1616
+ });
1617
+ diagnostics.push(...result.diagnostics);
1618
+ for (const p of result.passages) {
1619
+ storyAdd(story, p, diagnostics);
1620
+ }
1621
+ } catch (e) {
1622
+ diagnostics.push({
1623
+ level: "error",
1624
+ message: `load ${filename}: ${e instanceof Error ? e.message : String(e)}`,
1625
+ file: filename
1626
+ });
1627
+ continue;
1628
+ }
1629
+ processedFiles.add(filename);
1630
+ }
1631
+ for (const key of cache.keys()) {
1632
+ if (!currentFiles.has(key)) {
1633
+ cache.delete(key);
1634
+ }
1635
+ }
1636
+ if (story.name !== "" && !story.passages.some((p) => p.name === "StoryTitle")) {
1637
+ storyPrepend(story, { name: "StoryTitle", tags: [], text: story.name }, diagnostics);
1638
+ }
1481
1639
  }
1482
1640
 
1483
1641
  // src/version.ts
@@ -1485,20 +1643,20 @@ var VERSION = "1.2.0";
1485
1643
 
1486
1644
  // src/output-twine2.ts
1487
1645
  var CREATOR_NAME = "Twee-ts";
1488
- function toTwine2Archive(story, startName, diagnostics) {
1489
- return getTwine2DataChunk(story, startName, diagnostics) + "\n";
1646
+ function toTwine2Archive(story, startName, diagnostics, options) {
1647
+ return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
1490
1648
  }
1491
- function toTwine2HTML(story, format, startName, diagnostics) {
1649
+ function toTwine2HTML(story, format, startName, diagnostics, options) {
1492
1650
  let template = readFormatSource(format);
1493
1651
  if (template.includes("{{STORY_NAME}}")) {
1494
1652
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
1495
1653
  }
1496
1654
  if (template.includes("{{STORY_DATA}}")) {
1497
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics));
1655
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
1498
1656
  }
1499
1657
  return template;
1500
1658
  }
1501
- function getTwine2DataChunk(story, startName, diagnostics) {
1659
+ function getTwine2DataChunk(story, startName, diagnostics, options) {
1502
1660
  ensureIFID(story, diagnostics);
1503
1661
  const parts = [];
1504
1662
  let startID = "";
@@ -1553,7 +1711,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1553
1711
  if (p.name === "StorySettings" && story.twine1.settings.size === 0) {
1554
1712
  continue;
1555
1713
  }
1556
- parts.push(passageToPassagedata(p, pid));
1714
+ parts.push(passageToPassagedata(p, pid, options));
1557
1715
  if (startName === p.name) {
1558
1716
  startID = String(pid);
1559
1717
  }
@@ -1563,9 +1721,9 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1563
1721
  for (const [opt, val] of story.twine2.options) {
1564
1722
  if (val) opts.push(opt);
1565
1723
  }
1566
- const options = opts.join(" ");
1724
+ const optionsStr = opts.join(" ");
1567
1725
  const zoom = String(story.twine2.zoom);
1568
- 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)}" tags="${attrEscape(story.twine2.tags)}" hidden>`;
1726
+ 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>`;
1569
1727
  return wrapper + parts.join("") + "</tw-storydata>";
1570
1728
  }
1571
1729
  function ensureIFID(story, diagnostics) {
@@ -1587,7 +1745,7 @@ function ensureIFID(story, diagnostics) {
1587
1745
  }
1588
1746
 
1589
1747
  // src/output-twine1.ts
1590
- var import_node_fs4 = require("fs");
1748
+ var import_node_fs5 = require("fs");
1591
1749
  var import_node_path6 = require("path");
1592
1750
  var CREATOR_NAME2 = "twee-ts";
1593
1751
  function toTwine1Archive(story, _startName) {
@@ -1620,7 +1778,7 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1620
1778
  } else {
1621
1779
  let footer;
1622
1780
  try {
1623
- footer = (0, import_node_fs4.readFileSync)((0, import_node_path6.join)(formatDir, "footer.html"), "utf-8");
1781
+ footer = (0, import_node_fs5.readFileSync)((0, import_node_path6.join)(formatDir, "footer.html"), "utf-8");
1624
1782
  } catch {
1625
1783
  footer = "</div>\n</body>\n</html>\n";
1626
1784
  }
@@ -1650,7 +1808,7 @@ function getTwine1PassageChunk(story) {
1650
1808
  function tryReplaceComponent(template, placeholder, componentPath, required) {
1651
1809
  if (!template.includes(placeholder)) return template;
1652
1810
  try {
1653
- let content = (0, import_node_fs4.readFileSync)(componentPath, "utf-8");
1811
+ let content = (0, import_node_fs5.readFileSync)(componentPath, "utf-8");
1654
1812
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
1655
1813
  return template.replace(placeholder, content);
1656
1814
  } catch (e) {
@@ -1746,7 +1904,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
1746
1904
  }
1747
1905
 
1748
1906
  // src/remote-formats.ts
1749
- var import_node_fs5 = require("fs");
1907
+ var import_node_fs6 = require("fs");
1750
1908
  var import_node_path7 = require("path");
1751
1909
  var import_node_os2 = require("os");
1752
1910
  var DEFAULT_SFA_INDICES = [
@@ -1847,9 +2005,9 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
1847
2005
  }
1848
2006
  const cacheDir = getCacheDir();
1849
2007
  const formatDir = (0, import_node_path7.join)(cacheDir, entry.name, entry.version);
1850
- (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
2008
+ (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1851
2009
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1852
- (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
2010
+ (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1853
2011
  return {
1854
2012
  id,
1855
2013
  filename: formatPath,
@@ -1873,9 +2031,9 @@ async function fetchDirectFormat(url) {
1873
2031
  const id = makeFormatId(data.name, data.version);
1874
2032
  const cacheDir = getCacheDir();
1875
2033
  const formatDir = (0, import_node_path7.join)(cacheDir, data.name, data.version);
1876
- (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
2034
+ (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1877
2035
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1878
- (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
2036
+ (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1879
2037
  return {
1880
2038
  id,
1881
2039
  filename: formatPath,
@@ -1924,8 +2082,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
1924
2082
  function getCachedFormat(name, version) {
1925
2083
  const formatPath = (0, import_node_path7.join)(getCacheDir(), name, version, "format.js");
1926
2084
  try {
1927
- if (!(0, import_node_fs5.existsSync)(formatPath)) return void 0;
1928
- const source = (0, import_node_fs5.readFileSync)(formatPath, "utf-8");
2085
+ if (!(0, import_node_fs6.existsSync)(formatPath)) return void 0;
2086
+ const source = (0, import_node_fs6.readFileSync)(formatPath, "utf-8");
1929
2087
  const id = makeFormatId(name, version);
1930
2088
  const data = parseFormatJSON(source, id);
1931
2089
  if (!data) return void 0;
@@ -1945,26 +2103,26 @@ function discoverCachedFormats() {
1945
2103
  const formats = /* @__PURE__ */ new Map();
1946
2104
  const cacheDir = getCacheDir();
1947
2105
  try {
1948
- if (!(0, import_node_fs5.existsSync)(cacheDir)) return formats;
2106
+ if (!(0, import_node_fs6.existsSync)(cacheDir)) return formats;
1949
2107
  } catch {
1950
2108
  return formats;
1951
2109
  }
1952
2110
  let names;
1953
2111
  try {
1954
- names = (0, import_node_fs5.readdirSync)(cacheDir);
2112
+ names = (0, import_node_fs6.readdirSync)(cacheDir);
1955
2113
  } catch {
1956
2114
  return formats;
1957
2115
  }
1958
2116
  for (const name of names) {
1959
2117
  const nameDir = (0, import_node_path7.join)(cacheDir, name);
1960
2118
  try {
1961
- if (!(0, import_node_fs5.statSync)(nameDir).isDirectory()) continue;
2119
+ if (!(0, import_node_fs6.statSync)(nameDir).isDirectory()) continue;
1962
2120
  } catch {
1963
2121
  continue;
1964
2122
  }
1965
2123
  let versions;
1966
2124
  try {
1967
- versions = (0, import_node_fs5.readdirSync)(nameDir);
2125
+ versions = (0, import_node_fs6.readdirSync)(nameDir);
1968
2126
  } catch {
1969
2127
  continue;
1970
2128
  }
@@ -1994,17 +2152,18 @@ async function compile(options) {
1994
2152
  }
1995
2153
  async function compileToFile(options) {
1996
2154
  const result = await compile(options);
1997
- (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
2155
+ (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
1998
2156
  return result;
1999
2157
  }
2000
2158
  async function watch(options) {
2001
2159
  const controller = new AbortController();
2160
+ const cache = /* @__PURE__ */ new Map();
2002
2161
  const filePaths = options.sources.filter((s) => typeof s === "string");
2003
2162
  const modulePaths = options.modules ?? [];
2004
2163
  const allPaths = [...filePaths, ...modulePaths];
2005
- const handle = watchFilesystem(allPaths, options.outFile, () => {
2006
- buildOutput(options).then((result) => {
2007
- (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
2164
+ const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
2165
+ buildOutput(options, cache, changedFiles).then((result) => {
2166
+ (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
2008
2167
  options.onBuild?.(result);
2009
2168
  }).catch((e) => {
2010
2169
  options.onError?.(e instanceof Error ? e : new Error(String(e)));
@@ -2013,13 +2172,14 @@ async function watch(options) {
2013
2172
  controller.signal.addEventListener("abort", () => handle.close());
2014
2173
  return controller;
2015
2174
  }
2016
- async function buildOutput(options) {
2175
+ async function buildOutput(options, cache, changedFiles) {
2017
2176
  const diagnostics = [];
2018
2177
  const outputMode = options.outputMode ?? "html";
2019
2178
  const trim = options.trim ?? true;
2020
2179
  const twee2Compat = options.twee2Compat ?? false;
2021
2180
  const testMode = options.testMode ?? false;
2022
2181
  const noRemote = options.noRemote ?? false;
2182
+ const sourceInfo = options.sourceInfo ?? false;
2023
2183
  clearIndexCache();
2024
2184
  const filePaths = [];
2025
2185
  const inlineSources = [];
@@ -2033,7 +2193,11 @@ async function buildOutput(options) {
2033
2193
  const sourceFilenames = getFilenames(filePaths);
2034
2194
  const story = createStory();
2035
2195
  const processedFiles = /* @__PURE__ */ new Set();
2036
- loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2196
+ if (cache) {
2197
+ loadSourcesCached(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles, cache, changedFiles);
2198
+ } else {
2199
+ loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2200
+ }
2037
2201
  loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
2038
2202
  if (options.tagAliases) {
2039
2203
  story.passages = applyTagAliases(story.passages, options.tagAliases);
@@ -2081,7 +2245,7 @@ async function buildOutput(options) {
2081
2245
  output = toTwee(story, outputMode);
2082
2246
  break;
2083
2247
  case "twine2-archive":
2084
- output = toTwine2Archive(story, startName, diagnostics);
2248
+ output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
2085
2249
  break;
2086
2250
  case "twine1-archive":
2087
2251
  output = toTwine1Archive(story, startName);
@@ -2100,7 +2264,7 @@ async function buildOutput(options) {
2100
2264
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
2101
2265
  }
2102
2266
  if (format.isTwine2) {
2103
- output = toTwine2HTML(story, format, startName, diagnostics);
2267
+ output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
2104
2268
  } else {
2105
2269
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
2106
2270
  diagnostics.push({
@@ -2274,17 +2438,17 @@ function storyInspect(story) {
2274
2438
  }
2275
2439
 
2276
2440
  // src/config.ts
2277
- var import_node_fs7 = require("fs");
2441
+ var import_node_fs8 = require("fs");
2278
2442
  var import_node_path8 = require("path");
2279
2443
  var CONFIG_FILENAME = "twee-ts.config.json";
2280
2444
  var VALID_OUTPUT_MODES = ["html", "twee3", "twee1", "twine2-archive", "twine1-archive", "json"];
2281
2445
  function loadConfig(dir) {
2282
2446
  const base = dir ?? process.cwd();
2283
2447
  const configPath = (0, import_node_path8.join)(base, CONFIG_FILENAME);
2284
- if (!(0, import_node_fs7.existsSync)(configPath)) return null;
2448
+ if (!(0, import_node_fs8.existsSync)(configPath)) return null;
2285
2449
  let raw;
2286
2450
  try {
2287
- raw = (0, import_node_fs7.readFileSync)(configPath, "utf-8");
2451
+ raw = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
2288
2452
  } catch {
2289
2453
  return null;
2290
2454
  }
@@ -2304,7 +2468,7 @@ function loadConfig(dir) {
2304
2468
  function loadConfigFile(filePath) {
2305
2469
  let raw;
2306
2470
  try {
2307
- raw = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
2471
+ raw = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
2308
2472
  } catch (e) {
2309
2473
  throw new Error(`Cannot read config file ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
2310
2474
  }
@@ -2333,7 +2497,7 @@ function validateConfig(data) {
2333
2497
  errors.push(`"${key}" must be a string.`);
2334
2498
  }
2335
2499
  }
2336
- for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote"]) {
2500
+ for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote", "sourceInfo"]) {
2337
2501
  if (key in obj && typeof obj[key] !== "boolean") {
2338
2502
  errors.push(`"${key}" must be a boolean.`);
2339
2503
  }