@rohal12/twee-ts 1.18.1 → 1.18.2
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 +132 -21
- package/dist/bin/twee-ts.js.map +1 -1
- package/dist/index.cjs +131 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +132 -21
- package/dist/index.js.map +1 -1
- package/dist/{paths-CUv1qfz-.js → paths-BuTAg2av.js} +141 -23
- package/dist/paths-BuTAg2av.js.map +1 -0
- package/dist/plugins/rollup.js +1 -1
- package/dist/plugins/vite.js +28 -10
- package/dist/plugins/vite.js.map +1 -1
- package/package.json +1 -1
- package/dist/paths-CUv1qfz-.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -158,8 +158,9 @@ interface Story {
|
|
|
158
158
|
twine1: Twine1Metadata;
|
|
159
159
|
twine2: Twine2Metadata;
|
|
160
160
|
}
|
|
161
|
-
type ReadonlyPassage = Readonly<Passage
|
|
161
|
+
type ReadonlyPassage = Readonly<Omit<Passage, 'tags' | 'metadata'>> & {
|
|
162
162
|
readonly tags: readonly string[];
|
|
163
|
+
readonly metadata?: Readonly<PassageMetadata>;
|
|
163
164
|
};
|
|
164
165
|
type ReadonlyStory = Readonly<Omit<Story, 'passages' | 'twine1' | 'twine2'>> & {
|
|
165
166
|
readonly passages: readonly ReadonlyPassage[];
|
package/dist/index.d.ts
CHANGED
|
@@ -158,8 +158,9 @@ interface Story {
|
|
|
158
158
|
twine1: Twine1Metadata;
|
|
159
159
|
twine2: Twine2Metadata;
|
|
160
160
|
}
|
|
161
|
-
type ReadonlyPassage = Readonly<Passage
|
|
161
|
+
type ReadonlyPassage = Readonly<Omit<Passage, 'tags' | 'metadata'>> & {
|
|
162
162
|
readonly tags: readonly string[];
|
|
163
|
+
readonly metadata?: Readonly<PassageMetadata>;
|
|
163
164
|
};
|
|
164
165
|
type ReadonlyStory = Readonly<Omit<Story, 'passages' | 'twine1' | 'twine2'>> & {
|
|
165
166
|
readonly passages: readonly ReadonlyPassage[];
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as nodePath from "node:path";
|
|
2
2
|
import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
|
-
import { accessSync, chmodSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch as watch$1, writeFileSync } from "node:fs";
|
|
4
|
+
import { accessSync, chmodSync, constants, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, watch as watch$1, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
//#region src/ifid.ts
|
|
@@ -1599,6 +1599,89 @@ function stripSetupFunction(chunk) {
|
|
|
1599
1599
|
for (const match of chunk.matchAll(SETUP_FUNCTION_PROPERTY)) lastIndex = match.index;
|
|
1600
1600
|
return lastIndex === -1 ? void 0 : chunk.slice(0, lastIndex) + "}";
|
|
1601
1601
|
}
|
|
1602
|
+
/** Index after the whitespace and `//` or block comments starting at `i`. */
|
|
1603
|
+
function skipTrivia(text, i) {
|
|
1604
|
+
let pos = i;
|
|
1605
|
+
for (;;) {
|
|
1606
|
+
const ch = text[pos];
|
|
1607
|
+
if (ch !== void 0 && /\s/.test(ch)) pos++;
|
|
1608
|
+
else if (text.startsWith("//", pos)) {
|
|
1609
|
+
const newline = text.indexOf("\n", pos);
|
|
1610
|
+
pos = newline === -1 ? text.length : newline + 1;
|
|
1611
|
+
} else if (text.startsWith("/*", pos)) {
|
|
1612
|
+
const close = text.indexOf("*/", pos + 2);
|
|
1613
|
+
pos = close === -1 ? text.length : close + 2;
|
|
1614
|
+
} else return pos;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
/** Index after the quoted string starting at `i` (a quote character); template literals nest `${…}`. */
|
|
1618
|
+
function skipString(text, i) {
|
|
1619
|
+
const quote = text[i];
|
|
1620
|
+
let pos = i + 1;
|
|
1621
|
+
while (pos < text.length) {
|
|
1622
|
+
const ch = text[pos];
|
|
1623
|
+
if (ch === "\\") pos += 2;
|
|
1624
|
+
else if (ch === quote) return pos + 1;
|
|
1625
|
+
else if (quote === "`" && ch === "$" && text[pos + 1] === "{") {
|
|
1626
|
+
const close = findClosingBrace(text, pos + 1);
|
|
1627
|
+
if (close === -1) return text.length;
|
|
1628
|
+
pos = close + 1;
|
|
1629
|
+
} else pos++;
|
|
1630
|
+
}
|
|
1631
|
+
return text.length;
|
|
1632
|
+
}
|
|
1633
|
+
function startsComment(text, pos) {
|
|
1634
|
+
return text[pos] === "/" && (text[pos + 1] === "/" || text[pos + 1] === "*");
|
|
1635
|
+
}
|
|
1636
|
+
function startsString(text, pos) {
|
|
1637
|
+
const ch = text[pos];
|
|
1638
|
+
return ch === "\"" || ch === "'" || ch === "`";
|
|
1639
|
+
}
|
|
1640
|
+
/** Index of the `}` matching the `{` at `open`, skipping comments and strings; -1 when unbalanced. */
|
|
1641
|
+
function findClosingBrace(text, open) {
|
|
1642
|
+
let depth = 0;
|
|
1643
|
+
let pos = open;
|
|
1644
|
+
while (pos < text.length) if (startsComment(text, pos)) pos = skipTrivia(text, pos);
|
|
1645
|
+
else if (startsString(text, pos)) pos = skipString(text, pos);
|
|
1646
|
+
else {
|
|
1647
|
+
if (text[pos] === "{") depth++;
|
|
1648
|
+
else if (text[pos] === "}" && --depth === 0) return pos;
|
|
1649
|
+
pos++;
|
|
1650
|
+
}
|
|
1651
|
+
return -1;
|
|
1652
|
+
}
|
|
1653
|
+
const STORY_FORMAT = "storyFormat";
|
|
1654
|
+
const IDENTIFIER_CHAR = /[\w$]/;
|
|
1655
|
+
/**
|
|
1656
|
+
* Locate the object literal passed to `storyFormat(` as `[start, end)` offsets, ignoring comments and
|
|
1657
|
+
* strings outside it and never evaluating anything. Without such a call, the first `{` outside comments
|
|
1658
|
+
* and strings starts the object. When its closing brace cannot be matched (a Harlowe setup function can
|
|
1659
|
+
* hold regular expression literals), the last `}` of the file ends it.
|
|
1660
|
+
*/
|
|
1661
|
+
function locateFormatObject(text) {
|
|
1662
|
+
let start = -1;
|
|
1663
|
+
let firstBrace = -1;
|
|
1664
|
+
let pos = 0;
|
|
1665
|
+
while (pos < text.length && start === -1) if (startsComment(text, pos)) pos = skipTrivia(text, pos);
|
|
1666
|
+
else if (startsString(text, pos)) pos = skipString(text, pos);
|
|
1667
|
+
else if (text[pos] === "{") {
|
|
1668
|
+
if (firstBrace === -1) firstBrace = pos;
|
|
1669
|
+
pos++;
|
|
1670
|
+
} else if (text.startsWith(STORY_FORMAT, pos) && !IDENTIFIER_CHAR.test(text[pos - 1] ?? "")) {
|
|
1671
|
+
const paren = skipTrivia(text, pos + 11);
|
|
1672
|
+
const brace = text[paren] === "(" ? skipTrivia(text, paren + 1) : -1;
|
|
1673
|
+
if (text[brace] === "{") start = brace;
|
|
1674
|
+
pos += 11;
|
|
1675
|
+
} else pos++;
|
|
1676
|
+
if (start === -1) start = firstBrace;
|
|
1677
|
+
if (start === -1) return void 0;
|
|
1678
|
+
const close = findClosingBrace(text, start);
|
|
1679
|
+
const end = close === -1 ? text.lastIndexOf("}") : close;
|
|
1680
|
+
return end < start ? void 0 : {
|
|
1681
|
+
start,
|
|
1682
|
+
end: end + 1
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1602
1685
|
/**
|
|
1603
1686
|
* Parse the format object at `start`–`end` of `text`: strict JSON first (fast), then the
|
|
1604
1687
|
* JavaScript literal subset formats use. Error positions point into `text`.
|
|
@@ -1669,15 +1752,14 @@ function toFormatJSON(raw) {
|
|
|
1669
1752
|
* `harlowe-3` directory or a direct download.
|
|
1670
1753
|
*/
|
|
1671
1754
|
function decodeFormatJSON(source) {
|
|
1672
|
-
const
|
|
1673
|
-
|
|
1674
|
-
if (first === -1 || last < first) return {
|
|
1755
|
+
const located = locateFormatObject(source);
|
|
1756
|
+
if (located === void 0) return {
|
|
1675
1757
|
ok: false,
|
|
1676
1758
|
reason: "Could not find Twine 2 style story format JSON chunk."
|
|
1677
1759
|
};
|
|
1678
|
-
const parsed = parseFormatObject(source,
|
|
1760
|
+
const parsed = parseFormatObject(source, located.start, located.end);
|
|
1679
1761
|
if (parsed.ok) return toFormatJSON(parsed.value);
|
|
1680
|
-
const stripped = stripSetupFunction(source.slice(
|
|
1762
|
+
const stripped = stripSetupFunction(source.slice(located.start, located.end));
|
|
1681
1763
|
const retried = stripped === void 0 ? void 0 : parseFormatObject(stripped, 0, stripped.length);
|
|
1682
1764
|
if (retried?.ok) return toFormatJSON(retried.value);
|
|
1683
1765
|
return {
|
|
@@ -1981,16 +2063,17 @@ function readFormatSource(format, diagnostics) {
|
|
|
1981
2063
|
/**
|
|
1982
2064
|
* Writes `data` to `path` so that a reader sees either the previous file or the complete new
|
|
1983
2065
|
* one, never a partly written file. The data goes to a temporary file in the same folder,
|
|
1984
|
-
* which is then renamed over `path`. When `path` is a symlink, the file it
|
|
1985
|
-
* replaced, and an existing file keeps its permissions.
|
|
2066
|
+
* which is then renamed over `path`. When `path` is a symlink (even a dangling one, or a chain of links), the file it
|
|
2067
|
+
* finally points to is replaced or created and the link stays, and an existing file keeps its permissions.
|
|
1986
2068
|
*
|
|
1987
2069
|
* On failure (a full disk, a missing folder) the temporary file is removed, `path` is left
|
|
1988
2070
|
* as it was, and the error is thrown with `path` in its message and its original `code`.
|
|
1989
2071
|
*/
|
|
1990
2072
|
function writeFileAtomic(path, data) {
|
|
1991
|
-
|
|
1992
|
-
const temp = join(dirname(target), `.${basename(target)}.${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
|
|
2073
|
+
let temp;
|
|
1993
2074
|
try {
|
|
2075
|
+
const target = resolveWriteTarget(path);
|
|
2076
|
+
temp = join(dirname(target), `.${basename(target)}.${process.pid}-${randomBytes(6).toString("hex")}.tmp`);
|
|
1994
2077
|
writeFileSync(temp, data, {
|
|
1995
2078
|
encoding: "utf-8",
|
|
1996
2079
|
flag: "wx"
|
|
@@ -1998,17 +2081,25 @@ function writeFileAtomic(path, data) {
|
|
|
1998
2081
|
keepPermissions(target, temp);
|
|
1999
2082
|
renameSync(temp, target);
|
|
2000
2083
|
} catch (e) {
|
|
2001
|
-
rmSync(temp, { force: true });
|
|
2084
|
+
if (temp !== void 0) rmSync(temp, { force: true });
|
|
2002
2085
|
throw writeError(path, e);
|
|
2003
2086
|
}
|
|
2004
2087
|
}
|
|
2005
|
-
/**
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2088
|
+
/** Links followed before a path counts as a cycle (Linux stops at 40 as well). */
|
|
2089
|
+
const MAX_LINK_DEPTH = 40;
|
|
2090
|
+
/**
|
|
2091
|
+
* The file a symlink at `path` finally points to, or `path` itself when it is no link. Links are
|
|
2092
|
+
* followed one by one, so a link whose target does not exist yet (absolute, relative, or a chain
|
|
2093
|
+
* of them) still resolves to the file that should be created. A cycle or an unreadable link throws.
|
|
2094
|
+
*/
|
|
2095
|
+
function resolveWriteTarget(path) {
|
|
2096
|
+
let current = path;
|
|
2097
|
+
for (let depth = 0; depth <= MAX_LINK_DEPTH; depth++) {
|
|
2098
|
+
const stat = lstatSync(current, { throwIfNoEntry: false });
|
|
2099
|
+
if (stat === void 0 || !stat.isSymbolicLink()) return current;
|
|
2100
|
+
current = resolve(dirname(current), readlinkSync(current));
|
|
2011
2101
|
}
|
|
2102
|
+
throw Object.assign(/* @__PURE__ */ new Error(`ELOOP: too many levels of symbolic links, ${path}`), { code: "ELOOP" });
|
|
2012
2103
|
}
|
|
2013
2104
|
/**
|
|
2014
2105
|
* Gives `temp` the permission bits of `target`, when `target` exists. Best effort: on a file
|
|
@@ -2350,6 +2441,16 @@ async function resolveFormatUrls(request, urls, options = {}) {
|
|
|
2350
2441
|
}
|
|
2351
2442
|
if (lastError) throw lastError;
|
|
2352
2443
|
}
|
|
2444
|
+
/**
|
|
2445
|
+
* Find the best format among the cached copies of direct format URLs, without network access.
|
|
2446
|
+
* Matching follows {@link selectFormatCandidate}, so `allowOlder` admits a same-major older version.
|
|
2447
|
+
* Each URL keeps its own copy; the shared name and version downloads are never consulted.
|
|
2448
|
+
*
|
|
2449
|
+
* Internal, for format resolution; not part of the public API.
|
|
2450
|
+
*/
|
|
2451
|
+
function findCachedUrlFormat(request, urls, options = {}) {
|
|
2452
|
+
return selectFormatCandidate(request, urls.flatMap((url) => getCachedDirectFormat(url) ?? []), (f) => f, options);
|
|
2453
|
+
}
|
|
2353
2454
|
/** Whether `info` is a format the request accepts. */
|
|
2354
2455
|
function answers(request, info) {
|
|
2355
2456
|
return selectFormatCandidate(request, [info], (f) => f) !== void 0;
|
|
@@ -2602,8 +2703,8 @@ function findLocalFormat(all, pruned, request) {
|
|
|
2602
2703
|
* then format indices (unless `noRemote`). Names and IDs match the same way in each (see
|
|
2603
2704
|
* `selectFormatCandidate`), so a request that a local format answers never reaches the cache or the
|
|
2604
2705
|
* network. Local formats that cannot be used are reported as warnings. When a StoryData request
|
|
2605
|
-
* names a version that none of them has, an older version of
|
|
2606
|
-
*
|
|
2706
|
+
* names a version that none of them has, an older version of the same format and major version
|
|
2707
|
+
* is used with a warning, from local formats, the project's cached format URLs, or the shared cache. Anything else is an error diagnostic,
|
|
2607
2708
|
* and the result is `undefined`.
|
|
2608
2709
|
*/
|
|
2609
2710
|
async function resolveStoryFormat(request, options, diagnostics) {
|
|
@@ -2641,7 +2742,7 @@ async function resolveStoryFormat(request, options, diagnostics) {
|
|
|
2641
2742
|
if (remote) return remote;
|
|
2642
2743
|
}
|
|
2643
2744
|
if (request.kind === "name") {
|
|
2644
|
-
const older = selectFormatCandidate(request, rankedTwine2Formats(pruned), (f) => f, { allowOlder: true }) ?? findCachedFormat(request, { allowOlder: true });
|
|
2745
|
+
const older = selectFormatCandidate(request, rankedTwine2Formats(pruned), (f) => f, { allowOlder: true }) ?? findCachedUrlFormat(request, options.formatUrls ?? [], { allowOlder: true }) ?? findCachedFormat(request, { allowOlder: true });
|
|
2645
2746
|
if (older) {
|
|
2646
2747
|
diagnostics.push({
|
|
2647
2748
|
level: "warning",
|
|
@@ -6524,8 +6625,16 @@ const isLetter = (c) => c !== void 0 && /^[A-Za-z]$/.test(c);
|
|
|
6524
6625
|
* a full HTML tokenizer; it needs no more than the tags and where they end.
|
|
6525
6626
|
*/
|
|
6526
6627
|
function scanHeadTags(html) {
|
|
6628
|
+
const { closingHead, bodyStart } = scanTags$1(html);
|
|
6629
|
+
return {
|
|
6630
|
+
closingHead,
|
|
6631
|
+
bodyStart
|
|
6632
|
+
};
|
|
6633
|
+
}
|
|
6634
|
+
function scanTags$1(html) {
|
|
6527
6635
|
let closingHead;
|
|
6528
6636
|
let bodyStart;
|
|
6637
|
+
let headStartEnd;
|
|
6529
6638
|
let i = 0;
|
|
6530
6639
|
while (i < html.length) {
|
|
6531
6640
|
const lt = html.indexOf("<", i);
|
|
@@ -6544,13 +6653,15 @@ function scanHeadTags(html) {
|
|
|
6544
6653
|
} else if (isLetter(next)) {
|
|
6545
6654
|
const { name, end } = readTag(html, lt + 1);
|
|
6546
6655
|
if (name === "body" && bodyStart === void 0) bodyStart = lt;
|
|
6656
|
+
if (name === "head" && headStartEnd === void 0) headStartEnd = end;
|
|
6547
6657
|
i = RAW_TEXT_ELEMENTS.has(name) ? rawTextEnd(html, name, end) : end;
|
|
6548
6658
|
} else i = lt + 1;
|
|
6549
6659
|
if (closingHead !== void 0 && bodyStart !== void 0) break;
|
|
6550
6660
|
}
|
|
6551
6661
|
return {
|
|
6552
6662
|
closingHead,
|
|
6553
|
-
bodyStart
|
|
6663
|
+
bodyStart,
|
|
6664
|
+
headStartEnd
|
|
6554
6665
|
};
|
|
6555
6666
|
}
|
|
6556
6667
|
/** Read a tag whose name starts at `from`: its lowercase name, and the offset after its `>` (or the end). */
|