@rohal12/twee-ts 1.6.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.
@@ -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
  }
@@ -2272,6 +2275,171 @@ function storyToJSON(story) {
2272
2275
  return JSON.stringify(obj, null, 2);
2273
2276
  }
2274
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
+
2275
2443
  // src/config.ts
2276
2444
  import { readFileSync as readFileSync4, existsSync as existsSync2 } from "fs";
2277
2445
  import { join as join5 } from "path";
@@ -2388,6 +2556,7 @@ var { values, positionals } = parseArgs({
2388
2556
  "archive-twine1": { type: "boolean" },
2389
2557
  "twee2-compat": { type: "boolean" },
2390
2558
  "no-trim": { type: "boolean" },
2559
+ lint: { type: "boolean" },
2391
2560
  test: { type: "boolean", short: "t" },
2392
2561
  watch: { type: "boolean", short: "w" },
2393
2562
  "log-stats": { type: "boolean", short: "l" },
@@ -2456,6 +2625,27 @@ async function main() {
2456
2625
  tagAliases[pair.slice(0, eq)] = pair.slice(eq + 1);
2457
2626
  }
2458
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
+ }
2459
2649
  const compileOptions = {
2460
2650
  sources,
2461
2651
  outputMode,
@@ -2588,6 +2778,7 @@ Options:
2588
2778
  --archive-twine1 Output as Twine 1 archive
2589
2779
  --json Output as JSON
2590
2780
  --twee2-compat Enable Twee2 syntax compatibility
2781
+ --lint Lint story structure (broken links, dead ends, orphans)
2591
2782
  --no-trim Don't trim passage whitespace
2592
2783
  -t, --test Enable test/debug mode
2593
2784
  -w, --watch Watch for changes and rebuild