@rohal12/twee-ts 1.18.0 → 1.18.1
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 +229 -107
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/index.cjs +230 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +230 -107
- package/dist/index.js.map +1 -1
- package/dist/{paths-DgO9oo6G.js → paths-CUv1qfz-.js} +228 -97
- package/dist/paths-CUv1qfz-.js.map +1 -0
- package/dist/plugins/rollup.js +1 -1
- package/dist/plugins/vite.js +1 -1
- package/package.json +1 -1
- package/dist/paths-DgO9oo6G.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1600,10 +1600,9 @@ function readUTF8(filename, diagnostics) {
|
|
|
1600
1600
|
function readBase64(filename) {
|
|
1601
1601
|
return (0, node_fs.readFileSync)(filename).toString("base64");
|
|
1602
1602
|
}
|
|
1603
|
-
/** Get the filename without extension,
|
|
1603
|
+
/** Get the filename without extension, a dotfile keeps its whole name. */
|
|
1604
1604
|
function baseNameWithoutExt(filename) {
|
|
1605
|
-
|
|
1606
|
-
return name || ext;
|
|
1605
|
+
return (0, node_path.parse)(filename).name;
|
|
1607
1606
|
}
|
|
1608
1607
|
//#endregion
|
|
1609
1608
|
//#region src/formats.ts
|
|
@@ -1647,6 +1646,8 @@ function parseFormatObject(text, start, end) {
|
|
|
1647
1646
|
}
|
|
1648
1647
|
}
|
|
1649
1648
|
/** Check a parsed format object's fields. Per spec, `name` is optional; `version` and `source` are required. */
|
|
1649
|
+
/** The name given to a format.js that names no format. */
|
|
1650
|
+
const UNNAMED_FORMAT_NAME = "Untitled Story Format";
|
|
1650
1651
|
function toFormatJSON(raw) {
|
|
1651
1652
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return {
|
|
1652
1653
|
ok: false,
|
|
@@ -1666,7 +1667,7 @@ function toFormatJSON(raw) {
|
|
|
1666
1667
|
reason: `Story format version ${JSON.stringify(obj.version)} is not a SemVer version.`
|
|
1667
1668
|
};
|
|
1668
1669
|
const data = {
|
|
1669
|
-
name: typeof obj.name === "string" ? obj.name :
|
|
1670
|
+
name: typeof obj.name === "string" ? obj.name : UNNAMED_FORMAT_NAME,
|
|
1670
1671
|
version: obj.version,
|
|
1671
1672
|
source: obj.source,
|
|
1672
1673
|
proofing: obj.proofing === true
|
|
@@ -2142,31 +2143,37 @@ function clearIndexCache() {
|
|
|
2142
2143
|
/** Requests in progress, by what they fetch, so concurrent compiles make each request once. */
|
|
2143
2144
|
const sharedRequests = /* @__PURE__ */ new Map();
|
|
2144
2145
|
/**
|
|
2145
|
-
* Runs `start` once for all callers that ask for `key` while it is in progress.
|
|
2146
|
-
*
|
|
2147
|
-
*
|
|
2146
|
+
* Runs `start` once for all callers that ask for `key` while it is in progress. Each caller waits
|
|
2147
|
+
* under its own limits: a caller whose `signal` aborts, or whose `timeout` passes, stops waiting
|
|
2148
|
+
* at once and rejects, while the others keep waiting. The request itself is aborted (through
|
|
2149
|
+
* the signal `start` receives) once no caller waits on it. `start` must not depend on any one
|
|
2150
|
+
* caller: what differs between callers belongs after the shared result.
|
|
2148
2151
|
*/
|
|
2149
|
-
function shareRequest(key, signal, start) {
|
|
2152
|
+
function shareRequest(key, { signal, timeout, timedOut }, start) {
|
|
2150
2153
|
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
2151
2154
|
const request = sharedRequests.get(key) ?? startSharedRequest(key, start);
|
|
2152
2155
|
request.waiters++;
|
|
2153
2156
|
return new Promise((resolve, reject) => {
|
|
2154
2157
|
let waiting = true;
|
|
2158
|
+
let timer;
|
|
2155
2159
|
const stopWaiting = () => {
|
|
2156
2160
|
if (!waiting) return;
|
|
2157
2161
|
waiting = false;
|
|
2158
2162
|
signal?.removeEventListener("abort", onAbort);
|
|
2163
|
+
clearTimeout(timer);
|
|
2159
2164
|
request.waiters--;
|
|
2160
2165
|
};
|
|
2161
|
-
const
|
|
2166
|
+
const giveUp = (reason) => {
|
|
2162
2167
|
stopWaiting();
|
|
2163
2168
|
if (request.waiters === 0) {
|
|
2164
2169
|
if (sharedRequests.get(key) === request) sharedRequests.delete(key);
|
|
2165
|
-
request.controller.abort(
|
|
2170
|
+
request.controller.abort(reason);
|
|
2166
2171
|
}
|
|
2167
|
-
reject(
|
|
2172
|
+
reject(reason);
|
|
2168
2173
|
};
|
|
2174
|
+
const onAbort = () => giveUp(signal?.reason);
|
|
2169
2175
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2176
|
+
if (timeout > 0 && timeout <= MAX_TIMER_DELAY) timer = setTimeout(() => giveUp(timedOut()), timeout);
|
|
2170
2177
|
request.promise.then((value) => {
|
|
2171
2178
|
stopWaiting();
|
|
2172
2179
|
resolve(value);
|
|
@@ -2196,20 +2203,32 @@ function requestTimeout(options) {
|
|
|
2196
2203
|
if (!(timeout >= 0)) throw new RangeError(`A story format request timeout must be 0 or more milliseconds, not ${timeout}`);
|
|
2197
2204
|
return timeout;
|
|
2198
2205
|
}
|
|
2206
|
+
/** How one caller waits for `url`: under its own signal and timeout. `what` names the request in errors. */
|
|
2207
|
+
function waitOptions(options, what, url) {
|
|
2208
|
+
const timeout = requestTimeout(options);
|
|
2209
|
+
return {
|
|
2210
|
+
signal: options.signal,
|
|
2211
|
+
timeout,
|
|
2212
|
+
timedOut: () => /* @__PURE__ */ new Error(`Failed to ${what} from ${url}: timed out after ${timeout} ms`)
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2199
2215
|
/**
|
|
2200
|
-
* Fetch `url` as
|
|
2201
|
-
*
|
|
2216
|
+
* Fetch `url` as bytes, aborting when `signal` does. `what` names the request in errors, as in
|
|
2217
|
+
* "Failed to <what> from <url>". The bytes are returned undecoded, so checksums cover what was served.
|
|
2202
2218
|
*/
|
|
2203
|
-
async function
|
|
2204
|
-
const
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2219
|
+
async function fetchBytes(url, what, signal) {
|
|
2220
|
+
const res = await fetch(url, { signal });
|
|
2221
|
+
if (!res.ok) throw new Error(`Failed to ${what} from ${url}: ${res.status} ${res.statusText}`);
|
|
2222
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
2223
|
+
}
|
|
2224
|
+
/** Download a format.js once for every caller in this process that asks for `url` meanwhile. */
|
|
2225
|
+
function shareDownload(url, options) {
|
|
2226
|
+
return shareRequest(`download\0${url}`, waitOptions(options, "download format", url), (signal) => fetchBytes(url, "download format", signal));
|
|
2227
|
+
}
|
|
2228
|
+
/** Decode a downloaded format.js as local ones are (UTF-8, else Windows-1252), without a leading BOM. */
|
|
2229
|
+
function decodeDownload(bytes, url) {
|
|
2230
|
+
const { text } = decodeText(bytes, url);
|
|
2231
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
2213
2232
|
}
|
|
2214
2233
|
/** Validate that a JSON value is a valid SFAIndexEntry. */
|
|
2215
2234
|
function isValidEntry(val) {
|
|
@@ -2231,9 +2250,8 @@ async function fetchIndex(url, options = {}) {
|
|
|
2231
2250
|
options.signal?.throwIfAborted();
|
|
2232
2251
|
const cached = indexCache.get(url);
|
|
2233
2252
|
if (cached) return cached;
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
const text = await fetchText(url, "fetch format index", signal, timeout);
|
|
2253
|
+
return shareRequest(`index\0${url}`, waitOptions(options, "fetch format index", url), async (signal) => {
|
|
2254
|
+
const text = decodeDownload(await fetchBytes(url, "fetch format index", signal), url);
|
|
2237
2255
|
const data = validateSFAIndex(JSON.parse(text));
|
|
2238
2256
|
indexCache.set(url, data);
|
|
2239
2257
|
return data;
|
|
@@ -2270,26 +2288,37 @@ function cachedFormatInfo(id, filename, data) {
|
|
|
2270
2288
|
};
|
|
2271
2289
|
}
|
|
2272
2290
|
/**
|
|
2273
|
-
*
|
|
2274
|
-
*
|
|
2291
|
+
* Check that a downloaded format is the one the index entry promised. The name matches without
|
|
2292
|
+
* regard to case, as format requests do, and the version by SemVer precedence, so "1.0" and "v1.0.0"
|
|
2293
|
+
* agree. A format.js that names no format ({@link UNNAMED_FORMAT_NAME}) is allowed: such formats
|
|
2294
|
+
* exist, and the index entry then supplies the name it is cached under.
|
|
2295
|
+
*/
|
|
2296
|
+
function checkFormatIdentity(entry, data, downloadUrl) {
|
|
2297
|
+
if ((data.name === "Untitled Story Format" || data.name.toLowerCase() === entry.name.toLowerCase()) && sameVersion(data.version, entry.version)) return;
|
|
2298
|
+
throw new Error(`Story format mismatch for ${downloadUrl}: the index lists ${entry.name} ${entry.version}, but the download is ${data.name} ${data.version}`);
|
|
2299
|
+
}
|
|
2300
|
+
/**
|
|
2301
|
+
* Download a format, verify its checksum, check that it is the format the entry names, write it to
|
|
2302
|
+
* the cache shared by name and version, and return its StoryFormatInfo. Concurrent calls for one
|
|
2303
|
+
* download URL share one request for the bytes; each call then verifies them against its own
|
|
2304
|
+
* entry, within its own timeout.
|
|
2275
2305
|
*/
|
|
2276
2306
|
async function fetchAndCacheFormat(entry, downloadUrl, options = {}) {
|
|
2277
2307
|
cachedFormatDir(entry.name, entry.version);
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
const
|
|
2282
|
-
if (
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
});
|
|
2308
|
+
const bytes = await shareDownload(downloadUrl, options);
|
|
2309
|
+
const checksumKey = Object.keys(entry.checksums ?? {}).find((k) => k.endsWith("format.js"));
|
|
2310
|
+
if (checksumKey) {
|
|
2311
|
+
const expected = entry.checksums[checksumKey];
|
|
2312
|
+
if (!expected) throw new Error(`Missing checksum value for key "${checksumKey}"`);
|
|
2313
|
+
if (!await verifySHA256(bytes, expected)) throw new Error(`Checksum verification failed for ${entry.name} ${entry.version}`);
|
|
2314
|
+
}
|
|
2315
|
+
const text = decodeDownload(bytes, downloadUrl);
|
|
2316
|
+
const id = makeFormatId(entry.name, entry.version);
|
|
2317
|
+
const data = parseFormatJSON(text, id);
|
|
2318
|
+
if (!data) throw new Error(`Failed to parse format JSON from ${downloadUrl}`);
|
|
2319
|
+
checkFormatIdentity(entry, data, downloadUrl);
|
|
2320
|
+
options.signal?.throwIfAborted();
|
|
2321
|
+
return cachedFormatInfo(id, writeCacheEntry((0, node_path.resolve)(getCacheDir()), [entry.name, entry.version], text), data);
|
|
2293
2322
|
}
|
|
2294
2323
|
/**
|
|
2295
2324
|
* Download a direct format.js URL, parse its JSON, cache it under that URL (apart from the
|
|
@@ -2297,16 +2326,13 @@ async function fetchAndCacheFormat(entry, downloadUrl, options = {}) {
|
|
|
2297
2326
|
* one URL share one request.
|
|
2298
2327
|
*/
|
|
2299
2328
|
async function fetchDirectFormat(url, options = {}) {
|
|
2300
|
-
const
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
const formatPath = writeCacheEntry(getUrlCacheDir(), [urlCacheKey(url)], text);
|
|
2308
|
-
return cachedFormatInfo(makeFormatId(data.name, data.version), formatPath, data);
|
|
2309
|
-
});
|
|
2329
|
+
const text = decodeDownload(await shareDownload(url, options), url);
|
|
2330
|
+
const data = parseFormatJSON(text);
|
|
2331
|
+
if (!data) throw new Error(`Failed to parse format JSON from ${url}`);
|
|
2332
|
+
cachedFormatDir(data.name, data.version);
|
|
2333
|
+
options.signal?.throwIfAborted();
|
|
2334
|
+
const formatPath = writeCacheEntry(getUrlCacheDir(), [urlCacheKey(url)], text);
|
|
2335
|
+
return cachedFormatInfo(makeFormatId(data.name, data.version), formatPath, data);
|
|
2310
2336
|
}
|
|
2311
2337
|
/** The copy of a direct format URL in the download cache, if it has been downloaded before. */
|
|
2312
2338
|
function getCachedDirectFormat(url) {
|
|
@@ -2887,8 +2913,6 @@ function* tweeLexer(input) {
|
|
|
2887
2913
|
const items = ctx.drainItems();
|
|
2888
2914
|
for (const item of items) yield item;
|
|
2889
2915
|
}
|
|
2890
|
-
const remaining = ctx.drainItems();
|
|
2891
|
-
for (const item of remaining) yield item;
|
|
2892
2916
|
}
|
|
2893
2917
|
/** Convenience class wrapper for the lexer generator. */
|
|
2894
2918
|
var TweeLexer = class {
|
|
@@ -3041,9 +3065,7 @@ function parseTwee(source, options = {}) {
|
|
|
3041
3065
|
};
|
|
3042
3066
|
}
|
|
3043
3067
|
try {
|
|
3044
|
-
const
|
|
3045
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("expected a JSON object");
|
|
3046
|
-
const parsed = raw;
|
|
3068
|
+
const parsed = JSON.parse(item.val);
|
|
3047
3069
|
const meta = {};
|
|
3048
3070
|
for (const [key, value] of Object.entries(parsed)) if (typeof value === "string") meta[key] = value;
|
|
3049
3071
|
current.metadata = meta;
|
|
@@ -6297,8 +6319,8 @@ function loadFont(story, filename, diagnostics) {
|
|
|
6297
6319
|
* Load sources with mtime-based caching for incremental rebuilds.
|
|
6298
6320
|
* Always creates a fresh Story; cached passages are replayed via storyAdd().
|
|
6299
6321
|
*/
|
|
6300
|
-
function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles) {
|
|
6301
|
-
const currentFiles =
|
|
6322
|
+
function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles, cache, changedFiles, buildFiles) {
|
|
6323
|
+
const currentFiles = new Set(buildFiles);
|
|
6302
6324
|
const changedPaths = changedFiles === void 0 ? void 0 : new Set([...changedFiles].map((f) => (0, node_path.resolve)(f)));
|
|
6303
6325
|
for (const filename of filenames) {
|
|
6304
6326
|
if (processedFiles.has(filename)) {
|
|
@@ -6363,16 +6385,6 @@ function loadSourcesCached(story, filenames, opts, diagnostics, processedFiles,
|
|
|
6363
6385
|
}
|
|
6364
6386
|
//#endregion
|
|
6365
6387
|
//#region src/template.ts
|
|
6366
|
-
/**
|
|
6367
|
-
* An HTML closing head tag: `</head` in any letter case, followed by whitespace, `/` or `>`, up to the next `>`.
|
|
6368
|
-
* This is how the HTML tokenizer reads an end tag; it ignores whatever sits between the tag name and the `>`.
|
|
6369
|
-
*/
|
|
6370
|
-
const CLOSING_HEAD_TAG = "<\\/[Hh][Ee][Aa][Dd](?=[\\t\\n\\f\\r />])[^>]*>";
|
|
6371
|
-
/**
|
|
6372
|
-
* The start of an HTML body start tag: `<body` in any letter case, followed by whitespace, `/` or `>` (so not
|
|
6373
|
-
* `<bodyx>`). Only the `<body` is matched; the attributes and `>` after it are left alone.
|
|
6374
|
-
*/
|
|
6375
|
-
const BODY_START_TAG = "<[Bb][Oo][Dd][Yy](?=[\\t\\n\\f\\r />])";
|
|
6376
6388
|
/** The regular expression source matching exactly `text`. */
|
|
6377
6389
|
function literal(text) {
|
|
6378
6390
|
return text.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
@@ -6398,6 +6410,8 @@ function fillTemplateParts(parts) {
|
|
|
6398
6410
|
return part.text.replace(pattern, (match, ...groups) => {
|
|
6399
6411
|
const slot = slots.find((_, i) => groups[i] !== void 0);
|
|
6400
6412
|
if (slot === void 0 || slot.occurrences === "first" && filled.has(slot)) return match;
|
|
6413
|
+
const offset = groups[slots.length];
|
|
6414
|
+
if (slot.accepts !== void 0 && typeof offset === "number" && !slot.accepts(part.text, offset)) return match;
|
|
6401
6415
|
filled.add(slot);
|
|
6402
6416
|
return slot.replacement(match);
|
|
6403
6417
|
});
|
|
@@ -6488,15 +6502,18 @@ function placeHead(content, texts, owner) {
|
|
|
6488
6502
|
slot: void 0,
|
|
6489
6503
|
diagnostics: []
|
|
6490
6504
|
};
|
|
6491
|
-
const
|
|
6492
|
-
|
|
6493
|
-
|
|
6505
|
+
const scans = texts.map((text) => ({
|
|
6506
|
+
text,
|
|
6507
|
+
tags: scanHeadTags(text)
|
|
6508
|
+
}));
|
|
6509
|
+
if (scans.some(({ tags }) => tags.closingHead !== void 0)) return {
|
|
6510
|
+
slot: beforeTag(CLOSING_HEAD_PREFIX, "closingHead", content),
|
|
6494
6511
|
diagnostics: []
|
|
6495
6512
|
};
|
|
6496
|
-
if (
|
|
6513
|
+
if (scans.some(({ tags }) => tags.bodyStart !== void 0)) {
|
|
6497
6514
|
const message = `${owner} has no closing head tag; the modules and head file were injected before its body start tag.`;
|
|
6498
6515
|
return {
|
|
6499
|
-
slot: beforeTag(
|
|
6516
|
+
slot: beforeTag(BODY_START_PREFIX, "bodyStart", content),
|
|
6500
6517
|
diagnostics: [{
|
|
6501
6518
|
level: "warning",
|
|
6502
6519
|
message
|
|
@@ -6511,12 +6528,98 @@ function placeHead(content, texts, owner) {
|
|
|
6511
6528
|
}]
|
|
6512
6529
|
};
|
|
6513
6530
|
}
|
|
6514
|
-
/**
|
|
6515
|
-
|
|
6531
|
+
/** `</head` in any letter case, followed by whitespace, `/` or `>`; the rest of the tag is left alone. */
|
|
6532
|
+
const CLOSING_HEAD_PREFIX = "<\\/[Hh][Ee][Aa][Dd](?=[\\t\\n\\f\\r />])";
|
|
6533
|
+
/** `<body` in any letter case, followed by whitespace, `/` or `>` (so not `<bodyx>`). */
|
|
6534
|
+
const BODY_START_PREFIX = "<[Bb][Oo][Dd][Yy](?=[\\t\\n\\f\\r />])";
|
|
6535
|
+
/** Elements whose content is text, so a tag written there is not a tag. */
|
|
6536
|
+
const RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set([
|
|
6537
|
+
"script",
|
|
6538
|
+
"style",
|
|
6539
|
+
"textarea",
|
|
6540
|
+
"title"
|
|
6541
|
+
]);
|
|
6542
|
+
const isSpace = (c) => c === " " || c === " " || c === "\n" || c === "\f" || c === "\r";
|
|
6543
|
+
const isLetter = (c) => c !== void 0 && /^[A-Za-z]$/.test(c);
|
|
6544
|
+
/**
|
|
6545
|
+
* Find the first closing head tag and body start tag that HTML would read as tags: not those in a comment, in
|
|
6546
|
+
* the text of a script, style, textarea or title element, or in a quoted attribute value. A small scanner, not
|
|
6547
|
+
* a full HTML tokenizer; it needs no more than the tags and where they end.
|
|
6548
|
+
*/
|
|
6549
|
+
function scanHeadTags(html) {
|
|
6550
|
+
let closingHead;
|
|
6551
|
+
let bodyStart;
|
|
6552
|
+
let i = 0;
|
|
6553
|
+
while (i < html.length) {
|
|
6554
|
+
const lt = html.indexOf("<", i);
|
|
6555
|
+
if (lt === -1) break;
|
|
6556
|
+
const next = html[lt + 1];
|
|
6557
|
+
if (html.startsWith("<!--", lt)) {
|
|
6558
|
+
const end = html.indexOf("-->", lt + 4);
|
|
6559
|
+
i = end === -1 ? html.length : end + 3;
|
|
6560
|
+
} else if (next === "!" || next === "?") {
|
|
6561
|
+
const end = html.indexOf(">", lt);
|
|
6562
|
+
i = end === -1 ? html.length : end + 1;
|
|
6563
|
+
} else if (next === "/" && isLetter(html[lt + 2])) {
|
|
6564
|
+
const { name, end } = readTag(html, lt + 2);
|
|
6565
|
+
if (name === "head" && closingHead === void 0) closingHead = lt;
|
|
6566
|
+
i = end;
|
|
6567
|
+
} else if (isLetter(next)) {
|
|
6568
|
+
const { name, end } = readTag(html, lt + 1);
|
|
6569
|
+
if (name === "body" && bodyStart === void 0) bodyStart = lt;
|
|
6570
|
+
i = RAW_TEXT_ELEMENTS.has(name) ? rawTextEnd(html, name, end) : end;
|
|
6571
|
+
} else i = lt + 1;
|
|
6572
|
+
if (closingHead !== void 0 && bodyStart !== void 0) break;
|
|
6573
|
+
}
|
|
6574
|
+
return {
|
|
6575
|
+
closingHead,
|
|
6576
|
+
bodyStart
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6579
|
+
/** Read a tag whose name starts at `from`: its lowercase name, and the offset after its `>` (or the end). */
|
|
6580
|
+
function readTag(html, from) {
|
|
6581
|
+
let i = from;
|
|
6582
|
+
while (i < html.length && !isSpace(html[i]) && html[i] !== "/" && html[i] !== ">") i++;
|
|
6583
|
+
const name = html.slice(from, i).toLowerCase();
|
|
6584
|
+
while (i < html.length && html[i] !== ">") {
|
|
6585
|
+
if (html[i] !== "=") {
|
|
6586
|
+
i++;
|
|
6587
|
+
continue;
|
|
6588
|
+
}
|
|
6589
|
+
i++;
|
|
6590
|
+
while (isSpace(html[i])) i++;
|
|
6591
|
+
const quote = html[i];
|
|
6592
|
+
if (quote === "\"" || quote === "'") {
|
|
6593
|
+
const close = html.indexOf(quote, i + 1);
|
|
6594
|
+
i = close === -1 ? html.length : close + 1;
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
return {
|
|
6598
|
+
name,
|
|
6599
|
+
end: Math.min(i + 1, html.length)
|
|
6600
|
+
};
|
|
6601
|
+
}
|
|
6602
|
+
/** The offset of the end tag that closes a raw text element, given where its text starts (or the end). */
|
|
6603
|
+
function rawTextEnd(html, name, from) {
|
|
6604
|
+
const close = new RegExp(`</${name}(?=[\\t\\n\\f\\r />])`, "gi");
|
|
6605
|
+
close.lastIndex = from;
|
|
6606
|
+
return close.exec(html)?.index ?? html.length;
|
|
6607
|
+
}
|
|
6608
|
+
/**
|
|
6609
|
+
* The slot that inserts `content` on its own line before the first real tag of the kind `which` names (see
|
|
6610
|
+
* `scanHeadTags()`), which is kept as written. A look-alike inside a script or comment is left alone.
|
|
6611
|
+
*/
|
|
6612
|
+
function beforeTag(pattern, which, content) {
|
|
6613
|
+
const cache = /* @__PURE__ */ new Map();
|
|
6614
|
+
const offsetIn = (text) => {
|
|
6615
|
+
if (!cache.has(text)) cache.set(text, scanHeadTags(text)[which]);
|
|
6616
|
+
return cache.get(text);
|
|
6617
|
+
};
|
|
6516
6618
|
return {
|
|
6517
|
-
pattern
|
|
6619
|
+
pattern,
|
|
6518
6620
|
occurrences: "first",
|
|
6519
|
-
replacement: (match) => `${content}\n${match}
|
|
6621
|
+
replacement: (match) => `${content}\n${match}`,
|
|
6622
|
+
accepts: (text, offset) => offsetIn(text) === offset
|
|
6520
6623
|
};
|
|
6521
6624
|
}
|
|
6522
6625
|
//#endregion
|
|
@@ -6962,6 +7065,7 @@ async function compile(options) {
|
|
|
6962
7065
|
*/
|
|
6963
7066
|
async function compileToFile(options) {
|
|
6964
7067
|
const result = await compileForOutputFile(options, options.outFile);
|
|
7068
|
+
options.signal?.throwIfAborted();
|
|
6965
7069
|
writeFileAtomic(options.outFile, result.output);
|
|
6966
7070
|
return result;
|
|
6967
7071
|
}
|
|
@@ -7123,34 +7227,61 @@ async function buildOutput(options, cache, changedFiles, outputs) {
|
|
|
7123
7227
|
const timeout = options.formatFetchTimeout;
|
|
7124
7228
|
if (timeout !== void 0 && !(timeout >= 0)) throw new TweeTsError(`formatFetchTimeout must be 0 or more milliseconds, not ${timeout}.`);
|
|
7125
7229
|
clearIndexCache();
|
|
7126
|
-
const
|
|
7127
|
-
const
|
|
7128
|
-
for (const source of options.sources)
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7230
|
+
const groups = [];
|
|
7231
|
+
const outputSources = [];
|
|
7232
|
+
for (const source of options.sources) {
|
|
7233
|
+
const last = groups[groups.length - 1];
|
|
7234
|
+
if (typeof source === "string") {
|
|
7235
|
+
if (last?.kind === "paths") last.paths.push(source);
|
|
7236
|
+
else groups.push({
|
|
7237
|
+
kind: "paths",
|
|
7238
|
+
paths: [source]
|
|
7239
|
+
});
|
|
7240
|
+
} else if (last?.kind === "inline") last.sources.push(source);
|
|
7241
|
+
else groups.push({
|
|
7242
|
+
kind: "inline",
|
|
7243
|
+
sources: [source]
|
|
7244
|
+
});
|
|
7245
|
+
}
|
|
7246
|
+
const walked = groups.map((group) => {
|
|
7247
|
+
if (group.kind === "inline") return group;
|
|
7248
|
+
const found = getFilenames(group.paths, written, options.exclude);
|
|
7249
|
+
diagnostics.push(...found.diagnostics);
|
|
7250
|
+
outputSources.push(...found.outputSources);
|
|
7251
|
+
return {
|
|
7252
|
+
kind: "files",
|
|
7253
|
+
filenames: found.filenames
|
|
7254
|
+
};
|
|
7255
|
+
});
|
|
7256
|
+
rejectOutputSources(outputSources, diagnostics);
|
|
7257
|
+
const buildFiles = new Set(walked.flatMap((group) => group.kind === "files" ? group.filenames : []));
|
|
7134
7258
|
const story = createStory();
|
|
7135
7259
|
const processedFiles = /* @__PURE__ */ new Set();
|
|
7136
|
-
if (
|
|
7260
|
+
for (const group of walked) if (group.kind === "inline") loadInlineSources(story, group.sources, {
|
|
7137
7261
|
trim,
|
|
7138
7262
|
twee2Compat
|
|
7139
|
-
}, diagnostics
|
|
7140
|
-
else
|
|
7263
|
+
}, diagnostics);
|
|
7264
|
+
else if (cache) loadSourcesCached(story, group.filenames, {
|
|
7265
|
+
trim,
|
|
7266
|
+
twee2Compat
|
|
7267
|
+
}, diagnostics, processedFiles, cache, changedFiles, buildFiles);
|
|
7268
|
+
else loadSources(story, group.filenames, {
|
|
7141
7269
|
trim,
|
|
7142
7270
|
twee2Compat
|
|
7143
7271
|
}, diagnostics, processedFiles);
|
|
7144
|
-
|
|
7272
|
+
if (cache && buildFiles.size === 0) loadSourcesCached(story, [], {
|
|
7145
7273
|
trim,
|
|
7146
7274
|
twee2Compat
|
|
7147
|
-
}, diagnostics);
|
|
7275
|
+
}, diagnostics, processedFiles, cache, changedFiles);
|
|
7148
7276
|
if (options.tagAliases) story.passages = applyTagAliases(story.passages, options.tagAliases);
|
|
7149
7277
|
let format;
|
|
7150
|
-
if (outputMode === "html")
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7278
|
+
if (outputMode === "html") {
|
|
7279
|
+
format = await resolveStoryFormat(formatRequestFor(options.formatId, story.twine2.format, story.twine2.formatVersion, DEFAULT_FORMAT_ID), {
|
|
7280
|
+
...options,
|
|
7281
|
+
noRemote
|
|
7282
|
+
}, diagnostics);
|
|
7283
|
+
options.signal?.throwIfAborted();
|
|
7284
|
+
}
|
|
7154
7285
|
if (options.startPassage) story.twine2.start = options.startPassage;
|
|
7155
7286
|
const startName = story.twine2.start || DEFAULT_START_NAME;
|
|
7156
7287
|
if (testMode) story.twine2.options.set("debug", true);
|
|
@@ -7202,6 +7333,7 @@ async function buildOutput(options, cache, changedFiles, outputs) {
|
|
|
7202
7333
|
...getStoryStats(story, options.wordCountMethod),
|
|
7203
7334
|
files: [...processedFiles]
|
|
7204
7335
|
};
|
|
7336
|
+
options.signal?.throwIfAborted();
|
|
7205
7337
|
return {
|
|
7206
7338
|
output,
|
|
7207
7339
|
story,
|
|
@@ -8967,22 +9099,13 @@ const foldKey = (key) => key.toLowerCase().replace(/[-_]/g, "");
|
|
|
8967
9099
|
*
|
|
8968
9100
|
* @param diagnostics Receives warnings that do not stop the config from loading: keys the config does not
|
|
8969
9101
|
* define, and a file that is not valid UTF-8 (it is read as Windows-1252).
|
|
8970
|
-
* @throws When the file is not valid JSON or fails {@link validateConfig}.
|
|
9102
|
+
* @throws When the file exists but cannot be read, is not valid JSON or fails {@link validateConfig}.
|
|
8971
9103
|
*/
|
|
8972
9104
|
function loadConfig(dir, diagnostics) {
|
|
8973
9105
|
const base = dir ?? process.cwd();
|
|
8974
9106
|
const configPath = (0, node_path.join)(base, CONFIG_FILENAME);
|
|
8975
9107
|
if (!(0, node_fs.existsSync)(configPath)) return null;
|
|
8976
|
-
|
|
8977
|
-
let raw;
|
|
8978
|
-
try {
|
|
8979
|
-
raw = readUTF8(configPath, readDiagnostics);
|
|
8980
|
-
} catch {
|
|
8981
|
-
return null;
|
|
8982
|
-
}
|
|
8983
|
-
const config = parseConfig(raw, configPath, readDiagnostics);
|
|
8984
|
-
diagnostics?.push(...readDiagnostics);
|
|
8985
|
-
return config;
|
|
9108
|
+
return loadConfigFile(configPath, diagnostics);
|
|
8986
9109
|
}
|
|
8987
9110
|
/**
|
|
8988
9111
|
* Load a config from a specific file path.
|