@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/index.cjs CHANGED
@@ -1622,6 +1622,89 @@ function stripSetupFunction(chunk) {
1622
1622
  for (const match of chunk.matchAll(SETUP_FUNCTION_PROPERTY)) lastIndex = match.index;
1623
1623
  return lastIndex === -1 ? void 0 : chunk.slice(0, lastIndex) + "}";
1624
1624
  }
1625
+ /** Index after the whitespace and `//` or block comments starting at `i`. */
1626
+ function skipTrivia(text, i) {
1627
+ let pos = i;
1628
+ for (;;) {
1629
+ const ch = text[pos];
1630
+ if (ch !== void 0 && /\s/.test(ch)) pos++;
1631
+ else if (text.startsWith("//", pos)) {
1632
+ const newline = text.indexOf("\n", pos);
1633
+ pos = newline === -1 ? text.length : newline + 1;
1634
+ } else if (text.startsWith("/*", pos)) {
1635
+ const close = text.indexOf("*/", pos + 2);
1636
+ pos = close === -1 ? text.length : close + 2;
1637
+ } else return pos;
1638
+ }
1639
+ }
1640
+ /** Index after the quoted string starting at `i` (a quote character); template literals nest `${…}`. */
1641
+ function skipString(text, i) {
1642
+ const quote = text[i];
1643
+ let pos = i + 1;
1644
+ while (pos < text.length) {
1645
+ const ch = text[pos];
1646
+ if (ch === "\\") pos += 2;
1647
+ else if (ch === quote) return pos + 1;
1648
+ else if (quote === "`" && ch === "$" && text[pos + 1] === "{") {
1649
+ const close = findClosingBrace(text, pos + 1);
1650
+ if (close === -1) return text.length;
1651
+ pos = close + 1;
1652
+ } else pos++;
1653
+ }
1654
+ return text.length;
1655
+ }
1656
+ function startsComment(text, pos) {
1657
+ return text[pos] === "/" && (text[pos + 1] === "/" || text[pos + 1] === "*");
1658
+ }
1659
+ function startsString(text, pos) {
1660
+ const ch = text[pos];
1661
+ return ch === "\"" || ch === "'" || ch === "`";
1662
+ }
1663
+ /** Index of the `}` matching the `{` at `open`, skipping comments and strings; -1 when unbalanced. */
1664
+ function findClosingBrace(text, open) {
1665
+ let depth = 0;
1666
+ let pos = open;
1667
+ while (pos < text.length) if (startsComment(text, pos)) pos = skipTrivia(text, pos);
1668
+ else if (startsString(text, pos)) pos = skipString(text, pos);
1669
+ else {
1670
+ if (text[pos] === "{") depth++;
1671
+ else if (text[pos] === "}" && --depth === 0) return pos;
1672
+ pos++;
1673
+ }
1674
+ return -1;
1675
+ }
1676
+ const STORY_FORMAT = "storyFormat";
1677
+ const IDENTIFIER_CHAR = /[\w$]/;
1678
+ /**
1679
+ * Locate the object literal passed to `storyFormat(` as `[start, end)` offsets, ignoring comments and
1680
+ * strings outside it and never evaluating anything. Without such a call, the first `{` outside comments
1681
+ * and strings starts the object. When its closing brace cannot be matched (a Harlowe setup function can
1682
+ * hold regular expression literals), the last `}` of the file ends it.
1683
+ */
1684
+ function locateFormatObject(text) {
1685
+ let start = -1;
1686
+ let firstBrace = -1;
1687
+ let pos = 0;
1688
+ while (pos < text.length && start === -1) if (startsComment(text, pos)) pos = skipTrivia(text, pos);
1689
+ else if (startsString(text, pos)) pos = skipString(text, pos);
1690
+ else if (text[pos] === "{") {
1691
+ if (firstBrace === -1) firstBrace = pos;
1692
+ pos++;
1693
+ } else if (text.startsWith(STORY_FORMAT, pos) && !IDENTIFIER_CHAR.test(text[pos - 1] ?? "")) {
1694
+ const paren = skipTrivia(text, pos + 11);
1695
+ const brace = text[paren] === "(" ? skipTrivia(text, paren + 1) : -1;
1696
+ if (text[brace] === "{") start = brace;
1697
+ pos += 11;
1698
+ } else pos++;
1699
+ if (start === -1) start = firstBrace;
1700
+ if (start === -1) return void 0;
1701
+ const close = findClosingBrace(text, start);
1702
+ const end = close === -1 ? text.lastIndexOf("}") : close;
1703
+ return end < start ? void 0 : {
1704
+ start,
1705
+ end: end + 1
1706
+ };
1707
+ }
1625
1708
  /**
1626
1709
  * Parse the format object at `start`–`end` of `text`: strict JSON first (fast), then the
1627
1710
  * JavaScript literal subset formats use. Error positions point into `text`.
@@ -1692,15 +1775,14 @@ function toFormatJSON(raw) {
1692
1775
  * `harlowe-3` directory or a direct download.
1693
1776
  */
