@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/README.md
CHANGED
package/dist/bin/twee-ts.js
CHANGED
|
@@ -4627,14 +4627,16 @@ function parseDocument(data, options) {
|
|
|
4627
4627
|
*/
|
|
4628
4628
|
/**
|
|
4629
4629
|
* Parse a Twine 2 or Twine 1 compiled HTML file back into a Story model.
|
|
4630
|
+
* Tweego always trims passage text; here `trim: false` keeps it exactly, as for Twee sources.
|
|
4630
4631
|
*/
|
|
4631
|
-
function decompileHTML(html) {
|
|
4632
|
+
function decompileHTML(html, options = {}) {
|
|
4632
4633
|
const diagnostics = [];
|
|
4633
4634
|
const story = createStory();
|
|
4635
|
+
const passageText = options.trim === false ? (text) => text : (text) => text.trim();
|
|
4634
4636
|
const doc = parseDocument(html);
|
|
4635
4637
|
const twine2Data = findElement(doc, "tw-storydata");
|
|
4636
4638
|
if (twine2Data) {
|
|
4637
|
-
decompileTwine2(twine2Data, story, diagnostics);
|
|
4639
|
+
decompileTwine2(twine2Data, story, passageText, diagnostics);
|
|
4638
4640
|
return {
|
|
4639
4641
|
story,
|
|
4640
4642
|
diagnostics
|
|
@@ -4642,7 +4644,7 @@ function decompileHTML(html) {
|
|
|
4642
4644
|
}
|
|
4643
4645
|
const twine1Data = findElementByIdPattern(doc, /^store(?:-a|A)rea$/);
|
|
4644
4646
|
if (twine1Data) {
|
|
4645
|
-
decompileTwine1(twine1Data, story, diagnostics);
|
|
4647
|
+
decompileTwine1(twine1Data, story, passageText, diagnostics);
|
|
4646
4648
|
return {
|
|
4647
4649
|
story,
|
|
4648
4650
|
diagnostics
|
|
@@ -4657,7 +4659,7 @@ function decompileHTML(html) {
|
|
|
4657
4659
|
diagnostics
|
|
4658
4660
|
};
|
|
4659
4661
|
}
|
|
4660
|
-
function decompileTwine2(storyData, story, diagnostics) {
|
|
4662
|
+
function decompileTwine2(storyData, story, passageText, diagnostics) {
|
|
4661
4663
|
let startnode = 0;
|
|
4662
4664
|
const attrs = storyData.attribs ?? {};
|
|
4663
4665
|
if (attrs["name"]) story.name = attrs["name"];
|
|
@@ -4687,8 +4689,9 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4687
4689
|
switch (node.name) {
|
|
4688
4690
|
case "style":
|
|
4689
4691
|
case "script": {
|
|
4690
|
-
const
|
|
4691
|
-
if (
|
|
4692
|
+
const content = getTextContent(node);
|
|
4693
|
+
if (content.trim().length === 0) continue;
|
|
4694
|
+
const text = passageText(content);
|
|
4692
4695
|
storyAdd(story, {
|
|
4693
4696
|
name: node.name === "style" ? "Story Stylesheet" : "Story JavaScript",
|
|
4694
4697
|
tags: node.name === "style" ? ["stylesheet"] : ["script"],
|
|
@@ -4723,7 +4726,7 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4723
4726
|
const passage = {
|
|
4724
4727
|
name,
|
|
4725
4728
|
tags,
|
|
4726
|
-
text: getTextContent(node)
|
|
4729
|
+
text: passageText(getTextContent(node))
|
|
4727
4730
|
};
|
|
4728
4731
|
if (metadata.position || metadata.size) passage.metadata = metadata;
|
|
4729
4732
|
storyAdd(story, passage, diagnostics);
|
|
@@ -4737,8 +4740,8 @@ function decompileTwine2(storyData, story, diagnostics) {
|
|
|
4737
4740
|
text: marshalStoryData(story)
|
|
4738
4741
|
}, diagnostics);
|
|
4739
4742
|
}
|
|
4740
|
-
function decompileTwine1(storeArea, story, diagnostics) {
|
|
4741
|
-
const passages = (storeArea.children ?? []).filter(isTiddler).map(tiddlerToPassage);
|
|
4743
|
+
function decompileTwine1(storeArea, story, passageText, diagnostics) {
|
|
4744
|
+
const passages = (storeArea.children ?? []).filter(isTiddler).map((node) => tiddlerToPassage(node, passageText));
|
|
4742
4745
|
const obfuscated = isRot13Obfuscated(passages);
|
|
4743
4746
|
for (const p of passages) storyAdd(story, obfuscated && p.name !== "StorySettings" ? {
|
|
4744
4747
|
...p,
|
|
@@ -4748,12 +4751,12 @@ function decompileTwine1(storeArea, story, diagnostics) {
|
|
|
4748
4751
|
function isTiddler(node) {
|
|
4749
4752
|
return node.type === "tag" && node.name === "div" && node.attribs !== void 0 && "tiddler" in node.attribs;
|
|
4750
4753
|
}
|
|
4751
|
-
function tiddlerToPassage(node) {
|
|
4754
|
+
function tiddlerToPassage(node, passageText) {
|
|
4752
4755
|
const nodeAttrs = node.attribs ?? {};
|
|
4753
4756
|
const passage = {
|
|
4754
4757
|
name: nodeAttrs["tiddler"] ?? "",
|
|
4755
4758
|
tags: nodeAttrs["tags"] ? nodeAttrs["tags"].split(/\s+/).filter((s) => s.length > 0) : [],
|
|
4756
|
-
text: tiddlerUnescape(getTextContent(node))
|
|
4759
|
+
text: passageText(tiddlerUnescape(getTextContent(node)))
|
|
4757
4760
|
};
|
|
4758
4761
|
const position = nodeAttrs["twine-position"];
|
|
4759
4762
|
if (position) passage.metadata = { position };
|
|
@@ -4818,7 +4821,7 @@ function loadSources(story, filenames, opts, diagnostics, processedFiles) {
|
|
|
4818
4821
|
break;
|
|
4819
4822
|
case "htm":
|
|
4820
4823
|
case "html":
|
|
4821
|
-
loadHTML(story, filename, diagnostics);
|
|
4824
|
+
loadHTML(story, filename, opts, diagnostics);
|
|
4822
4825
|
break;
|
|
4823
4826
|
case "css":
|
|
4824
4827
|
loadTagged(story, "stylesheet", filename, diagnostics);
|
|
@@ -4972,8 +4975,8 @@ function parseFontFile(filename) {
|
|
|
4972
4975
|
diagnostics: []
|
|
4973
4976
|
};
|
|
4974
4977
|
}
|
|
4975
|
-
function parseHTMLFile(filename) {
|
|
4976
|
-
const { story, diagnostics } = decompileHTML(readUTF8(filename));
|
|
4978
|
+
function parseHTMLFile(filename, opts) {
|
|
4979
|
+
const { story, diagnostics } = decompileHTML(readUTF8(filename), { trim: opts.trim ?? true });
|
|
4977
4980
|
return {
|
|
4978
4981
|
passages: story.name !== "" && !storyHas(story, "StoryTitle") ? [{
|
|
4979
4982
|
name: "StoryTitle",
|
|
@@ -4985,14 +4988,17 @@ function parseHTMLFile(filename) {
|
|
|
4985
4988
|
}
|
|
4986
4989
|
/**
|
|
4987
4990
|
* Identity of the options that affect how a file parses, stored on its cache entry.
|
|
4988
|
-
*
|
|
4991
|
+
* Twee files depend on `trim` and `twee2Compat`, HTML files on `trim`; every other file type
|
|
4992
|
+
* yields the same key for all options.
|
|
4989
4993
|
*/
|
|
4990
4994
|
function parseOptionsKey(filename, opts) {
|
|
4991
4995
|
const ext = normalizedFileExt(filename);
|
|
4996
|
+
const trim = opts.trim ?? true;
|
|
4997
|
+
if (ext === "htm" || ext === "html") return JSON.stringify({ trim });
|
|
4992
4998
|
const twee2File = ext === "tw2" || ext === "twee2";
|
|
4993
4999
|
if (!twee2File && ext !== "tw" && ext !== "twee") return "";
|
|
4994
5000
|
return JSON.stringify({
|
|
4995
|
-
trim
|
|
5001
|
+
trim,
|
|
4996
5002
|
twee2Compat: twee2File || (opts.twee2Compat ?? false)
|
|
4997
5003
|
});
|
|
4998
5004
|
}
|
|
@@ -5006,7 +5012,7 @@ function parseFile(filename, opts) {
|
|
|
5006
5012
|
twee2Compat: true
|
|
5007
5013
|
});
|
|
5008
5014
|
case "htm":
|
|
5009
|
-
case "html": return parseHTMLFile(filename);
|
|
5015
|
+
case "html": return parseHTMLFile(filename, opts);
|
|
5010
5016
|
case "css": return parseTaggedFile("stylesheet", filename);
|
|
5011
5017
|
case "js": return parseTaggedFile("script", filename);
|
|
5012
5018
|
case "otf":
|
|
@@ -5038,8 +5044,8 @@ function parseFile(filename, opts) {
|
|
|
5038
5044
|
default: return;
|
|
5039
5045
|
}
|
|
5040
5046
|
}
|
|
5041
|
-
function loadHTML(story, filename, diagnostics) {
|
|
5042
|
-
const result = parseHTMLFile(filename);
|
|
5047
|
+
function loadHTML(story, filename, opts, diagnostics) {
|
|
5048
|
+
const result = parseHTMLFile(filename, opts);
|
|
5043
5049
|
diagnostics.push(...result.diagnostics);
|
|
5044
5050
|
for (const p of result.passages) storyAdd(story, p, diagnostics);
|
|
5045
5051
|
}
|
|
@@ -5078,7 +5084,8 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5078
5084
|
const optionsKey = parseOptionsKey(filename, opts);
|
|
5079
5085
|
const entry = cache.get(filename);
|
|
5080
5086
|
const cached = entry?.parseOptionsKey === optionsKey ? entry : void 0;
|
|
5081
|
-
|
|
5087
|
+
const changed = changedFiles?.has(filename) ?? false;
|
|
5088
|
+
if (changedFiles && cached && !changed) {
|
|
5082
5089
|
diagnostics.push(...cached.diagnostics);
|
|
5083
5090
|
for (const p of cached.passages) storyAdd(story, p, diagnostics);
|
|
5084
5091
|
processedFiles.add(filename);
|
|
@@ -5090,7 +5097,7 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
5090
5097
|
} catch {
|
|
5091
5098
|
continue;
|
|
5092
5099
|
}
|
|
5093
|
-
if (cached && cached.mtimeMs === mtimeMs) {
|
|
5100
|
+
if (cached && !changed && cached.mtimeMs === mtimeMs) {
|
|
5094
5101
|
diagnostics.push(...cached.diagnostics);
|
|
5095
5102
|
for (const p of cached.passages) storyAdd(story, p, diagnostics);
|
|
5096
5103
|
processedFiles.add(filename);
|
|
@@ -5338,10 +5345,39 @@ function tryReplaceComponent(template, placeholder, componentPath, required) {
|
|
|
5338
5345
|
}
|
|
5339
5346
|
//#endregion
|
|
5340
5347
|
//#region src/output-twee.ts
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5348
|
+
/**
|
|
5349
|
+
* Serialize the story as Twee. The StoryData passage is written from the story model rather than
|
|
5350
|
+
* from its loaded text, so changes made after loading (a start passage override, test mode) are kept.
|
|
5351
|
+
* Tweego writes the loaded text; its own normalization makes the two the same for an unchanged story.
|
|
5352
|
+
*
|
|
5353
|
+
* `addStoryData`: add a StoryData passage when the story has none, so that StoryData fields the
|
|
5354
|
+
* compile options set are recorded. Default: false.
|
|
5355
|
+
*/
|
|
5356
|
+
function toTwee(story, outMode, options = {}) {
|
|
5357
|
+
return withEffectiveStoryData(story, options.addStoryData ?? false).map((p) => passageToTwee(p, outMode)).join("");
|
|
5358
|
+
}
|
|
5359
|
+
function withEffectiveStoryData(story, addStoryData) {
|
|
5360
|
+
const storyData = {
|
|
5361
|
+
name: "StoryData",
|
|
5362
|
+
tags: [],
|
|
5363
|
+
text: marshalStoryData(story)
|
|
5364
|
+
};
|
|
5365
|
+
const index = story.passages.findIndex((p) => p.name === "StoryData");
|
|
5366
|
+
if (index !== -1) return story.passages.map((p, i) => i === index && isStoryDataJSON(p.text) ? {
|
|
5367
|
+
...p,
|
|
5368
|
+
text: storyData.text
|
|
5369
|
+
} : p);
|
|
5370
|
+
if (!addStoryData) return story.passages;
|
|
5371
|
+
const at = story.passages[0]?.name === "StoryTitle" ? 1 : 0;
|
|
5372
|
+
return [
|
|
5373
|
+
...story.passages.slice(0, at),
|
|
5374
|
+
storyData,
|
|
5375
|
+
...story.passages.slice(at)
|
|
5376
|
+
];
|
|
5377
|
+
}
|
|
5378
|
+
/** Whether the text loaded as StoryData. Text that did not is kept as written, for the author to fix. */
|
|
5379
|
+
function isStoryDataJSON(text) {
|
|
5380
|
+
return unmarshalStoryData(createStory(), text) === null;
|
|
5345
5381
|
}
|
|
5346
5382
|
//#endregion
|
|
5347
5383
|
//#region src/modules.ts
|
|
@@ -5413,7 +5449,19 @@ function modifyHead(html, modulePaths, headFile, diagnostics) {
|
|
|
5413
5449
|
return html;
|
|
5414
5450
|
}
|
|
5415
5451
|
//#endregion
|
|
5416
|
-
//#region src/
|
|
5452
|
+
//#region src/passage-omission.ts
|
|
5453
|
+
/**
|
|
5454
|
+
* Why the `target` output leaves `passage` out of its passage elements, or `undefined` when it
|
|
5455
|
+
* emits the passage (and so a link or the start can name it).
|
|
5456
|
+
*/
|
|
5457
|
+
function passageOmission(story, passage, target) {
|
|
5458
|
+
switch (target) {
|
|
5459
|
+
case "twine2": return twine2PassageOmission(story, passage);
|
|
5460
|
+
case "twine1": return twine1PassageOmission(passage);
|
|
5461
|
+
default: throw new Error(`unhandled passage output target: ${String(target)}`);
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
/** Why a passage is left out, as a predicate: `Passage "X" ${describeOmission(o)}`. */
|
|
5417
5465
|
function describeOmission(omission) {
|
|
5418
5466
|
switch (omission.kind) {
|
|
5419
5467
|
case "special-name": return `is the special "${omission.name}" passage`;
|
|
@@ -5422,13 +5470,8 @@ function describeOmission(omission) {
|
|
|
5422
5470
|
default: throw new Error(`unhandled passage omission: ${JSON.stringify(omission)}`);
|
|
5423
5471
|
}
|
|
5424
5472
|
}
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
case "twine2": return twine2PassageOmission(story, passage);
|
|
5428
|
-
case "twine1": return twine1PassageOmission(passage);
|
|
5429
|
-
default: throw new Error(`unhandled start passage target: ${String(target)}`);
|
|
5430
|
-
}
|
|
5431
|
-
}
|
|
5473
|
+
//#endregion
|
|
5474
|
+
//#region src/start-passage.ts
|
|
5432
5475
|
/**
|
|
5433
5476
|
* Check that `startName` names a passage the `target` output emits.
|
|
5434
5477
|
* Returns the error diagnostics to report (empty when the start is usable).
|
|
@@ -5439,7 +5482,7 @@ function startPassageDiagnostics(story, startName, target) {
|
|
|
5439
5482
|
level: "error",
|
|
5440
5483
|
message: `Starting passage "${startName}" not found.`
|
|
5441
5484
|
}];
|
|
5442
|
-
const omission =
|
|
5485
|
+
const omission = passageOmission(story, passage, target);
|
|
5443
5486
|
if (omission === void 0) return [];
|
|
5444
5487
|
return [{
|
|
5445
5488
|
level: "error",
|
|
@@ -5465,15 +5508,24 @@ var TweeTsError = class extends Error {
|
|
|
5465
5508
|
}
|
|
5466
5509
|
};
|
|
5467
5510
|
/**
|
|
5468
|
-
*
|
|
5511
|
+
* Builds as compileToFile() does for `outFile`, without writing it: the output may sit
|
|
5512
|
+
* inside a source folder, so its last build is left out of the sources and modules and
|
|
5513
|
+
* never read back. The caller decides whether the result is written. With no `outFile`
|
|
5514
|
+
* (output to stdout, or none), this is compile().
|
|
5515
|
+
*
|
|
5516
|
+
* Internal, for the CLI; not part of the public API.
|
|
5469
5517
|
*/
|
|
5470
|
-
async function
|
|
5471
|
-
return buildOutput(options);
|
|
5518
|
+
async function compileForOutputFile(options, outFile) {
|
|
5519
|
+
return buildOutput(options, void 0, void 0, outFile);
|
|
5472
5520
|
}
|
|
5473
5521
|
/**
|
|
5474
|
-
*
|
|
5522
|
+
* watch(), writing to `outFile` only the builds `shouldWrite` accepts. A rejected build
|
|
5523
|
+
* leaves the output file as it was; `onBuild` receives every build either way.
|
|
5524
|
+
*
|
|
5525
|
+
* Internal, for the CLI, which keeps the last good output when a rebuild reports errors;
|
|
5526
|
+
* not part of the public API.
|
|
5475
5527
|
*/
|
|
5476
|
-
async function
|
|
5528
|
+
async function watchWithWriteFilter(options, shouldWrite) {
|
|
5477
5529
|
const controller = new AbortController();
|
|
5478
5530
|
const cache = /* @__PURE__ */ new Map();
|
|
5479
5531
|
const filePaths = options.sources.filter((s) => typeof s === "string");
|
|
@@ -5493,7 +5545,7 @@ async function watch$1(options) {
|
|
|
5493
5545
|
};
|
|
5494
5546
|
const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
|
|
5495
5547
|
buildOutput(options, cache, changedFiles, options.outFile).then((result) => {
|
|
5496
|
-
writeFileSync(options.outFile, result.output, "utf-8");
|
|
5548
|
+
if (shouldWrite(result)) writeFileSync(options.outFile, result.output, "utf-8");
|
|
5497
5549
|
options.onBuild?.(result);
|
|
5498
5550
|
}).catch((e) => {
|
|
5499
5551
|
options.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
@@ -5549,7 +5601,7 @@ async function buildOutput(options, cache, changedFiles, outFile) {
|
|
|
5549
5601
|
switch (outputMode) {
|
|
5550
5602
|
case "twee3":
|
|
5551
5603
|
case "twee1":
|
|
5552
|
-
output = toTwee(story, outputMode);
|
|
5604
|
+
output = toTwee(story, outputMode, { addStoryData: Boolean(options.startPassage) || testMode });
|
|
5553
5605
|
break;
|
|
5554
5606
|
case "twine2-archive":
|
|
5555
5607
|
output = toTwine2Archive(story, startName, { sourceInfo });
|
|
@@ -5992,6 +6044,11 @@ function endOfRegularExpression(source, pos) {
|
|
|
5992
6044
|
}
|
|
5993
6045
|
//#endregion
|
|
5994
6046
|
//#region src/link-markup.ts
|
|
6047
|
+
/** Records that the reading has looked at the characters before `pos`, and returns `result`. */
|
|
6048
|
+
function readTo(reader, pos, result) {
|
|
6049
|
+
reader.scanned = Math.max(reader.scanned, pos);
|
|
6050
|
+
return result;
|
|
6051
|
+
}
|
|
5995
6052
|
/**
|
|
5996
6053
|
* Reads link or image markup that starts at `start` in `text`, or returns `undefined` where
|
|
5997
6054
|
* SugarCube rejects it (and so prints the text instead).
|
|
@@ -5999,15 +6056,17 @@ function endOfRegularExpression(source, pos) {
|
|
|
5999
6056
|
* `budget.left`, if given, is how many characters the reading may look at; it is reduced by as
|
|
6000
6057
|
* many as were read, and once it runs out, every reading fails. Unclosed markup is read to the end
|
|
6001
6058
|
* of its line, so reading every `[[` of a long line that never closes one would otherwise take
|
|
6002
|
-
* quadratic time.
|
|
6059
|
+
* quadratic time. A reading is charged for the characters it looked at, up to where it ended or
|
|
6060
|
+
* failed (a line feed, say), and no more, so markup left unclosed on short lines costs little.
|
|
6003
6061
|
*/
|
|
6004
6062
|
function readSquareBracketedMarkup(text, start, budget = { left: Infinity }) {
|
|
6005
6063
|
const reader = {
|
|
6006
6064
|
text,
|
|
6007
|
-
limit: Math.min(text.length, start + Math.max(budget.left, 0))
|
|
6065
|
+
limit: Math.min(text.length, start + Math.max(budget.left, 0)),
|
|
6066
|
+
scanned: start
|
|
6008
6067
|
};
|
|
6009
6068
|
const markup = readMarkup(reader, start);
|
|
6010
|
-
budget.left -= (markup?.end ??
|
|
6069
|
+
budget.left -= Math.max(reader.scanned, markup?.end ?? start) - start;
|
|
6011
6070
|
return markup;
|
|
6012
6071
|
}
|
|
6013
6072
|
function readMarkup(reader, start) {
|
|
@@ -6083,7 +6142,7 @@ function readCoreComponents(reader, from) {
|
|
|
6083
6142
|
const ch = text[pos];
|
|
6084
6143
|
pos += 1;
|
|
6085
6144
|
switch (ch) {
|
|
6086
|
-
case "\n": return;
|
|
6145
|
+
case "\n": return readTo(reader, pos, void 0);
|
|
6087
6146
|
case "\"": {
|
|
6088
6147
|
const end = readQuoted(reader, pos, "\"");
|
|
6089
6148
|
if (end === void 0) return;
|
|
@@ -6117,7 +6176,7 @@ function readCoreComponents(reader, from) {
|
|
|
6117
6176
|
case "]":
|
|
6118
6177
|
depth -= 1;
|
|
6119
6178
|
if (depth === 1) {
|
|
6120
|
-
const end = componentEnd(
|
|
6179
|
+
const end = componentEnd(reader, pos);
|
|
6121
6180
|
if (end === void 0) return;
|
|
6122
6181
|
return {
|
|
6123
6182
|
link: link ?? text.slice(componentStart, end.textEnd),
|
|
@@ -6126,6 +6185,7 @@ function readCoreComponents(reader, from) {
|
|
|
6126
6185
|
}
|
|
6127
6186
|
}
|
|
6128
6187
|
}
|
|
6188
|
+
return readTo(reader, pos, void 0);
|
|
6129
6189
|
}
|
|
6130
6190
|
/**
|
|
6131
6191
|
* Reads a component after `][` that holds no delimiter: an image's link (`setter` false) or a
|
|
@@ -6139,7 +6199,7 @@ function readComponent(reader, from, setter) {
|
|
|
6139
6199
|
const ch = text[pos];
|
|
6140
6200
|
pos += 1;
|
|
6141
6201
|
switch (ch) {
|
|
6142
|
-
case "\n": return;
|
|
6202
|
+
case "\n": return readTo(reader, pos, void 0);
|
|
6143
6203
|
case "\"":
|
|
6144
6204
|
case "'": {
|
|
6145
6205
|
if (ch === "'" && !setter) break;
|
|
@@ -6153,9 +6213,10 @@ function readComponent(reader, from, setter) {
|
|
|
6153
6213
|
break;
|
|
6154
6214
|
case "]":
|
|
6155
6215
|
depth -= 1;
|
|
6156
|
-
if (depth === 1) return componentEnd(
|
|
6216
|
+
if (depth === 1) return componentEnd(reader, pos);
|
|
6157
6217
|
}
|
|
6158
6218
|
}
|
|
6219
|
+
return readTo(reader, pos, void 0);
|
|
6159
6220
|
}
|
|
6160
6221
|
/** Reads a setter, the last component; returns the index after its closing `]]`. */
|
|
6161
6222
|
function readSetter(reader, from) {
|
|
@@ -6163,19 +6224,19 @@ function readSetter(reader, from) {
|
|
|
6163
6224
|
return end?.last === true ? end.next : void 0;
|
|
6164
6225
|
}
|
|
6165
6226
|
/** What the `]` just before `pos`, which brings the depth back to one, ends; `undefined` if malformed. */
|
|
6166
|
-
function componentEnd(
|
|
6167
|
-
switch (text[pos]) {
|
|
6168
|
-
case "[": return {
|
|
6227
|
+
function componentEnd(reader, pos) {
|
|
6228
|
+
switch (reader.text[pos]) {
|
|
6229
|
+
case "[": return readTo(reader, pos + 1, {
|
|
6169
6230
|
textEnd: pos - 1,
|
|
6170
6231
|
next: pos + 1,
|
|
6171
6232
|
last: false
|
|
6172
|
-
};
|
|
6173
|
-
case "]": return {
|
|
6233
|
+
});
|
|
6234
|
+
case "]": return readTo(reader, pos + 1, {
|
|
6174
6235
|
textEnd: pos - 1,
|
|
6175
6236
|
next: pos + 1,
|
|
6176
6237
|
last: true
|
|
6177
|
-
};
|
|
6178
|
-
default: return;
|
|
6238
|
+
});
|
|
6239
|
+
default: return readTo(reader, pos + 1, void 0);
|
|
6179
6240
|
}
|
|
6180
6241
|
}
|
|
6181
6242
|
/**
|
|
@@ -6190,12 +6251,13 @@ function readQuoted(reader, from, quote) {
|
|
|
6190
6251
|
const ch = text[pos];
|
|
6191
6252
|
pos += 1;
|
|
6192
6253
|
if (ch === quote) return pos;
|
|
6193
|
-
if (ch === "\n") return;
|
|
6254
|
+
if (ch === "\n") return readTo(reader, pos, void 0);
|
|
6194
6255
|
if (ch === "\\") {
|
|
6195
|
-
if (pos >= reader.limit || text[pos] === "\n") return;
|
|
6256
|
+
if (pos >= reader.limit || text[pos] === "\n") return readTo(reader, Math.min(pos + 1, reader.limit), void 0);
|
|
6196
6257
|
pos += 1;
|
|
6197
6258
|
}
|
|
6198
6259
|
}
|
|
6260
|
+
return readTo(reader, pos, void 0);
|
|
6199
6261
|
}
|
|
6200
6262
|
//#endregion
|
|
6201
6263
|
//#region src/sugarcube-macros.ts
|
|
@@ -7053,6 +7115,29 @@ function extractLinksFromText(text, isScript) {
|
|
|
7053
7115
|
].flatMap((via) => found.filter((link) => link.via === via).map((link) => link.passage));
|
|
7054
7116
|
return [...new Set(ordered)];
|
|
7055
7117
|
}
|
|
7118
|
+
/**
|
|
7119
|
+
* The passages that `p` links to. A script passage is JavaScript, read for links only in its
|
|
7120
|
+
* strings. A stylesheet is CSS, which the story puts in a style element and never wikifies, so it
|
|
7121
|
+
* links nowhere; a passage tagged both is a script, as Twine 2 output takes it.
|
|
7122
|
+
*/
|
|
7123
|
+
function passageLinks(p) {
|
|
7124
|
+
if (hasTag(p, "script")) return extractLinksFromText(textAsRead(p), true);
|
|
7125
|
+
if (hasTag(p, "stylesheet")) return [];
|
|
7126
|
+
return extractLinksFromText(textAsRead(p), false);
|
|
7127
|
+
}
|
|
7128
|
+
/**
|
|
7129
|
+
* Each passage name, mapped to why the `target` output leaves that passage out, or to `undefined`
|
|
7130
|
+
* when a link can reach it: always without a target, and otherwise when the output emits a
|
|
7131
|
+
* passage of that name.
|
|
7132
|
+
*/
|
|
7133
|
+
function destinationOmissions(story, { target }) {
|
|
7134
|
+
const omissions = /* @__PURE__ */ new Map();
|
|
7135
|
+
for (const p of story.passages) {
|
|
7136
|
+
const omission = target === void 0 ? void 0 : passageOmission(story, p, target);
|
|
7137
|
+
if (!omissions.has(p.name) || omission === void 0) omissions.set(p.name, omission);
|
|
7138
|
+
}
|
|
7139
|
+
return omissions;
|
|
7140
|
+
}
|
|
7056
7141
|
/** Passages SugarCube runs from their raw text, never joining their lines. */
|
|
7057
7142
|
const RAW_TEXT_PASSAGES = /* @__PURE__ */ new Set([
|
|
7058
7143
|
"StoryInit",
|
|
@@ -7094,9 +7179,14 @@ function joinLines(text) {
|
|
|
7094
7179
|
* expect(map.brokenLinks).toEqual([]);
|
|
7095
7180
|
* expect(map.deadEnds).not.toContain('Start');
|
|
7096
7181
|
* ```
|
|
7182
|
+
*
|
|
7183
|
+
* By default, a link is broken only when no passage has its name. Pass a `target` to check links
|
|
7184
|
+
* against what that output emits: `storyInspect(result.story, { target: 'twine2' })` also reports
|
|
7185
|
+
* links to passages Twine 2 output leaves out, such as script, stylesheet and `Twine.private`
|
|
7186
|
+
* passages.
|
|
7097
7187
|
*/
|
|
7098
|
-
function storyInspect(story) {
|
|
7099
|
-
const
|
|
7188
|
+
function storyInspect(story, options = {}) {
|
|
7189
|
+
const destinations = destinationOmissions(story, options);
|
|
7100
7190
|
const passages = [];
|
|
7101
7191
|
const storyPassages = [];
|
|
7102
7192
|
const infoPassages = [];
|
|
@@ -7118,14 +7208,24 @@ function storyInspect(story) {
|
|
|
7118
7208
|
}
|
|
7119
7209
|
list.push(p.name);
|
|
7120
7210
|
}
|
|
7121
|
-
|
|
7122
|
-
links.set(p.name, targets);
|
|
7211
|
+
links.set(p.name, passageLinks(p));
|
|
7123
7212
|
}
|
|
7124
7213
|
const brokenLinks = [];
|
|
7125
|
-
for (const [from, targets] of links) for (const to of targets)
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7214
|
+
for (const [from, targets] of links) for (const to of targets) {
|
|
7215
|
+
if (!destinations.has(to)) {
|
|
7216
|
+
brokenLinks.push({
|
|
7217
|
+
from,
|
|
7218
|
+
to
|
|
7219
|
+
});
|
|
7220
|
+
continue;
|
|
7221
|
+
}
|
|
7222
|
+
const omission = destinations.get(to);
|
|
7223
|
+
if (omission !== void 0) brokenLinks.push({
|
|
7224
|
+
from,
|
|
7225
|
+
to,
|
|
7226
|
+
omission
|
|
7227
|
+
});
|
|
7228
|
+
}
|
|
7129
7229
|
const deadEnds = [];
|
|
7130
7230
|
for (const name of storyPassages) {
|
|
7131
7231
|
const targets = links.get(name);
|
|
@@ -7153,16 +7253,18 @@ function storyInspect(story) {
|
|
|
7153
7253
|
//#endregion
|
|
7154
7254
|
//#region src/lint.ts
|
|
7155
7255
|
/**
|
|
7156
|
-
*
|
|
7157
|
-
*
|
|
7158
|
-
*
|
|
7256
|
+
* lint() for a project whose builds are written to `outFile`: like a build for that file,
|
|
7257
|
+
* it leaves `outFile` out of the sources and modules, so an earlier build inside a source
|
|
7258
|
+
* folder is not linted as a source.
|
|
7259
|
+
*
|
|
7260
|
+
* Internal, for the CLI; not part of the public API.
|
|
7159
7261
|
*/
|
|
7160
|
-
async function
|
|
7161
|
-
const result = await
|
|
7262
|
+
async function lintForOutputFile(options, outFile) {
|
|
7263
|
+
const result = await compileForOutputFile({
|
|
7162
7264
|
...options,
|
|
7163
7265
|
outputMode: "json"
|
|
7164
|
-
});
|
|
7165
|
-
const map = storyInspect(result.story);
|
|
7266
|
+
}, outFile);
|
|
7267
|
+
const map = storyInspect(result.story, { target: "twine2" });
|
|
7166
7268
|
return {
|
|
7167
7269
|
diagnostics: [...result.diagnostics, ...startPassageDiagnostics(result.story, map.start, "twine2")],
|
|
7168
7270
|
stats: result.stats,
|
|
@@ -7189,7 +7291,10 @@ function formatLintReport(result) {
|
|
|
7189
7291
|
if (result.brokenLinks.length > 0) {
|
|
7190
7292
|
lines.push("");
|
|
7191
7293
|
lines.push(`Broken links (${result.brokenLinks.length}):`);
|
|
7192
|
-
for (const link of result.brokenLinks)
|
|
7294
|
+
for (const link of result.brokenLinks) {
|
|
7295
|
+
const why = link.omission === void 0 ? "does not exist" : `${describeOmission(link.omission)}, so it is left out of the story data`;
|
|
7296
|
+
lines.push(` ${link.from} -> ${link.to} (passage "${link.to}" ${why})`);
|
|
7297
|
+
}
|
|
7193
7298
|
}
|
|
7194
7299
|
if (result.deadEnds.length > 0) {
|
|
7195
7300
|
lines.push("");
|
|
@@ -7472,8 +7577,10 @@ async function main() {
|
|
|
7472
7577
|
}
|
|
7473
7578
|
return raw;
|
|
7474
7579
|
})();
|
|
7580
|
+
const outFile = values.output ?? config?.output ?? "-";
|
|
7581
|
+
const outPath = outFile === "-" ? void 0 : outFile;
|
|
7475
7582
|
if (values.lint) {
|
|
7476
|
-
const lintResult = await
|
|
7583
|
+
const lintResult = await lintForOutputFile({
|
|
7477
7584
|
sources,
|
|
7478
7585
|
exclude: values.exclude ?? config?.exclude,
|
|
7479
7586
|
formatId: values.format ?? config?.formatId,
|
|
@@ -7490,7 +7597,7 @@ async function main() {
|
|
|
7490
7597
|
tagAliases,
|
|
7491
7598
|
sourceInfo: values["source-info"] ?? config?.sourceInfo ?? false,
|
|
7492
7599
|
wordCountMethod
|
|
7493
|
-
});
|
|
7600
|
+
}, outPath);
|
|
7494
7601
|
console.log(formatLintReport(lintResult));
|
|
7495
7602
|
const hasErrors = lintResult.brokenLinks.length > 0 || lintResult.diagnostics.some((d) => d.level === "error");
|
|
7496
7603
|
process.exit(hasErrors ? 1 : 0);
|
|
@@ -7515,36 +7622,35 @@ async function main() {
|
|
|
7515
7622
|
sourceInfo: values["source-info"] ?? config?.sourceInfo ?? false,
|
|
7516
7623
|
wordCountMethod
|
|
7517
7624
|
};
|
|
7518
|
-
const outFile = values.output ?? config?.output ?? "-";
|
|
7519
7625
|
if (values.watch) {
|
|
7520
|
-
if (
|
|
7626
|
+
if (outPath === void 0) {
|
|
7521
7627
|
console.error("Error: Watch mode requires an output file (-o).");
|
|
7522
7628
|
process.exit(1);
|
|
7523
7629
|
}
|
|
7524
7630
|
console.log("Watch mode started. Press CTRL+C to stop.");
|
|
7525
|
-
await
|
|
7631
|
+
await watchWithWriteFilter({
|
|
7526
7632
|
...compileOptions,
|
|
7527
|
-
outFile,
|
|
7633
|
+
outFile: outPath,
|
|
7528
7634
|
onBuild(result) {
|
|
7529
7635
|
console.log(`Built: ${result.stats.passages} passages, ${result.stats.words} words`);
|
|
7530
7636
|
logDiagnostics(result.diagnostics);
|
|
7531
7637
|
const errors = countErrors(result.diagnostics);
|
|
7532
|
-
if (errors > 0) console.error(`Build has ${pluralize(errors, "error")};
|
|
7638
|
+
if (errors > 0) console.error(`Build has ${pluralize(errors, "error")}; output not written. Still watching for changes.`);
|
|
7533
7639
|
},
|
|
7534
7640
|
onError(error) {
|
|
7535
7641
|
console.error(`Build error: ${error.message}`);
|
|
7536
7642
|
}
|
|
7537
|
-
});
|
|
7643
|
+
}, (result) => countErrors(result.diagnostics) === 0);
|
|
7538
7644
|
} else {
|
|
7539
|
-
const result = await
|
|
7645
|
+
const result = await compileForOutputFile(compileOptions, outPath);
|
|
7540
7646
|
logDiagnostics(result.diagnostics);
|
|
7541
7647
|
const errors = countErrors(result.diagnostics);
|
|
7542
7648
|
if (errors > 0) {
|
|
7543
7649
|
console.error(`Compilation failed with ${pluralize(errors, "error")}; output not written.`);
|
|
7544
7650
|
process.exitCode = 1;
|
|
7545
|
-
} else if (
|
|
7546
|
-
else writeFileSync(
|
|
7547
|
-
if (
|
|
7651
|
+
} else if (outPath === void 0) process.stdout.write(result.output);
|
|
7652
|
+
else writeFileSync(outPath, result.output, "utf-8");
|
|
7653
|
+
if (outPath !== void 0 && values["log-files"]) console.log(`\nFiles: ${result.stats.files.join(", ")}`);
|
|
7548
7654
|
if (values["log-stats"]) logStats(result);
|
|
7549
7655
|
}
|
|
7550
7656
|
}
|