@rohal12/twee-ts 1.5.0 → 1.7.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/bin/twee-ts.js +393 -43
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/{chunk-KI4QMLEN.js → chunk-HXL3JC4L.js} +191 -38
- package/dist/chunk-HXL3JC4L.js.map +1 -0
- package/dist/index.cjs +288 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +267 -43
- package/dist/index.js.map +1 -1
- package/dist/plugins/rollup.js +1 -1
- package/dist/plugins/vite.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-KI4QMLEN.js.map +0 -1
package/dist/bin/twee-ts.js
CHANGED
|
@@ -151,6 +151,9 @@ function hasInfoTags(p) {
|
|
|
151
151
|
function hasInfoName(p) {
|
|
152
152
|
return INFO_PASSAGE_NAMES.has(p.name);
|
|
153
153
|
}
|
|
154
|
+
function isInfoPassage(p) {
|
|
155
|
+
return hasInfoName(p) || hasInfoTags(p);
|
|
156
|
+
}
|
|
154
157
|
function isStoryPassage(p) {
|
|
155
158
|
return !hasInfoName(p) && !hasInfoTags(p);
|
|
156
159
|
}
|
|
@@ -262,8 +265,21 @@ function quote(s) {
|
|
|
262
265
|
}
|
|
263
266
|
|
|
264
267
|
// src/story.ts
|
|
268
|
+
var passageIndex = /* @__PURE__ */ new WeakMap();
|
|
269
|
+
function getIndex(story) {
|
|
270
|
+
let index = passageIndex.get(story);
|
|
271
|
+
if (!index) {
|
|
272
|
+
index = /* @__PURE__ */ new Map();
|
|
273
|
+
for (let i = 0; i < story.passages.length; i++) {
|
|
274
|
+
const passage = story.passages[i];
|
|
275
|
+
if (passage) index.set(passage.name, i);
|
|
276
|
+
}
|
|
277
|
+
passageIndex.set(story, index);
|
|
278
|
+
}
|
|
279
|
+
return index;
|
|
280
|
+
}
|
|
265
281
|
function createStory() {
|
|
266
|
-
|
|
282
|
+
const story = {
|
|
267
283
|
name: "",
|
|
268
284
|
ifid: "",
|
|
269
285
|
passages: [],
|
|
@@ -279,16 +295,17 @@ function createStory() {
|
|
|
279
295
|
zoom: 1
|
|
280
296
|
}
|
|
281
297
|
};
|
|
298
|
+
passageIndex.set(story, /* @__PURE__ */ new Map());
|
|
299
|
+
return story;
|
|
282
300
|
}
|
|
283
301
|
function storyHas(story, name) {
|
|
284
|
-
return story.
|
|
285
|
-
}
|
|
286
|
-
function storyIndex(story, name) {
|
|
287
|
-
return story.passages.findIndex((p) => p.name === name);
|
|
302
|
+
return getIndex(story).has(name);
|
|
288
303
|
}
|
|
289
304
|
function storyAppend(story, p, diagnostics) {
|
|
290
|
-
const
|
|
305
|
+
const index = getIndex(story);
|
|
306
|
+
const i = index.get(p.name) ?? -1;
|
|
291
307
|
if (i === -1) {
|
|
308
|
+
index.set(p.name, story.passages.length);
|
|
292
309
|
story.passages.push(p);
|
|
293
310
|
} else {
|
|
294
311
|
diagnostics.push({
|
|
@@ -299,9 +316,15 @@ function storyAppend(story, p, diagnostics) {
|
|
|
299
316
|
}
|
|
300
317
|
}
|
|
301
318
|
function storyPrepend(story, p, diagnostics) {
|
|
302
|
-
const
|
|
319
|
+
const index = getIndex(story);
|
|
320
|
+
const i = index.get(p.name) ?? -1;
|
|
303
321
|
if (i === -1) {
|
|
304
322
|
story.passages.unshift(p);
|
|
323
|
+
index.clear();
|
|
324
|
+
for (let j = 0; j < story.passages.length; j++) {
|
|
325
|
+
const passage = story.passages[j];
|
|
326
|
+
if (passage) index.set(passage.name, j);
|
|
327
|
+
}
|
|
305
328
|
} else {
|
|
306
329
|
diagnostics.push({
|
|
307
330
|
level: "warning",
|
|
@@ -591,11 +614,15 @@ function watchFilesystem(pathnames, outFilename, callback) {
|
|
|
591
614
|
const watchers = [];
|
|
592
615
|
let buildTimer = null;
|
|
593
616
|
const BUILD_DEBOUNCE = 500;
|
|
594
|
-
|
|
617
|
+
const pendingFiles = /* @__PURE__ */ new Set();
|
|
618
|
+
function scheduleBuild(changedFile) {
|
|
619
|
+
if (changedFile) pendingFiles.add(changedFile);
|
|
595
620
|
if (buildTimer) clearTimeout(buildTimer);
|
|
596
621
|
buildTimer = setTimeout(() => {
|
|
597
622
|
buildTimer = null;
|
|
598
|
-
|
|
623
|
+
const files = pendingFiles.size > 0 ? new Set(pendingFiles) : void 0;
|
|
624
|
+
pendingFiles.clear();
|
|
625
|
+
callback(files);
|
|
599
626
|
}, BUILD_DEBOUNCE);
|
|
600
627
|
}
|
|
601
628
|
for (const pathname of pathnames) {
|
|
@@ -605,7 +632,8 @@ function watchFilesystem(pathnames, outFilename, callback) {
|
|
|
605
632
|
const abs = resolve(pathname, filename);
|
|
606
633
|
if (abs === absOutFile) return;
|
|
607
634
|
if (isKnownFileType(filename)) {
|
|
608
|
-
|
|
635
|
+
const rel = relative(process.cwd(), abs);
|
|
636
|
+
scheduleBuild(rel || abs);
|
|
609
637
|
}
|
|
610
638
|
});
|
|
611
639
|
watchers.push(watcher);
|
|
@@ -864,6 +892,7 @@ function readFormatSource(format) {
|
|
|
864
892
|
|
|
865
893
|
// src/loader.ts
|
|
866
894
|
import { basename } from "path";
|
|
895
|
+
import { statSync as statSync3 } from "fs";
|
|
867
896
|
|
|
868
897
|
// src/lexer.ts
|
|
869
898
|
var EOF = -1;
|
|
@@ -1371,54 +1400,182 @@ function loadInlineSources(story, sources, opts, diagnostics) {
|
|
|
1371
1400
|
}
|
|
1372
1401
|
}
|
|
1373
1402
|
}
|
|
1374
|
-
function
|
|
1403
|
+
function parseTweeFile(filename, opts) {
|
|
1375
1404
|
const source = readUTF8(filename);
|
|
1376
1405
|
const result = parseTwee(source, {
|
|
1377
1406
|
filename,
|
|
1378
1407
|
trim: opts.trim ?? true,
|
|
1379
1408
|
twee2Compat: opts.twee2Compat ?? false
|
|
1380
1409
|
});
|
|
1381
|
-
diagnostics
|
|
1382
|
-
for (const p of result.passages) {
|
|
1383
|
-
storyAdd(story, p, diagnostics);
|
|
1384
|
-
}
|
|
1410
|
+
return { passages: result.passages, diagnostics: [...result.diagnostics] };
|
|
1385
1411
|
}
|
|
1386
|
-
function
|
|
1412
|
+
function parseTaggedFile(tag, filename) {
|
|
1387
1413
|
const source = readUTF8(filename);
|
|
1388
|
-
|
|
1414
|
+
return { passages: [{ name: basename(filename), tags: [tag], text: source }], diagnostics: [] };
|
|
1389
1415
|
}
|
|
1390
|
-
function
|
|
1416
|
+
function parseMediaFile(tag, filename) {
|
|
1391
1417
|
const source = readBase64(filename);
|
|
1392
1418
|
const name = baseNameWithoutExt(filename);
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
tags: [tag],
|
|
1398
|
-
text: `data:${mediaTypeFromFilename(filename)};base64,${source}`
|
|
1399
|
-
},
|
|
1400
|
-
diagnostics
|
|
1401
|
-
);
|
|
1419
|
+
return {
|
|
1420
|
+
passages: [{ name, tags: [tag], text: `data:${mediaTypeFromFilename(filename)};base64,${source}` }],
|
|
1421
|
+
diagnostics: []
|
|
1422
|
+
};
|
|
1402
1423
|
}
|
|
1403
|
-
function
|
|
1424
|
+
function parseFontFile(filename) {
|
|
1404
1425
|
const source = readBase64(filename);
|
|
1405
1426
|
const name = basename(filename);
|
|
1406
1427
|
const family = baseNameWithoutExt(filename);
|
|
1407
1428
|
const ext = normalizedFileExt(filename);
|
|
1408
1429
|
const mediaType = mediaTypeFromExt(ext);
|
|
1409
1430
|
const hint = fontFormatHint(ext);
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1431
|
+
return {
|
|
1432
|
+
passages: [
|
|
1433
|
+
{
|
|
1434
|
+
name,
|
|
1435
|
+
tags: ["stylesheet"],
|
|
1436
|
+
text: `@font-face {
|
|
1416
1437
|
font-family: "${family}";
|
|
1417
1438
|
src: url("data:${mediaType};base64,${source}") format("${hint}");
|
|
1418
1439
|
}`
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1440
|
+
}
|
|
1441
|
+
],
|
|
1442
|
+
diagnostics: []
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function parseFile(filename, opts) {
|
|
1446
|
+
const ext = normalizedFileExt(filename);
|
|
1447
|
+
switch (ext) {
|
|
1448
|
+
case "tw":
|
|
1449
|
+
case "twee":
|
|
1450
|
+
return parseTweeFile(filename, opts);
|
|
1451
|
+
case "tw2":
|
|
1452
|
+
case "twee2":
|
|
1453
|
+
return parseTweeFile(filename, { ...opts, twee2Compat: true });
|
|
1454
|
+
case "css":
|
|
1455
|
+
return parseTaggedFile("stylesheet", filename);
|
|
1456
|
+
case "js":
|
|
1457
|
+
return parseTaggedFile("script", filename);
|
|
1458
|
+
case "otf":
|
|
1459
|
+
case "ttf":
|
|
1460
|
+
case "woff":
|
|
1461
|
+
case "woff2":
|
|
1462
|
+
return parseFontFile(filename);
|
|
1463
|
+
case "gif":
|
|
1464
|
+
case "jpeg":
|
|
1465
|
+
case "jpg":
|
|
1466
|
+
case "png":
|
|
1467
|
+
case "svg":
|
|
1468
|
+
case "tif":
|
|
1469
|
+
case "tiff":
|
|
1470
|
+
case "webp":
|
|
1471
|
+
return parseMediaFile("Twine.image", filename);
|
|
1472
|
+
case "aac":
|
|
1473
|
+
case "flac":
|
|
1474
|
+
case "m4a":
|
|
1475
|
+
case "mp3":
|
|
1476
|
+
case "oga":
|
|
1477
|
+
case "ogg":
|
|
1478
|
+
case "opus":
|
|
1479
|
+
case "wav":
|
|
1480
|
+
case "wave":
|
|
1481
|
+
case "weba":
|
|
1482
|
+
return parseMediaFile("Twine.audio", filename);
|
|
1483
|
+
case "mp4":
|
|
1484
|
+
case "ogv":
|
|
1485
|
+
case "webm":
|
|
1486
|
+
return parseMediaFile("Twine.video", filename);
|
|
1487
|
+
case "vtt":
|
|
1488
|
+
return parseMediaFile("Twine.vtt", filename);
|
|
1489
|
+
default:
|
|
1490
|
+
return void 0;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
function loadTwee(story, filename, opts, diagnostics) {
|
|
1494
|
+
const result = parseTweeFile(filename, opts);
|
|
1495
|
+
diagnostics.push(...result.diagnostics);
|
|
1496
|
+
for (const p of result.passages) {
|
|
1497
|
+
storyAdd(story, p, diagnostics);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
function loadTagged(story, tag, filename, diagnostics) {
|
|
1501
|
+
const result = parseTaggedFile(tag, filename);
|
|
1502
|
+
for (const p of result.passages) {
|
|
1503
|
+
storyAdd(story, p, diagnostics);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
function loadMedia(story, tag, filename, diagnostics) {
|
|
1507
|
+
const result = parseMediaFile(tag, filename);
|
|
1508
|
+
for (const p of result.passages) {
|
|
1509
|
+
storyAdd(story, p, diagnostics);
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
function loadFont(story, filename, diagnostics) {
|
|
1513
|
+
const result = parseFontFile(filename);
|
|
1514
|
+
for (const p of result.passages) {
|
|
1515
|
+
storyAdd(story, p, diagnostics);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles) {
|
|
1519
|
+
const currentFiles = /* @__PURE__ */ new Set();
|
|
1520
|
+
for (const filename of filenames) {
|
|
1521
|
+
if (processedFiles.has(filename)) {
|
|
1522
|
+
diagnostics.push({ level: "warning", message: `load ${filename}: Skipping duplicate.` });
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
currentFiles.add(filename);
|
|
1526
|
+
const cached = cache.get(filename);
|
|
1527
|
+
if (changedFiles && cached && !changedFiles.has(filename)) {
|
|
1528
|
+
diagnostics.push(...cached.diagnostics);
|
|
1529
|
+
for (const p of cached.passages) {
|
|
1530
|
+
storyAdd(story, p, diagnostics);
|
|
1531
|
+
}
|
|
1532
|
+
processedFiles.add(filename);
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
let mtimeMs;
|
|
1536
|
+
try {
|
|
1537
|
+
mtimeMs = statSync3(filename).mtimeMs;
|
|
1538
|
+
} catch {
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
if (cached && cached.mtimeMs === mtimeMs) {
|
|
1542
|
+
diagnostics.push(...cached.diagnostics);
|
|
1543
|
+
for (const p of cached.passages) {
|
|
1544
|
+
storyAdd(story, p, diagnostics);
|
|
1545
|
+
}
|
|
1546
|
+
processedFiles.add(filename);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const result = parseFile(filename, opts);
|
|
1551
|
+
if (!result) continue;
|
|
1552
|
+
cache.set(filename, {
|
|
1553
|
+
mtimeMs,
|
|
1554
|
+
passages: result.passages,
|
|
1555
|
+
diagnostics: result.diagnostics
|
|
1556
|
+
});
|
|
1557
|
+
diagnostics.push(...result.diagnostics);
|
|
1558
|
+
for (const p of result.passages) {
|
|
1559
|
+
storyAdd(story, p, diagnostics);
|
|
1560
|
+
}
|
|
1561
|
+
} catch (e) {
|
|
1562
|
+
diagnostics.push({
|
|
1563
|
+
level: "error",
|
|
1564
|
+
message: `load ${filename}: ${e instanceof Error ? e.message : String(e)}`,
|
|
1565
|
+
file: filename
|
|
1566
|
+
});
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
processedFiles.add(filename);
|
|
1570
|
+
}
|
|
1571
|
+
for (const key of cache.keys()) {
|
|
1572
|
+
if (!currentFiles.has(key)) {
|
|
1573
|
+
cache.delete(key);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
if (story.name !== "" && !story.passages.some((p) => p.name === "StoryTitle")) {
|
|
1577
|
+
storyPrepend(story, { name: "StoryTitle", tags: [], text: story.name }, diagnostics);
|
|
1578
|
+
}
|
|
1422
1579
|
}
|
|
1423
1580
|
|
|
1424
1581
|
// src/version.ts
|
|
@@ -1687,7 +1844,7 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
|
1687
1844
|
}
|
|
1688
1845
|
|
|
1689
1846
|
// src/remote-formats.ts
|
|
1690
|
-
import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as
|
|
1847
|
+
import { mkdirSync, readFileSync as readFileSync3, writeFileSync, existsSync, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1691
1848
|
import { join as join4 } from "path";
|
|
1692
1849
|
import { homedir as homedir2 } from "os";
|
|
1693
1850
|
var DEFAULT_SFA_INDICES = [
|
|
@@ -1899,7 +2056,7 @@ function discoverCachedFormats() {
|
|
|
1899
2056
|
for (const name of names) {
|
|
1900
2057
|
const nameDir = join4(cacheDir, name);
|
|
1901
2058
|
try {
|
|
1902
|
-
if (!
|
|
2059
|
+
if (!statSync4(nameDir).isDirectory()) continue;
|
|
1903
2060
|
} catch {
|
|
1904
2061
|
continue;
|
|
1905
2062
|
}
|
|
@@ -1940,11 +2097,12 @@ async function compileToFile(options) {
|
|
|
1940
2097
|
}
|
|
1941
2098
|
async function watch(options) {
|
|
1942
2099
|
const controller = new AbortController();
|
|
2100
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1943
2101
|
const filePaths = options.sources.filter((s) => typeof s === "string");
|
|
1944
2102
|
const modulePaths = options.modules ?? [];
|
|
1945
2103
|
const allPaths = [...filePaths, ...modulePaths];
|
|
1946
|
-
const handle = watchFilesystem(allPaths, options.outFile, () => {
|
|
1947
|
-
buildOutput(options).then((result) => {
|
|
2104
|
+
const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
|
|
2105
|
+
buildOutput(options, cache, changedFiles).then((result) => {
|
|
1948
2106
|
writeFileSync2(options.outFile, result.output, "utf-8");
|
|
1949
2107
|
options.onBuild?.(result);
|
|
1950
2108
|
}).catch((e) => {
|
|
@@ -1954,7 +2112,7 @@ async function watch(options) {
|
|
|
1954
2112
|
controller.signal.addEventListener("abort", () => handle.close());
|
|
1955
2113
|
return controller;
|
|
1956
2114
|
}
|
|
1957
|
-
async function buildOutput(options) {
|
|
2115
|
+
async function buildOutput(options, cache, changedFiles) {
|
|
1958
2116
|
const diagnostics = [];
|
|
1959
2117
|
const outputMode = options.outputMode ?? "html";
|
|
1960
2118
|
const trim = options.trim ?? true;
|
|
@@ -1975,7 +2133,11 @@ async function buildOutput(options) {
|
|
|
1975
2133
|
const sourceFilenames = getFilenames(filePaths);
|
|
1976
2134
|
const story = createStory();
|
|
1977
2135
|
const processedFiles = /* @__PURE__ */ new Set();
|
|
1978
|
-
|
|
2136
|
+
if (cache) {
|
|
2137
|
+
loadSourcesCached(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles, cache, changedFiles);
|
|
2138
|
+
} else {
|
|
2139
|
+
loadSources(story, sourceFilenames, { trim, twee2Compat }, diagnostics, processedFiles);
|
|
2140
|
+
}
|
|
1979
2141
|
loadInlineSources(story, inlineSources, { trim, twee2Compat }, diagnostics);
|
|
1980
2142
|
if (options.tagAliases) {
|
|
1981
2143
|
story.passages = applyTagAliases(story.passages, options.tagAliases);
|
|
@@ -2113,6 +2275,171 @@ function storyToJSON(story) {
|
|
|
2113
2275
|
return JSON.stringify(obj, null, 2);
|
|
2114
2276
|
}
|
|
2115
2277
|
|
|
2278
|
+
// src/inspect.ts
|
|
2279
|
+
function extractLinksFromText(text) {
|
|
2280
|
+
const targets = /* @__PURE__ */ new Set();
|
|
2281
|
+
const wikiLinkRe = /\[\[([^\]]+)\]\]/g;
|
|
2282
|
+
let m;
|
|
2283
|
+
while ((m = wikiLinkRe.exec(text)) !== null) {
|
|
2284
|
+
const content = m[1];
|
|
2285
|
+
const arrowIdx = content.indexOf("->");
|
|
2286
|
+
if (arrowIdx !== -1) {
|
|
2287
|
+
targets.add(content.slice(arrowIdx + 2).trim());
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
const pipeIdx = content.indexOf("|");
|
|
2291
|
+
if (pipeIdx !== -1) {
|
|
2292
|
+
targets.add(content.slice(pipeIdx + 1).trim());
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
targets.add(content.trim());
|
|
2296
|
+
}
|
|
2297
|
+
const gotoRe = /<<goto\s+["']([^"']+)["']\s*>>/g;
|
|
2298
|
+
while ((m = gotoRe.exec(text)) !== null) {
|
|
2299
|
+
targets.add(m[1]);
|
|
2300
|
+
}
|
|
2301
|
+
const linkMacroRe = /<<link\s+["'][^"']*["']\s+["']([^"']+)["']\s*>>/g;
|
|
2302
|
+
while ((m = linkMacroRe.exec(text)) !== null) {
|
|
2303
|
+
targets.add(m[1]);
|
|
2304
|
+
}
|
|
2305
|
+
return [...targets];
|
|
2306
|
+
}
|
|
2307
|
+
function storyInspect(story) {
|
|
2308
|
+
const allNames = new Set(story.passages.map((p) => p.name));
|
|
2309
|
+
const passages = [];
|
|
2310
|
+
const storyPassages = [];
|
|
2311
|
+
const infoPassages = [];
|
|
2312
|
+
const tagSet = /* @__PURE__ */ new Set();
|
|
2313
|
+
const passagesByTag = /* @__PURE__ */ new Map();
|
|
2314
|
+
const tagsByPassage = /* @__PURE__ */ new Map();
|
|
2315
|
+
const links = /* @__PURE__ */ new Map();
|
|
2316
|
+
for (const p of story.passages) {
|
|
2317
|
+
passages.push(p.name);
|
|
2318
|
+
if (isStoryPassage(p)) {
|
|
2319
|
+
storyPassages.push(p.name);
|
|
2320
|
+
}
|
|
2321
|
+
if (isInfoPassage(p)) {
|
|
2322
|
+
infoPassages.push(p.name);
|
|
2323
|
+
}
|
|
2324
|
+
tagsByPassage.set(p.name, [...p.tags]);
|
|
2325
|
+
for (const tag of p.tags) {
|
|
2326
|
+
tagSet.add(tag);
|
|
2327
|
+
let list = passagesByTag.get(tag);
|
|
2328
|
+
if (!list) {
|
|
2329
|
+
list = [];
|
|
2330
|
+
passagesByTag.set(tag, list);
|
|
2331
|
+
}
|
|
2332
|
+
list.push(p.name);
|
|
2333
|
+
}
|
|
2334
|
+
const targets = extractLinksFromText(p.text);
|
|
2335
|
+
links.set(p.name, targets);
|
|
2336
|
+
}
|
|
2337
|
+
const brokenLinks = [];
|
|
2338
|
+
for (const [from, targets] of links) {
|
|
2339
|
+
for (const to of targets) {
|
|
2340
|
+
if (!allNames.has(to)) {
|
|
2341
|
+
brokenLinks.push({ from, to });
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
const deadEnds = [];
|
|
2346
|
+
for (const name of storyPassages) {
|
|
2347
|
+
const targets = links.get(name);
|
|
2348
|
+
if (!targets || targets.length === 0) {
|
|
2349
|
+
deadEnds.push(name);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
const start = story.twine2.start || "Start";
|
|
2353
|
+
const linkedTo = /* @__PURE__ */ new Set();
|
|
2354
|
+
for (const targets of links.values()) {
|
|
2355
|
+
for (const t of targets) {
|
|
2356
|
+
linkedTo.add(t);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
const orphans = [];
|
|
2360
|
+
for (const name of storyPassages) {
|
|
2361
|
+
if (name !== start && !linkedTo.has(name)) {
|
|
2362
|
+
orphans.push(name);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
passages,
|
|
2367
|
+
storyPassages,
|
|
2368
|
+
infoPassages,
|
|
2369
|
+
tags: [...tagSet].sort(),
|
|
2370
|
+
passagesByTag,
|
|
2371
|
+
tagsByPassage,
|
|
2372
|
+
links,
|
|
2373
|
+
brokenLinks,
|
|
2374
|
+
deadEnds,
|
|
2375
|
+
orphans,
|
|
2376
|
+
start
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// src/lint.ts
|
|
2381
|
+
async function lint(options) {
|
|
2382
|
+
const result = await compile({ ...options, outputMode: "json" });
|
|
2383
|
+
const map = storyInspect(result.story);
|
|
2384
|
+
return {
|
|
2385
|
+
diagnostics: result.diagnostics,
|
|
2386
|
+
stats: result.stats,
|
|
2387
|
+
formatName: result.story.twine2.format,
|
|
2388
|
+
formatVersion: result.story.twine2.formatVersion,
|
|
2389
|
+
start: map.start,
|
|
2390
|
+
passages: map.passages.length,
|
|
2391
|
+
storyPassages: map.storyPassages.length,
|
|
2392
|
+
infoPassages: map.infoPassages.length,
|
|
2393
|
+
brokenLinks: map.brokenLinks,
|
|
2394
|
+
deadEnds: map.deadEnds,
|
|
2395
|
+
orphans: map.orphans
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
function formatLintReport(result) {
|
|
2399
|
+
const lines = [];
|
|
2400
|
+
const formatStr = result.formatName ? `${result.formatName}${result.formatVersion ? " " + result.formatVersion : ""}` : "unknown";
|
|
2401
|
+
lines.push(`Format: ${formatStr}`);
|
|
2402
|
+
lines.push(
|
|
2403
|
+
`Passages: ${result.passages} total (${result.storyPassages} story, ${result.infoPassages} info), ${result.stats.words.toLocaleString()} words, ${result.stats.files.length} files`
|
|
2404
|
+
);
|
|
2405
|
+
lines.push(`Start: ${result.start}`);
|
|
2406
|
+
if (result.brokenLinks.length > 0) {
|
|
2407
|
+
lines.push("");
|
|
2408
|
+
lines.push(`Broken links (${result.brokenLinks.length}):`);
|
|
2409
|
+
for (const link of result.brokenLinks) {
|
|
2410
|
+
lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" does not exist)`);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
if (result.deadEnds.length > 0) {
|
|
2414
|
+
lines.push("");
|
|
2415
|
+
lines.push(`Dead ends (${result.deadEnds.length}): ${result.deadEnds.join(", ")}`);
|
|
2416
|
+
}
|
|
2417
|
+
if (result.orphans.length > 0) {
|
|
2418
|
+
lines.push("");
|
|
2419
|
+
lines.push(`Orphans (${result.orphans.length}): ${result.orphans.join(", ")}`);
|
|
2420
|
+
}
|
|
2421
|
+
const errors = result.diagnostics.filter((d) => d.level === "error");
|
|
2422
|
+
const warnings = result.diagnostics.filter((d) => d.level === "warning");
|
|
2423
|
+
if (errors.length > 0 || warnings.length > 0) {
|
|
2424
|
+
lines.push("");
|
|
2425
|
+
lines.push(`Diagnostics: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
2426
|
+
for (const d of errors) {
|
|
2427
|
+
lines.push(` error: ${d.message}`);
|
|
2428
|
+
}
|
|
2429
|
+
for (const d of warnings) {
|
|
2430
|
+
lines.push(` warning: ${d.message}`);
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
lines.push("");
|
|
2434
|
+
const hasErrors = errors.length > 0 || result.brokenLinks.length > 0;
|
|
2435
|
+
if (hasErrors) {
|
|
2436
|
+
lines.push("Lint failed.");
|
|
2437
|
+
} else {
|
|
2438
|
+
lines.push("Lint passed.");
|
|
2439
|
+
}
|
|
2440
|
+
return lines.join("\n");
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2116
2443
|
// src/config.ts
|
|
2117
2444
|
import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
|
|
2118
2445
|
import { join as join5 } from "path";
|
|
@@ -2229,6 +2556,7 @@ var { values, positionals } = parseArgs({
|
|
|
2229
2556
|
"archive-twine1": { type: "boolean" },
|
|
2230
2557
|
"twee2-compat": { type: "boolean" },
|
|
2231
2558
|
"no-trim": { type: "boolean" },
|
|
2559
|
+
lint: { type: "boolean" },
|
|
2232
2560
|
test: { type: "boolean", short: "t" },
|
|
2233
2561
|
watch: { type: "boolean", short: "w" },
|
|
2234
2562
|
"log-stats": { type: "boolean", short: "l" },
|
|
@@ -2297,6 +2625,27 @@ async function main() {
|
|
|
2297
2625
|
tagAliases[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
2298
2626
|
}
|
|
2299
2627
|
}
|
|
2628
|
+
if (values.lint) {
|
|
2629
|
+
const lintResult = await lint({
|
|
2630
|
+
sources,
|
|
2631
|
+
formatId: values.format ?? config?.formatId,
|
|
2632
|
+
startPassage: values.start ?? config?.startPassage,
|
|
2633
|
+
formatPaths: config?.formatPaths,
|
|
2634
|
+
modules: values.module ?? config?.modules,
|
|
2635
|
+
headFile: values.head ?? config?.headFile,
|
|
2636
|
+
trim: values["no-trim"] ? false : config?.trim ?? true,
|
|
2637
|
+
twee2Compat: values["twee2-compat"] ?? config?.twee2Compat ?? false,
|
|
2638
|
+
testMode: values.test ?? config?.testMode ?? false,
|
|
2639
|
+
formatIndices: values["format-index"] ?? config?.formatIndices,
|
|
2640
|
+
formatUrls: values["format-url"] ?? config?.formatUrls,
|
|
2641
|
+
noRemote: values["no-remote"] ?? config?.noRemote ?? false,
|
|
2642
|
+
tagAliases,
|
|
2643
|
+
sourceInfo: values["source-info"] ?? config?.sourceInfo ?? false
|
|
2644
|
+
});
|
|
2645
|
+
console.log(formatLintReport(lintResult));
|
|
2646
|
+
const hasErrors = lintResult.brokenLinks.length > 0 || lintResult.diagnostics.some((d) => d.level === "error");
|
|
2647
|
+
process.exit(hasErrors ? 1 : 0);
|
|
2648
|
+
}
|
|
2300
2649
|
const compileOptions = {
|
|
2301
2650
|
sources,
|
|
2302
2651
|
outputMode,
|
|
@@ -2429,6 +2778,7 @@ Options:
|
|
|
2429
2778
|
--archive-twine1 Output as Twine 1 archive
|
|
2430
2779
|
--json Output as JSON
|
|
2431
2780
|
--twee2-compat Enable Twee2 syntax compatibility
|
|
2781
|
+
--lint Lint story structure (broken links, dead ends, orphans)
|
|
2432
2782
|
--no-trim Don't trim passage whitespace
|
|
2433
2783
|
-t, --test Enable test/debug mode
|
|
2434
2784
|
-w, --watch Watch for changes and rebuild
|