@rohal12/twee-ts 1.15.3 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/twee-ts.js +1177 -14
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/{compiler-Bfi1w-to.js → compiler-BiDNwR9q.js} +22 -6
- package/dist/compiler-BiDNwR9q.js.map +1 -0
- package/dist/index.cjs +1192 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1170 -14
- package/dist/index.js.map +1 -1
- package/dist/plugins/rollup.d.ts +1 -1
- package/dist/plugins/rollup.js +1 -1
- package/dist/plugins/vite.d.ts +1 -1
- package/dist/plugins/vite.js +62 -12
- package/dist/plugins/vite.js.map +1 -1
- package/dist/{types-BFyb40Zq.d.ts → types-dR5vTVu2.d.ts} +5 -0
- package/package.json +1 -1
- package/schemas/twee-ts.config.schema.json +5 -0
- package/dist/compiler-Bfi1w-to.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
2
24
|
let node_fs = require("node:fs");
|
|
3
|
-
let node_crypto = require("node:crypto");
|
|
4
25
|
let node_path = require("node:path");
|
|
26
|
+
node_path = __toESM(node_path, 1);
|
|
27
|
+
let node_crypto = require("node:crypto");
|
|
5
28
|
let node_os = require("node:os");
|
|
6
29
|
let node_url = require("node:url");
|
|
7
30
|
//#region src/ifid.ts
|
|
@@ -632,11 +655,25 @@ function fontFormatHint(ext) {
|
|
|
632
655
|
* Ported from filesystem.go.
|
|
633
656
|
*/
|
|
634
657
|
/**
|
|
658
|
+
* Whether `filename` matches one of the `exclude` globs. The globs are matched
|
|
659
|
+
* against the file's path relative to the working directory, the path
|
|
660
|
+
* getFilenames reports; an absolute `filename` is made relative first. A leading
|
|
661
|
+
* `./` in a glob is dropped. Matching is Node's `path.matchesGlob`.
|
|
662
|
+
*/
|
|
663
|
+
function isExcluded(filename, exclude) {
|
|
664
|
+
if (exclude.length === 0) return false;
|
|
665
|
+
const { matchesGlob } = node_path;
|
|
666
|
+
if (typeof matchesGlob !== "function") throw new Error(`The exclude option needs Node.js 22.5 or newer (found ${process.version}).`);
|
|
667
|
+
const rel = (0, node_path.relative)(process.cwd(), (0, node_path.resolve)(filename)) || filename;
|
|
668
|
+
return exclude.some((pattern) => matchesGlob(rel, pattern.replace(/^\.\//, "")));
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
635
671
|
* Recursively walk directories, collecting regular file paths.
|
|
636
|
-
* Filters out the output file to prevent circular compilation
|
|
672
|
+
* Filters out the output file to prevent circular compilation, and the files
|
|
673
|
+
* that match an `exclude` glob (see isExcluded).
|
|
637
674
|
* Like Tweego, a path that cannot be read is reported as a warning and skipped.
|
|
638
675
|
*/
|
|
639
|
-
function getFilenames(pathnames, outFilename) {
|
|
676
|
+
function getFilenames(pathnames, outFilename, exclude = []) {
|
|
640
677
|
const filenames = [];
|
|
641
678
|
const diagnostics = [];
|
|
642
679
|
const absOutFile = outFilename ? (0, node_path.resolve)(outFilename) : "";
|
|
@@ -658,6 +695,7 @@ function getFilenames(pathnames, outFilename) {
|
|
|
658
695
|
const abs = (0, node_path.resolve)(pathname);
|
|
659
696
|
if (abs === absOutFile) return;
|
|
660
697
|
const rel = (0, node_path.relative)(process.cwd(), abs);
|
|
698
|
+
if (isExcluded(rel || abs, exclude)) return;
|
|
661
699
|
filenames.push(rel || abs);
|
|
662
700
|
} else if (stat.isDirectory()) {
|
|
663
701
|
let entries;
|
|
@@ -678,9 +716,11 @@ function getFilenames(pathnames, outFilename) {
|
|
|
678
716
|
}
|
|
679
717
|
/**
|
|
680
718
|
* Watch paths for changes, calling the build callback on known file type changes.
|
|
719
|
+
* A change to a file `ignore` returns true for (given the path relative to the
|
|
720
|
+
* working directory) schedules no build.
|
|
681
721
|
* Uses debouncing to avoid rapid rebuilds.
|
|
682
722
|
*/
|
|
683
|
-
function watchFilesystem(pathnames, outFilename, callback) {
|
|
723
|
+
function watchFilesystem(pathnames, outFilename, callback, ignore = () => false) {
|
|
684
724
|
const absOutFile = (0, node_path.resolve)(outFilename);
|
|
685
725
|
const watchers = [];
|
|
686
726
|
let buildTimer = null;
|
|
@@ -701,7 +741,10 @@ function watchFilesystem(pathnames, outFilename, callback) {
|
|
|
701
741
|
if (!filename) return;
|
|
702
742
|
const abs = (0, node_path.resolve)(pathname, filename);
|
|
703
743
|
if (abs === absOutFile) return;
|
|
704
|
-
if (isKnownFileType(filename))
|
|
744
|
+
if (isKnownFileType(filename)) {
|
|
745
|
+
const rel = (0, node_path.relative)(process.cwd(), abs);
|
|
746
|
+
if (!ignore(rel || abs)) scheduleBuild(rel || abs);
|
|
747
|
+
}
|
|
705
748
|
});
|
|
706
749
|
watchers.push(watcher);
|
|
707
750
|
} catch {}
|
|
@@ -5172,14 +5215,22 @@ async function watch(options) {
|
|
|
5172
5215
|
const cache = /* @__PURE__ */ new Map();
|
|
5173
5216
|
const filePaths = options.sources.filter((s) => typeof s === "string");
|
|
5174
5217
|
const modulePaths = options.modules ?? [];
|
|
5175
|
-
const
|
|
5218
|
+
const allPaths = [...filePaths, ...modulePaths];
|
|
5219
|
+
const exclude = options.exclude ?? [];
|
|
5220
|
+
const moduleRoots = modulePaths.map((p) => (0, node_path.resolve)(p));
|
|
5221
|
+
const ignore = (filename) => {
|
|
5222
|
+
if (!isExcluded(filename, exclude)) return false;
|
|
5223
|
+
const abs = (0, node_path.resolve)(filename);
|
|
5224
|
+
return !moduleRoots.some((root) => abs === root || abs.startsWith(root + node_path.sep));
|
|
5225
|
+
};
|
|
5226
|
+
const handle = watchFilesystem(allPaths, options.outFile, (changedFiles) => {
|
|
5176
5227
|
buildOutput(options, cache, changedFiles).then((result) => {
|
|
5177
5228
|
(0, node_fs.writeFileSync)(options.outFile, result.output, "utf-8");
|
|
5178
5229
|
options.onBuild?.(result);
|
|
5179
5230
|
}).catch((e) => {
|
|
5180
5231
|
options.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
5181
5232
|
});
|
|
5182
|
-
});
|
|
5233
|
+
}, ignore);
|
|
5183
5234
|
controller.signal.addEventListener("abort", () => handle.close());
|
|
5184
5235
|
return controller;
|
|
5185
5236
|
}
|
|
@@ -5196,7 +5247,7 @@ async function buildOutput(options, cache, changedFiles) {
|
|
|
5196
5247
|
const inlineSources = [];
|
|
5197
5248
|
for (const source of options.sources) if (typeof source === "string") filePaths.push(source);
|
|
5198
5249
|
else inlineSources.push(source);
|
|
5199
|
-
const { filenames: sourceFilenames, diagnostics: sourcePathDiagnostics } = getFilenames(filePaths);
|
|
5250
|
+
const { filenames: sourceFilenames, diagnostics: sourcePathDiagnostics } = getFilenames(filePaths, void 0, options.exclude);
|
|
5200
5251
|
diagnostics.push(...sourcePathDiagnostics);
|
|
5201
5252
|
const story = createStory();
|
|
5202
5253
|
const processedFiles = /* @__PURE__ */ new Set();
|
|
@@ -5349,6 +5400,1101 @@ function storyToJSON(story) {
|
|
|
5349
5400
|
obj.passages = storyPassages;
|
|
5350
5401
|
return JSON.stringify(obj, null, 2);
|
|
5351
5402
|
}
|
|
5403
|
+
const SINGLE_ESCAPES = /* @__PURE__ */ new Map([
|
|
5404
|
+
["b", "\b"],
|
|
5405
|
+
["f", "\f"],
|
|
5406
|
+
["n", "\n"],
|
|
5407
|
+
["r", "\r"],
|
|
5408
|
+
["t", " "],
|
|
5409
|
+
["v", "\v"]
|
|
5410
|
+
]);
|
|
5411
|
+
const HEX_RE = /^[0-9A-Fa-f]+$/;
|
|
5412
|
+
const DIGIT_RE = /^[0-9]$/;
|
|
5413
|
+
const LINE_TERMINATORS = "\n\r\u2028\u2029";
|
|
5414
|
+
/**
|
|
5415
|
+
* The value of a quoted JavaScript string literal (quotes included), as strict-mode JavaScript
|
|
5416
|
+
* evaluates it: `\"`, `\'`, `\\`, `\n`, `\xHH`, `\uHHHH`, `\u{H…}`, line continuations, and a
|
|
5417
|
+
* backslash before any other character gives that character. Returns `undefined` for what
|
|
5418
|
+
* strict mode rejects, such as `\1` or a raw carriage return.
|
|
5419
|
+
*/
|
|
5420
|
+
function evalStringLiteral(literal) {
|
|
5421
|
+
const quote = literal[0];
|
|
5422
|
+
const last = literal.length - 1;
|
|
5423
|
+
if (quote !== "\"" && quote !== "'" || last < 1 || literal[last] !== quote) return;
|
|
5424
|
+
return cookEscapes(literal.slice(1, last), quote);
|
|
5425
|
+
}
|
|
5426
|
+
/**
|
|
5427
|
+
* Decodes the body of a string literal, given its quote, or a piece of a template literal
|
|
5428
|
+
* (`quote` undefined), as strict-mode JavaScript does. A string may not hold its own quote or a
|
|
5429
|
+
* raw line feed or carriage return; a template piece may, and turns `\r\n` and `\r` into `\n`.
|
|
5430
|
+
*/
|
|
5431
|
+
function cookEscapes(body, quote) {
|
|
5432
|
+
let value = "";
|
|
5433
|
+
let i = 0;
|
|
5434
|
+
while (i < body.length) {
|
|
5435
|
+
const ch = body.charAt(i);
|
|
5436
|
+
if (ch !== "\\") {
|
|
5437
|
+
if (quote !== void 0 && (ch === quote || ch === "\n" || ch === "\r")) return;
|
|
5438
|
+
if (ch === "\r") {
|
|
5439
|
+
value += "\n";
|
|
5440
|
+
i += body.charAt(i + 1) === "\n" ? 2 : 1;
|
|
5441
|
+
} else {
|
|
5442
|
+
value += ch;
|
|
5443
|
+
i += 1;
|
|
5444
|
+
}
|
|
5445
|
+
continue;
|
|
5446
|
+
}
|
|
5447
|
+
if (i + 1 >= body.length) return;
|
|
5448
|
+
const escaped = body.charAt(i + 1);
|
|
5449
|
+
i += 2;
|
|
5450
|
+
const single = SINGLE_ESCAPES.get(escaped);
|
|
5451
|
+
if (single !== void 0) {
|
|
5452
|
+
value += single;
|
|
5453
|
+
continue;
|
|
5454
|
+
}
|
|
5455
|
+
switch (escaped) {
|
|
5456
|
+
case "0":
|
|
5457
|
+
if (DIGIT_RE.test(body.charAt(i))) return;
|
|
5458
|
+
value += "\0";
|
|
5459
|
+
break;
|
|
5460
|
+
case "1":
|
|
5461
|
+
case "2":
|
|
5462
|
+
case "3":
|
|
5463
|
+
case "4":
|
|
5464
|
+
case "5":
|
|
5465
|
+
case "6":
|
|
5466
|
+
case "7":
|
|
5467
|
+
case "8":
|
|
5468
|
+
case "9": return;
|
|
5469
|
+
case "x": {
|
|
5470
|
+
const hex = body.slice(i, i + 2);
|
|
5471
|
+
if (hex.length !== 2 || !HEX_RE.test(hex)) return;
|
|
5472
|
+
value += String.fromCharCode(parseInt(hex, 16));
|
|
5473
|
+
i += 2;
|
|
5474
|
+
break;
|
|
5475
|
+
}
|
|
5476
|
+
case "u": {
|
|
5477
|
+
const unicode = readUnicodeEscape(body, i);
|
|
5478
|
+
if (unicode === void 0) return;
|
|
5479
|
+
value += unicode.char;
|
|
5480
|
+
i = unicode.end;
|
|
5481
|
+
break;
|
|
5482
|
+
}
|
|
5483
|
+
case "\r":
|
|
5484
|
+
if (body[i] === "\n") i += 1;
|
|
5485
|
+
break;
|
|
5486
|
+
case "\n":
|
|
5487
|
+
case "\u2028":
|
|
5488
|
+
case "\u2029": break;
|
|
5489
|
+
default: value += escaped;
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
return value;
|
|
5493
|
+
}
|
|
5494
|
+
/** Reads the part of `\uHHHH` or `\u{H…}` after the `u`. */
|
|
5495
|
+
function readUnicodeEscape(body, from) {
|
|
5496
|
+
if (body[from] === "{") {
|
|
5497
|
+
const close = body.indexOf("}", from + 1);
|
|
5498
|
+
if (close === -1) return;
|
|
5499
|
+
const hex = body.slice(from + 1, close);
|
|
5500
|
+
if (!HEX_RE.test(hex)) return;
|
|
5501
|
+
const codePoint = parseInt(hex, 16);
|
|
5502
|
+
return codePoint > 1114111 ? void 0 : {
|
|
5503
|
+
char: String.fromCodePoint(codePoint),
|
|
5504
|
+
end: close + 1
|
|
5505
|
+
};
|
|
5506
|
+
}
|
|
5507
|
+
const hex = body.slice(from, from + 4);
|
|
5508
|
+
if (hex.length !== 4 || !HEX_RE.test(hex)) return;
|
|
5509
|
+
return {
|
|
5510
|
+
char: String.fromCharCode(parseInt(hex, 16)),
|
|
5511
|
+
end: from + 4
|
|
5512
|
+
};
|
|
5513
|
+
}
|
|
5514
|
+
const REGEX_KEYWORDS = /* @__PURE__ */ new Set([
|
|
5515
|
+
"await",
|
|
5516
|
+
"case",
|
|
5517
|
+
"delete",
|
|
5518
|
+
"do",
|
|
5519
|
+
"else",
|
|
5520
|
+
"in",
|
|
5521
|
+
"instanceof",
|
|
5522
|
+
"new",
|
|
5523
|
+
"of",
|
|
5524
|
+
"return",
|
|
5525
|
+
"throw",
|
|
5526
|
+
"typeof",
|
|
5527
|
+
"void",
|
|
5528
|
+
"yield"
|
|
5529
|
+
]);
|
|
5530
|
+
const WORD_RE = /[$\p{ID_Continue}\u200c\u200d]+/uy;
|
|
5531
|
+
const WHITESPACE_RE = /\s/;
|
|
5532
|
+
const LINE_TERMINATOR_RE$1 = /[\n\r\u2028\u2029]/g;
|
|
5533
|
+
/**
|
|
5534
|
+
* The values of the string and template literals in JavaScript source. A template literal's
|
|
5535
|
+
* value keeps its text and has `SUBSTITUTION` where each `${…}` was; the strings inside a
|
|
5536
|
+
* substitution are values of their own. Comments and regular expression literals are skipped;
|
|
5537
|
+
* whether a `/` starts a regular expression is judged from the token before it. Malformed
|
|
5538
|
+
* source is read on as far as it can be.
|
|
5539
|
+
*/
|
|
5540
|
+
function javaScriptStrings(source) {
|
|
5541
|
+
const strings = [];
|
|
5542
|
+
const frames = [{
|
|
5543
|
+
kind: "code",
|
|
5544
|
+
braces: 0
|
|
5545
|
+
}];
|
|
5546
|
+
let regexAllowed = true;
|
|
5547
|
+
let afterDot = false;
|
|
5548
|
+
const failedUntil = /* @__PURE__ */ new Map();
|
|
5549
|
+
let i = 0;
|
|
5550
|
+
while (i < source.length) {
|
|
5551
|
+
const frame = frames[frames.length - 1];
|
|
5552
|
+
if (frame === void 0) break;
|
|
5553
|
+
const ch = source.charAt(i);
|
|
5554
|
+
const next = source.charAt(i + 1);
|
|
5555
|
+
if (frame.kind === "template") {
|
|
5556
|
+
if (ch === "`" || ch === "$" && next === "{") {
|
|
5557
|
+
const piece = cookEscapes(source.slice(frame.pieceStart, i), void 0);
|
|
5558
|
+
const value = frame.value + (piece ?? "");
|
|
5559
|
+
const valid = frame.valid && piece !== void 0;
|
|
5560
|
+
frames.pop();
|
|
5561
|
+
if (ch === "`") {
|
|
5562
|
+
if (valid) strings.push(value);
|
|
5563
|
+
i += 1;
|
|
5564
|
+
regexAllowed = false;
|
|
5565
|
+
afterDot = false;
|
|
5566
|
+
} else {
|
|
5567
|
+
frames.push({
|
|
5568
|
+
kind: "template",
|
|
5569
|
+
value: value + "",
|
|
5570
|
+
pieceStart: -1,
|
|
5571
|
+
valid
|
|
5572
|
+
});
|
|
5573
|
+
frames.push({
|
|
5574
|
+
kind: "code",
|
|
5575
|
+
braces: 0
|
|
5576
|
+
});
|
|
5577
|
+
i += 2;
|
|
5578
|
+
regexAllowed = true;
|
|
5579
|
+
afterDot = false;
|
|
5580
|
+
}
|
|
5581
|
+
} else i += ch === "\\" ? 2 : 1;
|
|
5582
|
+
continue;
|
|
5583
|
+
}
|
|
5584
|
+
if (ch === "/" && next === "/") {
|
|
5585
|
+
const lineEnd = searchFrom$1(LINE_TERMINATOR_RE$1, source, i + 2);
|
|
5586
|
+
i = lineEnd === -1 ? source.length : lineEnd;
|
|
5587
|
+
continue;
|
|
5588
|
+
}
|
|
5589
|
+
if (ch === "/" && next === "*") {
|
|
5590
|
+
const close = source.indexOf("*/", i + 2);
|
|
5591
|
+
i = close === -1 ? source.length : close + 2;
|
|
5592
|
+
continue;
|
|
5593
|
+
}
|
|
5594
|
+
if (ch === "/" && regexAllowed && i >= (failedUntil.get("/") ?? 0)) {
|
|
5595
|
+
const end = endOfRegularExpression(source, i);
|
|
5596
|
+
if (end.end !== void 0) {
|
|
5597
|
+
i = end.end;
|
|
5598
|
+
regexAllowed = false;
|
|
5599
|
+
afterDot = false;
|
|
5600
|
+
continue;
|
|
5601
|
+
}
|
|
5602
|
+
failedUntil.set("/", end.stop);
|
|
5603
|
+
}
|
|
5604
|
+
if ((ch === "\"" || ch === "'") && i >= (failedUntil.get(ch) ?? 0)) {
|
|
5605
|
+
const end = endOfJavaScriptString(source, i);
|
|
5606
|
+
if (end.end !== void 0) {
|
|
5607
|
+
const value = evalStringLiteral(source.slice(i, end.end));
|
|
5608
|
+
if (value !== void 0) strings.push(value);
|
|
5609
|
+
i = end.end;
|
|
5610
|
+
regexAllowed = false;
|
|
5611
|
+
afterDot = false;
|
|
5612
|
+
continue;
|
|
5613
|
+
}
|
|
5614
|
+
failedUntil.set(ch, end.stop);
|
|
5615
|
+
}
|
|
5616
|
+
if (ch === "`") {
|
|
5617
|
+
frames.push({
|
|
5618
|
+
kind: "template",
|
|
5619
|
+
value: "",
|
|
5620
|
+
pieceStart: i + 1,
|
|
5621
|
+
valid: true
|
|
5622
|
+
});
|
|
5623
|
+
i += 1;
|
|
5624
|
+
continue;
|
|
5625
|
+
}
|
|
5626
|
+
const word = wordAt(source, i);
|
|
5627
|
+
if (word !== null) {
|
|
5628
|
+
regexAllowed = !afterDot && REGEX_KEYWORDS.has(word[0]);
|
|
5629
|
+
afterDot = false;
|
|
5630
|
+
i += word[0].length;
|
|
5631
|
+
continue;
|
|
5632
|
+
}
|
|
5633
|
+
if ((ch === "+" || ch === "-") && next === ch) {
|
|
5634
|
+
regexAllowed = false;
|
|
5635
|
+
afterDot = false;
|
|
5636
|
+
i += 2;
|
|
5637
|
+
continue;
|
|
5638
|
+
}
|
|
5639
|
+
if (frame.kind === "code" && frames.length > 1 && (ch === "{" || ch === "}")) {
|
|
5640
|
+
if (ch === "{") frames[frames.length - 1] = {
|
|
5641
|
+
kind: "code",
|
|
5642
|
+
braces: frame.braces + 1
|
|
5643
|
+
};
|
|
5644
|
+
else if (frame.braces > 0) frames[frames.length - 1] = {
|
|
5645
|
+
kind: "code",
|
|
5646
|
+
braces: frame.braces - 1
|
|
5647
|
+
};
|
|
5648
|
+
else {
|
|
5649
|
+
frames.pop();
|
|
5650
|
+
const template = frames.pop();
|
|
5651
|
+
if (template?.kind === "template") frames.push({
|
|
5652
|
+
...template,
|
|
5653
|
+
pieceStart: i + 1
|
|
5654
|
+
});
|
|
5655
|
+
i += 1;
|
|
5656
|
+
continue;
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
if (!WHITESPACE_RE.test(ch)) {
|
|
5660
|
+
regexAllowed = !")]}".includes(ch);
|
|
5661
|
+
afterDot = ch === ".";
|
|
5662
|
+
}
|
|
5663
|
+
i += 1;
|
|
5664
|
+
}
|
|
5665
|
+
return strings;
|
|
5666
|
+
}
|
|
5667
|
+
/** The name, keyword or number that starts at `pos`, if one does. */
|
|
5668
|
+
function wordAt(source, pos) {
|
|
5669
|
+
const code = source.charCodeAt(pos);
|
|
5670
|
+
if (code < 128 && !(code >= 48 && code <= 57 || (code | 32) >= 97 && (code | 32) <= 122 || code === 95 || code === 36)) return null;
|
|
5671
|
+
WORD_RE.lastIndex = pos;
|
|
5672
|
+
return WORD_RE.exec(source);
|
|
5673
|
+
}
|
|
5674
|
+
/** Index of the first match of a global `re` at or after `from`, or -1. */
|
|
5675
|
+
function searchFrom$1(re, text, from) {
|
|
5676
|
+
re.lastIndex = from;
|
|
5677
|
+
const m = re.exec(text);
|
|
5678
|
+
return m === null ? -1 : m.index;
|
|
5679
|
+
}
|
|
5680
|
+
/**
|
|
5681
|
+
* Where a string literal that opens at `pos` ends, or, if it doesn't close on its line, where
|
|
5682
|
+
* the scan stopped. A string with the same quote that starts before that stop, inside this one,
|
|
5683
|
+
* doesn't close either.
|
|
5684
|
+
*/
|
|
5685
|
+
function endOfJavaScriptString(source, pos) {
|
|
5686
|
+
const quote = source.charAt(pos);
|
|
5687
|
+
let i = pos + 1;
|
|
5688
|
+
while (i < source.length) {
|
|
5689
|
+
const ch = source.charAt(i);
|
|
5690
|
+
if (ch === quote) return {
|
|
5691
|
+
end: i + 1,
|
|
5692
|
+
stop: i
|
|
5693
|
+
};
|
|
5694
|
+
if (ch === "\n" || ch === "\r") return {
|
|
5695
|
+
end: void 0,
|
|
5696
|
+
stop: i
|
|
5697
|
+
};
|
|
5698
|
+
if (ch === "\\") i += source.startsWith("\r\n", i + 1) ? 3 : 2;
|
|
5699
|
+
else i += 1;
|
|
5700
|
+
}
|
|
5701
|
+
return {
|
|
5702
|
+
end: void 0,
|
|
5703
|
+
stop: source.length
|
|
5704
|
+
};
|
|
5705
|
+
}
|
|
5706
|
+
/**
|
|
5707
|
+
* Where a regular expression literal that opens at `pos` ends, flags included, or, if none
|
|
5708
|
+
* closes on the line, where the line ends. A `/` before that is then read as a division.
|
|
5709
|
+
*/
|
|
5710
|
+
function endOfRegularExpression(source, pos) {
|
|
5711
|
+
let inClass = false;
|
|
5712
|
+
let i = pos + 1;
|
|
5713
|
+
while (i < source.length) {
|
|
5714
|
+
const ch = source.charAt(i);
|
|
5715
|
+
if (LINE_TERMINATORS.includes(ch)) return {
|
|
5716
|
+
end: void 0,
|
|
5717
|
+
stop: i
|
|
5718
|
+
};
|
|
5719
|
+
if (ch === "\\") {
|
|
5720
|
+
const escaped = source.charAt(i + 1);
|
|
5721
|
+
if (escaped === "" || LINE_TERMINATORS.includes(escaped)) return {
|
|
5722
|
+
end: void 0,
|
|
5723
|
+
stop: i + 1
|
|
5724
|
+
};
|
|
5725
|
+
i += 2;
|
|
5726
|
+
continue;
|
|
5727
|
+
}
|
|
5728
|
+
if (ch === "[") inClass = true;
|
|
5729
|
+
else if (ch === "]") inClass = false;
|
|
5730
|
+
else if (ch === "/" && !inClass) {
|
|
5731
|
+
WORD_RE.lastIndex = i + 1;
|
|
5732
|
+
const flags = WORD_RE.exec(source);
|
|
5733
|
+
return {
|
|
5734
|
+
end: i + 1 + (flags === null ? 0 : flags[0].length),
|
|
5735
|
+
stop: i
|
|
5736
|
+
};
|
|
5737
|
+
}
|
|
5738
|
+
i += 1;
|
|
5739
|
+
}
|
|
5740
|
+
return {
|
|
5741
|
+
end: void 0,
|
|
5742
|
+
stop: source.length
|
|
5743
|
+
};
|
|
5744
|
+
}
|
|
5745
|
+
//#endregion
|
|
5746
|
+
//#region src/sugarcube-macros.ts
|
|
5747
|
+
/**
|
|
5748
|
+
* Reads SugarCube 2 macro calls the way SugarCube 2.37.3 does, so the link check sees the same
|
|
5749
|
+
* passage names that `<<link>>` and `<<goto>>` use when the story plays.
|
|
5750
|
+
*
|
|
5751
|
+
* Each step follows a part of SugarCube's macro parser:
|
|
5752
|
+
* - `findMacroTags` finds where a tag starts and ends (the parser's `lookahead` pattern);
|
|
5753
|
+
* - `parseMacroArgs` splits the text after the macro name into arguments (`parseArgs`);
|
|
5754
|
+
* - quoted arguments are evaluated as strict-mode JavaScript strings (`evalStringLiteral`).
|
|
5755
|
+
*
|
|
5756
|
+
* Of the rest of the wikifier, only comments, `<script>` elements and `<<script>>` bodies are
|
|
5757
|
+
* told apart; other markup, such as verbatim text, is read as markup (see
|
|
5758
|
+
* `findMacroPassageLinks`).
|
|
5759
|
+
*/
|
|
5760
|
+
const NAME_RE = /\/?[A-Za-z][\w-]*|[=-]/y;
|
|
5761
|
+
const SPACES_RE = /\s*/y;
|
|
5762
|
+
const LINE_TERMINATOR_RE = /[\n\r\u2028\u2029]/g;
|
|
5763
|
+
const SQUARE_BRACKET_OPEN_RE = /\[(?:[<>]?[Ii][Mm][Gg])?\[/y;
|
|
5764
|
+
const SPACE_RE = /[\s\u180e]/g;
|
|
5765
|
+
const NOT_SPACE_RE = /[^\s\u180e]/g;
|
|
5766
|
+
const VARIABLE_RE = /^[$_][$A-Z_a-z][$0-9A-Z_a-z]*/;
|
|
5767
|
+
const SETTINGS_OR_SETUP_RE = /^(?:settings|setup)[.[]/;
|
|
5768
|
+
const IMAGE_MARKUP_CHARS = "<>IiMmGg";
|
|
5769
|
+
/**
|
|
5770
|
+
* Macros that SugarCube doesn't split into arguments (`skipArgs` in 2.37.3), with their child
|
|
5771
|
+
* tags that do the same: `<<elseif>>` and `<<else>>` of `<<if>>`, and `<<optionsfrom>>` of
|
|
5772
|
+
* `<<cycle>>` and `<<listbox>>`. Their argument text is TwineScript, or for `<<capture>>`,
|
|
5773
|
+
* `<<unset>>` and `<<for>>` a syntax of their own; either way its strings are JavaScript strings.
|
|
5774
|
+
*/
|
|
5775
|
+
const RAW_ARGUMENT_MACROS = /* @__PURE__ */ new Set([
|
|
5776
|
+
"break",
|
|
5777
|
+
"capture",
|
|
5778
|
+
"continue",
|
|
5779
|
+
"done",
|
|
5780
|
+
"else",
|
|
5781
|
+
"elseif",
|
|
5782
|
+
"for",
|
|
5783
|
+
"if",
|
|
5784
|
+
"nobr",
|
|
5785
|
+
"optionsfrom",
|
|
5786
|
+
"print",
|
|
5787
|
+
"=",
|
|
5788
|
+
"-",
|
|
5789
|
+
"run",
|
|
5790
|
+
"set",
|
|
5791
|
+
"silent",
|
|
5792
|
+
"silently",
|
|
5793
|
+
"stop",
|
|
5794
|
+
"switch",
|
|
5795
|
+
"unset",
|
|
5796
|
+
"waitforaudio"
|
|
5797
|
+
]);
|
|
5798
|
+
/**
|
|
5799
|
+
* How deep strings inside macro arguments are read for further calls. Stories nest two or three
|
|
5800
|
+
* levels; the limit keeps the work linear in the size of the text on any input.
|
|
5801
|
+
*/
|
|
5802
|
+
const MAX_STRING_DEPTH = 10;
|
|
5803
|
+
/** A context for reading `text`: the closer search may read four times it, plus an allowance. */
|
|
5804
|
+
function readContext(text) {
|
|
5805
|
+
return {
|
|
5806
|
+
depth: 0,
|
|
5807
|
+
budget: { left: 4 * text.length + 1e5 }
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5810
|
+
function deeper(context) {
|
|
5811
|
+
return {
|
|
5812
|
+
depth: context.depth + 1,
|
|
5813
|
+
budget: context.budget
|
|
5814
|
+
};
|
|
5815
|
+
}
|
|
5816
|
+
/** Index of the first match of a global `re` at or after `from`, or -1. */
|
|
5817
|
+
function searchFrom(re, text, from) {
|
|
5818
|
+
re.lastIndex = from;
|
|
5819
|
+
const m = re.exec(text);
|
|
5820
|
+
return m === null ? -1 : m.index;
|
|
5821
|
+
}
|
|
5822
|
+
/**
|
|
5823
|
+
* Finds the macro tags in text, in order, trying a tag at every `<<` as SugarCube's macro
|
|
5824
|
+
* parser does. A tag that doesn't parse is skipped, and the search goes on after its `<<`.
|
|
5825
|
+
*
|
|
5826
|
+
* Gives the same tags as SugarCube's pattern, without its backtracking: that pattern takes
|
|
5827
|
+
* exponential time when no `>>` follows a `<<name`, which a script passage can contain (`1<<n`).
|
|
5828
|
+
*/
|
|
5829
|
+
function findMacroTags(text) {
|
|
5830
|
+
return scanTags(text).map((scanned) => scanned.tag);
|
|
5831
|
+
}
|
|
5832
|
+
function scanTags(text) {
|
|
5833
|
+
const matchTagAt = tagMatcher(text);
|
|
5834
|
+
const tags = [];
|
|
5835
|
+
let start = text.indexOf("<<");
|
|
5836
|
+
while (start !== -1) {
|
|
5837
|
+
const scanned = matchTagAt(start);
|
|
5838
|
+
if (scanned === void 0) start = text.indexOf("<<", start + 2);
|
|
5839
|
+
else {
|
|
5840
|
+
tags.push(scanned);
|
|
5841
|
+
start = text.indexOf("<<", scanned.tag.end);
|
|
5842
|
+
}
|
|
5843
|
+
}
|
|
5844
|
+
return tags;
|
|
5845
|
+
}
|
|
5846
|
+
/**
|
|
5847
|
+
* Returns a function that matches a tag at a `<<` in `text`. It must be called for positions in
|
|
5848
|
+
* increasing order.
|
|
5849
|
+
*
|
|
5850
|
+
* The arguments may contain comments, backquoted expressions, quoted strings and link markup,
|
|
5851
|
+
* whose `>>` doesn't end the tag. SugarCube's pattern treats such a part as a unit only if a
|
|
5852
|
+
* `>>` still follows it, and otherwise as plain characters; `lastClose`, the last `>>` in the
|
|
5853
|
+
* text, answers that in constant time. Once a part is read as plain characters, a part of the
|
|
5854
|
+
* same kind that starts inside it can't end before `lastClose` either, in this tag or a later
|
|
5855
|
+
* one, so it isn't scanned again: `scanFrom` records where each kind may next be scanned. This
|
|
5856
|
+
* keeps the search linear. `onWork` is told how many characters each step reads. (Exported for
|
|
5857
|
+
* tests; the module is not part of the package's API.)
|
|
5858
|
+
*/
|
|
5859
|
+
function tagMatcher(text, lastClose = text.lastIndexOf(">>"), onWork = () => {}) {
|
|
5860
|
+
const scanFrom = /* @__PURE__ */ new Map();
|
|
5861
|
+
return (start) => {
|
|
5862
|
+
NAME_RE.lastIndex = start + 2;
|
|
5863
|
+
const nameMatch = NAME_RE.exec(text);
|
|
5864
|
+
if (nameMatch === null) {
|
|
5865
|
+
onWork(2);
|
|
5866
|
+
return;
|
|
5867
|
+
}
|
|
5868
|
+
SPACES_RE.lastIndex = NAME_RE.lastIndex;
|
|
5869
|
+
SPACES_RE.exec(text);
|
|
5870
|
+
const argsStart = SPACES_RE.lastIndex;
|
|
5871
|
+
onWork(argsStart - start);
|
|
5872
|
+
let innerCall;
|
|
5873
|
+
let pos = argsStart;
|
|
5874
|
+
while (pos <= lastClose) {
|
|
5875
|
+
if (text.startsWith(">>", pos)) return {
|
|
5876
|
+
tag: {
|
|
5877
|
+
name: nameMatch[0],
|
|
5878
|
+
args: text.slice(argsStart, pos),
|
|
5879
|
+
start,
|
|
5880
|
+
end: pos + 2
|
|
5881
|
+
},
|
|
5882
|
+
innerCall
|
|
5883
|
+
};
|
|
5884
|
+
const kind = argumentPartKindAt(text, pos);
|
|
5885
|
+
const from = kind === void 0 ? void 0 : scanFrom.get(kind);
|
|
5886
|
+
if (kind !== void 0 && (from === void 0 || pos >= from)) {
|
|
5887
|
+
const part = scanArgumentPart(text, pos, kind);
|
|
5888
|
+
onWork(part.stop - pos);
|
|
5889
|
+
if (part.end !== void 0 && part.end <= lastClose) {
|
|
5890
|
+
pos = part.end;
|
|
5891
|
+
continue;
|
|
5892
|
+
}
|
|
5893
|
+
scanFrom.set(kind, part.stop);
|
|
5894
|
+
}
|
|
5895
|
+
if (innerCall === void 0 && startsPassageLinkCall(text, pos)) innerCall = pos;
|
|
5896
|
+
onWork(1);
|
|
5897
|
+
pos += 1;
|
|
5898
|
+
}
|
|
5899
|
+
};
|
|
5900
|
+
}
|
|
5901
|
+
/** Whether a `<<link` or `<<goto` tag, and not a longer macro name, starts at `pos`. */
|
|
5902
|
+
function startsPassageLinkCall(text, pos) {
|
|
5903
|
+
if (!text.startsWith("<<", pos)) return false;
|
|
5904
|
+
NAME_RE.lastIndex = pos + 2;
|
|
5905
|
+
const name = NAME_RE.exec(text)?.[0];
|
|
5906
|
+
return name === "link" || name === "goto";
|
|
5907
|
+
}
|
|
5908
|
+
/** The kind of argument part that opens at `pos`, if one does. */
|
|
5909
|
+
function argumentPartKindAt(text, pos) {
|
|
5910
|
+
const ch = text[pos];
|
|
5911
|
+
switch (ch) {
|
|
5912
|
+
case "/": {
|
|
5913
|
+
const next = text[pos + 1];
|
|
5914
|
+
return next === "*" ? "/*" : next === "/" ? "//" : void 0;
|
|
5915
|
+
}
|
|
5916
|
+
case "`":
|
|
5917
|
+
case "\"":
|
|
5918
|
+
case "'":
|
|
5919
|
+
case "[": return ch;
|
|
5920
|
+
default: return;
|
|
5921
|
+
}
|
|
5922
|
+
}
|
|
5923
|
+
function scanArgumentPart(text, pos, kind) {
|
|
5924
|
+
switch (kind) {
|
|
5925
|
+
case "/*": {
|
|
5926
|
+
const close = text.indexOf("*/", pos + 2);
|
|
5927
|
+
return close === -1 ? {
|
|
5928
|
+
end: void 0,
|
|
5929
|
+
stop: text.length
|
|
5930
|
+
} : {
|
|
5931
|
+
end: close + 2,
|
|
5932
|
+
stop: close
|
|
5933
|
+
};
|
|
5934
|
+
}
|
|
5935
|
+
case "//": {
|
|
5936
|
+
const lineEnd = searchFrom(LINE_TERMINATOR_RE, text, pos + 2);
|
|
5937
|
+
if (lineEnd === -1) return {
|
|
5938
|
+
end: void 0,
|
|
5939
|
+
stop: text.length
|
|
5940
|
+
};
|
|
5941
|
+
return {
|
|
5942
|
+
end: text[lineEnd] === "\n" ? lineEnd + 1 : void 0,
|
|
5943
|
+
stop: lineEnd
|
|
5944
|
+
};
|
|
5945
|
+
}
|
|
5946
|
+
case "`":
|
|
5947
|
+
case "\"":
|
|
5948
|
+
case "'": return scanQuoted(text, pos, kind);
|
|
5949
|
+
case "[": return scanSquareBracketed(text, pos);
|
|
5950
|
+
default: throw new Error(`unhandled argument part: ${String(kind)}`);
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
/** A quoted part of a tag: a backslash escapes any character except a line terminator. */
|
|
5954
|
+
function scanQuoted(text, pos, quote) {
|
|
5955
|
+
let i = pos + 1;
|
|
5956
|
+
while (i < text.length) {
|
|
5957
|
+
const ch = text[i];
|
|
5958
|
+
if (ch === quote) return {
|
|
5959
|
+
end: i + 1,
|
|
5960
|
+
stop: i
|
|
5961
|
+
};
|
|
5962
|
+
if (ch === "\\") {
|
|
5963
|
+
const next = text[i + 1];
|
|
5964
|
+
if (next === void 0 || "\n\r\u2028\u2029".includes(next)) return {
|
|
5965
|
+
end: void 0,
|
|
5966
|
+
stop: i
|
|
5967
|
+
};
|
|
5968
|
+
i += 2;
|
|
5969
|
+
} else i += 1;
|
|
5970
|
+
}
|
|
5971
|
+
return {
|
|
5972
|
+
end: void 0,
|
|
5973
|
+
stop: text.length
|
|
5974
|
+
};
|
|
5975
|
+
}
|
|
5976
|
+
/** Link or image markup in a tag: up to the first `]]` on the same line, plus any further `]`. */
|
|
5977
|
+
function scanSquareBracketed(text, pos) {
|
|
5978
|
+
SQUARE_BRACKET_OPEN_RE.lastIndex = pos;
|
|
5979
|
+
if (!SQUARE_BRACKET_OPEN_RE.test(text)) return {
|
|
5980
|
+
end: void 0,
|
|
5981
|
+
stop: pos + 1
|
|
5982
|
+
};
|
|
5983
|
+
for (let i = SQUARE_BRACKET_OPEN_RE.lastIndex; i < text.length; i++) {
|
|
5984
|
+
const ch = text[i];
|
|
5985
|
+
if (ch === "\n" || ch === "\r") return {
|
|
5986
|
+
end: void 0,
|
|
5987
|
+
stop: i
|
|
5988
|
+
};
|
|
5989
|
+
if (ch === "]" && text[i + 1] === "]") {
|
|
5990
|
+
let end = i + 2;
|
|
5991
|
+
while (text[end] === "]") end += 1;
|
|
5992
|
+
return {
|
|
5993
|
+
end,
|
|
5994
|
+
stop: i
|
|
5995
|
+
};
|
|
5996
|
+
}
|
|
5997
|
+
}
|
|
5998
|
+
return {
|
|
5999
|
+
end: void 0,
|
|
6000
|
+
stop: text.length
|
|
6001
|
+
};
|
|
6002
|
+
}
|
|
6003
|
+
function lexMacroArgs(raw) {
|
|
6004
|
+
const args = [];
|
|
6005
|
+
let pos = searchFrom(NOT_SPACE_RE, raw, 0);
|
|
6006
|
+
while (pos !== -1) {
|
|
6007
|
+
const lexed = lexArgument(raw, pos);
|
|
6008
|
+
if (lexed === void 0) return;
|
|
6009
|
+
args.push(lexed.arg);
|
|
6010
|
+
pos = searchFrom(NOT_SPACE_RE, raw, lexed.end);
|
|
6011
|
+
}
|
|
6012
|
+
return args;
|
|
6013
|
+
}
|
|
6014
|
+
function lexArgument(raw, pos) {
|
|
6015
|
+
const first = raw[pos];
|
|
6016
|
+
switch (first) {
|
|
6017
|
+
case "`": {
|
|
6018
|
+
const end = slurpQuote(raw, pos + 1, "`");
|
|
6019
|
+
return end === void 0 ? void 0 : {
|
|
6020
|
+
arg: {
|
|
6021
|
+
kind: "expression",
|
|
6022
|
+
code: raw.slice(pos + 1, end - 1)
|
|
6023
|
+
},
|
|
6024
|
+
end
|
|
6025
|
+
};
|
|
6026
|
+
}
|
|
6027
|
+
case "\"":
|
|
6028
|
+
case "'": {
|
|
6029
|
+
const end = slurpQuote(raw, pos + 1, first);
|
|
6030
|
+
if (end === void 0) return;
|
|
6031
|
+
const value = evalStringLiteral(raw.slice(pos, end));
|
|
6032
|
+
return value === void 0 ? void 0 : {
|
|
6033
|
+
arg: {
|
|
6034
|
+
kind: "string",
|
|
6035
|
+
value
|
|
6036
|
+
},
|
|
6037
|
+
end
|
|
6038
|
+
};
|
|
6039
|
+
}
|
|
6040
|
+
case "[": {
|
|
6041
|
+
const end = lexSquareBracketed(raw, pos + 1);
|
|
6042
|
+
return end === void 0 ? void 0 : {
|
|
6043
|
+
arg: { kind: "markup" },
|
|
6044
|
+
end
|
|
6045
|
+
};
|
|
6046
|
+
}
|
|
6047
|
+
default: {
|
|
6048
|
+
const space = searchFrom(SPACE_RE, raw, pos + 1);
|
|
6049
|
+
const end = space === -1 ? raw.length : space;
|
|
6050
|
+
return {
|
|
6051
|
+
arg: {
|
|
6052
|
+
kind: "word",
|
|
6053
|
+
text: raw.slice(pos, end)
|
|
6054
|
+
},
|
|
6055
|
+
end
|
|
6056
|
+
};
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
/**
|
|
6061
|
+
* Reads a quoted argument from just after its opening quote. A newline, or a backslash before a
|
|
6062
|
+
* newline, ends it unterminated. Returns the index after the closing quote.
|
|
6063
|
+
*/
|
|
6064
|
+
function slurpQuote(raw, from, endQuote) {
|
|
6065
|
+
let i = from;
|
|
6066
|
+
while (i < raw.length) {
|
|
6067
|
+
const ch = raw[i];
|
|
6068
|
+
if (ch === endQuote) return i + 1;
|
|
6069
|
+
if (ch === "\n") return;
|
|
6070
|
+
if (ch === "\\") {
|
|
6071
|
+
const next = raw[i + 1];
|
|
6072
|
+
if (next === void 0 || next === "\n") return;
|
|
6073
|
+
i += 2;
|
|
6074
|
+
} else i += 1;
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
/**
|
|
6078
|
+
* Reads link or image markup from just after its first `[`, counting nested brackets.
|
|
6079
|
+
* Returns the index after the closing `]]`.
|
|
6080
|
+
*/
|
|
6081
|
+
function lexSquareBracketed(raw, from) {
|
|
6082
|
+
let i = from;
|
|
6083
|
+
while (i < raw.length && IMAGE_MARKUP_CHARS.includes(raw.charAt(i))) i += 1;
|
|
6084
|
+
if (raw[i] !== "[") return;
|
|
6085
|
+
i += 1;
|
|
6086
|
+
let depth = 2;
|
|
6087
|
+
while (i < raw.length) {
|
|
6088
|
+
const ch = raw[i];
|
|
6089
|
+
i += 1;
|
|
6090
|
+
switch (ch) {
|
|
6091
|
+
case "\\": {
|
|
6092
|
+
const next = raw[i];
|
|
6093
|
+
if (next === void 0 || next === "\n") return;
|
|
6094
|
+
i += 1;
|
|
6095
|
+
break;
|
|
6096
|
+
}
|
|
6097
|
+
case "\n": return;
|
|
6098
|
+
case "[":
|
|
6099
|
+
depth += 1;
|
|
6100
|
+
break;
|
|
6101
|
+
case "]":
|
|
6102
|
+
depth -= 1;
|
|
6103
|
+
if (depth < 0) return;
|
|
6104
|
+
if (depth === 1 && raw[i] === "]") return i + 1;
|
|
6105
|
+
}
|
|
6106
|
+
}
|
|
6107
|
+
}
|
|
6108
|
+
/** An argument's value, following SugarCube's conversions. */
|
|
6109
|
+
function argValue(arg) {
|
|
6110
|
+
switch (arg.kind) {
|
|
6111
|
+
case "string": return {
|
|
6112
|
+
type: "string",
|
|
6113
|
+
value: arg.value
|
|
6114
|
+
};
|
|
6115
|
+
case "expression": return { type: "other" };
|
|
6116
|
+
case "markup": return { type: "markup" };
|
|
6117
|
+
case "word": return bareWord(arg.text);
|
|
6118
|
+
default: throw new Error(`unhandled macro argument: ${JSON.stringify(arg)}`);
|
|
6119
|
+
}
|
|
6120
|
+
}
|
|
6121
|
+
/** A bare word's value, following SugarCube's conversions. */
|
|
6122
|
+
function bareWord(word) {
|
|
6123
|
+
if (VARIABLE_RE.test(word) || SETTINGS_OR_SETUP_RE.test(word)) return { type: "other" };
|
|
6124
|
+
switch (word) {
|
|
6125
|
+
case "null": return { type: "null" };
|
|
6126
|
+
case "undefined":
|
|
6127
|
+
case "true":
|
|
6128
|
+
case "false": return { type: "other" };
|
|
6129
|
+
case "NaN": return {
|
|
6130
|
+
type: "number",
|
|
6131
|
+
value: NaN
|
|
6132
|
+
};
|
|
6133
|
+
default: {
|
|
6134
|
+
const value = Number(word);
|
|
6135
|
+
return Number.isNaN(value) ? {
|
|
6136
|
+
type: "string",
|
|
6137
|
+
value: word
|
|
6138
|
+
} : {
|
|
6139
|
+
type: "number",
|
|
6140
|
+
value
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
/** The passage name an argument gives, if it can be known without playing the story. */
|
|
6146
|
+
function passageName(arg) {
|
|
6147
|
+
switch (arg.type) {
|
|
6148
|
+
case "string": return arg.value.includes("") ? void 0 : arg.value;
|
|
6149
|
+
case "number": return String(arg.value);
|
|
6150
|
+
case "markup":
|
|
6151
|
+
case "null":
|
|
6152
|
+
case "other": return;
|
|
6153
|
+
default: throw new Error(`unhandled macro argument: ${JSON.stringify(arg)}`);
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
/**
|
|
6157
|
+
* The argument that names the passage in a `<<link>>` or `<<goto>>` call:
|
|
6158
|
+
* - `<<link linkText [passageName]>>`, unless the first argument is link or image markup, which
|
|
6159
|
+
* then names the passage itself and any further argument is ignored;
|
|
6160
|
+
* - `<<goto passageName>>`.
|
|
6161
|
+
*
|
|
6162
|
+
* In text built from a template literal, a bare word with a `${…}` in it may become several
|
|
6163
|
+
* words in play, so no argument after it is known.
|
|
6164
|
+
*/
|
|
6165
|
+
function passageArgument(macro, lexed) {
|
|
6166
|
+
const index = macro === "link" ? 1 : 0;
|
|
6167
|
+
if (lexed.slice(0, index + 1).some((arg) => arg.kind === "word" && arg.text.includes(""))) return;
|
|
6168
|
+
const args = lexed.map(argValue);
|
|
6169
|
+
switch (macro) {
|
|
6170
|
+
case "link": {
|
|
6171
|
+
const [label, passage] = args;
|
|
6172
|
+
return label === void 0 || label.type === "markup" || label.type === "null" ? void 0 : passage;
|
|
6173
|
+
}
|
|
6174
|
+
case "goto": return args[0];
|
|
6175
|
+
default: throw new Error(`unhandled macro: ${String(macro)}`);
|
|
6176
|
+
}
|
|
6177
|
+
}
|
|
6178
|
+
function isPassageLinkMacro(name) {
|
|
6179
|
+
return name === "goto" || name === "link";
|
|
6180
|
+
}
|
|
6181
|
+
/** The passage a `<<link>>` or `<<goto>>` tag names itself, if it can be known before play. */
|
|
6182
|
+
function ownPassageLink(tag, lexed) {
|
|
6183
|
+
if (!isPassageLinkMacro(tag.name)) return;
|
|
6184
|
+
const arg = passageArgument(tag.name, lexed);
|
|
6185
|
+
const passage = arg === void 0 ? void 0 : passageName(arg);
|
|
6186
|
+
return passage === void 0 ? void 0 : {
|
|
6187
|
+
macro: tag.name,
|
|
6188
|
+
passage
|
|
6189
|
+
};
|
|
6190
|
+
}
|
|
6191
|
+
/**
|
|
6192
|
+
* The passages a tag names: its own, if it is a `<<link>>` or `<<goto>>` call, then those of the
|
|
6193
|
+
* calls inside the strings of its arguments, which the macro may print or pass on
|
|
6194
|
+
* (`<<set _out to '<<link "Go" "Room">><</link>>'>>`). The arguments of the macros SugarCube
|
|
6195
|
+
* doesn't split are read as JavaScript; in others, a backquoted expression is.
|
|
6196
|
+
*/
|
|
6197
|
+
function tagPassageLinks(tag, context) {
|
|
6198
|
+
if (RAW_ARGUMENT_MACROS.has(tag.name)) return javaScriptPassageLinks(tag.args, context);
|
|
6199
|
+
const lexed = lexMacroArgs(tag.args);
|
|
6200
|
+
if (lexed === void 0) return [];
|
|
6201
|
+
const own = ownPassageLink(tag, lexed);
|
|
6202
|
+
const inner = lexed.flatMap((arg) => {
|
|
6203
|
+
switch (arg.kind) {
|
|
6204
|
+
case "string": return markupPassageLinks(arg.value, deeper(context));
|
|
6205
|
+
case "expression": return javaScriptPassageLinks(arg.code, context);
|
|
6206
|
+
case "markup":
|
|
6207
|
+
case "word": return [];
|
|
6208
|
+
default: throw new Error(`unhandled macro argument: ${JSON.stringify(arg)}`);
|
|
6209
|
+
}
|
|
6210
|
+
});
|
|
6211
|
+
return own === void 0 ? inner : [own, ...inner];
|
|
6212
|
+
}
|
|
6213
|
+
/**
|
|
6214
|
+
* The passage named by a `<<link>>` or `<<goto>>` that appears unquoted inside another tag's
|
|
6215
|
+
* arguments, at `start`. SugarCube never sees such an outer "tag" when its `<<` lies in text it
|
|
6216
|
+
* reads as something else, such as link markup (`[[<<-- Back|Prev]]`); the call inside it then
|
|
6217
|
+
* runs. The call ends where the outer tag ends.
|
|
6218
|
+
*/
|
|
6219
|
+
function innerPassageLink(matchTagAt, start) {
|
|
6220
|
+
const inner = matchTagAt(start)?.tag;
|
|
6221
|
+
const lexed = inner === void 0 ? void 0 : lexMacroArgs(inner.args);
|
|
6222
|
+
return inner === void 0 || lexed === void 0 ? void 0 : ownPassageLink(inner, lexed);
|
|
6223
|
+
}
|
|
6224
|
+
const REGION_OPEN_RE = /\/\/|\/\*|\/%|<!--|<[Ss][Cc][Rr][Ii][Pp][Tt]/g;
|
|
6225
|
+
const HAS_SCRIPT_OPEN_RE = /<[Ss][Cc][Rr][Ii][Pp][Tt]/;
|
|
6226
|
+
const SCRIPT_CLOSE_RE = /<\/[Ss][Cc][Rr][Ii][Pp][Tt]>|[\r\u2028\u2029]/g;
|
|
6227
|
+
const COMMENT_CLOSE_RES = {
|
|
6228
|
+
"/*": /\*\/|[\r\u2028\u2029]/g,
|
|
6229
|
+
"/%": /%\/|[\r\u2028\u2029]/g,
|
|
6230
|
+
"<!--": /-->|[\r\u2028\u2029]/g
|
|
6231
|
+
};
|
|
6232
|
+
/**
|
|
6233
|
+
* Returns a function that finds where a comment that opens at `start` ends, as SugarCube's
|
|
6234
|
+
* `commentByBlock` parser reads it, or `undefined` if it isn't closed. Calls of one kind must come
|
|
6235
|
+
* in increasing order: a search that fails stops at a line terminator or the end, and a later one
|
|
6236
|
+
* of the same kind that starts before that stop fails too, so it isn't made.
|
|
6237
|
+
*/
|
|
6238
|
+
function commentReader(text) {
|
|
6239
|
+
const failedUntil = /* @__PURE__ */ new Map();
|
|
6240
|
+
return (kind, start) => {
|
|
6241
|
+
if (start < (failedUntil.get(kind) ?? -1)) return;
|
|
6242
|
+
const re = COMMENT_CLOSE_RES[kind];
|
|
6243
|
+
re.lastIndex = start + kind.length;
|
|
6244
|
+
const m = re.exec(text);
|
|
6245
|
+
if (m !== null && m[0].length > 1) return m.index + m[0].length;
|
|
6246
|
+
failedUntil.set(kind, m === null ? text.length : m.index);
|
|
6247
|
+
};
|
|
6248
|
+
}
|
|
6249
|
+
/**
|
|
6250
|
+
* Returns a function that reads a `<script>` element whose opener starts at a position, as
|
|
6251
|
+
* SugarCube's `verbatimScriptTag` parser reads it: the opener up to its `>`, then the content
|
|
6252
|
+
* up to the first `<\/script>`. Calls must come in increasing order; each part of the text is
|
|
6253
|
+
* searched once.
|
|
6254
|
+
*/
|
|
6255
|
+
function scriptElementReader(text) {
|
|
6256
|
+
let angle = -1;
|
|
6257
|
+
let noAngleAfter = Infinity;
|
|
6258
|
+
let failedBefore = -1;
|
|
6259
|
+
let noCloseAfter = Infinity;
|
|
6260
|
+
return (start) => {
|
|
6261
|
+
if (start + 7 >= noAngleAfter) return {
|
|
6262
|
+
openerEnd: void 0,
|
|
6263
|
+
close: void 0
|
|
6264
|
+
};
|
|
6265
|
+
if (angle < start + 7) {
|
|
6266
|
+
angle = text.indexOf(">", start + 7);
|
|
6267
|
+
if (angle === -1) {
|
|
6268
|
+
noAngleAfter = start + 7;
|
|
6269
|
+
return {
|
|
6270
|
+
openerEnd: void 0,
|
|
6271
|
+
close: void 0
|
|
6272
|
+
};
|
|
6273
|
+
}
|
|
6274
|
+
}
|
|
6275
|
+
const openerEnd = angle + 1;
|
|
6276
|
+
if (openerEnd < failedBefore || openerEnd >= noCloseAfter) return {
|
|
6277
|
+
openerEnd,
|
|
6278
|
+
close: void 0
|
|
6279
|
+
};
|
|
6280
|
+
SCRIPT_CLOSE_RE.lastIndex = openerEnd;
|
|
6281
|
+
const m = SCRIPT_CLOSE_RE.exec(text);
|
|
6282
|
+
if (m === null) {
|
|
6283
|
+
noCloseAfter = openerEnd;
|
|
6284
|
+
return {
|
|
6285
|
+
openerEnd,
|
|
6286
|
+
close: void 0
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
if (m[0].length === 1) {
|
|
6290
|
+
failedBefore = m.index;
|
|
6291
|
+
return {
|
|
6292
|
+
openerEnd,
|
|
6293
|
+
close: void 0
|
|
6294
|
+
};
|
|
6295
|
+
}
|
|
6296
|
+
return {
|
|
6297
|
+
openerEnd,
|
|
6298
|
+
close: m.index
|
|
6299
|
+
};
|
|
6300
|
+
};
|
|
6301
|
+
}
|
|
6302
|
+
/**
|
|
6303
|
+
* Returns a function that finds the tag closing the `<<script>>` body that `opener` opens, as
|
|
6304
|
+
* SugarCube's `parseBody` finds it: scanning tags from every `<<` after the opener, without
|
|
6305
|
+
* skipping anything, for the first `<<\/script>>` or `<<endscript>>` that isn't matched by a
|
|
6306
|
+
* later `<<script>>`. (It doesn't reproduce parseBody's rejection of a closing tag with
|
|
6307
|
+
* arguments, nor its resuming inside the arguments of a tag whose name starts with `/` or
|
|
6308
|
+
* `end`.)
|
|
6309
|
+
*
|
|
6310
|
+
* The tags found from the start of the text are those parseBody finds from any opener among
|
|
6311
|
+
* them, so for such an opener the answer takes constant time. An opener the walk reached after
|
|
6312
|
+
* a comment may not be among them, when a tag that starts in the comment runs over it; then the
|
|
6313
|
+
* scan starts at the body and goes on until it finds the closer or reaches a tag of that list,
|
|
6314
|
+
* whose answers it then uses. All such scans in one reading of a passage, strings included, stop
|
|
6315
|
+
* once they have read four times as many characters as the passage has, plus a fixed allowance
|
|
6316
|
+
* (`budget`, see `readContext`); on input built to make them read more, the openers after that
|
|
6317
|
+
* are taken as unclosed. That keeps the time linear. (Exported for tests; the module is not part
|
|
6318
|
+
* of the package's API.)
|
|
6319
|
+
*/
|
|
6320
|
+
function scriptBodyCloser(text, budget) {
|
|
6321
|
+
const tags = findMacroTags(text);
|
|
6322
|
+
const indexByStart = new Map(tags.map((tag, index) => [tag.start, index]));
|
|
6323
|
+
const lastClose = text.lastIndexOf(">>");
|
|
6324
|
+
const spend = (characters) => {
|
|
6325
|
+
budget.left -= characters;
|
|
6326
|
+
};
|
|
6327
|
+
const step = (tag) => tag.name === "script" ? 1 : tag.name === "/script" || tag.name === "endscript" ? -1 : 0;
|
|
6328
|
+
const balance = [0];
|
|
6329
|
+
tags.forEach((tag, k) => balance.push((balance[k] ?? 0) + step(tag)));
|
|
6330
|
+
const firstBelow = [];
|
|
6331
|
+
const candidates = [];
|
|
6332
|
+
for (let k = balance.length - 1; k >= 0; k--) {
|
|
6333
|
+
const here = balance[k] ?? 0;
|
|
6334
|
+
let top = candidates[candidates.length - 1];
|
|
6335
|
+
while (top !== void 0 && (balance[top] ?? 0) >= here) {
|
|
6336
|
+
candidates.pop();
|
|
6337
|
+
top = candidates[candidates.length - 1];
|
|
6338
|
+
}
|
|
6339
|
+
firstBelow[k] = top;
|
|
6340
|
+
candidates.push(k);
|
|
6341
|
+
}
|
|
6342
|
+
/**
|
|
6343
|
+
* The closer when `open` bodies are open just before `tags[index]`: each hop to
|
|
6344
|
+
* `firstBelow` closes one. `open` is at most one more than the openers scanned to get here.
|
|
6345
|
+
*/
|
|
6346
|
+
const closeFrom = (index, open) => {
|
|
6347
|
+
let k = index;
|
|
6348
|
+
for (let left = open; left > 0 && k !== void 0; left--) k = firstBelow[k];
|
|
6349
|
+
return k === void 0 ? void 0 : tags[k - 1];
|
|
6350
|
+
};
|
|
6351
|
+
return (opener) => {
|
|
6352
|
+
const index = indexByStart.get(opener.start);
|
|
6353
|
+
if (index !== void 0) return closeFrom(index + 1, 1);
|
|
6354
|
+
const matchTagAt = tagMatcher(text, lastClose, spend);
|
|
6355
|
+
let open = 1;
|
|
6356
|
+
let from = opener.end;
|
|
6357
|
+
while (budget.left >= 0) {
|
|
6358
|
+
const start = text.indexOf("<<", from);
|
|
6359
|
+
spend((start === -1 ? text.length : start) - from);
|
|
6360
|
+
if (start === -1) return;
|
|
6361
|
+
const known = indexByStart.get(start);
|
|
6362
|
+
if (known !== void 0) return closeFrom(known, open);
|
|
6363
|
+
const tag = matchTagAt(start)?.tag;
|
|
6364
|
+
if (tag === void 0) {
|
|
6365
|
+
from = start + 2;
|
|
6366
|
+
continue;
|
|
6367
|
+
}
|
|
6368
|
+
open += step(tag);
|
|
6369
|
+
if (open === 0) return tag;
|
|
6370
|
+
from = tag.end;
|
|
6371
|
+
}
|
|
6372
|
+
};
|
|
6373
|
+
}
|
|
6374
|
+
/** Adds `more` to the end of `links`, without spreading, which fails for very long lists. */
|
|
6375
|
+
function append(links, more) {
|
|
6376
|
+
for (const link of more) links.push(link);
|
|
6377
|
+
}
|
|
6378
|
+
/**
|
|
6379
|
+
* The passages that `<<link>>` and `<<goto>>` calls in passage markup name. Calls whose passage
|
|
6380
|
+
* is known only in play (a variable or an expression) are left out. A tag's own passage comes
|
|
6381
|
+
* before those of the calls inside its arguments' strings.
|
|
6382
|
+
*
|
|
6383
|
+
* The markup is walked in order, as SugarCube's wikifier walks it, with a tag tried at each
|
|
6384
|
+
* `<<` the walk reaches. Of the wikifier's other parsers, only those whose text holds no call
|
|
6385
|
+
* are told apart: comments (`/* … *` + `/`, `/% … %/`, `<!-- … -->`), whose calls never run,
|
|
6386
|
+
* and `<script>` elements and `<<script>>` bodies, which are JavaScript: only the calls in their
|
|
6387
|
+
* strings are read (see `findJavaScriptPassageLinks`). Everything else, verbatim text included,
|
|
6388
|
+
* is read as markup.
|
|
6389
|
+
*
|
|
6390
|
+
* Where this differs from SugarCube:
|
|
6391
|
+
* - a comment or a `<<script>>` body is skipped whole, even where `parseBody` ends a container
|
|
6392
|
+
* such as `<<if>>` at a closing tag inside it and the rest then runs;
|
|
6393
|
+
* - a `/*`, `<!--` or `<script` inside other markup that holds no call, such as link markup or a
|
|
6394
|
+
* `<style>` element, still starts a comment or an element;
|
|
6395
|
+
* - a `<<` in such other markup still starts a tag. When that tag runs over a `<<link` or
|
|
6396
|
+
* `<<goto` outside the parts of its arguments that the tag pattern reads as units, that call
|
|
6397
|
+
* is still read (see `innerPassageLink`), even where the tag's arguments would be JavaScript,
|
|
6398
|
+
* as the check always read it. One that such a part covers is lost: a quoted string, or a `//`
|
|
6399
|
+
* part up to a line feed (italics, a URL) when a `>>` follows later; and a `<<script>>` body
|
|
6400
|
+
* the tag runs over is read as markup. The same recovery also reads a call inside a stray tag
|
|
6401
|
+
* that SugarCube does see, and rejects as an unknown macro (`<<Back <<goto R>>`);
|
|
6402
|
+
* - a `<<script>>` body's end is found as `parseBody` finds it, except that parseBody rejects a
|
|
6403
|
+
* closing tag with arguments, and resumes inside the arguments of a tag whose name starts with
|
|
6404
|
+
* `/` or `end`;
|
|
6405
|
+
* - the lines of a `<<nobr>>` body, and of every passage when a story sets
|
|
6406
|
+
* `Config.passages.nobr`, are not joined before they are read (see `storyInspect` for passages
|
|
6407
|
+
* tagged `nobr`).
|
|
6408
|
+
*/
|
|
6409
|
+
function findMacroPassageLinks(text) {
|
|
6410
|
+
return markupPassageLinks(text, readContext(text));
|
|
6411
|
+
}
|
|
6412
|
+
function markupPassageLinks(text, context) {
|
|
6413
|
+
if (context.depth > MAX_STRING_DEPTH || !text.includes("<<") && !HAS_SCRIPT_OPEN_RE.test(text)) return [];
|
|
6414
|
+
const matchTagAt = tagMatcher(text);
|
|
6415
|
+
const matchInnerTagAt = tagMatcher(text);
|
|
6416
|
+
const readComment = commentReader(text);
|
|
6417
|
+
const readScriptElement = scriptElementReader(text);
|
|
6418
|
+
let findScriptCloser;
|
|
6419
|
+
const links = [];
|
|
6420
|
+
/** Reads the region `opener` opens, adding the calls in a `<script>` element; returns where the walk goes on. */
|
|
6421
|
+
const regionEnd = (opener) => {
|
|
6422
|
+
const start = opener.index;
|
|
6423
|
+
const kind = opener[0];
|
|
6424
|
+
switch (kind) {
|
|
6425
|
+
case "//": return start + 2;
|
|
6426
|
+
case "/*":
|
|
6427
|
+
case "/%":
|
|
6428
|
+
case "<!--": return readComment(kind, start) ?? start + kind.length;
|
|
6429
|
+
default: {
|
|
6430
|
+
const element = readScriptElement(start);
|
|
6431
|
+
if (element.openerEnd === void 0) return start + 1;
|
|
6432
|
+
if (element.close === void 0) return element.openerEnd;
|
|
6433
|
+
append(links, javaScriptPassageLinks(text.slice(element.openerEnd, element.close), context));
|
|
6434
|
+
return element.close + 9;
|
|
6435
|
+
}
|
|
6436
|
+
}
|
|
6437
|
+
};
|
|
6438
|
+
let macro = -2;
|
|
6439
|
+
let region;
|
|
6440
|
+
let pos = 0;
|
|
6441
|
+
for (;;) {
|
|
6442
|
+
if (macro !== -1 && macro < pos) macro = text.indexOf("<<", pos);
|
|
6443
|
+
if (region !== null && (region === void 0 || region.index < pos)) {
|
|
6444
|
+
REGION_OPEN_RE.lastIndex = pos;
|
|
6445
|
+
region = REGION_OPEN_RE.exec(text);
|
|
6446
|
+
}
|
|
6447
|
+
if (region !== null && (macro === -1 || region.index < macro)) {
|
|
6448
|
+
pos = regionEnd(region);
|
|
6449
|
+
continue;
|
|
6450
|
+
}
|
|
6451
|
+
if (macro === -1) return links;
|
|
6452
|
+
const scanned = matchTagAt(macro);
|
|
6453
|
+
if (scanned === void 0) {
|
|
6454
|
+
pos = macro + 2;
|
|
6455
|
+
continue;
|
|
6456
|
+
}
|
|
6457
|
+
const { tag, innerCall } = scanned;
|
|
6458
|
+
append(links, tagPassageLinks(tag, context));
|
|
6459
|
+
if (innerCall !== void 0) {
|
|
6460
|
+
const inner = innerPassageLink(matchInnerTagAt, innerCall);
|
|
6461
|
+
if (inner !== void 0) links.push(inner);
|
|
6462
|
+
}
|
|
6463
|
+
pos = tag.end;
|
|
6464
|
+
if (tag.name === "script") {
|
|
6465
|
+
findScriptCloser ??= scriptBodyCloser(text, context.budget);
|
|
6466
|
+
const closer = findScriptCloser(tag);
|
|
6467
|
+
if (closer !== void 0) {
|
|
6468
|
+
append(links, javaScriptPassageLinks(text.slice(tag.end, closer.start), context));
|
|
6469
|
+
pos = closer.end;
|
|
6470
|
+
}
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
}
|
|
6474
|
+
/**
|
|
6475
|
+
* The passages that `<<link>>` and `<<goto>>` calls in the strings of JavaScript source name,
|
|
6476
|
+
* such as a script passage or a `<<script>>` body: `$.wiki('<<goto "Room">>')`. Code outside
|
|
6477
|
+
* the strings isn't markup, so a call put together while the story plays, such as
|
|
6478
|
+
* `'<<goto "' + target + '">>'`, names no passage here.
|
|
6479
|
+
*/
|
|
6480
|
+
function findJavaScriptPassageLinks(source) {
|
|
6481
|
+
return javaScriptPassageLinks(source, readContext(source));
|
|
6482
|
+
}
|
|
6483
|
+
/**
|
|
6484
|
+
* What JavaScript source must contain for one of its string or template literals to hold a call
|
|
6485
|
+
* that is read. A value holding `<<`: each `<` in it is written as `<`, `\<`, `\x3c`, `\u003c` or
|
|
6486
|
+
* `\u{…}`, two can be next to each other with only line continuations between, and the first
|
|
6487
|
+
* is `<` or `\<` (then the source holds `<<` or `<\`) or one of the others. Or a value holding a
|
|
6488
|
+
* `<script>` element, whose own strings can make `<<` from escapes the outer string encodes.
|
|
6489
|
+
*/
|
|
6490
|
+
const MAY_HOLD_MACRO_RE = /<<|<\\|\\x3c|\\u003c|\\u\{|<script/i;
|
|
6491
|
+
function javaScriptPassageLinks(source, context) {
|
|
6492
|
+
if (context.depth >= MAX_STRING_DEPTH || !MAY_HOLD_MACRO_RE.test(source)) return [];
|
|
6493
|
+
const inner = deeper(context);
|
|
6494
|
+
const links = [];
|
|
6495
|
+
for (const value of javaScriptStrings(source)) append(links, markupPassageLinks(value, inner));
|
|
6496
|
+
return links;
|
|
6497
|
+
}
|
|
5352
6498
|
//#endregion
|
|
5353
6499
|
//#region src/inspect.ts
|
|
5354
6500
|
/**
|
|
@@ -5360,8 +6506,15 @@ function storyToJSON(story) {
|
|
|
5360
6506
|
* - `[[Display Text|PassageName]]` (Twine 1 / Harlowe pipe)
|
|
5361
6507
|
* - `<<goto "PassageName">>` (SugarCube macro)
|
|
5362
6508
|
* - `<<link "Display" "PassageName">>` (SugarCube macro)
|
|
6509
|
+
*
|
|
6510
|
+
* SugarCube macro arguments may be double- or single-quoted, with backslash escapes, or bare
|
|
6511
|
+
* words; a passage named by a variable or an expression is known only in play and is skipped.
|
|
6512
|
+
* Macro calls are also read inside the quoted strings of other macros' arguments, and inside the
|
|
6513
|
+
* strings of `<<script>>` bodies and `<script>` elements; calls in comments are not read. A
|
|
6514
|
+
* script passage (`isScript`) is JavaScript, so its macro calls are read only inside its strings.
|
|
6515
|
+
* See `sugarcube-macros.ts` for the details and the known differences from SugarCube.
|
|
5363
6516
|
*/
|
|
5364
|
-
function extractLinksFromText(text) {
|
|
6517
|
+
function extractLinksFromText(text, isScript) {
|
|
5365
6518
|
const targets = /* @__PURE__ */ new Set();
|
|
5366
6519
|
const wikiLinkRe = /\[\[([^\]]+)\]\]/g;
|
|
5367
6520
|
let m;
|
|
@@ -5379,12 +6532,36 @@ function extractLinksFromText(text) {
|
|
|
5379
6532
|
}
|
|
5380
6533
|
targets.add(content.trim());
|
|
5381
6534
|
}
|
|
5382
|
-
const
|
|
5383
|
-
|
|
5384
|
-
const linkMacroRe = /<<link\s+["'][^"']*["']\s+["']([^"']+)["']\s*>>/g;
|
|
5385
|
-
while ((m = linkMacroRe.exec(text)) !== null) targets.add(m[1]);
|
|
6535
|
+
const macroLinks = isScript ? findJavaScriptPassageLinks(text) : findMacroPassageLinks(text);
|
|
6536
|
+
for (const macro of ["goto", "link"]) for (const link of macroLinks) if (link.macro === macro) targets.add(link.passage);
|
|
5386
6537
|
return [...targets];
|
|
5387
6538
|
}
|
|
6539
|
+
/** Passages SugarCube runs from their raw text, never joining their lines. */
|
|
6540
|
+
const RAW_TEXT_PASSAGES = /* @__PURE__ */ new Set([
|
|
6541
|
+
"StoryInit",
|
|
6542
|
+
"PassageReady",
|
|
6543
|
+
"PassageDone"
|
|
6544
|
+
]);
|
|
6545
|
+
/**
|
|
6546
|
+
* A passage's text as SugarCube reads it for its links. A passage tagged `nobr` has its line
|
|
6547
|
+
* breaks joined (`Passage.processText`): leading and trailing ones removed, each run inside made
|
|
6548
|
+
* one space. Script passages, `init`-tagged passages, StoryInit, PassageReady and PassageDone are
|
|
6549
|
+
* run from their raw text, so they keep theirs.
|
|
6550
|
+
*/
|
|
6551
|
+
function textAsRead(p) {
|
|
6552
|
+
return hasTag(p, "nobr") && !hasTag(p, "script") && !hasTag(p, "init") && !RAW_TEXT_PASSAGES.has(p.name) ? joinLines(p.text) : p.text;
|
|
6553
|
+
}
|
|
6554
|
+
/**
|
|
6555
|
+
* What SugarCube's `text.replace(/^\n+|\n+$/g, '').replace(/\n+/g, ' ')` gives, without that
|
|
6556
|
+
* pattern's backtracking, which takes quadratic time on a long run of line feeds.
|
|
6557
|
+
*/
|
|
6558
|
+
function joinLines(text) {
|
|
6559
|
+
let start = 0;
|
|
6560
|
+
while (text[start] === "\n") start += 1;
|
|
6561
|
+
let end = text.length;
|
|
6562
|
+
while (end > start && text[end - 1] === "\n") end -= 1;
|
|
6563
|
+
return text.slice(start, end).replace(/\n+/g, " ");
|
|
6564
|
+
}
|
|
5388
6565
|
/**
|
|
5389
6566
|
* Inspect a story and return its full structural map.
|
|
5390
6567
|
*
|
|
@@ -5424,7 +6601,7 @@ function storyInspect(story) {
|
|
|
5424
6601
|
}
|
|
5425
6602
|
list.push(p.name);
|
|
5426
6603
|
}
|
|
5427
|
-
const targets = extractLinksFromText(p
|
|
6604
|
+
const targets = extractLinksFromText(textAsRead(p), hasTag(p, "script"));
|
|
5428
6605
|
links.set(p.name, targets);
|
|
5429
6606
|
}
|
|
5430
6607
|
const brokenLinks = [];
|
|
@@ -5595,6 +6772,7 @@ function validateConfig(data) {
|
|
|
5595
6772
|
]) if (key in obj && typeof obj[key] !== "boolean") errors.push(`"${key}" must be a boolean.`);
|
|
5596
6773
|
for (const key of [
|
|
5597
6774
|
"sources",
|
|
6775
|
+
"exclude",
|
|
5598
6776
|
"formatPaths",
|
|
5599
6777
|
"formatIndices",
|
|
5600
6778
|
"formatUrls",
|