1694
1777
  function decodeFormatJSON(source) {
1695
- const first = source.indexOf("{");
1696
- const last = source.lastIndexOf("}");
1697
- if (first === -1 || last < first) return {
1778
+ const located = locateFormatObject(source);
1779
+ if (located === void 0) return {
1698
1780
  ok: false,
1699
1781
  reason: "Could not find Twine 2 style story format JSON chunk."
1700
1782
  };
1701
- const parsed = parseFormatObject(source, first, last + 1);
1783
+ const parsed = parseFormatObject(source, located.start, located.end);
1702
1784
  if (parsed.ok) return toFormatJSON(parsed.value);
1703
- const stripped = stripSetupFunction(source.slice(first, last + 1));
1785
+ const stripped = stripSetupFunction(source.slice(located.start, located.end));
1704
1786
  const retried = stripped === void 0 ? void 0 : parseFormatObject(stripped, 0, stripped.length);
1705
1787
  if (retried?.ok) return toFormatJSON(retried.value);
1706
1788
  return {
@@ -2004,16 +2086,17 @@ function readFormatSource(format, diagnostics) {
2004
2086
  /**
2005
2087
  * Writes `data` to `path` so that a reader sees either the previous file or the complete new
2006
2088
  * one, never a partly written file. The data goes to a temporary file in the same folder,
2007
- * which is then renamed over `path`. When `path` is a symlink, the file it points to is
2008
- * replaced, and an existing file keeps its permissions.
2089
+ * which is then renamed over `path`. When `path` is a symlink (even a dangling one, or a chain of links), the file it
2090
+ * finally points to is replaced or created and the link stays, and an existing file keeps its permissions.
2009
2091
  *
2010
2092
  * On failure (a full disk, a missing folder) the temporary file is removed, `path` is left
2011
2093
  * as it was, and the error is thrown with `path` in its message and its original `code`.
2012
2094
  */
2013
2095
  function writeFileAtomic(path, data) {
2014
- const target = followSymlink(path);
2015
- const temp = (0, node_path.join)((0, node_path.dirname)(target), `.${(0, node_path.basename)(target)}.${process.pid}-${(0, node_crypto.randomBytes)(6).toString("hex")}.tmp`);
2096
+ let temp;
2016
2097
  try {
2098
+ const target = resolveWriteTarget(path);
2099
+ temp = (0, node_path.join)((0, node_path.dirname)(target), `.${(0, node_path.basename)(target)}.${process.pid}-${(0, node_crypto.randomBytes)(6).toString("hex")}.tmp`);
2017
2100
  (0, node_fs.writeFileSync)(temp, data, {
2018
2101
  encoding: "utf-8",
2019
2102
  flag: "wx"
@@ -2021,17 +2104,25 @@ function writeFileAtomic(path, data) {
2021
2104
  keepPermissions(target, temp);
2022
2105
  (0, node_fs.renameSync)(temp, target);
2023
2106
  } catch (e) {
2024
- (0, node_fs.rmSync)(temp, { force: true });
2107
+ if (temp !== void 0) (0, node_fs.rmSync)(temp, { force: true });
2025
2108
  throw writeError(path, e);
2026
2109
  }
2027
2110
  }
2028
- /** The file a symlink at `path` points to, or `path` itself (also when it doesn't exist yet). */
2029
- function followSymlink(path) {
2030
- try {
2031
- return (0, node_fs.realpathSync)(path);
2032
- } catch {
2033
- return path;
2111
+ /** Links followed before a path counts as a cycle (Linux stops at 40 as well). */
2112
+ const MAX_LINK_DEPTH = 40;
2113
+ /**
2114
+ * The file a symlink at `path` finally points to, or `path` itself when it is no link. Links are
2115
+ * followed one by one, so a link whose target does not exist yet (absolute, relative, or a chain
2116
+ * of them) still resolves to the file that should be created. A cycle or an unreadable link throws.
2117
+ */
2118
+ function resolveWriteTarget(path) {
2119
+ let current = path;
2120
+ for (let depth = 0; depth <= MAX_LINK_DEPTH; depth++) {
2121
+ const stat = (0, node_fs.lstatSync)(current, { throwIfNoEntry: false });
2122
+ if (stat === void 0 || !stat.isSymbolicLink()) return current;
2123
+ current = (0, node_path.resolve)((0, node_path.dirname)(current), (0, node_fs.readlinkSync)(current));
2034
2124
  }
2125
+ throw Object.assign(/* @__PURE__ */ new Error(`ELOOP: too many levels of symbolic links, ${path}`), { code: "ELOOP" });
2035
2126
  }
2036
2127
  /**
2037
2128
  * Gives `temp` the permission bits of `target`, when `target` exists. Best effort: on a file
@@ -2373,6 +2464,16 @@ async function resolveFormatUrls(request, urls, options = {}) {
2373
2464
  }
2374
2465
  if (lastError) throw lastError;
2375
2466
  }
2467
+ /**
2468
+ * Find the best format among the cached copies of direct format URLs, without network access.
2469
+ * Matching follows {@link selectFormatCandidate}, so `allowOlder` admits a same-major older version.
2470
+ * Each URL keeps its own copy; the shared name and version downloads are never consulted.
2471
+ *
2472
+ * Internal, for format resolution; not part of the public API.
2473
+ */
2474
+ function findCachedUrlFormat(request, urls, options = {}) {
2475
+ return selectFormatCandidate(request, urls.flatMap((url) => getCachedDirectFormat(url) ?? []), (f) => f, options);
2476
+ }
2376
2477
  /** Whether `info` is a format the request accepts. */
2377
2478
  function answers(request, info) {
2378
2479
  return selectFormatCandidate(request, [info], (f) => f) !== void 0;
@@ -2625,8 +2726,8 @@ function findLocalFormat(all, pruned, request) {
2625
2726
  * then format indices (unless `noRemote`). Names and IDs match the same way in each (see
2626
2727
  * `selectFormatCandidate`), so a request that a local format answers never reaches the cache or the
2627
2728
  * network. Local formats that cannot be used are reported as warnings. When a StoryData request
2628
- * names a version that none of them has, an older version of
2629
- * the same format and major version is used with a warning. Anything else is an error diagnostic,
2729
+ * names a version that none of them has, an older version of the same format and major version
2730
+ * is used with a warning, from local formats, the project's cached format URLs, or the shared cache. Anything else is an error diagnostic,
2630
2731
  * and the result is `undefined`.
2631
2732
  */
2632
2733
  async function resolveStoryFormat(request, options, diagnostics) {
@@ -2664,7 +2765,7 @@ async function resolveStoryFormat(request, options, diagnostics) {
2664
2765
  if (remote) return remote;
2665
2766
  }
2666
2767
  if (request.kind === "name") {
2667
- const older = selectFormatCandidate(request, rankedTwine2Formats(pruned), (f) => f, { allowOlder: true }) ?? findCachedFormat(request, { allowOlder: true });
2768
+ const older = selectFormatCandidate(request, rankedTwine2Formats(pruned), (f) => f, { allowOlder: true }) ?? findCachedUrlFormat(request, options.formatUrls ?? [], { allowOlder: true }) ?? findCachedFormat(request, { allowOlder: true });
2668
2769
  if (older) {
2669
2770
  diagnostics.push({
2670
2771
  level: "warning",
@@ -6547,8 +6648,16 @@ const isLetter = (c) => c !== void 0 && /^[A-Za-z]$/.test(c);
6547
6648
  * a full HTML tokenizer; it needs no more than the tags and where they end.
6548
6649
  */
6549
6650
  function scanHeadTags(html) {
6651
+ const { closingHead, bodyStart } = scanTags$1(html);
6652
+ return {
6653
+ closingHead,
6654
+ bodyStart
6655
+ };
6656
+ }
6657
+ function scanTags$1(html) {
6550
6658
  let closingHead;
6551
6659
  let bodyStart;
6660
+ let headStartEnd;
6552
6661
  let i = 0;
6553
6662
  while (i < html.length) {
6554
6663
  const lt = html.indexOf("<", i);
@@ -6567,13 +6676,15 @@ function scanHeadTags(html) {
6567
6676
  } else if (isLetter(next)) {
6568
6677
  const { name, end } = readTag(html, lt + 1);
6569
6678
  if (name === "body" && bodyStart === void 0) bodyStart = lt;
6679
+ if (name === "head" && headStartEnd === void 0) headStartEnd = end;
6570
6680
  i = RAW_TEXT_ELEMENTS.has(name) ? rawTextEnd(html, name, end) : end;
6571
6681
  } else i = lt + 1;
6572
6682
  if (closingHead !== void 0 && bodyStart !== void 0) break;
6573
6683
  }
6574
6684
  return {
6575
6685
  closingHead,
6576
- bodyStart
6686
+ bodyStart,
6687
+ headStartEnd
6577
6688
  };
6578
6689
  }
6579
6690
  /** Read a tag whose name starts at `from`: its lowercase name, and the offset after its `>` (or the end). */