@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.js
CHANGED
|
@@ -4696,14 +4696,16 @@ function parseDocument(data, options) {
|
|
|
4696
4696
|
*/
|
|
4697
4697
|
/**
|
|
4698
4698
|
* Parse a Twine 2 or Twine 1 compiled HTML file back into a Story model.
|
|
4699
|
+
* Tweego always trims passage text; here `trim: false` keeps it exactly, as for Twee sources.
|
|
4699
4700
|
*/
|
|
4700
|
-
function decompileHTML(html) {
|
|
4701
|
+
function decompileHTML(html, options = {}) {
|
|
4701
4702
|
const diagnostics = [];
|
|
4702
4703
|
const story = createStory();
|
|
4704
|
+
const passageText = options.trim === false ? (text) => text : (text) => text.trim();
|
|
4703
4705
|
const doc = parseDocument(html);
|
|
4704
4706
|
const twine2Data = findElement(doc, "tw-storydata");
|
|
4705
4707
|
if (twine2Data) {
|
|
4706
|
-
decompileTwine2(twine2Data, story, diagnostics);
|
|
4708
|
+
decompileTwine2(twine2Data, story, passageText, diagnostics);
|
|
4707
4709
|
return {
|
|
4708
4710
|
story,
|
|
4709
4711
|
diagnostics
|
|
@@ -4711,7 +4713,7 @@ function decompileHTML(html) {
|
|
|
4711
4713
|
}
|
|
4712
4714
|
const twine1Data = findElementByIdPattern(doc, /^store(?:-a|A)rea$/);
|
|
4713
4715
|
if (twine1Data) {
|
|
4714
|
-
decompileTwine1(twine1Data, story, diagnostics);
|
|
4716
|
+
decompileTwine1(twine1Data, story, passageText, diagnostics);
|
|
4715
4717
|
return {
|
|
4716
4718
|
story,
|
|
4717
4719
|
diagnostics
|
|
@@ -4726,7 +4728,7 @@ function decompileHTML(html) {
|
|
|
4726
4728
|
diagnostics
|
|
4727
4729
|
};
|
|
4728
4730
|
}
|
|
4729
|
-
function decompileTwine2(storyData, story, diagnostics) {
|
|
4731
|
+
function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
4730
4732
|
let startnode = 0;
|
|
4731
4733
|
const attrs = storyData.attribs ?? {};
|
|
4732
4734
|
if (attrs["name"]) story.name = attrs["name"];
|
|
@@ -4756,8 +4758,9 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4756
4758
|
switch (node.name) {
|
|
4757
4759
|
case "style":
|
|
4758
4760
|
case "script": {
|
|
4759
|
-
const
|
|
4760
|
-
if (
|
|
4761
|
+
const content = getTextContent(node);
|
|
4762
|
+
if (content.trim().length === 0) continue;
|
|
4763
|
+
const text = passageText(content);
|
|
4761
4764
|
storyAdd(story, {
|
|
4762
4765
|
name: node.name === "style" ? "Story Stylesheet" : "Story JavaScript",
|
|
4763
4766
|
tags: node.name === "style" ? ["stylesheet"] : ["script"],
|
|
@@ -4792,7 +4795,7 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4792
4795
|
const passage = {
|
|
4793
4796
|
name,
|
|
4794
4797
|
tags,
|
|
4795
|
-
text: getTextContent(node)
|
|
4798
|
+
text: passageText(getTextContent(node))
|
|
4796
4799
|
};
|
|
4797
4800
|
if (metadata.position || metadata.size) passage.metadata = metadata;
|
|
4798
4801
|
storyAdd(story, passage, diagnostics);
|
|
@@ -4806,8 +4809,8 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4806
4809
|
text: marshalStoryData(story)
|
|
4807
4810
|
}, diagnostics);
|
|
4808
4811
|
}
|
|
4809
|
-
function decompileTwine1(storeArea, story, diagnostics) {
|
|
4810
|
-
const passages = (storeArea.children ?? []).filter(isTiddler).map(tiddlerToPassage);
|
|
4812
|
+
function decompileTwine1(storeArea, story, passageText, diagnostics) {
|
|
4813
|
+
const passages = (storeArea.children ?? []).filter(isTiddler).map((node) => tiddlerToPassage(node, passageText));
|
|
4811
4814
|
const obfuscated = isRot13Obfuscated(passages);
|
|
4812
4815
|
for (const p of passages) storyAdd(story, obfuscated && p.name !== "StorySettings" ? {
|
|
4813
4816
|
...p,
|
|
@@ -4817,12 +4820,12 @@ function decompileTwine1(storeArea, story, diagnostics) {
|
|
|
4817
4820
|
function isTiddler(node) {
|
|
4818
4821
|
return node.type === "tag" && node.name === "div" && node.attribs !== void 0 && "tiddler" in node.attribs;
|
|
4819
4822
|
}
|
|
4820
|
-
function tiddlerToPassage(node) {
|
|
4823
|
+
function tiddlerToPassage(node, passageText) {
|
|
4821
4824
|
const nodeAttrs = node.attribs ?? {};
|
|
4822
4825
|
const passage = {
|
|
4823
4826
|
name: nodeAttrs["tiddler"] ?? "",
|
|
4824
4827
|
tags: nodeAttrs["tags"] ? nodeAttrs["tags"].split(/\s+/).filter((s) => s.length > 0) : [],
|
|
4825
|
-
text: tiddlerUnescape(getTextContent(node))
|
|
4828
|
+
text: passageText(tiddlerUnescape(getTextContent(node)))
|
|
4826
4829
|
};
|
|
4827
4830
|
const position = nodeAttrs["twine-position"];
|
|
4828
4831
|
if (position) passage.metadata = { position };
|
|
@@ -4887,7 +4890,7 @@ function loadSources(story, filenames, opts, diagnostics, processedFiles) {
|
|
|
4887
4890
|
break;
|
|
4888
4891
|
case "htm":
|
|
4889
4892
|
case "html":
|
|
4890
|
-
loadHTML(story, filename, diagnostics);
|
|
4893
|
+
loadHTML(story, filename, opts, diagnostics);
|
|
4891
4894
|
break;
|
|
4892
4895
|
case "css":
|
|
4893
4896
|
loadTagged(story, "stylesheet", filename, diagnostics);
|
|
@@ -5041,8 +5044,8 @@ function parseFontFile(filename) {
|
|
|
5041
5044
|
diagnostics: []
|
|
5042
5045
|
};
|
|
5043
5046
|
}
|
|
5044
|
-
function parseHTMLFile(filename) {
|
|
5045
|
-
const { story, diagnostics } = decompileHTML(readUTF8(filename));
|
|
5047
|
+
function parseHTMLFile(filename, opts) {
|
|
5048
|
+
const { story, diagnostics } = decompileHTML(readUTF8(filename), { trim: opts.trim ?? true });
|
|
5046
5049
|
return {
|
|
5047
5050
|
passages: story.name !== "" && !storyHas(story, "StoryTitle") ? [{
|
|
5048
5051
|
name: "StoryTitle",
|
|
@@ -5054,14 +5057,17 @@ function parseHTMLFile(filename) {
|
|
|
5054
5057
|
}
|
|
5055
5058
|
/**
|
|
5056
5059
|
* Identity of the options that affect how a file parses, stored on its cache entry.
|
|
5057
|
-
*
|
|
5060
|
+
* Twee files depend on `trim` and `twee2Compat`, HTML files on `trim`; every other file type
|
|
5061
|
+
* yields the same key for all options.
|
|
5058
5062
|
*/
|
|
5059
5063
|
function parseOptionsKey(filename, opts) {
|
|
5060
5064
|
const ext = normalizedFileExt(filename);
|
|
5065
|
+
const trim = opts.trim ?? true;
|
|
5066
|
+
if (ext === "htm" || ext === "html") return JSON.stringify({ trim });
|
|
5061
5067
|
const twee2File = ext === "tw2" || ext === "twee2";
|
|
5062
5068
|
if (!twee2File && ext !== "tw" && ext !== "twee") return "";
|
|
5063
5069
|
return JSON.stringify({
|
|
5064
|
-
trim
|
|
5070
|
+
trim,
|
|
5065
5071
|
twee2Compat: twee2File || (opts.twee2Compat ?? false)
|
|
5066
5072
|
});
|
|
5067
5073
|
}
|
|
@@ -5075,7 +5081,7 @@ function parseFile(filename, opts) {
|
|
|
5075
5081
|
twee2Compat: true
|
|
5076
5082
|
});
|
|
5077
5083
|
case "htm":
|
|
5078
|
-
case "html": return parseHTMLFile(filename);
|
|
5084
|
+
case "html": return parseHTMLFile(filename, opts);
|
|
5079
5085
|
case "css": return parseTaggedFile("stylesheet", filename);
|
|
5080
5086
|
case "js": return parseTaggedFile("script", filename);
|
|
5081
5087
|
case "otf":
|
|
@@ -5107,8 +5113,8 @@ function parseFile(filename, opts) {
|
|
|
5107
5113
|
default: return;
|
|
5108
5114
|
}
|
|
5109
5115
|
}
|
|
5110
|
-
function loadHTML(story, filename, diagnostics) {
|
|
5111
|
-
const result = parseHTMLFile(filename);
|
|
5116
|
+
function loadHTML(story, filename, opts, diagnostics) {
|
|
5117
|
+
const result = parseHTMLFile(filename, opts);
|
|
5112
5118
|
diagnostics.push(...result.diagnostics);
|
|
5113
5119
|
for (const p of result.passages) storyAdd(story, p, diagnostics);
|
|
5114
5120
|
}
|
|
@@ -5147,7 +5153,8 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5147
5153
|
const optionsKey = parseOptionsKey(filename, opts);
|
|
5148
5154
|
const entry = cache.get(filename);
|
|
5149
5155
|
const cached = entry?.parseOptionsKey === optionsKey ? entry : void 0;
|
|
5150
|
-
|
|
5156
|
+
const changed = changedFiles?.has(filename) ?? false;
|
|
5157
|
+
if (changedFiles && cached && !changed) {
|
|
5151
5158
|
diagnostics.push(...cached.diagnostics);
|
|
5152
5159
|
for (const p of cached.passages) storyAdd(story, p, diagnostics);
|
|
5153
5160
|
processedFiles.add(filename);
|
|
@@ -5159,7 +5166,7 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5159
5166
|
} catch {
|
|
5160
5167
|
continue;
|
|
5161
5168
|
}
|
|
5162
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
5169
|
+
if (cached && !changed && cached.mtimeMs === mtimeMs) {
|
|
5163
5170
|
diagnostics.push(...cached.diagnostics);
|
|
5164
5171
|
for (const p of cached.passages) storyAdd(story, p, diagnostics);
|
|
5165
5172
|
processedFiles.add(filename);
|
|
@@ -5407,10 +5414,39 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
|
|
|
5407
5414
|
}
|
|
5408
5415
|
//#endregion
|
|
5409
5416
|
//#region src/output-twee.ts
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5413
|
-
|
|
5417
|
+
/**
|
|
5418
|
+
* Serialize the story as Twee. The StoryData passage is written from the story model rather than
|
|
5419
|
+
* from its loaded text, so changes made after loading (a start passage override, test mode) are kept.
|
|
5420
|
+
* Tweego writes the loaded text; its own normalization makes the two the same for an unchanged story.
|
|
5421
|
+
*
|
|
5422
|
+
* `addStoryData`: add a StoryData passage when the story has none, so that StoryData fields the
|
|
5423
|
+
* compile options set are recorded. Default: false.
|
|
5424
|
+
*/
|
|
5425
|
+
function toTwee(story, outMode, options = {}) {
|
|
5426
|
+
return withEffectiveStoryData(story, options.addStoryData ?? false).map((p) => passageToTwee(p, outMode)).join("");
|
|
5427
|
+
}
|
|
5428
|
+
function withEffectiveStoryData(story, addStoryData) {
|
|
5429
|
+
const storyData = {
|
|
5430
|
+
name: "StoryData",
|
|
5431
|
+
tags: [],
|
|
5432
|
+
text: marshalStoryData(story)
|
|
5433
|
+
};
|
|
5434
|
+
const index = story.passages.findIndex((p) => p.name === "StoryData");
|
|
5435
|
+
if (index !== -1) return story.passages.map((p, i) => i === index && isStoryDataJSON(p.text) ? {
|
|
5436
|
+
...p,
|
|
5437
|
+
text: storyData.text
|
|
5438
|
+
} : p);
|
|
5439
|
+
if (!addStoryData) return story.passages;
|
|
5440
|
+
const at = story.passages[0]?.name === "StoryTitle" ? 1 : 0;
|
|
5441
|
+
return [
|
|
5442
|
+
...story.passages.slice(0, at),
|
|
5443
|
+
storyData,
|
|
5444
|
+
...story.passages.slice(at)
|
|
5445
|
+
];
|
|
5446
|
+
}
|
|
5447
|
+
/** Whether the text loaded as StoryData. Text that did not is kept as written, for the author to fix. */
|
|
5448
|
+
function isStoryDataJSON(text) {
|
|
5449
|
+
return unmarshalStoryData(createStory(), text) === null;
|
|
5414
5450
|
}
|
|
5415
5451
|
//#endregion
|
|
5416
5452
|
//#region src/modules.ts
|
|
@@ -5482,7 +5518,19 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
|
5482
5518
|
return html;
|
|
5483
5519
|
}
|
|
5484
5520
|
//#endregion
|
|
5485
|
-
//#region src/
|
|
5521
|
+
//#region src/passage-omission.ts
|
|
5522
|
+
/**
|
|
5523
|
+
* Why the `target` output leaves `passage` out of its passage elements, or `undefined` when it
|
|
5524
|
+
* emits the passage (and so a link or the start can name it).
|
|
5525
|
+
*/
|
|
5526
|
+
function passageOmission(story, passage, target) {
|
|
5527
|
+
switch (target) {
|
|
5528
|
+
case "twine2": return twine2PassageOmission(story, passage);
|
|
5529
|
+
case "twine1": return twine1PassageOmission(passage);
|
|
5530
|
+
default: throw new Error(`unhandled passage output target: ${String(target)}`);
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
/** Why a passage is left out, as a predicate: `Passage "X" ${describeOmission(o)}`. */
|
|
5486
5534
|
function describeOmission(omission) {
|
|
5487
5535
|
switch (omission.kind) {
|
|
5488
5536
|
case "special-name": return `is the special "${omission.name}" passage`;
|
|
@@ -5491,13 +5539,8 @@ function describeOmission(omission) {
|
|
|
5491
5539
|
default: throw new Error(`unhandled passage omission: ${JSON.stringify(omission)}`);
|
|
5492
5540
|
}
|
|
5493
5541
|
}
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
case "twine2": return twine2PassageOmission(story, passage);
|
|
5497
|
-
case "twine1": return twine1PassageOmission(passage);
|
|
5498
|
-
default: throw new Error(`unhandled start passage target: ${String(target)}`);
|
|
5499
|
-
}
|
|
5500
|
-
}
|
|
5542
|
+
//#endregion
|
|
5543
|
+
//#region src/start-passage.ts
|
|
5501
5544
|
/**
|
|
5502
5545
|
* Check that `startName` names a passage the `target` output emits.
|
|
5503
5546
|
* Returns the error diagnostics to report (empty when the start is usable).
|
|
@@ -5508,7 +5551,7 @@ function startPassageDiagnostics(story, startName, target) {
|
|
|
5508
5551
|
level: "error",
|
|
5509
5552
|
message: `Starting passage "${startName}" not found.`
|
|
5510
5553
|
}];
|
|
5511
|
-
const omission =
|
|
5554
|
+
const omission = passageOmission(story, passage, target);
|
|
5512
5555
|
if (omission === void 0) return [];
|
|
5513
5556
|
return [{
|
|
5514
5557
|
level: "error",
|
|
@@ -5543,21 +5586,48 @@ async function compile(options) {
|
|
|
5543
5586
|
* Compile and write to a file.
|
|
5544
5587
|
*/
|
|
5545
5588
|
async function compileToFile(options) {
|
|
5546
|
-
const result = await
|
|
5589
|
+
const result = await compileForOutputFile(options, options.outFile);
|
|
5547
5590
|
writeFileSync(options.outFile, result.output, "utf-8");
|
|
5548
5591
|
return result;
|
|
5549
5592
|
}
|
|
5550
5593
|
/**
|
|
5594
|
+
* Builds as compileToFile() does for `outFile`, without writing it: the output may sit
|
|
5595
|
+
* inside a source folder, so its last build is left out of the sources and modules and
|
|
5596
|
+
* never read back. The caller decides whether the result is written. With no `outFile`
|
|
5597
|
+
* (output to stdout, or none), this is compile().
|
|
5598
|
+
*
|
|
5599
|
+
* Internal, for the CLI; not part of the public API.
|
|
5600
|
+
*/
|
|
5601
|
+
async function compileForOutputFile(options, outFile) {
|
|
5602
|
+
return buildOutput(options, void 0, void 0, outFile);
|
|
5603
|
+
}
|
|
5604
|
+
/**
|
|
5551
5605
|
* Compile with incremental caching support.
|
|
5552
5606
|
* Plugins and advanced users can manage their own cache and changed-file tracking.
|
|
5607
|
+
*
|
|
5608
|
+
* Without `changedFiles`, a cached file is reused while its modification time is unchanged.
|
|
5609
|
+
* With it, a file it names (by the path source discovery gives it, relative to the working
|
|
5610
|
+
* directory) is always reparsed, whatever its modification time, and every other cached
|
|
5611
|
+
* file is reused as it is.
|
|
5553
5612
|
*/
|
|
5554
5613
|
async function compileIncremental(options, cache, changedFiles) {
|
|
5555
5614
|
return buildOutput(options, cache, changedFiles);
|
|
5556
5615
|
}
|
|
5557
5616
|
/**
|
|
5558
|
-
* Watch for file changes and recompile.
|
|
5617
|
+
* Watch for file changes and recompile. Every build is written to `outFile`, including
|
|
5618
|
+
* one whose diagnostics report errors.
|
|
5559
5619
|
*/
|
|
5560
5620
|
async function watch(options) {
|
|
5621
|
+
return watchWithWriteFilter(options, () => true);
|
|
5622
|
+
}
|
|
5623
|
+
/**
|
|
5624
|
+
* watch(), writing to `outFile` only the builds `shouldWrite` accepts. A rejected build
|
|
5625
|
+
* leaves the output file as it was; `onBuild` receives every build either way.
|
|
5626
|
+
*
|
|
5627
|
+
* Internal, for the CLI, which keeps the last good output when a rebuild reports errors;
|
|
5628
|
+
* not part of the public API.
|
|
5629
|
+
*/
|
|
5630
|
+
async function watchWithWriteFilter(options, shouldWrite) {
|
|
5561
5631
|
const controller = new AbortController();
|
|
5562
5632
|
const cache = /* @__PURE__ */ new Map();
|
|
5563
5633
|
const filePaths = options.sources.filter((s) => typeof s === "string");
|
|
@@ -5577,7 +5647,7 @@ async function watch(options) {
|
|
|
5577
5647
|
};
|
|
5578
5648
|
const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
|
|
5579
5649
|
buildOutput(options, cache, changedFiles, options.outFile).then((result) => {
|
|
5580
|
-
writeFileSync(options.outFile, result.output, "utf-8");
|
|
5650
|
+
if (shouldWrite(result)) writeFileSync(options.outFile, result.output, "utf-8");
|
|
5581
5651
|
options.onBuild?.(result);
|
|
5582
5652
|
}).catch((e) => {
|
|
5583
5653
|
options.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5633,7 +5703,7 @@ async function buildOutput(options, cache, changedFiles, outFile) {
|
|
|
5633
5703
|
switch (outputMode) {
|
|
5634
5704
|
case "twee3":
|
|
5635
5705
|
case "twee1":
|
|
5636
|
-
output = toTwee(story, outputMode);
|
|
5706
|
+
output = toTwee(story, outputMode, { addStoryData: Boolean(options.startPassage) || testMode });
|
|
5637
5707
|
break;
|
|
5638
5708
|
case "twine2-archive":
|
|
5639
5709
|
output = toTwine2Archive(story, startName, { sourceInfo });
|
|
@@ -6076,6 +6146,11 @@ function endOfRegularExpression(source, pos) {
|
|
|
6076
6146
|
}
|
|
6077
6147
|
//#endregion
|
|
6078
6148
|
//#region src/link-markup.ts
|
|
6149
|
+
/** Records that the reading has looked at the characters before `pos`, and returns `result`. */
|
|
6150
|
+
function readTo(reader, pos, result) {
|
|
6151
|
+
reader.scanned = Math.max(reader.scanned, pos);
|
|
6152
|
+
return result;
|
|
6153
|
+
}
|
|
6079
6154
|
/**
|
|
6080
6155
|
* Reads link or image markup that starts at `start` in `text`, or returns `undefined` where
|
|
6081
6156
|
* SugarCube rejects it (and so prints the text instead).
|
|
@@ -6083,15 +6158,17 @@ function endOfRegularExpression(source, pos) {
|
|
|
6083
6158
|
* `budget.left`, if given, is how many characters the reading may look at; it is reduced by as
|
|
6084
6159
|
* many as were read, and once it runs out, every reading fails. Unclosed markup is read to the end
|
|
6085
6160
|
* of its line, so reading every `[[` of a long line that never closes one would otherwise take
|
|
6086
|
-
* quadratic time.
|
|
6161
|
+
* quadratic time. A reading is charged for the characters it looked at, up to where it ended or
|
|
6162
|
+
* failed (a line feed, say), and no more, so markup left unclosed on short lines costs little.
|
|
6087
6163
|
*/
|
|
6088
6164
|
function readSquareBracketedMarkup(text, start, budget = { left: Infinity }) {
|
|
6089
6165
|
const reader = {
|
|
6090
6166
|
text,
|
|
6091
|
-
limit: Math.min(text.length, start + Math.max(budget.left, 0))
|
|
6167
|
+
limit: Math.min(text.length, start + Math.max(budget.left, 0)),
|
|
6168
|
+
scanned: start
|
|
6092
6169
|
};
|
|
6093
6170
|
const markup = readMarkup(reader, start);
|
|
6094
|
-
budget.left -= (markup?.end ??
|
|
6171
|
+
budget.left -= Math.max(reader.scanned, markup?.end ?? start) - start;
|
|
6095
6172
|
return markup;
|
|
6096
6173
|
}
|
|
6097
6174
|
function readMarkup(reader, start) {
|
|
@@ -6167,7 +6244,7 @@ function readCoreComponents(reader, from) {
|
|
|
6167
6244
|
const ch = text[pos];
|
|
6168
6245
|
pos += 1;
|
|
6169
6246
|
switch (ch) {
|
|
6170
|
-
case "\n": return;
|
|
6247
|
+
case "\n": return readTo(reader, pos, void 0);
|
|
6171
6248
|
case "\"": {
|
|
6172
6249
|
const end = readQuoted(reader, pos, "\"");
|
|
6173
6250
|
if (end === void 0) return;
|
|
@@ -6201,7 +6278,7 @@ function readCoreComponents(reader, from) {
|
|
|
6201
6278
|
case "]":
|
|
6202
6279
|
depth -= 1;
|
|
6203
6280
|
if (depth === 1) {
|
|
6204
|
-
const end = componentEnd(
|
|
6281
|
+
const end = componentEnd(reader, pos);
|
|
6205
6282
|
if (end === void 0) return;
|
|
6206
6283
|
return {
|
|
6207
6284
|
link: link ?? text.slice(componentStart, end.textEnd),
|
|
@@ -6210,6 +6287,7 @@ function readCoreComponents(reader, from) {
|
|
|
6210
6287
|
}
|
|
6211
6288
|
}
|
|
6212
6289
|
}
|
|
6290
|
+
return readTo(reader, pos, void 0);
|
|
6213
6291
|
}
|
|
6214
6292
|
/**
|
|
6215
6293
|
* Reads a component after `][` that holds no delimiter: an image's link (`setter` false) or a
|
|
@@ -6223,7 +6301,7 @@ function readComponent(reader, from, setter) {
|
|
|
6223
6301
|
const ch = text[pos];
|
|
6224
6302
|
pos += 1;
|
|
6225
6303
|
switch (ch) {
|
|
6226
|
-
case "\n": return;
|
|
6304
|
+
case "\n": return readTo(reader, pos, void 0);
|
|
6227
6305
|
case "\"":
|
|
6228
6306
|
case "'": {
|
|
6229
6307
|
if (ch === "'" && !setter) break;
|
|
@@ -6237,9 +6315,10 @@ function readComponent(reader, from, setter) {
|
|
|
6237
6315
|
break;
|
|
6238
6316
|
case "]":
|
|
6239
6317
|
depth -= 1;
|
|
6240
|
-
if (depth === 1) return componentEnd(
|
|
6318
|
+
if (depth === 1) return componentEnd(reader, pos);
|
|
6241
6319
|
}
|
|
6242
6320
|
}
|
|
6321
|
+
return readTo(reader, pos, void 0);
|
|
6243
6322
|
}
|
|
6244
6323
|
/** Reads a setter, the last component; returns the index after its closing `]]`. */
|
|
6245
6324
|
function readSetter(reader, from) {
|
|
@@ -6247,19 +6326,19 @@ function readSetter(reader, from) {
|
|
|
6247
6326
|
return end?.last === true ? end.next : void 0;
|
|
6248
6327
|
}
|
|
6249
6328
|
/** What the `]` just before `pos`, which brings the depth back to one, ends; `undefined` if malformed. */
|
|
6250
|
-
function componentEnd(
|
|
6251
|
-
switch (text[pos]) {
|
|
6252
|
-
case "[": return {
|
|
6329
|
+
function componentEnd(reader, pos) {
|
|
6330
|
+
switch (reader.text[pos]) {
|
|
6331
|
+
case "[": return readTo(reader, pos + 1, {
|
|
6253
6332
|
textEnd: pos - 1,
|
|
6254
6333
|
next: pos + 1,
|
|
6255
6334
|
last: false
|
|
6256
|
-
};
|
|
6257
|
-
case "]": return {
|
|
6335
|
+
});
|
|
6336
|
+
case "]": return readTo(reader, pos + 1, {
|
|
6258
6337
|
textEnd: pos - 1,
|
|
6259
6338
|
next: pos + 1,
|
|
6260
6339
|
last: true
|
|
6261
|
-
};
|
|
6262
|
-
default: return;
|
|
6340
|
+
});
|
|
6341
|
+
default: return readTo(reader, pos + 1, void 0);
|
|
6263
6342
|
}
|
|
6264
6343
|
}
|
|
6265
6344
|
/**
|
|
@@ -6274,12 +6353,13 @@ function readQuoted(reader, from, quote) {
|
|
|
6274
6353
|
const ch = text[pos];
|
|
6275
6354
|
pos += 1;
|
|
6276
6355
|
if (ch === quote) return pos;
|
|
6277
|
-
if (ch === "\n") return;
|
|
6356
|
+
if (ch === "\n") return readTo(reader, pos, void 0);
|
|
6278
6357
|
if (ch === "\\") {
|
|
6279
|
-
if (pos >= reader.limit || text[pos] === "\n") return;
|
|
6358
|
+
if (pos >= reader.limit || text[pos] === "\n") return readTo(reader, Math.min(pos + 1, reader.limit), void 0);
|
|
6280
6359
|
pos += 1;
|
|
6281
6360
|
}
|
|
6282
6361
|
}
|
|
6362
|
+
return readTo(reader, pos, void 0);
|
|
6283
6363
|
}
|
|
6284
6364
|
//#endregion
|
|
6285
6365
|
//#region src/sugarcube-macros.ts
|
|
@@ -7137,6 +7217,29 @@ function extractLinksFromText(text, isScript) {
|
|
|
7137
7217
|
].flatMap((via) => found.filter((link) => link.via === via).map((link) => link.passage));
|
|
7138
7218
|
return [...new Set(ordered)];
|
|
7139
7219
|
}
|
|
7220
|
+
/**
|
|
7221
|
+
* The passages that `p` links to. A script passage is JavaScript, read for links only in its
|
|
7222
|
+
* strings. A stylesheet is CSS, which the story puts in a style element and never wikifies, so it
|
|
7223
|
+
* links nowhere; a passage tagged both is a script, as Twine 2 output takes it.
|
|
7224
|
+
*/
|
|
7225
|
+
function passageLinks(p) {
|
|
7226
|
+
if (hasTag(p, "script")) return extractLinksFromText(textAsRead(p), true);
|
|
7227
|
+
if (hasTag(p, "stylesheet")) return [];
|
|
7228
|
+
return extractLinksFromText(textAsRead(p), false);
|
|
7229
|
+
}
|
|
7230
|
+
/**
|
|
7231
|
+
* Each passage name, mapped to why the `target` output leaves that passage out, or to `undefined`
|
|
7232
|
+
* when a link can reach it: always without a target, and otherwise when the output emits a
|
|
7233
|
+
* passage of that name.
|
|
7234
|
+
*/
|
|
7235
|
+
function destinationOmissions(story, { target }) {
|
|
7236
|
+
const omissions = /* @__PURE__ */ new Map();
|
|
7237
|
+
for (const p of story.passages) {
|
|
7238
|
+
const omission = target === void 0 ? void 0 : passageOmission(story, p, target);
|
|
7239
|
+
if (!omissions.has(p.name) || omission === void 0) omissions.set(p.name, omission);
|
|
7240
|
+
}
|
|
7241
|
+
return omissions;
|
|
7242
|
+
}
|
|
7140
7243
|
/** Passages SugarCube runs from their raw text, never joining their lines. */
|
|
7141
7244
|
const RAW_TEXT_PASSAGES = /* @__PURE__ */ new Set([
|
|
7142
7245
|
"StoryInit",
|
|
@@ -7178,9 +7281,14 @@ function joinLines(text) {
|
|
|
7178
7281
|
* expect(map.brokenLinks).toEqual([]);
|
|
7179
7282
|
* expect(map.deadEnds).not.toContain('Start');
|
|
7180
7283
|
* ```
|
|
7284
|
+
*
|
|
7285
|
+
* By default, a link is broken only when no passage has its name. Pass a `target` to check links
|
|
7286
|
+
* against what that output emits: `storyInspect(result.story, { target: 'twine2' })` also reports
|
|
7287
|
+
* links to passages Twine 2 output leaves out, such as script, stylesheet and `Twine.private`
|
|
7288
|
+
* passages.
|
|
7181
7289
|
*/
|
|
7182
|
-
function storyInspect(story) {
|
|
7183
|
-
const
|
|
7290
|
+
function storyInspect(story, options = {}) {
|
|
7291
|
+
const destinations = destinationOmissions(story, options);
|
|
7184
7292
|
const passages = [];
|
|
7185
7293
|
const storyPassages = [];
|
|
7186
7294
|
const infoPassages = [];
|
|
@@ -7202,14 +7310,24 @@ function storyInspect(story) {
|
|
|
7202
7310
|
}
|
|
7203
7311
|
list.push(p.name);
|
|
7204
7312
|
}
|
|
7205
|
-
|
|
7206
|
-
links.set(p.name, targets);
|
|
7313
|
+
links.set(p.name, passageLinks(p));
|
|
7207
7314
|
}
|
|
7208
7315
|
const brokenLinks = [];
|
|
7209
|
-
for (const [from, targets] of links) for (const to of targets)
|
|
7210
|
-
|
|
7211
|
-
|
|
7212
|
-
|
|
7316
|
+
for (const [from, targets] of links) for (const to of targets) {
|
|
7317
|
+
if (!destinations.has(to)) {
|
|
7318
|
+
brokenLinks.push({
|
|
7319
|
+
from,
|
|
7320
|
+
to
|
|
7321
|
+
});
|
|
7322
|
+
continue;
|
|
7323
|
+
}
|
|
7324
|
+
const omission = destinations.get(to);
|
|
7325
|
+
if (omission !== void 0) brokenLinks.push({
|
|
7326
|
+
from,
|
|
7327
|
+
to,
|
|
7328
|
+
omission
|
|
7329
|
+
});
|
|
7330
|
+
}
|
|
7213
7331
|
const deadEnds = [];
|
|
7214
7332
|
for (const name of storyPassages) {
|
|
7215
7333
|
const targets = links.get(name);
|
|
@@ -7239,14 +7357,26 @@ function storyInspect(story) {
|
|
|
7239
7357
|
/**
|
|
7240
7358
|
* Lint a story: compile without output rendering, then inspect structure.
|
|
7241
7359
|
* Uses JSON output mode internally to avoid format resolution, so it checks
|
|
7242
|
-
* the starting passage itself, against the Twine 2 passage rules.
|
|
7360
|
+
* the starting passage itself, against the Twine 2 passage rules. Link
|
|
7361
|
+
* destinations are checked against the same rules: a link to a passage that
|
|
7362
|
+
* Twine 2 output leaves out is broken.
|
|
7243
7363
|
*/
|
|
7244
7364
|
async function lint(options) {
|
|
7245
|
-
|
|
7365
|
+
return lintForOutputFile(options, void 0);
|
|
7366
|
+
}
|
|
7367
|
+
/**
|
|
7368
|
+
* lint() for a project whose builds are written to `outFile`: like a build for that file,
|
|
7369
|
+
* it leaves `outFile` out of the sources and modules, so an earlier build inside a source
|
|
7370
|
+
* folder is not linted as a source.
|
|
7371
|
+
*
|
|
7372
|
+
* Internal, for the CLI; not part of the public API.
|
|
7373
|
+
*/
|
|
7374
|
+
async function lintForOutputFile(options, outFile) {
|
|
7375
|
+
const result = await compileForOutputFile({
|
|
7246
7376
|
...options,
|
|
7247
7377
|
outputMode: "json"
|
|
7248
|
-
});
|
|
7249
|
-
const map = storyInspect(result.story);
|
|
7378
|
+
}, outFile);
|
|
7379
|
+
const map = storyInspect(result.story, { target: "twine2" });
|
|
7250
7380
|
return {
|
|
7251
7381
|
diagnostics: [...result.diagnostics, ...startPassageDiagnostics(result.story, map.start, "twine2")],
|
|
7252
7382
|
stats: result.stats,
|
|
@@ -7273,7 +7403,10 @@ function formatLintReport(result) {
|
|
|
7273
7403
|
if (result.brokenLinks.length > 0) {
|
|
7274
7404
|
lines.push("");
|
|
7275
7405
|
lines.push(`Broken links (${result.brokenLinks.length}):`);
|
|
7276
|
-
for (const link of result.brokenLinks)
|
|
7406
|
+
for (const link of result.brokenLinks) {
|
|
7407
|
+
const why = link.omission === void 0 ? "does not exist" : `${describeOmission(link.omission)}, so it is left out of the story data`;
|
|
7408
|
+
lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" ${why})`);
|
|
7409
|
+
}
|
|
7277
7410
|
}
|
|
7278
7411
|
if (result.deadEnds.length > 0) {
|
|
7279
7412
|
lines.push("");
|