@rohal12/twee-ts 1.16.1 → 1.17.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/README.md +1 -1
- package/dist/bin/twee-ts.js +191 -85
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/{compiler-Bmpbelqj.js → diagnostics-DZNS7W41.js} +114 -37
- package/dist/diagnostics-DZNS7W41.js.map +1 -0
- package/dist/index.cjs +200 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -10
- package/dist/index.d.ts +67 -10
- package/dist/index.js +200 -67
- package/dist/index.js.map +1 -1
- package/dist/plugins/rollup.d.ts +14 -1
- package/dist/plugins/rollup.js +20 -9
- package/dist/plugins/rollup.js.map +1 -1
- package/dist/plugins/vite.js +1 -28
- package/dist/plugins/vite.js.map +1 -1
- package/package.json +2 -1
- package/dist/compiler-Bmpbelqj.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -4719,14 +4719,16 @@ function parseDocument(data, options) {
|
|
|
4719
4719
|
*/
|
|
4720
4720
|
/**
|
|
4721
4721
|
* Parse a Twine 2 or Twine 1 compiled HTML file back into a Story model.
|
|
4722
|
+
* Tweego always trims passage text; here `trim: false` keeps it exactly, as for Twee sources.
|
|
4722
4723
|
*/
|
|
4723
|
-
function decompileHTML(html) {
|
|
4724
|
+
function decompileHTML(html, options = {}) {
|
|
4724
4725
|
const diagnostics = [];
|
|
4725
4726
|
const story = createStory();
|
|
4727
|
+
const passageText = options.trim === false ? (text) => text : (text) => text.trim();
|
|
4726
4728
|
const doc = parseDocument(html);
|
|
4727
4729
|
const twine2Data = findElement(doc, "tw-storydata");
|
|
4728
4730
|
if (twine2Data) {
|
|
4729
|
-
decompileTwine2(twine2Data, story, diagnostics);
|
|
4731
|
+
decompileTwine2(twine2Data, story, passageText, diagnostics);
|
|
4730
4732
|
return {
|
|
4731
4733
|
story,
|
|
4732
4734
|
diagnostics
|
|
@@ -4734,7 +4736,7 @@ function decompileHTML(html) {
|
|
|
4734
4736
|
}
|
|
4735
4737
|
const twine1Data = findElementByIdPattern(doc, /^store(?:-a|A)rea$/);
|
|
4736
4738
|
if (twine1Data) {
|
|
4737
|
-
decompileTwine1(twine1Data, story, diagnostics);
|
|
4739
|
+
decompileTwine1(twine1Data, story, passageText, diagnostics);
|
|
4738
4740
|
return {
|
|
4739
4741
|
story,
|
|
4740
4742
|
diagnostics
|
|
@@ -4749,7 +4751,7 @@ function decompileHTML(html) {
|
|
|
4749
4751
|
diagnostics
|
|
4750
4752
|
};
|
|
4751
4753
|
}
|
|
4752
|
-
function decompileTwine2(storyData, story, diagnostics) {
|
|
4754
|
+
function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
4753
4755
|
let startnode = 0;
|
|
4754
4756
|
const attrs = storyData.attribs ?? {};
|
|
4755
4757
|
if (attrs["name"]) story.name = attrs["name"];
|
|
@@ -4779,8 +4781,9 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4779
4781
|
switch (node.name) {
|
|
4780
4782
|
case "style":
|
|
4781
4783
|
case "script": {
|
|
4782
|
-
const
|
|
4783
|
-
if (
|
|
4784
|
+
const content = getTextContent(node);
|
|
4785
|
+
if (content.trim().length === 0) continue;
|
|
4786
|
+
const text = passageText(content);
|
|
4784
4787
|
storyAdd(story, {
|
|
4785
4788
|
name: node.name === "style" ? "Story Stylesheet" : "Story JavaScript",
|
|
4786
4789
|
tags: node.name === "style" ? ["stylesheet"] : ["script"],
|
|
@@ -4815,7 +4818,7 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4815
4818
|
const passage = {
|
|
4816
4819
|
name,
|
|
4817
4820
|
tags,
|
|
4818
|
-
text: getTextContent(node)
|
|
4821
|
+
text: passageText(getTextContent(node))
|
|
4819
4822
|
};
|
|
4820
4823
|
if (metadata.position || metadata.size) passage.metadata = metadata;
|
|
4821
4824
|
storyAdd(story, passage, diagnostics);
|
|
@@ -4829,8 +4832,8 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4829
4832
|
text: marshalStoryData(story)
|
|
4830
4833
|
}, diagnostics);
|
|
4831
4834
|
}
|
|
4832
|
-
function decompileTwine1(storeArea, story, diagnostics) {
|
|
4833
|
-
const passages = (storeArea.children ?? []).filter(isTiddler).map(tiddlerToPassage);
|
|
4835
|
+
function decompileTwine1(storeArea, story, passageText, diagnostics) {
|
|
4836
|
+
const passages = (storeArea.children ?? []).filter(isTiddler).map((node) => tiddlerToPassage(node, passageText));
|
|
4834
4837
|
const obfuscated = isRot13Obfuscated(passages);
|
|
4835
4838
|
for (const p of passages) storyAdd(story, obfuscated && p.name !== "StorySettings" ? {
|
|
4836
4839
|
...p,
|
|
@@ -4840,12 +4843,12 @@ function decompileTwine1(storeArea, story, diagnostics) {
|
|
|
4840
4843
|
function isTiddler(node) {
|
|
4841
4844
|
return node.type === "tag" && node.name === "div" && node.attribs !== void 0 && "tiddler" in node.attribs;
|
|
4842
4845
|
}
|
|
4843
|
-
function tiddlerToPassage(node) {
|
|
4846
|
+
function tiddlerToPassage(node, passageText) {
|
|
4844
4847
|
const nodeAttrs = node.attribs ?? {};
|
|
4845
4848
|
const passage = {
|
|
4846
4849
|
name: nodeAttrs["tiddler"] ?? "",
|
|
4847
4850
|
tags: nodeAttrs["tags"] ? nodeAttrs["tags"].split(/\s+/).filter((s) => s.length > 0) : [],
|
|
4848
|
-
text: tiddlerUnescape(getTextContent(node))
|
|
4851
|
+
text: passageText(tiddlerUnescape(getTextContent(node)))
|
|
4849
4852
|
};
|
|
4850
4853
|
const position = nodeAttrs["twine-position"];
|
|
4851
4854
|
if (position) passage.metadata = { position };
|
|
@@ -4910,7 +4913,7 @@ function loadSources(story, filenames, opts, diagnostics, processedFiles) {
|
|
|
4910
4913
|
break;
|
|
4911
4914
|
case "htm":
|
|
4912
4915
|
case "html":
|
|
4913
|
-
loadHTML(story, filename, diagnostics);
|
|
4916
|
+
loadHTML(story, filename, opts, diagnostics);
|
|
4914
4917
|
break;
|
|
4915
4918
|
case "css":
|
|
4916
4919
|
loadTagged(story, "stylesheet", filename, diagnostics);
|
|
@@ -5064,8 +5067,8 @@ function parseFontFile(filename) {
|
|
|
5064
5067
|
diagnostics: []
|
|
5065
5068
|
};
|
|
5066
5069
|
}
|
|
5067
|
-
function parseHTMLFile(filename) {
|
|
5068
|
-
const { story, diagnostics } = decompileHTML(readUTF8(filename));
|
|
5070
|
+
function parseHTMLFile(filename, opts) {
|
|
5071
|
+
const { story, diagnostics } = decompileHTML(readUTF8(filename), { trim: opts.trim ?? true });
|
|
5069
5072
|
return {
|
|
5070
5073
|
passages: story.name !== "" && !storyHas(story, "StoryTitle") ? [{
|
|
5071
5074
|
name: "StoryTitle",
|
|
@@ -5077,14 +5080,17 @@ function parseHTMLFile(filename) {
|
|
|
5077
5080
|
}
|
|
5078
5081
|
/**
|
|
5079
5082
|
* Identity of the options that affect how a file parses, stored on its cache entry.
|
|
5080
|
-
*
|
|
5083
|
+
* Twee files depend on `trim` and `twee2Compat`, HTML files on `trim`; every other file type
|
|
5084
|
+
* yields the same key for all options.
|
|
5081
5085
|
*/
|
|
5082
5086
|
function parseOptionsKey(filename, opts) {
|
|
5083
5087
|
const ext = normalizedFileExt(filename);
|
|
5088
|
+
const trim = opts.trim ?? true;
|
|
5089
|
+
if (ext === "htm" || ext === "html") return JSON.stringify({ trim });
|
|
5084
5090
|
const twee2File = ext === "tw2" || ext === "twee2";
|
|
5085
5091
|
if (!twee2File && ext !== "tw" && ext !== "twee") return "";
|
|
5086
5092
|
return JSON.stringify({
|
|
5087
|
-
trim
|
|
5093
|
+
trim,
|
|
5088
5094
|
twee2Compat: twee2File || (opts.twee2Compat ?? false)
|
|
5089
5095
|
});
|
|
5090
5096
|
}
|
|
@@ -5098,7 +5104,7 @@ function parseFile(filename, opts) {
|
|
|
5098
5104
|
twee2Compat: true
|
|
5099
5105
|
});
|
|
5100
5106
|
case "htm":
|
|
5101
|
-
case "html": return parseHTMLFile(filename);
|
|
5107
|
+
case "html": return parseHTMLFile(filename, opts);
|
|
5102
5108
|
case "css": return parseTaggedFile("stylesheet", filename);
|
|
5103
5109
|
case "js": return parseTaggedFile("script", filename);
|
|
5104
5110
|
case "otf":
|
|
@@ -5130,8 +5136,8 @@ function parseFile(filename, opts) {
|
|
|
5130
5136
|
default: return;
|
|
5131
5137
|
}
|
|
5132
5138
|
}
|
|
5133
|
-
function loadHTML(story, filename, diagnostics) {
|
|
5134
|
-
const result = parseHTMLFile(filename);
|
|
5139
|
+
function loadHTML(story, filename, opts, diagnostics) {
|
|
5140
|
+
const result = parseHTMLFile(filename, opts);
|
|
5135
5141
|
diagnostics.push(...result.diagnostics);
|
|
5136
5142
|
for (const p of result.passages) storyAdd(story, p, diagnostics);
|
|
5137
5143
|
}
|
|
@@ -5170,7 +5176,8 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5170
5176
|
const optionsKey = parseOptionsKey(filename, opts);
|
|
5171
5177
|
const entry = cache.get(filename);
|
|
5172
5178
|
const cached = entry?.parseOptionsKey === optionsKey ? entry : void 0;
|
|
5173
|
-
|
|
5179
|
+
const changed = changedFiles?.has(filename) ?? false;
|
|
5180
|
+
if (changedFiles && cached && !changed) {
|
|
5174
5181
|
diagnostics.push(...cached.diagnostics);
|
|
5175
5182
|
for (const p of cached.passages) storyAdd(story, p, diagnostics);
|
|
5176
5183
|
processedFiles.add(filename);
|
|
@@ -5182,7 +5189,7 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5182
5189
|
} catch {
|
|
5183
5190
|
continue;
|
|
5184
5191
|
}
|
|
5185
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
5192
|
+
if (cached && !changed && cached.mtimeMs === mtimeMs) {
|
|
5186
5193
|
diagnostics.push(...cached.diagnostics);
|
|
5187
5194
|
for (const p of cached.passages) storyAdd(story, p, diagnostics);
|
|
5188
5195
|
processedFiles.add(filename);
|
|
@@ -5430,10 +5437,39 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
|
|
|
5430
5437
|
}
|
|
5431
5438
|
//#endregion
|
|
5432
5439
|
//#region src/output-twee.ts
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5440
|
+
/**
|
|
5441
|
+
* Serialize the story as Twee. The StoryData passage is written from the story model rather than
|
|
5442
|
+
* from its loaded text, so changes made after loading (a start passage override, test mode) are kept.
|
|
5443
|
+
* Tweego writes the loaded text; its own normalization makes the two the same for an unchanged story.
|
|
5444
|
+
*
|
|
5445
|
+
* `addStoryData`: add a StoryData passage when the story has none, so that StoryData fields the
|
|
5446
|
+
* compile options set are recorded. Default: false.
|
|
5447
|
+
*/
|
|
5448
|
+
function toTwee(story, outMode, options = {}) {
|
|
5449
|
+
return withEffectiveStoryData(story, options.addStoryData ?? false).map((p) => passageToTwee(p, outMode)).join("");
|
|
5450
|
+
}
|
|
5451
|
+
function withEffectiveStoryData(story, addStoryData) {
|
|
5452
|
+
const storyData = {
|
|
5453
|
+
name: "StoryData",
|
|
5454
|
+
tags: [],
|
|
5455
|
+
text: marshalStoryData(story)
|
|
5456
|
+
};
|
|
5457
|
+
const index = story.passages.findIndex((p) => p.name === "StoryData");
|
|
5458
|
+
if (index !== -1) return story.passages.map((p, i) => i === index && isStoryDataJSON(p.text) ? {
|
|
5459
|
+
...p,
|
|
5460
|
+
text: storyData.text
|
|
5461
|
+
} : p);
|
|
5462
|
+
if (!addStoryData) return story.passages;
|
|
5463
|
+
const at = story.passages[0]?.name === "StoryTitle" ? 1 : 0;
|
|
5464
|
+
return [
|
|
5465
|
+
...story.passages.slice(0, at),
|
|
5466
|
+
storyData,
|
|
5467
|
+
...story.passages.slice(at)
|
|
5468
|
+
];
|
|
5469
|
+
}
|
|
5470
|
+
/** Whether the text loaded as StoryData. Text that did not is kept as written, for the author to fix. */
|
|
5471
|
+
function isStoryDataJSON(text) {
|
|
5472
|
+
return unmarshalStoryData(createStory(), text) === null;
|
|
5437
5473
|
}
|
|
5438
5474
|
//#endregion
|
|
5439
5475
|
//#region src/modules.ts
|
|
@@ -5505,7 +5541,19 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
|
5505
5541
|
return html;
|
|
5506
5542
|
}
|
|
5507
5543
|
//#endregion
|
|
5508
|
-
//#region src/
|
|
5544
|
+
//#region src/passage-omission.ts
|
|
5545
|
+
/**
|
|
5546
|
+
* Why the `target` output leaves `passage` out of its passage elements, or `undefined` when it
|
|
5547
|
+
* emits the passage (and so a link or the start can name it).
|
|
5548
|
+
*/
|
|
5549
|
+
function passageOmission(story, passage, target) {
|
|
5550
|
+
switch (target) {
|
|
5551
|
+
case "twine2": return twine2PassageOmission(story, passage);
|
|
5552
|
+
case "twine1": return twine1PassageOmission(passage);
|
|
5553
|
+
default: throw new Error(`unhandled passage output target: ${String(target)}`);
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
/** Why a passage is left out, as a predicate: `Passage "X" ${describeOmission(o)}`. */
|
|
5509
5557
|
function describeOmission(omission) {
|
|
5510
5558
|
switch (omission.kind) {
|
|
5511
5559
|
case "special-name": return `is the special "${omission.name}" passage`;
|
|
@@ -5514,13 +5562,8 @@ function describeOmission(omission) {
|
|
|
5514
5562
|
default: throw new Error(`unhandled passage omission: ${JSON.stringify(omission)}`);
|
|
5515
5563
|
}
|
|
5516
5564
|
}
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
case "twine2": return twine2PassageOmission(story, passage);
|
|
5520
|
-
case "twine1": return twine1PassageOmission(passage);
|
|
5521
|
-
default: throw new Error(`unhandled start passage target: ${String(target)}`);
|
|
5522
|
-
}
|
|
5523
|
-
}
|
|
5565
|
+
//#endregion
|
|
5566
|
+
//#region src/start-passage.ts
|
|
5524
5567
|
/**
|
|
5525
5568
|
* Check that `startName` names a passage the `target` output emits.
|
|
5526
5569
|
* Returns the error diagnostics to report (empty when the start is usable).
|
|
@@ -5531,7 +5574,7 @@ function startPassageDiagnostics(story, startName, target) {
|
|
|
5531
5574
|
level: "error",
|
|
5532
5575
|
message: `Starting passage "${startName}" not found.`
|
|
5533
5576
|
}];
|
|
5534
|
-
const omission =
|
|
5577
|
+
const omission = passageOmission(story, passage, target);
|
|
5535
5578
|
if (omission === void 0) return [];
|
|
5536
5579
|
return [{
|
|
5537
5580
|
level: "error",
|
|
@@ -5566,21 +5609,48 @@ async function compile(options) {
|
|
|
5566
5609
|
* Compile and write to a file.
|
|
5567
5610
|
*/
|
|
5568
5611
|
async function compileToFile(options) {
|
|
5569
|
-
const result = await
|
|
5612
|
+
const result = await compileForOutputFile(options, options.outFile);
|
|
5570
5613
|
(0, node_fs.writeFileSync)(options.outFile, result.output, "utf-8");
|
|
5571
5614
|
return result;
|
|
5572
5615
|
}
|
|
5573
5616
|
/**
|
|
5617
|
+
* Builds as compileToFile() does for `outFile`, without writing it: the output may sit
|
|
5618
|
+
* inside a source folder, so its last build is left out of the sources and modules and
|
|
5619
|
+
* never read back. The caller decides whether the result is written. With no `outFile`
|
|
5620
|
+
* (output to stdout, or none), this is compile().
|
|
5621
|
+
*
|
|
5622
|
+
* Internal, for the CLI; not part of the public API.
|
|
5623
|
+
*/
|
|
5624
|
+
async function compileForOutputFile(options, outFile) {
|
|
5625
|
+
return buildOutput(options, void 0, void 0, outFile);
|
|
5626
|
+
}
|
|
5627
|
+
/**
|
|
5574
5628
|
* Compile with incremental caching support.
|
|
5575
5629
|
* Plugins and advanced users can manage their own cache and changed-file tracking.
|
|
5630
|
+
*
|
|
5631
|
+
* Without `changedFiles`, a cached file is reused while its modification time is unchanged.
|
|
5632
|
+
* With it, a file it names (by the path source discovery gives it, relative to the working
|
|
5633
|
+
* directory) is always reparsed, whatever its modification time, and every other cached
|
|
5634
|
+
* file is reused as it is.
|
|
5576
5635
|
*/
|
|
5577
5636
|
async function compileIncremental(options, cache, changedFiles) {
|
|
5578
5637
|
return buildOutput(options, cache, changedFiles);
|
|
5579
5638
|
}
|
|
5580
5639
|
/**
|
|
5581
|
-
* Watch for file changes and recompile.
|
|
5640
|
+
* Watch for file changes and recompile. Every build is written to `outFile`, including
|
|
5641
|
+
* one whose diagnostics report errors.
|
|
5582
5642
|
*/
|
|
5583
5643
|
async function watch(options) {
|
|
5644
|
+
return watchWithWriteFilter(options, () => true);
|
|
5645
|
+
}
|
|
5646
|
+
/**
|
|
5647
|
+
* watch(), writing to `outFile` only the builds `shouldWrite` accepts. A rejected build
|
|
5648
|
+
* leaves the output file as it was; `onBuild` receives every build either way.
|
|
5649
|
+
*
|
|
5650
|
+
* Internal, for the CLI, which keeps the last good output when a rebuild reports errors;
|
|
5651
|
+
* not part of the public API.
|
|
5652
|
+
*/
|
|
5653
|
+
async function watchWithWriteFilter(options, shouldWrite) {
|
|
5584
5654
|
const controller = new AbortController();
|
|
5585
5655
|
const cache = /* @__PURE__ */ new Map();
|
|
5586
5656
|
const filePaths = options.sources.filter((s) => typeof s === "string");
|
|
@@ -5600,7 +5670,7 @@ async function watch(options) {
|
|
|
5600
5670
|
};
|
|
5601
5671
|
const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
|
|
5602
5672
|
buildOutput(options, cache, changedFiles, options.outFile).then((result) => {
|
|
5603
|
-
(0, node_fs.writeFileSync)(options.outFile, result.output, "utf-8");
|
|
5673
|
+
if (shouldWrite(result)) (0, node_fs.writeFileSync)(options.outFile, result.output, "utf-8");
|
|
5604
5674
|
options.onBuild?.(result);
|
|
5605
5675
|
}).catch((e) => {
|
|
5606
5676
|
options.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5656,7 +5726,7 @@ async function buildOutput(options, cache, changedFiles, outFile) {
|
|
|
5656
5726
|
switch (outputMode) {
|
|
5657
5727
|
case "twee3":
|
|
5658
5728
|
case "twee1":
|
|
5659
|
-
output = toTwee(story, outputMode);
|
|
5729
|
+
output = toTwee(story, outputMode, { addStoryData: Boolean(options.startPassage) || testMode });
|
|
5660
5730
|
break;
|
|
5661
5731
|
case "twine2-archive":
|
|
5662
5732
|
output = toTwine2Archive(story, startName, { sourceInfo });
|
|
@@ -6099,6 +6169,11 @@ function endOfRegularExpression(source, pos) {
|
|
|
6099
6169
|
}
|
|
6100
6170
|
//#endregion
|
|
6101
6171
|
//#region src/link-markup.ts
|
|
6172
|
+
/** Records that the reading has looked at the characters before `pos`, and returns `result`. */
|
|
6173
|
+
function readTo(reader, pos, result) {
|
|
6174
|
+
reader.scanned = Math.max(reader.scanned, pos);
|
|
6175
|
+
return result;
|
|
6176
|
+
}
|
|
6102
6177
|
/**
|
|
6103
6178
|
* Reads link or image markup that starts at `start` in `text`, or returns `undefined` where
|
|
6104
6179
|
* SugarCube rejects it (and so prints the text instead).
|
|
@@ -6106,15 +6181,17 @@ function endOfRegularExpression(source, pos) {
|
|
|
6106
6181
|
* `budget.left`, if given, is how many characters the reading may look at; it is reduced by as
|
|
6107
6182
|
* many as were read, and once it runs out, every reading fails. Unclosed markup is read to the end
|
|
6108
6183
|
* of its line, so reading every `[[` of a long line that never closes one would otherwise take
|
|
6109
|
-
* quadratic time.
|
|
6184
|
+
* quadratic time. A reading is charged for the characters it looked at, up to where it ended or
|
|
6185
|
+
* failed (a line feed, say), and no more, so markup left unclosed on short lines costs little.
|
|
6110
6186
|
*/
|
|
6111
6187
|
function readSquareBracketedMarkup(text, start, budget = { left: Infinity }) {
|
|
6112
6188
|
const reader = {
|
|
6113
6189
|
text,
|
|
6114
|
-
limit: Math.min(text.length, start + Math.max(budget.left, 0))
|
|
6190
|
+
limit: Math.min(text.length, start + Math.max(budget.left, 0)),
|
|
6191
|
+
scanned: start
|
|
6115
6192
|
};
|
|
6116
6193
|
const markup = readMarkup(reader, start);
|
|
6117
|
-
budget.left -= (markup?.end ??
|
|
6194
|
+
budget.left -= Math.max(reader.scanned, markup?.end ?? start) - start;
|
|
6118
6195
|
return markup;
|
|
6119
6196
|
}
|
|
6120
6197
|
function readMarkup(reader, start) {
|
|
@@ -6190,7 +6267,7 @@ function readCoreComponents(reader, from) {
|
|
|
6190
6267
|
const ch = text[pos];
|
|
6191
6268
|
pos += 1;
|
|
6192
6269
|
switch (ch) {
|
|
6193
|
-
case "\n": return;
|
|
6270
|
+
case "\n": return readTo(reader, pos, void 0);
|
|
6194
6271
|
case "\"": {
|
|
6195
6272
|
const end = readQuoted(reader, pos, "\"");
|
|
6196
6273
|
if (end === void 0) return;
|
|
@@ -6224,7 +6301,7 @@ function readCoreComponents(reader, from) {
|
|
|
6224
6301
|
case "]":
|
|
6225
6302
|
depth -= 1;
|
|
6226
6303
|
if (depth === 1) {
|
|
6227
|
-
const end = componentEnd(
|
|
6304
|
+
const end = componentEnd(reader, pos);
|
|
6228
6305
|
if (end === void 0) return;
|
|
6229
6306
|
return {
|
|
6230
6307
|
link: link ?? text.slice(componentStart, end.textEnd),
|
|
@@ -6233,6 +6310,7 @@ function readCoreComponents(reader, from) {
|
|
|
6233
6310
|
}
|
|
6234
6311
|
}
|
|
6235
6312
|
}
|
|
6313
|
+
return readTo(reader, pos, void 0);
|
|
6236
6314
|
}
|
|
6237
6315
|
/**
|
|
6238
6316
|
* Reads a component after `][` that holds no delimiter: an image's link (`setter` false) or a
|
|
@@ -6246,7 +6324,7 @@ function readComponent(reader, from, setter) {
|
|
|
6246
6324
|
const ch = text[pos];
|
|
6247
6325
|
pos += 1;
|
|
6248
6326
|
switch (ch) {
|
|
6249
|
-
case "\n": return;
|
|
6327
|
+
case "\n": return readTo(reader, pos, void 0);
|
|
6250
6328
|
case "\"":
|
|
6251
6329
|
case "'": {
|
|
6252
6330
|
if (ch === "'" && !setter) break;
|
|
@@ -6260,9 +6338,10 @@ function readComponent(reader, from, setter) {
|
|
|
6260
6338
|
break;
|
|
6261
6339
|
case "]":
|
|
6262
6340
|
depth -= 1;
|
|
6263
|
-
if (depth === 1) return componentEnd(
|
|
6341
|
+
if (depth === 1) return componentEnd(reader, pos);
|
|
6264
6342
|
}
|
|
6265
6343
|
}
|
|
6344
|
+
return readTo(reader, pos, void 0);
|
|
6266
6345
|
}
|
|
6267
6346
|
/** Reads a setter, the last component; returns the index after its closing `]]`. */
|
|
6268
6347
|
function readSetter(reader, from) {
|
|
@@ -6270,19 +6349,19 @@ function readSetter(reader, from) {
|
|
|
6270
6349
|
return end?.last === true ? end.next : void 0;
|
|
6271
6350
|
}
|
|
6272
6351
|
/** What the `]` just before `pos`, which brings the depth back to one, ends; `undefined` if malformed. */
|
|
6273
|
-
function componentEnd(
|
|
6274
|
-
switch (text[pos]) {
|
|
6275
|
-
case "[": return {
|
|
6352
|
+
function componentEnd(reader, pos) {
|
|
6353
|
+
switch (reader.text[pos]) {
|
|
6354
|
+
case "[": return readTo(reader, pos + 1, {
|
|
6276
6355
|
textEnd: pos - 1,
|
|
6277
6356
|
next: pos + 1,
|
|
6278
6357
|
last: false
|
|
6279
|
-
};
|
|
6280
|
-
case "]": return {
|
|
6358
|
+
});
|
|
6359
|
+
case "]": return readTo(reader, pos + 1, {
|
|
6281
6360
|
textEnd: pos - 1,
|
|
6282
6361
|
next: pos + 1,
|
|
6283
6362
|
last: true
|
|
6284
|
-
};
|
|
6285
|
-
default: return;
|
|
6363
|
+
});
|
|
6364
|
+
default: return readTo(reader, pos + 1, void 0);
|
|
6286
6365
|
}
|
|
6287
6366
|
}
|
|
6288
6367
|
/**
|
|
@@ -6297,12 +6376,13 @@ function readQuoted(reader, from, quote) {
|
|
|
6297
6376
|
const ch = text[pos];
|
|
6298
6377
|
pos += 1;
|
|
6299
6378
|
if (ch === quote) return pos;
|
|
6300
|
-
if (ch === "\n") return;
|
|
6379
|
+
if (ch === "\n") return readTo(reader, pos, void 0);
|
|
6301
6380
|
if (ch === "\\") {
|
|
6302
|
-
if (pos >= reader.limit || text[pos] === "\n") return;
|
|
6381
|
+
if (pos >= reader.limit || text[pos] === "\n") return readTo(reader, Math.min(pos + 1, reader.limit), void 0);
|
|
6303
6382
|
pos += 1;
|
|
6304
6383
|
}
|
|
6305
6384
|
}
|
|
6385
|
+
return readTo(reader, pos, void 0);
|
|
6306
6386
|
}
|
|
6307
6387
|
//#endregion
|
|
6308
6388
|
//#region src/sugarcube-macros.ts
|
|
@@ -7160,6 +7240,29 @@ function extractLinksFromText(text, isScript) {
|
|
|
7160
7240
|
].flatMap((via) => found.filter((link) => link.via === via).map((link) => link.passage));
|
|
7161
7241
|
return [...new Set(ordered)];
|
|
7162
7242
|
}
|
|
7243
|
+
/**
|
|
7244
|
+
* The passages that `p` links to. A script passage is JavaScript, read for links only in its
|
|
7245
|
+
* strings. A stylesheet is CSS, which the story puts in a style element and never wikifies, so it
|
|
7246
|
+
* links nowhere; a passage tagged both is a script, as Twine 2 output takes it.
|
|
7247
|
+
*/
|
|
7248
|
+
function passageLinks(p) {
|
|
7249
|
+
if (hasTag(p, "script")) return extractLinksFromText(textAsRead(p), true);
|
|
7250
|
+
if (hasTag(p, "stylesheet")) return [];
|
|
7251
|
+
return extractLinksFromText(textAsRead(p), false);
|
|
7252
|
+
}
|
|
7253
|
+
/**
|
|
7254
|
+
* Each passage name, mapped to why the `target` output leaves that passage out, or to `undefined`
|
|
7255
|
+
* when a link can reach it: always without a target, and otherwise when the output emits a
|
|
7256
|
+
* passage of that name.
|
|
7257
|
+
*/
|
|
7258
|
+
function destinationOmissions(story, { target }) {
|
|
7259
|
+
const omissions = /* @__PURE__ */ new Map();
|
|
7260
|
+
for (const p of story.passages) {
|
|
7261
|
+
const omission = target === void 0 ? void 0 : passageOmission(story, p, target);
|
|
7262
|
+
if (!omissions.has(p.name) || omission === void 0) omissions.set(p.name, omission);
|
|
7263
|
+
}
|
|
7264
|
+
return omissions;
|
|
7265
|
+
}
|
|
7163
7266
|
/** Passages SugarCube runs from their raw text, never joining their lines. */
|
|
7164
7267
|
const RAW_TEXT_PASSAGES = /* @__PURE__ */ new Set([
|
|
7165
7268
|
"StoryInit",
|
|
@@ -7201,9 +7304,14 @@ function joinLines(text) {
|
|
|
7201
7304
|
* expect(map.brokenLinks).toEqual([]);
|
|
7202
7305
|
* expect(map.deadEnds).not.toContain('Start');
|
|
7203
7306
|
* ```
|
|
7307
|
+
*
|
|
7308
|
+
* By default, a link is broken only when no passage has its name. Pass a `target` to check links
|
|
7309
|
+
* against what that output emits: `storyInspect(result.story, { target: 'twine2' })` also reports
|
|
7310
|
+
* links to passages Twine 2 output leaves out, such as script, stylesheet and `Twine.private`
|
|
7311
|
+
* passages.
|
|
7204
7312
|
*/
|
|
7205
|
-
function storyInspect(story) {
|
|
7206
|
-
const
|
|
7313
|
+
function storyInspect(story, options = {}) {
|
|
7314
|
+
const destinations = destinationOmissions(story, options);
|
|
7207
7315
|
const passages = [];
|
|
7208
7316
|
const storyPassages = [];
|
|
7209
7317
|
const infoPassages = [];
|
|
@@ -7225,14 +7333,24 @@ function storyInspect(story) {
|
|
|
7225
7333
|
}
|
|
7226
7334
|
list.push(p.name);
|
|
7227
7335
|
}
|
|
7228
|
-
|
|
7229
|
-
links.set(p.name, targets);
|
|
7336
|
+
links.set(p.name, passageLinks(p));
|
|
7230
7337
|
}
|
|
7231
7338
|
const brokenLinks = [];
|
|
7232
|
-
for (const [from, targets] of links) for (const to of targets)
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7339
|
+
for (const [from, targets] of links) for (const to of targets) {
|
|
7340
|
+
if (!destinations.has(to)) {
|
|
7341
|
+
brokenLinks.push({
|
|
7342
|
+
from,
|
|
7343
|
+
to
|
|
7344
|
+
});
|
|
7345
|
+
continue;
|
|
7346
|
+
}
|
|
7347
|
+
const omission = destinations.get(to);
|
|
7348
|
+
if (omission !== void 0) brokenLinks.push({
|
|
7349
|
+
from,
|
|
7350
|
+
to,
|
|
7351
|
+
omission
|
|
7352
|
+
});
|
|
7353
|
+
}
|
|
7236
7354
|
const deadEnds = [];
|
|
7237
7355
|
for (const name of storyPassages) {
|
|
7238
7356
|
const targets = links.get(name);
|
|
@@ -7262,14 +7380,26 @@ function storyInspect(story) {
|
|
|
7262
7380
|
/**
|
|
7263
7381
|
* Lint a story: compile without output rendering, then inspect structure.
|
|
7264
7382
|
* Uses JSON output mode internally to avoid format resolution, so it checks
|
|
7265
|
-
* the starting passage itself, against the Twine 2 passage rules.
|
|
7383
|
+
* the starting passage itself, against the Twine 2 passage rules. Link
|
|
7384
|
+
* destinations are checked against the same rules: a link to a passage that
|
|
7385
|
+
* Twine 2 output leaves out is broken.
|
|
7266
7386
|
*/
|
|
7267
7387
|
async function lint(options) {
|
|
7268
|
-
|
|
7388
|
+
return lintForOutputFile(options, void 0);
|
|
7389
|
+
}
|
|
7390
|
+
/**
|
|
7391
|
+
* lint() for a project whose builds are written to `outFile`: like a build for that file,
|
|
7392
|
+
* it leaves `outFile` out of the sources and modules, so an earlier build inside a source
|
|
7393
|
+
* folder is not linted as a source.
|
|
7394
|
+
*
|
|
7395
|
+
* Internal, for the CLI; not part of the public API.
|
|
7396
|
+
*/
|
|
7397
|
+
async function lintForOutputFile(options, outFile) {
|
|
7398
|
+
const result = await compileForOutputFile({
|
|
7269
7399
|
...options,
|
|
7270
7400
|
outputMode: "json"
|
|
7271
|
-
});
|
|
7272
|
-
const map = storyInspect(result.story);
|
|
7401
|
+
}, outFile);
|
|
7402
|
+
const map = storyInspect(result.story, { target: "twine2" });
|
|
7273
7403
|
return {
|
|
7274
7404
|
diagnostics: [...result.diagnostics, ...startPassageDiagnostics(result.story, map.start, "twine2")],
|
|
7275
7405
|
stats: result.stats,
|
|
@@ -7296,7 +7426,10 @@ function formatLintReport(result) {
|
|
|
7296
7426
|
if (result.brokenLinks.length > 0) {
|
|
7297
7427
|
lines.push("");
|
|
7298
7428
|
lines.push(`Broken links (${result.brokenLinks.length}):`);
|
|
7299
|
-
for (const link of result.brokenLinks)
|
|
7429
|
+
for (const link of result.brokenLinks) {
|
|
7430
|
+
const why = link.omission === void 0 ? "does not exist" : `${describeOmission(link.omission)}, so it is left out of the story data`;
|
|
7431
|
+
lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" ${why})`);
|
|
7432
|
+
}
|
|
7300
7433
|
}
|
|
7301
7434
|
if (result.deadEnds.length > 0) {
|
|
7302
7435
|
lines.push("");
|