@rohal12/twee-ts 1.5.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");
@@ -309,8 +309,21 @@ function quote(s) {
309
309
  }
310
310
 
311
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
+ }
312
325
  function createStory() {
313
- return {
326
+ const story = {
314
327
  name: "",
315
328
  ifid: "",
316
329
  passages: [],
@@ -326,16 +339,17 @@ function createStory() {
326
339
  zoom: 1
327
340
  }
328
341
  };
342
+ passageIndex.set(story, /* @__PURE__ */ new Map());
343
+ return story;
329
344
  }
330
345
  function storyHas(story, name) {
331
- return story.passages.some((p) => p.name === name);
332
- }
333
- function storyIndex(story, name) {
334
- return story.passages.findIndex((p) => p.name === name);
346
+ return getIndex(story).has(name);
335
347
  }
336
348
  function storyAppend(story, p, diagnostics) {
337
- const i = storyIndex(story, p.name);
349
+ const index = getIndex(story);
350
+ const i = index.get(p.name) ?? -1;
338
351
  if (i === -1) {
352
+ index.set(p.name, story.passages.length);
339
353
  story.passages.push(p);
340
354
  } else {
341
355
  diagnostics.push({
@@ -346,9 +360,15 @@ function storyAppend(story, p, diagnostics) {
346
360
  }
347
361
  }
348
362
  function storyPrepend(story, p, diagnostics) {
349
- const i = storyIndex(story, p.name);
363
+ const index = getIndex(story);
364
+ const i = index.get(p.name) ?? -1;
350
365
  if (i === -1) {
351
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
+ }
352
372
  } else {
353
373
  diagnostics.push({
354
374
  level: "warning",
@@ -638,11 +658,15 @@ function watchFilesystem(pathnames, outFilename, callback) {
638
658
  const watchers = [];
639
659
  let buildTimer = null;
640
660
  const BUILD_DEBOUNCE = 500;
641
- function scheduleBuild() {
661
+ const pendingFiles = /* @__PURE__ */ new Set();
662
+ function scheduleBuild(changedFile) {
663
+ if (changedFile) pendingFiles.add(changedFile);
642
664
  if (buildTimer) clearTimeout(buildTimer);
643
665
  buildTimer = setTimeout(() => {
644
666
  buildTimer = null;
645
- callback();
667
+ const files = pendingFiles.size > 0 ? new Set(pendingFiles) : void 0;
668
+ pendingFiles.clear();
669
+ callback(files);
646
670
  }, BUILD_DEBOUNCE);
647
671
  }
648
672
  for (const pathname of pathnames) {
@@ -652,7 +676,8 @@ function watchFilesystem(pathnames, outFilename, callback) {
652
676
  const abs = (0, import_node_path2.resolve)(pathname, filename);
653
677
  if (abs === absOutFile) return;
654
678
  if (isKnownFileType(filename)) {
655
- scheduleBuild();
679
+ const rel = (0, import_node_path2.relative)(process.cwd(), abs);
680
+ scheduleBuild(rel || abs);
656
681
  }
657
682
  });
658
683
  watchers.push(watcher);
@@ -911,6 +936,7 @@ function readFormatSource(format) {
911
936
 
912
937
  // src/loader.ts
913
938
  var import_node_path5 = require("path");
939
+ var import_node_fs4 = require("fs");
914
940
 
915
941
  // src/lexer.ts
916
942
  var EOF = -1;
@@ -1434,54 +1460,182 @@ function loadInlineSources(story, sources, opts, diagnostics) {
1434
1460
  }
1435
1461
  }
1436
1462
  }
1437
- function loadTwee(story, filename, opts, diagnostics) {
1463
+ function parseTweeFile(filename, opts) {
1438
1464
  const source = readUTF8(filename);
1439
1465
  const result = parseTwee(source, {
1440
1466
  filename,
1441
1467
  trim: opts.trim ?? true,
1442
1468
  twee2Compat: opts.twee2Compat ?? false
1443
1469
  });
1444
- diagnostics.push(...result.diagnostics);
1445
- for (const p of result.passages) {
1446
- storyAdd(story, p, diagnostics);
1447
- }
1470
+ return { passages: result.passages, diagnostics: [...result.diagnostics] };
1448
1471
  }
1449
- function loadTagged(story, tag, filename, diagnostics) {
1472
+ function parseTaggedFile(tag, filename) {
1450
1473
  const source = readUTF8(filename);
1451
- 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: [] };
1452
1475
  }
1453
- function loadMedia(story, tag, filename, diagnostics) {
1476
+ function parseMediaFile(tag, filename) {
1454
1477
  const source = readBase64(filename);
1455
1478
  const name = baseNameWithoutExt(filename);
1456
- storyAdd(
1457
- story,
1458
- {
1459
- name,
1460
- tags: [tag],
1461
- text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1462
- },
1463
- diagnostics
1464
- );
1479
+ return {
1480
+ passages: [{ name, tags: [tag], text: `data:${mediaTypeFromFilename(filename)};base64,${source}` }],
1481
+ diagnostics: []
1482
+ };
1465
1483
  }
1466
- function loadFont(story, filename, diagnostics) {
1484
+ function parseFontFile(filename) {
1467
1485
  const source = readBase64(filename);
1468
1486
  const name = (0, import_node_path5.basename)(filename);
1469
1487
  const family = baseNameWithoutExt(filename);
1470
1488
  const ext = normalizedFileExt(filename);
1471
1489
  const mediaType = mediaTypeFromExt(ext);
1472
1490
  const hint = fontFormatHint(ext);
1473
- storyAdd(
1474
- story,
1475
- {
1476
- name,
1477
- tags: ["stylesheet"],
1478
- text: `@font-face {
1491
+ return {
1492
+ passages: [
1493
+ {
1494
+ name,
1495
+ tags: ["stylesheet"],
1496
+ text: `@font-face {
1479
1497
  font-family: "${family}";
1480
1498
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1481
1499
  }`
1482
- },
1483
- diagnostics
1484
- );
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
+ }
1485
1639
  }
1486
1640
 
1487
1641
  // src/version.ts
@@ -1591,7 +1745,7 @@ function ensureIFID(story, diagnostics) {
1591
1745
  }
1592
1746
 
1593
1747
  // src/output-twine1.ts
1594
- var import_node_fs4 = require("fs");
1748
+ var import_node_fs5 = require("fs");
1595
1749
  var import_node_path6 = require("path");
1596
1750
  var CREATOR_NAME2 = "twee-ts";
1597
1751
  function toTwine1Archive(story, _startName) {
@@ -1624,7 +1778,7 @@ function toTwine1HTML(story, format, startName, _diagnostics) {
1624
1778
  } else {
1625
1779
  let footer;
1626
1780
  try {
1627
- 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");
1628
1782
  } catch {
1629
1783
  footer = "</div>\n</body>\n</html>\n";
1630
1784
  }
@@ -1654,7 +1808,7 @@ function getTwine1PassageChunk(story) {
1654
1808
  function tryReplaceComponent(template, placeholder, componentPath, required) {
1655
1809
  if (!template.includes(placeholder)) return template;
1656
1810
  try {
1657
- let content = (0, import_node_fs4.readFileSync)(componentPath, "utf-8");
1811
+ let content = (0, import_node_fs5.readFileSync)(componentPath, "utf-8");
1658
1812
  if (content.charCodeAt(0) === 65279) content = content.slice(1);
1659
1813
  return template.replace(placeholder, content);
1660
1814
  } catch (e) {
@@ -1750,7 +1904,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
1750
1904
  }
1751
1905
 
1752
1906
  // src/remote-formats.ts
1753
- var import_node_fs5 = require("fs");
1907
+ var import_node_fs6 = require("fs");
1754
1908
  var import_node_path7 = require("path");
1755
1909
  var import_node_os2 = require("os");
1756
1910
  var DEFAULT_SFA_INDICES = [
@@ -1851,9 +2005,9 @@ async function fetchAndCacheFormat(entry, downloadUrl) {
1851
2005
  }
1852
2006
  const cacheDir = getCacheDir();
1853
2007
  const formatDir = (0, import_node_path7.join)(cacheDir, entry.name, entry.version);
1854
- (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
2008
+ (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1855
2009
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1856
- (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
2010
+ (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1857
2011
  return {
1858
2012
  id,
1859
2013
  filename: formatPath,
@@ -1877,9 +2031,9 @@ async function fetchDirectFormat(url) {
1877
2031
  const id = makeFormatId(data.name, data.version);
1878
2032
  const cacheDir = getCacheDir();
1879
2033
  const formatDir = (0, import_node_path7.join)(cacheDir, data.name, data.version);
1880
- (0, import_node_fs5.mkdirSync)(formatDir, { recursive: true });
2034
+ (0, import_node_fs6.mkdirSync)(formatDir, { recursive: true });
1881
2035
  const formatPath = (0, import_node_path7.join)(formatDir, "format.js");
1882
- (0, import_node_fs5.writeFileSync)(formatPath, text, "utf-8");
2036
+ (0, import_node_fs6.writeFileSync)(formatPath, text, "utf-8");
1883
2037
  return {
1884
2038
  id,
1885
2039
  filename: formatPath,
@@ -1928,8 +2082,8 @@ async function resolveRemoteFormat(name, version, indices, urls) {
1928
2082
  function getCachedFormat(name, version) {
1929
2083
  const formatPath = (0, import_node_path7.join)(getCacheDir(), name, version, "format.js");
1930
2084
  try {
1931
- if (!(0, import_node_fs5.existsSync)(formatPath)) return void 0;
1932
- 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");
1933
2087
  const id = makeFormatId(name, version);
1934
2088
  const data = parseFormatJSON(source, id);
1935
2089
  if (!data) return void 0;
@@ -1949,26 +2103,26 @@ function discoverCachedFormats() {
1949
2103
  const formats = /* @__PURE__ */ new Map();
1950
2104
  const cacheDir = getCacheDir();
1951
2105
  try {
1952
- if (!(0, import_node_fs5.existsSync)(cacheDir)) return formats;
2106
+ if (!(0, import_node_fs6.existsSync)(cacheDir)) return formats;
1953
2107
  } catch {
1954
2108
  return formats;
1955
2109
  }
1956
2110
  let names;
1957
2111
  try {
1958
- names = (0, import_node_fs5.readdirSync)(cacheDir);
2112
+ names = (0, import_node_fs6.readdirSync)(cacheDir);
1959
2113
  } catch {
1960
2114
  return formats;
1961
2115
  }
1962
2116
  for (const name of names) {
1963
2117
  const nameDir = (0, import_node_path7.join)(cacheDir, name);
1964
2118
  try {
1965
- if (!(0, import_node_fs5.statSync)(nameDir).isDirectory()) continue;
2119
+ if (!(0, import_node_fs6.statSync)(nameDir).isDirectory()) continue;
1966
2120
  } catch {
1967
2121
  continue;
1968
2122
  }
1969
2123
  let versions;
1970
2124
  try {
1971
- versions = (0, import_node_fs5.readdirSync)(nameDir);
2125
+ versions = (0, import_node_fs6.readdirSync)(nameDir);
1972
2126
  } catch {
1973
2127
  continue;
1974
2128
  }
@@ -1998,17 +2152,18 @@ async function compile(options) {
1998
2152
  }
1999
2153
  async function compileToFile(options) {
2000
2154
  const result = await compile(options);
2001
- (0, import_node_fs6.writeFileSync)(options.outFile, result.output, "utf-8");
2155
+ (0, import_node_fs7.writeFileSync)(options.outFile, result.output, "utf-8");
2002
2156
  return result;
2003
2157
  }
2004
2158
  async function watch(options) {
2005
2159
  const controller = new AbortController();
2160
+ const cache = /* @__PURE__ */ new Map();
2006
2161
  const filePaths = options.sources.filter((s) => typeof s === "string");
2007
2162
  const modulePaths = options.modules ?? [];
2008
2163
  const allPaths = [...filePaths, ...modulePaths];
2009
- const handle = watchFilesystem(allPaths, options.outFile, () => {
2010
- buildOutput(options).then((result) => {
2011
- (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");
2012
2167
  options.onBuild?.(result);
2013
2168
  }).catch((e) => {
2014
2169
  options.onError?.(e instanceof Error ? e : new Error(String(e)));
@@ -2017,7 +2172,7 @@ async function watch(options) {
2017
2172
  controller.signal.addEventListener("abort", () => handle.close());
2018
2173
  return controller;
2019
2174
  }
2020
- async function buildOutput(options) {
2175
+ async function buildOutput(options, cache, changedFiles) {
2021
2176
  const diagnostics = [];
2022
2177
  const outputMode = options.outputMode ?? "html";
2023
2178
  const trim = options.trim ?? true;
@@ -2038,7 +2193,11 @@ async function buildOutput(options) {
2038
2193
  const sourceFilenames = getFilenames(filePaths);
2039
2194
  const story = createStory();
2040
2195
  const processedFiles = /* @__PURE__ */ new Set();
2041
- 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
+ }
2042
2201
  loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
2043
2202
  if (options.tagAliases) {
2044
2203
  story.passages = applyTagAliases(story.passages, options.tagAliases);
@@ -2279,17 +2438,17 @@ function storyInspect(story) {
2279
2438
  }
2280
2439
 
2281
2440
  // src/config.ts
2282
- var import_node_fs7 = require("fs");
2441
+ var import_node_fs8 = require("fs");
2283
2442
  var import_node_path8 = require("path");
2284
2443
  var CONFIG_FILENAME = "twee-ts.config.json";
2285
2444
  var VALID_OUTPUT_MODES = ["html", "twee3", "twee1", "twine2-archive", "twine1-archive", "json"];
2286
2445
  function loadConfig(dir) {
2287
2446
  const base = dir ?? process.cwd();
2288
2447
  const configPath = (0, import_node_path8.join)(base, CONFIG_FILENAME);
2289
- if (!(0, import_node_fs7.existsSync)(configPath)) return null;
2448
+ if (!(0, import_node_fs8.existsSync)(configPath)) return null;
2290
2449
  let raw;
2291
2450
  try {
2292
- raw = (0, import_node_fs7.readFileSync)(configPath, "utf-8");
2451
+ raw = (0, import_node_fs8.readFileSync)(configPath, "utf-8");
2293
2452
  } catch {
2294
2453
  return null;
2295
2454
  }
@@ -2309,7 +2468,7 @@ function loadConfig(dir) {
2309
2468
  function loadConfigFile(filePath) {
2310
2469
  let raw;
2311
2470
  try {
2312
- raw = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
2471
+ raw = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
2313
2472
  } catch (e) {
2314
2473
  throw new Error(`Cannot read config file ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
2315
2474
  }