@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.d.cts CHANGED
@@ -38,6 +38,8 @@ interface CompileOptions {
38
38
  noRemote?: boolean;
39
39
  /** Map alias tags to canonical special tags (e.g. { library: 'script' }). */
40
40
  tagAliases?: Record<string, string>;
41
+ /** Emit source file and line as data- attributes on passage elements. Default: false. */
42
+ sourceInfo?: boolean;
41
43
  }
42
44
  interface CompileToFileOptions extends CompileOptions {
43
45
  /** Output file path. */
@@ -73,6 +75,10 @@ interface CompileStats {
73
75
  words: number;
74
76
  files: string[];
75
77
  }
78
+ interface SourceLocation {
79
+ readonly file: string;
80
+ readonly line: number;
81
+ }
76
82
  interface PassageMetadata {
77
83
  position?: string;
78
84
  size?: string;
@@ -83,6 +89,7 @@ interface Passage {
83
89
  tags: string[];
84
90
  text: string;
85
91
  metadata?: PassageMetadata;
92
+ source?: SourceLocation;
86
93
  }
87
94
  interface Twine1Metadata {
88
95
  settings: Map<string, string>;
@@ -171,6 +178,7 @@ interface TweeTsConfig {
171
178
  testMode?: boolean;
172
179
  noRemote?: boolean;
173
180
  tagAliases?: Record<string, string>;
181
+ sourceInfo?: boolean;
174
182
  }
175
183
 
176
184
  declare class TweeTsError extends Error {
@@ -365,4 +373,4 @@ declare function resolveRemoteFormat(name: string, version: string, indices?: st
365
373
  /** Discover all cached remote formats. Returns a Map like discoverFormats(). */
366
374
  declare function discoverCachedFormats(): Map<string, StoryFormatInfo>;
367
375
 
368
- export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
376
+ export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type SourceLocation, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
package/dist/index.d.ts CHANGED
@@ -38,6 +38,8 @@ interface CompileOptions {
38
38
  noRemote?: boolean;
39
39
  /** Map alias tags to canonical special tags (e.g. { library: 'script' }). */
40
40
  tagAliases?: Record<string, string>;
41
+ /** Emit source file and line as data- attributes on passage elements. Default: false. */
42
+ sourceInfo?: boolean;
41
43
  }
42
44
  interface CompileToFileOptions extends CompileOptions {
43
45
  /** Output file path. */
@@ -73,6 +75,10 @@ interface CompileStats {
73
75
  words: number;
74
76
  files: string[];
75
77
  }
78
+ interface SourceLocation {
79
+ readonly file: string;
80
+ readonly line: number;
81
+ }
76
82
  interface PassageMetadata {
77
83
  position?: string;
78
84
  size?: string;
@@ -83,6 +89,7 @@ interface Passage {
83
89
  tags: string[];
84
90
  text: string;
85
91
  metadata?: PassageMetadata;
92
+ source?: SourceLocation;
86
93
  }
87
94
  interface Twine1Metadata {
88
95
  settings: Map<string, string>;
@@ -171,6 +178,7 @@ interface TweeTsConfig {
171
178
  testMode?: boolean;
172
179
  noRemote?: boolean;
173
180
  tagAliases?: Record<string, string>;
181
+ sourceInfo?: boolean;
174
182
  }
175
183
 
176
184
  declare class TweeTsError extends Error {
@@ -365,4 +373,4 @@ declare function resolveRemoteFormat(name: string, version: string, indices?: st
365
373
  /** Discover all cached remote formats. Returns a Map like discoverFormats(). */
366
374
  declare function discoverCachedFormats(): Map<string, StoryFormatInfo>;
367
375
 
368
- export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
376
+ export { type BrokenLink, CONFIG_FILENAME, type CompileOptions, type CompileResult, type CompileStats, type CompileToFileOptions, type Diagnostic, type InlineSource, ItemType, type LexerItem, type OutputMode, type Passage, type PassageMetadata, type SFAIndex, type SFAIndexEntry, type SourceInput, type SourceLocation, type Story, type StoryFormatInfo, type StoryMap, TweeLexer, type TweeTsConfig, TweeTsError, type WatchOptions, applyTagAliases, compile, compileToFile, discoverCachedFormats, discoverFormats, fetchAndCacheFormat, fetchDirectFormat, generateIFID, getCacheDir, getFormatSearchDirs, loadConfig, loadConfigFile, parseFormatJSON, parseSemver, parseTwee, resolveRemoteFormat, scaffoldConfig, semverCompare, storyInspect, tweeLexer, validateConfig, validateIFID, watch };
package/dist/index.js CHANGED
@@ -190,7 +190,7 @@ function passageToTwee(p, outMode) {
190
190
  output += "\n\n";
191
191
  return output;
192
192
  }
193
- function passageToPassagedata(p, pid) {
193
+ function passageToPassagedata(p, pid, options) {
194
194
  let position;
195
195
  let size;
196
196
  if (hasMetadataPosition(p)) {
@@ -207,7 +207,11 @@ function passageToPassagedata(p, pid) {
207
207
  } else {
208
208
  size = "100,100";
209
209
  }
210
- 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>`;
210
+ 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))}`;
211
+ if (options?.sourceInfo && p.source) {
212
+ attrs += ` data-source-file=${quote(attrEscape(p.source.file))} data-source-line="${p.source.line}"`;
213
+ }
214
+ return `${attrs}>${htmlEscape(p.text)}</tw-passagedata>`;
211
215
  }
212
216
  function passageToTiddler(p, pid, obfuscateRot13 = false) {
213
217
  let position;
@@ -254,8 +258,21 @@ function quote(s) {
254
258
  }
255
259
 
256
260
  // src/story.ts
261
+ var passageIndex = /* @__PURE__ */ new WeakMap();
262
+ function getIndex(story) {
263
+ let index = passageIndex.get(story);
264
+ if (!index) {
265
+ index = /* @__PURE__ */ new Map();
266
+ for (let i = 0; i < story.passages.length; i++) {
267
+ const passage = story.passages[i];
268
+ if (passage) index.set(passage.name, i);
269
+ }
270
+ passageIndex.set(story, index);
271
+ }
272
+ return index;
273
+ }
257
274
  function createStory() {
258
- return {
275
+ const story = {
259
276
  name: "",
260
277
  ifid: "",
261
278
  passages: [],
@@ -271,16 +288,17 @@ function createStory() {
271
288
  zoom: 1
272
289
  }
273
290
  };
291
+ passageIndex.set(story, /* @__PURE__ */ new Map());
292
+ return story;
274
293
  }
275
294
  function storyHas(story, name) {
276
- return story.passages.some((p) => p.name === name);
277
- }
278
- function storyIndex(story, name) {
279
- return story.passages.findIndex((p) => p.name === name);
295
+ return getIndex(story).has(name);
280
296
  }
281
297
  function storyAppend(story, p, diagnostics) {
282
- const i = storyIndex(story, p.name);
298
+ const index = getIndex(story);
299
+ const i = index.get(p.name) ?? -1;
283
300
  if (i === -1) {
301
+ index.set(p.name, story.passages.length);
284
302
  story.passages.push(p);
285
303
  } else {
286
304
  diagnostics.push({
@@ -291,9 +309,15 @@ function storyAppend(story, p, diagnostics) {
291
309
  }
292
310
  }
293
311
  function storyPrepend(story, p, diagnostics) {
294
- const i = storyIndex(story, p.name);
312
+ const index = getIndex(story);
313
+ const i = index.get(p.name) ?? -1;
295
314
  if (i === -1) {
296
315
  story.passages.unshift(p);
316
+ index.clear();
317
+ for (let j = 0; j < story.passages.length; j++) {
318
+ const passage = story.passages[j];
319
+ if (passage) index.set(passage.name, j);
320
+ }
297
321
  } else {
298
322
  diagnostics.push({
299
323
  level: "warning",
@@ -583,11 +607,15 @@ function watchFilesystem(pathnames, outFilename, callback) {
583
607
  const watchers = [];
584
608
  let buildTimer = null;
585
609
  const BUILD_DEBOUNCE = 500;
586
- function scheduleBuild() {
610
+ const pendingFiles = /* @__PURE__ */ new Set();
611
+ function scheduleBuild(changedFile) {
612
+ if (changedFile) pendingFiles.add(changedFile);
587
613
  if (buildTimer) clearTimeout(buildTimer);
588
614
  buildTimer = setTimeout(() => {
589
615
  buildTimer = null;
590
- callback();
616
+ const files = pendingFiles.size > 0 ? new Set(pendingFiles) : void 0;
617
+ pendingFiles.clear();
618
+ callback(files);
591
619
  }, BUILD_DEBOUNCE);
592
620
  }
593
621
  for (const pathname of pathnames) {
@@ -597,7 +625,8 @@ function watchFilesystem(pathnames, outFilename, callback) {
597
625
  const abs = resolve(pathname, filename);
598
626
  if (abs === absOutFile) return;
599
627
  if (isKnownFileType(filename)) {
600
- scheduleBuild();
628
+ const rel = relative(process.cwd(), abs);
629
+ scheduleBuild(rel || abs);
601
630
  }
602
631
  });
603
632
  watchers.push(watcher);
@@ -856,6 +885,7 @@ function readFormatSource(format) {
856
885
 
857
886
  // src/loader.ts
858
887
  import { basename } from "path";
888
+ import { statSync as statSync3 } from "fs";
859
889
 
860
890
  // src/lexer.ts
861
891
  var EOF = -1;
@@ -1187,7 +1217,7 @@ function parseTwee(source, options = {}) {
1187
1217
  if (pCount > 1 && current) {
1188
1218
  passages.push(current);
1189
1219
  }
1190
- current = { name: "", tags: [], text: "" };
1220
+ current = { name: "", tags: [], text: "", source: { file: filename, line: item.line } };
1191
1221
  break;
1192
1222
  case 3 /* Name */: {
1193
1223
  if (!current) break;
@@ -1379,54 +1409,182 @@ function loadInlineSources(story, sources, opts, diagnostics) {
1379
1409
  }
1380
1410
  }
1381
1411
  }
1382
- function loadTwee(story, filename, opts, diagnostics) {
1412
+ function parseTweeFile(filename, opts) {
1383
1413
  const source = readUTF8(filename);
1384
1414
  const result = parseTwee(source, {
1385
1415
  filename,
1386
1416
  trim: opts.trim ?? true,
1387
1417
  twee2Compat: opts.twee2Compat ?? false
1388
1418
  });
1389
- diagnostics.push(...result.diagnostics);
1390
- for (const p of result.passages) {
1391
- storyAdd(story, p, diagnostics);
1392
- }
1419
+ return { passages: result.passages, diagnostics: [...result.diagnostics] };
1393
1420
  }
1394
- function loadTagged(story, tag, filename, diagnostics) {
1421
+ function parseTaggedFile(tag, filename) {
1395
1422
  const source = readUTF8(filename);
1396
- storyAdd(story, { name: basename(filename), tags: [tag], text: source }, diagnostics);
1423
+ return { passages: [{ name: basename(filename), tags: [tag], text: source }], diagnostics: [] };
1397
1424
  }
1398
- function loadMedia(story, tag, filename, diagnostics) {
1425
+ function parseMediaFile(tag, filename) {
1399
1426
  const source = readBase64(filename);
1400
1427
  const name = baseNameWithoutExt(filename);
1401
- storyAdd(
1402
- story,
1403
- {
1404
- name,
1405
- tags: [tag],
1406
- text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
1407
- },
1408
- diagnostics
1409
- );
1428
+ return {
1429
+ passages: [{ name, tags: [tag], text: `data:${mediaTypeFromFilename(filename)};base64,${source}` }],
1430
+ diagnostics: []
1431
+ };
1410
1432
  }
1411
- function loadFont(story, filename, diagnostics) {
1433
+ function parseFontFile(filename) {
1412
1434
  const source = readBase64(filename);
1413
1435
  const name = basename(filename);
1414
1436
  const family = baseNameWithoutExt(filename);
1415
1437
  const ext = normalizedFileExt(filename);
1416
1438
  const mediaType = mediaTypeFromExt(ext);
1417
1439
  const hint = fontFormatHint(ext);
1418
- storyAdd(
1419
- story,
1420
- {
1421
- name,
1422
- tags: ["stylesheet"],
1423
- text: `@font-face {
1440
+ return {
1441
+ passages: [
1442
+ {
1443
+ name,
1444
+ tags: ["stylesheet"],
1445
+ text: `@font-face {
1424
1446
  font-family: "${family}";
1425
1447
  src: url("data:${mediaType};base64,${source}") format("${hint}");
1426
1448
  }`
1427
- },
1428
- diagnostics
1429
- );
1449
+ }
1450
+ ],
1451
+ diagnostics: []
1452
+ };
1453
+ }
1454
+ function parseFile(filename, opts) {
1455
+ const ext = normalizedFileExt(filename);
1456
+ switch (ext) {
1457
+ case "tw":
1458
+ case "twee":
1459
+ return parseTweeFile(filename, opts);
1460
+ case "tw2":
1461
+ case "twee2":
1462
+ return parseTweeFile(filename, { ...opts, twee2Compat: true });
1463
+ case "css":
1464
+ return parseTaggedFile("stylesheet", filename);
1465
+ case "js":
1466
+ return parseTaggedFile("script", filename);
1467
+ case "otf":
1468
+ case "ttf":
1469
+ case "woff":
1470
+ case "woff2":
1471
+ return parseFontFile(filename);
1472
+ case "gif":
1473
+ case "jpeg":
1474
+ case "jpg":
1475
+ case "png":
1476
+ case "svg":
1477
+ case "tif":
1478
+ case "tiff":
1479
+ case "webp":
1480
+ return parseMediaFile("Twine.image", filename);
1481
+ case "aac":
1482
+ case "flac":
1483
+ case "m4a":
1484
+ case "mp3":
1485
+ case "oga":
1486
+ case "ogg":
1487
+ case "opus":
1488
+ case "wav":
1489
+ case "wave":
1490
+ case "weba":
1491
+ return parseMediaFile("Twine.audio", filename);
1492
+ case "mp4":
1493
+ case "ogv":
1494
+ case "webm":
1495
+ return parseMediaFile("Twine.video", filename);
1496
+ case "vtt":
1497
+ return parseMediaFile("Twine.vtt", filename);
1498
+ default:
1499
+ return void 0;
1500
+ }
1501
+ }
1502
+ function loadTwee(story, filename, opts, diagnostics) {
1503
+ const result = parseTweeFile(filename, opts);
1504
+ diagnostics.push(...result.diagnostics);
1505
+ for (const p of result.passages) {
1506
+ storyAdd(story, p, diagnostics);
1507
+ }
1508
+ }
1509
+ function loadTagged(story, tag, filename, diagnostics) {
1510
+ const result = parseTaggedFile(tag, filename);
1511
+ for (const p of result.passages) {
1512
+ storyAdd(story, p, diagnostics);
1513
+ }
1514
+ }
1515
+ function loadMedia(story, tag, filename, diagnostics) {
1516
+ const result = parseMediaFile(tag, filename);
1517
+ for (const p of result.passages) {
1518
+ storyAdd(story, p, diagnostics);
1519
+ }
1520
+ }
1521
+ function loadFont(story, filename, diagnostics) {
1522
+ const result = parseFontFile(filename);
1523
+ for (const p of result.passages) {
1524
+ storyAdd(story, p, diagnostics);
1525
+ }
1526
+ }
1527
+ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles) {
1528
+ const currentFiles = /* @__PURE__ */ new Set();
1529
+ for (const filename of filenames) {
1530
+ if (processedFiles.has(filename)) {
1531
+ diagnostics.push({ level: "warning", message: `load ${filename}: Skipping duplicate.` });
1532
+ continue;
1533
+ }
1534
+ currentFiles.add(filename);
1535
+ const cached = cache.get(filename);
1536
+ if (changedFiles && cached && !changedFiles.has(filename)) {
1537
+ diagnostics.push(...cached.diagnostics);
1538
+ for (const p of cached.passages) {
1539
+ storyAdd(story, p, diagnostics);
1540
+ }
1541
+ processedFiles.add(filename);
1542
+ continue;
1543
+ }
1544
+ let mtimeMs;
1545
+ try {
1546
+ mtimeMs = statSync3(filename).mtimeMs;
1547
+ } catch {
1548
+ continue;
1549
+ }
1550
+ if (cached && cached.mtimeMs === mtimeMs) {
1551
+ diagnostics.push(...cached.diagnostics);
1552
+ for (const p of cached.passages) {
1553
+ storyAdd(story, p, diagnostics);
1554
+ }
1555
+ processedFiles.add(filename);
1556
+ continue;
1557
+ }
1558
+ try {
1559
+ const result = parseFile(filename, opts);
1560
+ if (!result) continue;
1561
+ cache.set(filename, {
1562
+ mtimeMs,
1563
+ passages: result.passages,
1564
+ diagnostics: result.diagnostics
1565
+ });
1566
+ diagnostics.push(...result.diagnostics);
1567
+ for (const p of result.passages) {
1568
+ storyAdd(story, p, diagnostics);
1569
+ }
1570
+ } catch (e) {
1571
+ diagnostics.push({
1572
+ level: "error",
1573
+ message: `load ${filename}: ${e instanceof Error ? e.message : String(e)}`,
1574
+ file: filename
1575
+ });
1576
+ continue;
1577
+ }
1578
+ processedFiles.add(filename);
1579
+ }
1580
+ for (const key of cache.keys()) {
1581
+ if (!currentFiles.has(key)) {
1582
+ cache.delete(key);
1583
+ }
1584
+ }
1585
+ if (story.name !== "" && !story.passages.some((p) => p.name === "StoryTitle")) {
1586
+ storyPrepend(story, { name: "StoryTitle", tags: [], text: story.name }, diagnostics);
1587
+ }
1430
1588
  }
1431
1589
 
1432
1590
  // src/version.ts
@@ -1434,20 +1592,20 @@ var VERSION = "1.2.0";
1434
1592
 
1435
1593
  // src/output-twine2.ts
1436
1594
  var CREATOR_NAME = "Twee-ts";
1437
- function toTwine2Archive(story, startName, diagnostics) {
1438
- return getTwine2DataChunk(story, startName, diagnostics) + "\n";
1595
+ function toTwine2Archive(story, startName, diagnostics, options) {
1596
+ return getTwine2DataChunk(story, startName, diagnostics, options) + "\n";
1439
1597
  }
1440
- function toTwine2HTML(story, format, startName, diagnostics) {
1598
+ function toTwine2HTML(story, format, startName, diagnostics, options) {
1441
1599
  let template = readFormatSource(format);
1442
1600
  if (template.includes("{{STORY_NAME}}")) {
1443
1601
  template = template.replaceAll("{{STORY_NAME}}", htmlEscape(story.name));
1444
1602
  }
1445
1603
  if (template.includes("{{STORY_DATA}}")) {
1446
- template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics));
1604
+ template = template.replace("{{STORY_DATA}}", getTwine2DataChunk(story, startName, diagnostics, options));
1447
1605
  }
1448
1606
  return template;
1449
1607
  }
1450
- function getTwine2DataChunk(story, startName, diagnostics) {
1608
+ function getTwine2DataChunk(story, startName, diagnostics, options) {
1451
1609
  ensureIFID(story, diagnostics);
1452
1610
  const parts = [];
1453
1611
  let startID = "";
@@ -1502,7 +1660,7 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1502
1660
  if (p.name === "StorySettings" && story.twine1.settings.size === 0) {
1503
1661
  continue;
1504
1662
  }
1505
- parts.push(passageToPassagedata(p, pid));
1663
+ parts.push(passageToPassagedata(p, pid, options));
1506
1664
  if (startName === p.name) {
1507
1665
  startID = String(pid);
1508
1666
  }
@@ -1512,9 +1670,9 @@ function getTwine2DataChunk(story, startName, diagnostics) {
1512
1670
  for (const [opt, val] of story.twine2.options) {
1513
1671
  if (val) opts.push(opt);
1514
1672
  }
1515
- const options = opts.join(" ");
1673
+ const optionsStr = opts.join(" ");
1516
1674
  const zoom = String(story.twine2.zoom);
1517
- 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>`;
1675
+ 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>`;
1518
1676
  return wrapper + parts.join("") + "</tw-storydata>";
1519
1677
  }
1520
1678
  function ensureIFID(story, diagnostics) {
@@ -1695,7 +1853,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
1695
1853
  }
1696
1854
 
1697
1855
  // src/remote-formats.ts
1698
- import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
1856
+ import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
1699
1857
  import { join as join4 } from "path";
1700
1858
  import { homedir as homedir2 } from "os";
1701
1859
  var DEFAULT_SFA_INDICES = [
@@ -1907,7 +2065,7 @@ function discoverCachedFormats() {
1907
2065
  for (const name of names) {
1908
2066
  const nameDir = join4(cacheDir, name);
1909
2067
  try {
1910
- if (!statSync3(nameDir).isDirectory()) continue;
2068
+ if (!statSync4(nameDir).isDirectory()) continue;
1911
2069
  } catch {
1912
2070
  continue;
1913
2071
  }
@@ -1948,11 +2106,12 @@ async function compileToFile(options) {
1948
2106
  }
1949
2107
  async function watch(options) {
1950
2108
  const controller = new AbortController();
2109
+ const cache = /* @__PURE__ */ new Map();
1951
2110
  const filePaths = options.sources.filter((s) => typeof s === "string");
1952
2111
  const modulePaths = options.modules ?? [];
1953
2112
  const allPaths = [...filePaths, ...modulePaths];
1954
- const handle = watchFilesystem(allPaths, options.outFile, () => {
1955
- buildOutput(options).then((result) => {
2113
+ const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
2114
+ buildOutput(options, cache, changedFiles).then((result) => {
1956
2115
  writeFileSync2(options.outFile, result.output, "utf-8");
1957
2116
  options.onBuild?.(result);
1958
2117
  }).catch((e) => {
@@ -1962,13 +2121,14 @@ async function watch(options) {
1962
2121
  controller.signal.addEventListener("abort", () => handle.close());
1963
2122
  return controller;
1964
2123
  }
1965
- async function buildOutput(options) {
2124
+ async function buildOutput(options, cache, changedFiles) {
1966
2125
  const diagnostics = [];
1967
2126
  const outputMode = options.outputMode ?? "html";
1968
2127
  const trim = options.trim ?? true;
1969
2128
  const twee2Compat = options.twee2Compat ?? false;
1970
2129
  const testMode = options.testMode ?? false;
1971
2130
  const noRemote = options.noRemote ?? false;
2131
+ const sourceInfo = options.sourceInfo ?? false;
1972
2132
  clearIndexCache();
1973
2133
  const filePaths = [];
1974
2134
  const inlineSources = [];
@@ -1982,7 +2142,11 @@ async function buildOutput(options) {
1982
2142
  const sourceFilenames = getFilenames(filePaths);
1983
2143
  const story = createStory();
1984
2144
  const processedFiles = /* @__PURE__ */ new Set();
1985
- loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2145
+ if (cache) {
2146
+ loadSourcesCached(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles, cache, changedFiles);
2147
+ } else {
2148
+ loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
2149
+ }
1986
2150
  loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
1987
2151
  if (options.tagAliases) {
1988
2152
  story.passages = applyTagAliases(story.passages, options.tagAliases);
@@ -2030,7 +2194,7 @@ async function buildOutput(options) {
2030
2194
  output = toTwee(story, outputMode);
2031
2195
  break;
2032
2196
  case "twine2-archive":
2033
- output = toTwine2Archive(story, startName, diagnostics);
2197
+ output = toTwine2Archive(story, startName, diagnostics, { sourceInfo });
2034
2198
  break;
2035
2199
  case "twine1-archive":
2036
2200
  output = toTwine1Archive(story, startName);
@@ -2049,7 +2213,7 @@ async function buildOutput(options) {
2049
2213
  throw new TweeTsError("No story format available for HTML output.", diagnostics);
2050
2214
  }
2051
2215
  if (format.isTwine2) {
2052
- output = toTwine2HTML(story, format, startName, diagnostics);
2216
+ output = toTwine2HTML(story, format, startName, diagnostics, { sourceInfo });
2053
2217
  } else {
2054
2218
  if (story.name === "" && !storyHas(story, "StoryTitle")) {
2055
2219
  diagnostics.push({
@@ -2282,7 +2446,7 @@ function validateConfig(data) {
2282
2446
  errors.push(`"${key}" must be a string.`);
2283
2447
  }
2284
2448
  }
2285
- for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote"]) {
2449
+ for (const key of ["useTweegoPath", "trim", "twee2Compat", "testMode", "noRemote", "sourceInfo"]) {
2286
2450
  if (key in obj && typeof obj[key] !== "boolean") {
2287
2451
  errors.push(`"${key}" must be a boolean.`);
2288
2452
  }