@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.
@@ -194,7 +194,7 @@ function passageToTwee(p, outMode) {
194
194
  output += "\n\n";
195
195
  return output;
196
196
  }
197
- function passageToPassagedata(p, pid) {
197
+ function passageToPassagedata(p, pid, options) {
198
198
  let position;
199
199
  let size;
200
200
  if (hasMetadataPosition(p)) {
@@ -211,7 +211,11 @@ function passageToPassagedata(p, pid) {
211
211
  } else {
212
212
  size = "100,100";
213
213
  }
214
- 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>`;
214
+ 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))}`;
215
+ if (options?.sourceInfo && p.source) {
216
+ attrs += ` data-source-file=${quote(attrEscape(p.source.file))} data-source-line="${p.source.line}"`;
217
+ }
218
+ return `${attrs}>${htmlEscape(p.text)}</tw-passagedata>`;
215
219
  }
216
220
  function passageToTiddler(p, pid, obfuscateRot13 = false) {
217
221
  let position;
@@ -258,8 +262,21 @@ function quote(s) {
258
262
  }
259
263
 
260
264
  // src/story.ts
265
+ var passageIndex = /* @__PURE__ */ new WeakMap();
266
+ function getIndex(story) {
267
+ let index = passageIndex.get(story);
268
+ if (!index) {
269
+ index = /* @__PURE__ */ new Map();
270
+ for (let i = 0; i < story.passages.length; i++) {
271
+ const passage = story.passages[i];
272
+ if (passage) index.set(passage.name, i);
273
+ }
274
+ passageIndex.set(story, index);
275
+ }
276
+ return index;
277
+ }
261
278
  function createStory() {
262
- return {
279
+ const story = {
263
280
  name: "",
264
281
  ifid: "",
265
282
  passages: [],
@@ -275,16 +292,17 @@ function createStory() {
275
292
  zoom: 1
276
293
  }
277
294
  };
295
+ passageIndex.set(story, /* @__PURE__ */ new Map());
296
+ return story;
278
297
  }
279
298
  function storyHas(story, name) {
280
- return story.passages.some((p) => p.name === name);
281
- }
282
- function storyIndex(story, name) {
283
- return story.passages.findIndex((p) => p.name === name);
299
+ return getIndex(story).has(name);
284
300
  }
285
301
  function storyAppend(story, p, diagnostics) {
286
- const i = storyIndex(story, p.name);
302
+ const index = getIndex(story);
303
+ const i = index.get(p.name) ?? -1;
287
304
  if (i === -1) {
305
+ index.set(p.name, story.passages.length);
288
306
  story.passages.push(p);
289
307
  } else {
290
308
  diagnostics.push({
@@ -295,9 +313,15 @@ function storyAppend(story, p, diagnostics) {
295
313
  }
296
314
  }
297
315
  function storyPrepend(story, p, diagnostics) {
298
- const i = storyIndex(story, p.name);
316
+ const index = getIndex(story);
317
+ const i = index.get(p.name) ?? -1;
299
318
  if (i === -1) {
300
319
  story.passages.unshift(p);
320
+ index.clear();
321
+ for (let j = 0; j < story.passages.length; j++) {
322
+ const passage = story.passages[j];
323
+ if (passage) index.set(passage.name, j);
324
+ }
301
325
  } else {
302
326
  diagnostics.push({
303
327
  level: "warning",
@@ -587,11 +611,15 @@ function watchFilesystem(pathnames, outFilename, callback) {
587
611
  const watchers = [];
588
612
  let buildTimer = null;
589
613
  const BUILD_DEBOUNCE = 500;
590
- function scheduleBuild() {
614
+ const pendingFiles = /* @__PURE__ */ new Set();
615
+ function scheduleBuild(changedFile) {
616
+ if (changedFile) pendingFiles.add(changedFile);
591
617
  if (buildTimer) clearTimeout(buildTimer);
592
618
  buildTimer = setTimeout(() => {
593
619
  buildTimer = null;
594
- callback();
620
+ const files = pendingFiles.size > 0 ? new Set(pendingFiles) : void 0;
621
+ pendingFiles.clear();
622
+ callback(files);
595
623
  }, BUILD_DEBOUNCE);
596
624
  }
597
625
  for (const pathname of pathnames) {
@@ -601,7 +629,8 @@ function watchFilesystem(pathnames, outFilename, callback) {
601
629
  const abs = resolve(pathname, filename);
602
630
  if (abs === absOutFile) return;
603
631
  if (isKnownFileType(filename)) {
604
- scheduleBuild();
632
+ const rel = relative(process.cwd(), abs);
633
+ scheduleBuild(rel || abs);
605
634
  }
606
635
  });
607
636
  watchers.push(watcher);
@@ -860,6 +889,7 @@ function readFormatSource(format) {
860
889
 
861
890
  // src/loader.ts
862
891
  import { basename } from "path";
892
+ import { statSync as statSync3 } from "fs";
863
893
 
864
894
  // src/lexer.ts
865
895
  var EOF = -1;
@@ -1175,7 +1205,7 @@ function parseTwee(source, options = {}) {
1175
1205
  if (pCount > 1 && current) {
1176
1206
  passages.push(current);
1177
1207
  }
1178
- current = { name: "", tags: [], text: "" };
1208
+ current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1179
1209
  break;
1180
1210
  case 3 /* Name */: {
1181
1211
  if (!current) break;
@@ -1367,54 +1397,182 @@ function loadInlineSources(story, sources, opts, diagnostics) {
1367
1397
  }
1368
1398
  }
1369
1399
  }
1370
- function loadTwee(story, filename, opts, diagnostics) {
1400
+ function parseTweeFile(filename, opts) {
1371
1401
  const source = readUTF8(filename);
1372
1402
  const result = parseTwee(source, {
1373
1403
  filename,
1374
1404
  trim: opts.trim ?? true,
1375
1405
  twee2Compat: opts.twee2Compat ?? false
1376
1406
  });
1377
- diagnostics.push(...result.diagnostics);
1378
- for (const p of result.passages) {
1379
- storyAdd(story, p, diagnostics);
1380
- }
1407
+ return { passages: result.passages, diagnostics: [...result.diagnostics] };
1381
1408
  }
1382
- function loadTagged(story, tag, filename, diagnostics) {
1409
+ function parseTaggedFile(tag, filename) {
1383
1410
  const source = readUTF8(filename);
1384
- storyAdd(story, { name: basename(filename), tags: [tag], text: source }, diagnostics);
1411
+ return { passages: [{ name: basename(filename), tags: [tag], text: source }], diagnostics: [] };
1385
1412
  }
1386
- function loadMedia(story, tag, filename, diagnostics) {
1413
+ function parseMediaFile(tag, filename) {
1387
1414
  const source = readBase64(filename);
1388
1415
  const name = baseNameWithoutExt(filename);
1389
- storyAdd(
1390
- story,
1391
- {
1392
- name,
1393
- tags: [tag],
1394
- text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1395
- },
1396
- diagnostics
1397
- );
1416
+ return {
1417
+ passages: [{ name, tags: [tag], text: `data:${mediaTypeFromFilename(filename)};base64,${source}` }],
1418
+ diagnostics: []
1419
+ };
1398
1420
  }
1399
- function loadFont(story, filename, diagnostics) {
1421
+ function parseFontFile(filename) {
1400
1422
  const source = readBase64(filename);
1401
1423
  const name = basename(filename);
1402
1424
  const family = baseNameWithoutExt(filename);
1403
1425
  const ext = normalizedFileExt(filename);
1404
1426
  const mediaType = mediaTypeFromExt(ext);
1405
1427
  const hint = fontFormatHint(ext);
1406
- storyAdd(
1407
- story,
1408
- {
1409
- name,
1410
- tags: ["stylesheet"],
1411
- text: `@font-face {
1428
+ return {
1429
+ passages: [
1430
+ {
1431
+ name,
1432
+ tags: ["stylesheet"],
1433
+ text: `@font-face {
1412
1434
  font-family: "${family}";
1413
1435
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1414
1436
  }`
1415
- },
1416
- diagnostics
1417
- );
1437
+ }
1438
+ ],
1439
+ diagnostics: []
1440
+ };
1441
+ }
1442
+ function parseFile(filename, opts) {
1443
+ const ext = normalizedFileExt(filename);
1444
+ switch (ext) {
1445
+ case "tw":
1446
+ case "twee":
1447
+ return parseTweeFile(filename, opts);
1448
+ case "tw2":
1449
+ case "twee2":
1450
+ return parseTweeFile(filename, { ...opts, twee2Compat: true });
1451
+ case "css":
1452
+ return parseTaggedFile("stylesheet", filename);
1453
+ case "js":
1454
+ return parseTaggedFile("script", filename);
1455
+ case "otf":
1456
+ case "ttf":
1457
+ case "woff":
1458
+ case "woff2":
1459
+ return parseFontFile(filename);
1460
+ case "gif":
1461
+ case "jpeg":
1462
+ case "jpg":
1463
+ case "png":
1464
+ case "svg":
1465
+ case "tif":
1466
+ case "tiff":
1467
+ case "webp":
1468
+ return parseMediaFile("Twine.image", filename);
1469
+ case "aac":
1470
+ case "flac":
1471
+ case "m4a":
1472
+ case "mp3":
1473
+ case "oga":
1474
+ case "ogg":
1475
+ case "opus":
1476
+ case "wav":
1477
+ case "wave":
1478
+ case "weba":
1479
+ return parseMediaFile("Twine.audio", filename);
1480
+ case "mp4":
1481
+ case "ogv":
1482
+ case "webm":
1483
+ return parseMediaFile("Twine.video", filename);
1484
+ case "vtt":
1485
+ return parseMediaFile("Twine.vtt", filename);
1486
+ default:
1487
+ return void 0;
1488
+ }
1489
+ }
1490
+ function loadTwee(story, filename, opts, diagnostics) {
1491
+ const result = parseTweeFile(filename, opts);
1492
+ diagnostics.push(...result.diagnostics);
1493
+ for (const p of result.passages) {
1494
+ storyAdd(story, p, diagnostics);
1495
+ }
1496
+ }
1497
+ function loadTagged(story, tag, filename, diagnostics) {
1498
+ const result = parseTaggedFile(tag, filename);
1499
+ for (const p of result.passages) {
1500
+ storyAdd(story, p, diagnostics);
1501
+ }
1502
+ }
1503
+ function loadMedia(story, tag, filename, diagnostics) {
1504
+ const result = parseMediaFile(tag, filename);
1505
+ for (const p of result.passages) {
1506
+ storyAdd(story, p, diagnostics);
1507
+ }
1508
+ }
1509
+ function loadFont(story, filename, diagnostics) {
1510
+ const result = parseFontFile(filename);
1511
+ for (const p of result.passages) {
1512
+ storyAdd(story, p, diagnostics);
1513
+ }
1514
+ }
1515
+ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles) {
1516
+ const currentFiles = /* @__PURE__ */ new Set();
1517
+ for (const filename of filenames) {
1518
+ if (processedFiles.has(filename)) {
1519
+ diagnostics.push({ level: "warning", message: `load ${filename}: Skipping duplicate.` });
1520
+ continue;
1521
+ }
1522
+ currentFiles.add(filename);
1523
+ const cached = cache.get(filename);
1524
+ if (changedFiles && cached && !changedFiles.has(filename)) {
1525
+ diagnostics.push(...cached.diagnostics);
1526
+ for (const p of cached.passages) {
1527
+ storyAdd(story, p, diagnostics);
1528
+ }
1529
+ processedFiles.add(filename);
1530
+ continue;
1531
+ }
1532
+ let mtimeMs;
1533
+ try {
1534
+ mtimeMs = statSync3(filename).mtimeMs;
1535
+ } catch {
1536
+ continue;
1537
+ }
1538
+ if (cached && cached.mtimeMs === mtimeMs) {
1539
+ diagnostics.push(...cached.diagnostics);
1540
+ for (const p of cached.passages) {
1541
+ storyAdd(story, p, diagnostics);
1542
+ }
1543
+ processedFiles.add(filename);
1544
+ continue;
1545
+ }
1546
+ try {
1547
+ const result = parseFile(filename, opts);
1548
+ if (!result) continue;
1549
+ cache.set(filename, {
1550
+ mtimeMs,
1551
+ passages: result.passages,
1552
+ diagnostics: result.diagnostics
1553
+ });
1554
+ diagnostics.push(...result.diagnostics);
1555
+ for (const p of result.passages) {
1556
+ storyAdd(story, p, diagnostics);
1557
+ }
1558
+ } catch (e) {
1559
+ diagnostics.push({
1560
+ level: "error",
1561
+ message: `load ${filename}: ${e instanceof Error ? e.message : String(e)}`,
1562
+ file: filename
1563
+ });
1564
+ continue;
1565
+ }
1566
+ processedFiles.add(filename);
1567
+ }
1568
+ for (const key of cache.keys()) {
1569
+ if (!currentFiles.has(key)) {
1570
+ cache.delete(key);
1571
+ }
1572
+ }
1573
+ if (story.name !== "" && !story.passages.some((p) => p.name === "StoryTitle")) {
1574
+ storyPrepend(story, { name: "StoryTitle", tags: [], text: story.name }, diagnostics);
1575
+ }
1418
1576
  }
1419
1577
 
1420
1578
  // src/version.ts
@@ -1422,20 +1580,20 @@ var VERSION = "1.2.0";
1422
1580
 
1423
1581
  // src/output-twine2.ts
1424
1582
  var CREATOR_NAME = "Twee-ts";
1425
- function toTwine2Archive(story, startName, diagnostics) {
1426
- return getTwine2DataChunk(story, startName, diagnostics) + "\n";
1583
+ function toTwine2Archive(story, startName, diagnostics, options) {
1584
+ return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
1427
1585
  }
1428
- function toTwine2HTML(story, format, startName, diagnostics) {
1586
+ function toTwine2HTML(story, format, startName, diagnostics, options) {
1429
1587
  let template = readFormatSource(format);
1430
1588
  if (template.includes("{{STORY_NAME}}")) {
1431
1589
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
1432
1590
  }
1433
1591
  if (template.includes("{{STORY_DATA}}")) {
1434
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics));
1592
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
1435
1593
  }
1436
1594
  return template;
1437
1595
  }
1438
- function getTwine2DataChunk(story, startName, diagnostics) {
1596
+ function getTwine2DataChunk(story, startName, diagnostics, options) {
1439
1597
  ensureIFID(story, diagnostics);
1440
1598
  const parts = [];
1441
1599
  let startID = "";
@@ -1490,7 +1648,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1490
1648
  if (p.name === "StorySettings" && story.twine1.settings.size === 0) {
1491
1649
  continue;
1492
1650
  }
1493
- parts.push(passageToPassagedata(p, pid));
1651
+ parts.push(passageToPassagedata(p, pid, options));
1494
1652
  if (startName === p.name) {
1495
1653
  startID = String(pid);
1496
1654
  }
@@ -1500,9 +1658,9 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1500
1658
  for (const [opt, val] of story.twine2.options) {
1501
1659
  if (val) opts.push(opt);
1502
1660
  }
1503
- const options = opts.join(" ");
1661
+ const optionsStr = opts.join(" ");
1504
1662
  const zoom = String(story.twine2.zoom);
1505
- 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>`;
1663
+ 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>`;
1506
1664
  return wrapper + parts.join("") + "</tw-storydata>";
1507
1665
  }
1508
1666
  function ensureIFID(story, diagnostics) {
@@ -1683,7 +1841,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
1683
1841
  }
1684
1842
 
1685
1843
  // src/remote-formats.ts
1686
- import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1844
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1687
1845
  import { join as join4 } from "path";
1688
1846
  import { homedir as homedir2 } from "os";
1689
1847
  var DEFAULT_SFA_INDICES = [
@@ -1895,7 +2053,7 @@ function discoverCachedFormats() {
1895
2053
  for (const name of names) {
1896
2054
  const nameDir = join4(cacheDir, name);
1897
2055
  try {
1898
- if (!statSync3(nameDir).isDirectory()) continue;
2056
+ if (!statSync4(nameDir).isDirectory()) continue;
1899
2057
  } catch {
1900
2058
  continue;
1901
2059
  }
@@ -1936,11 +2094,12 @@ async function compileToFile(options) {
1936
2094
  }
1937
2095
  async function watch(options) {
1938
2096
  const controller = new AbortController();
2097
+ const cache = /* @__PURE__ */ new Map();
1939
2098
  const filePaths = options.sources.filter((s) => typeof s === "string");
1940
2099
  const modulePaths = options.modules ?? [];
1941
2100
  const allPaths = [...filePaths, ...modulePaths];
1942
- const handle = watchFilesystem(allPaths, options.outFile, () => {
1943
- buildOutput(options).then((result) => {
2101
+ const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
2102
+ buildOutput(options, cache, changedFiles).then((result) => {
1944
2103
  writeFileSync2(options.outFile, result.output, "utf-8");
1945
2104
  options.onBuild?.(result);
1946
2105
  }).catch((e) => {
@@ -1950,13 +2109,14 @@ async function watch(options) {
1950
2109
  controller.signal.addEventListener("abort", () => handle.close());
1951
2110
  return controller;
1952
2111
  }
1953
- async function buildOutput(options) {
2112
+ async function buildOutput(options, cache, changedFiles) {
1954
2113
  const diagnostics = [];
1955
2114
  const outputMode = options.outputMode ?? "html";
1956
2115
  const trim = options.trim ?? true;
1957
2116
  const twee2Compat = options.twee2Compat ?? false;
1958
2117
  const testMode = options.testMode ?? false;
1959
2118
  const noRemote = options.noRemote ?? false;
2119
+ const sourceInfo = options.sourceInfo ?? false;
1960
2120
  clearIndexCache();
1961
2121
  const filePaths = [];
1962
2122
  const inlineSources = [];
@@ -1970,7 +2130,11 @@ async function buildOutput(options) {
1970
2130
  const sourceFilenames = getFilenames(filePaths);
1971
2131
  const story = createStory();
1972
2132
  const processedFiles = /* @__PURE__ */ new Set();
1973
- loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2133
+ if (cache) {
2134
+ loadSourcesCached(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles, cache, changedFiles);
2135
+ } else {
2136
+ loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2137
+ }
1974
2138
  loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
1975
2139
  if (options.tagAliases) {
1976
2140
  story.passages = applyTagAliases(story.passages, options.tagAliases);
@@ -2018,7 +2182,7 @@ async function buildOutput(options) {
2018
2182
  output = toTwee(story, outputMode);
2019
2183
  break;
2020
2184
  case "twine2-archive":
2021
- output = toTwine2Archive(story, startName, diagnostics);
2185
+ output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
2022
2186
  break;
2023
2187
  case "twine1-archive":
2024
2188
  output = toTwine1Archive(story, startName);
@@ -2037,7 +2201,7 @@ async function buildOutput(options) {
2037
2201
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
2038
2202
  }
2039
2203
  if (format.isTwine2) {
2040
- output = toTwine2HTML(story, format, startName, diagnostics);
2204
+ output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
2041
2205
  } else {
2042
2206
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
2043
2207
  diagnostics.push({
@@ -2168,7 +2332,7 @@ function validateConfig(data) {
2168
2332
  errors.push(`"${key}" must be a string.`);
2169
2333
  }
2170
2334
  }
2171
- for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote"]) {
2335
+ for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote", "sourceInfo"]) {
2172
2336
  if (key in obj && typeof obj[key] !== "boolean") {
2173
2337
  errors.push(`"${key}" must be a boolean.`);
2174
2338
  }
@@ -2238,6 +2402,7 @@ var { values, positionals } = parseArgs({
2238
2402
  "format-url": { type: "string", multiple: true },
2239
2403
  "no-remote": { type: "boolean" },
2240
2404
  "tag-alias": { type: "string", multiple: true },
2405
+ "source-info": { type: "boolean" },
2241
2406
  config: { type: "string", short: "c" },
2242
2407
  "no-config": { type: "boolean" }
2243
2408
  }
@@ -2306,7 +2471,8 @@ async function main() {
2306
2471
  formatIndices: values["format-index"] ?? config?.formatIndices,
2307
2472
  formatUrls: values["format-url"] ?? config?.formatUrls,
2308
2473
  noRemote: values["no-remote"] ?? config?.noRemote ?? false,
2309
- tagAliases
2474
+ tagAliases,
2475
+ sourceInfo: values["source-info"] ?? config?.sourceInfo ?? false
2310
2476
  };
2311
2477
  const outFile = values.output ?? config?.output ?? "-";
2312
2478
  if (values.watch) {
@@ -2432,6 +2598,7 @@ Options:
2432
2598
  --format-index <url> SFA-compatible format index URL (repeatable)
2433
2599
  --format-url <url> Direct format.js URL (repeatable)
2434
2600
  --tag-alias <alias=target> Map a tag to a special tag (repeatable)
2601
+ --source-info Emit source file/line as data- attributes on passages
2435
2602
  --no-remote Disable remote format fetching
2436
2603
  -c, --config <file> Config file path (default: ${CONFIG_FILENAME})
2437
2604
  --no-config Skip config file loading