@spintax/core 0.11.0 → 0.13.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/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/index.cjs +65 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +65 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,84 @@
|
|
|
3
3
|
All notable changes to `@spintax/core` are documented here. This project adheres to
|
|
4
4
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## 0.13.0 — 2026-10-19
|
|
7
|
+
|
|
8
|
+
A minor: the post-process moves rendered text for a token that starts with a dot, for a bare `//`
|
|
9
|
+
scheme, and for a sentence opening on `i` under Turkish or Azerbaijani. No verdict changes and no API
|
|
10
|
+
change; `render()` already took the `locale` the post-process now also reads.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **A dot that starts a token is not a sentence end (#91).** `.NET`, a dotfile, a file extension were
|
|
15
|
+
taken for sentence punctuation: the space before the dot was removed, a space was inserted after it and
|
|
16
|
+
the token was capitalized — `and .NET` rendered `and. NET`, `the .apk file` rendered `the. Apk file`.
|
|
17
|
+
A token that begins with a dot — `.NET`, `.gitignore`, `.env.local`, `.tar.gz`, `.xlsx`, `.5` — is now
|
|
18
|
+
shielded like a domain or a URL when it follows the text start, whitespace, an opening bracket, a quote,
|
|
19
|
+
a tag or a path separator (`src/.env`, `dir\.env`); trailing punctuation stays outside it (`.NET.`).
|
|
20
|
+
Unchanged: `word .` → `Word.`, `end . Next` → `End. Next`, `end.Next`, `ASP.NET`, `3.5`, and an ellipsis
|
|
21
|
+
glued to a word (`see...and` → `See... And`). The cost: `end .Next` — a sentence typed with the space on
|
|
22
|
+
the wrong side of the dot — is no longer repaired.
|
|
23
|
+
- **Turkish and Azerbaijani capital İ (#94).** Under `locale: 'tr'` or `'az'` (any region, `-` or `_`) a
|
|
24
|
+
sentence that opens on dotted `i` is capitalized to `İ`, not `I` — `istanbul` → `İstanbul`; dotless `ı`
|
|
25
|
+
still becomes `I`. An explicit mapping, not `toLocaleUpperCase`, so the result does not depend on the
|
|
26
|
+
host's ICU data. Other locales are unchanged.
|
|
27
|
+
- **A bare `//` scheme is a URL.** Prose that names the protocol — `(https://)` — got a space after its
|
|
28
|
+
colon: `(https: //)`. A `//` scheme with no body is now shielded like a URL, in any case (`HTTPS://`).
|
|
29
|
+
`mailto:` and `tel:` still need a body, so `Tel: 555` opening a sentence is capitalized and spaced as
|
|
30
|
+
before.
|
|
31
|
+
|
|
32
|
+
Measured before the change over the family's stored content (2 581 production templates in four
|
|
33
|
+
languages, 163 more in ten): every changed document is a fix — 21 renders of a published article that
|
|
34
|
+
read `the. Apk file`, 11 that read `(https: //)` — and nothing else moves.
|
|
35
|
+
|
|
36
|
+
### Corpus
|
|
37
|
+
|
|
38
|
+
424 cases (+16), all in `render-postprocess.json`: `postprocess/turkish-*`, `postprocess/azerbaijani-*`,
|
|
39
|
+
`postprocess/turkic-locale-case-and-underscore`, `postprocess/dotted-capital-only-under-turkic-locales`,
|
|
40
|
+
`postprocess/dot-token-*` (openers, punctuation, text and line start, a digit, UCP whitespace, both path
|
|
41
|
+
separators, and the negatives with the accepted cost), `postprocess/bare-scheme-*` and
|
|
42
|
+
`postprocess/bare-mailto-and-tel-are-not-uris`.
|
|
43
|
+
|
|
44
|
+
## 0.12.0 — 2026-10-06
|
|
45
|
+
|
|
46
|
+
A minor: rendered text moves for permutation separators in Arabic, Hebrew, Thai and Lao. No verdict
|
|
47
|
+
changes and no API change; `render()` already took the `locale` it now also reads here.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **An Arabic list can be written (#90).** The conjunction و ("and") — and ف ("and then") — is written
|
|
52
|
+
attached to the next word: `الكازينو والبث`, never `الكازينو و البث`. A letter-only separator was always
|
|
53
|
+
padded on both sides, whatever spaces the author wrote, so no shape of `lastsep` could produce it. Now,
|
|
54
|
+
under `locale: 'ar'`, a separator that is exactly و or ف keeps the space before it and none after when
|
|
55
|
+
the next element starts with an Arabic letter: `[<sep="، ";lastsep="و">…]` renders
|
|
56
|
+
`الكازينو، المراهنات الرياضية والبث المباشر`. Before anything else — a Latin brand name, a digit — both
|
|
57
|
+
spaces stay: `و Evolution`. Hebrew ו under `locale: 'he'` works the same way. The rule is keyed by
|
|
58
|
+
LANGUAGE, not script: Persian and Urdu write the same letter و as a word of its own, so under `fa` and
|
|
59
|
+
`ur` it is padded as before, and so is every word conjunction (أو, ثم, או) and و with no locale.
|
|
60
|
+
- **Thai and Lao separators join bare (#90),** as Han and kana have since 0.11.0: `[<lastsep="และ">แมว|สุนัข]`
|
|
61
|
+
renders `แมวและสุนัข`. Khmer and Myanmar take the same rule. A separator carrying a combining mark —
|
|
62
|
+
Thai `หรือ`, most Khmer and Myanmar words — is not all letters, so it was never padded and still renders
|
|
63
|
+
exactly as written, spaces included.
|
|
64
|
+
|
|
65
|
+
No template in the family's stored content uses any of these separators (measured over casino-platform,
|
|
66
|
+
content-gen, both sites and the plugin: the only hits were the probes that found the issue), so no
|
|
67
|
+
existing text changes.
|
|
68
|
+
|
|
69
|
+
### Corpus
|
|
70
|
+
|
|
71
|
+
408 cases (+22), all in `render-semantics.json`. `perm/sep-arabic-*` (و and ف attached, the author's
|
|
72
|
+
spaces, a regional locale, before a Latin word, a digit, an astral Arabic letter and U+0640, a word
|
|
73
|
+
conjunction, the issue's whole list with the post-process on), `perm/sep-persian-waw-padded`,
|
|
74
|
+
`perm/sep-urdu-waw-padded`, `perm/sep-waw-without-locale-padded`, `perm/sep-hebrew-*` (ו attached, before a Latin word, the word conjunction או), `perm/sep-thai-*`,
|
|
75
|
+
`perm/sep-lao-not-padded`, `perm/sep-khmer-not-padded`, `perm/sep-myanmar-not-padded`, and
|
|
76
|
+
`perm/sep-script-not-script-extensions`.
|
|
77
|
+
|
|
78
|
+
That last one pins a divergence 0.11.0 shipped in both PHP engines without anyone seeing it: PCRE2 10.40
|
|
79
|
+
and later read `\p{Han}` as Script_Extensions, so U+3006 〆, U+303C 〼, the kana repeat marks U+3031–3035
|
|
80
|
+
and the halfwidth sound marks U+FF9E/FF9F joined bare there and were padded here and in every port. The
|
|
81
|
+
PHP engines now exclude them, which is Script on any PCRE2 version. The same trap sat under the new Arabic
|
|
82
|
+
test (U+0640, tatweel); the next element is read by code point, not by UTF-16 unit.
|
|
83
|
+
|
|
6
84
|
## 0.11.0 — 2026-10-03
|
|
7
85
|
|
|
8
86
|
A minor: one verdict changes (a two-form `ar` plural is now an error) and rendered text moves for
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ parse, render, validate, extract, analyze, and neutralize spintax templates.
|
|
|
19
19
|
[`spintax-core`](https://pypi.org/project/spintax-core/) (Python), an Object Pascal engine,
|
|
20
20
|
[`Spintax.Core`](https://www.nuget.org/packages/Spintax.Core/) (.NET), and
|
|
21
21
|
the [Spintax WordPress plugin](https://wordpress.org/plugins/spintax/) are all held to the same
|
|
22
|
-
behavior contract by a **shared golden corpus of
|
|
22
|
+
behavior contract by a **shared golden corpus of 424 fixtures** — deterministic verdicts, plural
|
|
23
23
|
buckets, conditionals, `#set`/`#def` semantics and post-processing agree everywhere, so a
|
|
24
24
|
template is an asset you can move between runtimes, not a lock-in.
|
|
25
25
|
- **MIT** licensed.
|
package/dist/index.cjs
CHANGED
|
@@ -1389,6 +1389,37 @@ function trimFragment(fragment) {
|
|
|
1389
1389
|
if (fragment.lead === 0 && fragment.trail === 0) return fragment;
|
|
1390
1390
|
return new Window(fragment, fragment.lead, fragment.length - fragment.trail);
|
|
1391
1391
|
}
|
|
1392
|
+
function firstCodePointOf(fragment) {
|
|
1393
|
+
const hi = charCodeAtOf(fragment, 0);
|
|
1394
|
+
if (hi >= 55296 && hi <= 56319) {
|
|
1395
|
+
const lo = charCodeAtOf(fragment, 1);
|
|
1396
|
+
if (lo >= 56320 && lo <= 57343) return (hi - 55296) * 1024 + (lo - 56320) + 65536;
|
|
1397
|
+
}
|
|
1398
|
+
return hi;
|
|
1399
|
+
}
|
|
1400
|
+
function charCodeAtOf(fragment, index) {
|
|
1401
|
+
let piece = fragment;
|
|
1402
|
+
let at = index;
|
|
1403
|
+
for (; ; ) {
|
|
1404
|
+
if (at < 0 || at >= lengthOf(piece)) return -1;
|
|
1405
|
+
if (typeof piece === "string") return piece.charCodeAt(at);
|
|
1406
|
+
if (piece instanceof Window) {
|
|
1407
|
+
at += piece.from;
|
|
1408
|
+
piece = piece.base;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
let offset = 0;
|
|
1412
|
+
for (const child of piece.pieces) {
|
|
1413
|
+
const length = lengthOf(child);
|
|
1414
|
+
if (at < offset + length) {
|
|
1415
|
+
piece = child;
|
|
1416
|
+
at -= offset;
|
|
1417
|
+
break;
|
|
1418
|
+
}
|
|
1419
|
+
offset += length;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1392
1423
|
function flatten(fragment) {
|
|
1393
1424
|
if (typeof fragment === "string") return fragment;
|
|
1394
1425
|
const out = [];
|
|
@@ -1782,7 +1813,12 @@ function assemblePermutation(node, rendered, opts) {
|
|
|
1782
1813
|
max = Math.max(min, Math.min(max, total));
|
|
1783
1814
|
const pick = randomInt(opts.rng, min, max);
|
|
1784
1815
|
shuffle(elements, opts.rng);
|
|
1785
|
-
return joinWithSeparators(
|
|
1816
|
+
return joinWithSeparators(
|
|
1817
|
+
elements.slice(0, pick),
|
|
1818
|
+
config.sep,
|
|
1819
|
+
config.lastsep ?? config.sep,
|
|
1820
|
+
normalizeBaseLang(opts.locale)
|
|
1821
|
+
);
|
|
1786
1822
|
}
|
|
1787
1823
|
function shuffle(arr, rng) {
|
|
1788
1824
|
for (let i = arr.length - 1; i > 0; i -= 1) {
|
|
@@ -1792,7 +1828,7 @@ function shuffle(arr, rng) {
|
|
|
1792
1828
|
arr[j] = tmp;
|
|
1793
1829
|
}
|
|
1794
1830
|
}
|
|
1795
|
-
function joinWithSeparators(elements, globalSep, globalLastsep) {
|
|
1831
|
+
function joinWithSeparators(elements, globalSep, globalLastsep, lang) {
|
|
1796
1832
|
const count = elements.length;
|
|
1797
1833
|
if (count === 0) return "";
|
|
1798
1834
|
if (count === 1) return elements[0].text;
|
|
@@ -1800,14 +1836,23 @@ function joinWithSeparators(elements, globalSep, globalLastsep) {
|
|
|
1800
1836
|
for (let i = 1; i < count; i += 1) {
|
|
1801
1837
|
const el = elements[i];
|
|
1802
1838
|
const sep = el.sep ?? (i === count - 1 ? globalLastsep : globalSep);
|
|
1803
|
-
pieces.push(padSeparator(sep), el.text);
|
|
1839
|
+
pieces.push(padSeparator(sep, lang, el.text), el.text);
|
|
1804
1840
|
}
|
|
1805
1841
|
return joinFragments(pieces);
|
|
1806
1842
|
}
|
|
1807
|
-
var UNSPACED_SCRIPT_RE = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}
|
|
1808
|
-
|
|
1843
|
+
var UNSPACED_SCRIPT_RE = /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}ーー\p{Script=Thai}\p{Script=Lao}\p{Script=Khmer}\p{Script=Myanmar}]+$/u;
|
|
1844
|
+
var PROCLITICS = /* @__PURE__ */ new Map([
|
|
1845
|
+
["ar", { words: /* @__PURE__ */ new Set(["\u0648", "\u0641"]), letter: /^(?=\p{L})\p{Script=Arabic}$/u }],
|
|
1846
|
+
["he", { words: /* @__PURE__ */ new Set(["\u05D5"]), letter: /^(?=\p{L})\p{Script=Hebrew}$/u }]
|
|
1847
|
+
]);
|
|
1848
|
+
function padSeparator(sep, lang, next) {
|
|
1809
1849
|
const trimmed = phpTrim(sep);
|
|
1810
1850
|
if (trimmed === "") return sep;
|
|
1851
|
+
const proclitic = PROCLITICS.get(lang);
|
|
1852
|
+
if (proclitic !== void 0 && proclitic.words.has(trimmed)) {
|
|
1853
|
+
const first = firstCodePointOf(next);
|
|
1854
|
+
return first >= 0 && proclitic.letter.test(String.fromCodePoint(first)) ? ` ${trimmed}` : ` ${trimmed} `;
|
|
1855
|
+
}
|
|
1811
1856
|
if (/^\p{L}+$/u.test(trimmed) && !UNSPACED_SCRIPT_RE.test(trimmed)) return ` ${trimmed} `;
|
|
1812
1857
|
return sep;
|
|
1813
1858
|
}
|
|
@@ -1874,7 +1919,7 @@ var TLD_UPPER = "\\p{Lu}\\p{Lt}\\p{Lm}\\p{Lo}";
|
|
|
1874
1919
|
var TLD = `(?:${XN}[a-zA-Z0-9\\-\\u017F\\u212A]{2,59}|[${TLD_LOWER}][${TLD_LOWER}\\p{N}-]{1,62}|[${TLD_UPPER}][${TLD_UPPER}\\p{N}-]{1,62})`;
|
|
1875
1920
|
var DOMAIN_PART = `(?:${LABEL}\\.)+${TLD}`;
|
|
1876
1921
|
var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
|
|
1877
|
-
var URI_RE = new RegExp(`(?:(?:https?|ftp)
|
|
1922
|
+
var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/${URI_BODY}*|(?:mailto|tel):${URI_BODY}+)`, "giu");
|
|
1878
1923
|
var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
|
|
1879
1924
|
var DOMAIN_AT_RE = new RegExp(`${DOMAIN_PART}${WORD_BOUNDARY}`, "uy");
|
|
1880
1925
|
var DOMAIN_SCAN_RE = new RegExp(`${AFTER_NON_WORD}(?:(${DOMAIN_PART}${WORD_BOUNDARY})|(?:${LABEL}\\.)*${LABEL})`, "gu");
|
|
@@ -1923,12 +1968,15 @@ var SENTENCE_OPENERS = "\xBF\xA1";
|
|
|
1923
1968
|
var LEAD = `(?:<[^>]+>|[${SENTENCE_OPENERS}]|${S})*`;
|
|
1924
1969
|
var QUOTES = `"'\xAB\xBB\u2039\u203A\u201C\u201D\u2018\u2019`;
|
|
1925
1970
|
var CLOSER = `[)\\]]|[${QUOTES}]+(?=${S}|$|<|\\/?>|[.,;:!?\u2026)\\]\u2014\u2013])`;
|
|
1971
|
+
var DOT_TOKEN_RE = new RegExp(`(?<=^|${S}|[(\\[{>/\\\\${QUOTES}])\\.[\\p{L}\\p{N}][\\p{L}\\p{N}_.\\-]*`, "gu");
|
|
1926
1972
|
var SPACE_BEFORE_PUNCT_RE = new RegExp(`(?<!${S})${S}+([,;:!?.])`, "gu");
|
|
1927
1973
|
var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\p{Nd})(?!${S}|$|<|${CLOSER})`, "gu");
|
|
1928
1974
|
var SPACE_AFTER_SENTENCE_RE = new RegExp(`(?<![.!?])([.!?]+)(?![.!?])(?!\\p{Nd})(?!${S}|$|<|${CLOSER})`, "gu");
|
|
1929
1975
|
var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
|
|
1930
1976
|
var CAP_FIRST_RE = new RegExp(`^(${LEAD})(\\p{Ll})`, "u");
|
|
1931
1977
|
var up = (ch) => ch.toUpperCase();
|
|
1978
|
+
var upTurkic = (ch) => ch === "i" ? "\u0130" : ch.toUpperCase();
|
|
1979
|
+
var TURKIC_LANGS = /* @__PURE__ */ new Set(["tr", "az"]);
|
|
1932
1980
|
function indexLeads(text) {
|
|
1933
1981
|
const n = text.length;
|
|
1934
1982
|
const leadEnd = new Int32Array(n + 1);
|
|
@@ -1961,7 +2009,7 @@ function leadEndFrom(text, i, leads) {
|
|
|
1961
2009
|
}
|
|
1962
2010
|
return j < text.length ? leads().leadEnd[j] : j;
|
|
1963
2011
|
}
|
|
1964
|
-
function capitalizeAfter(text, lower, next, context) {
|
|
2012
|
+
function capitalizeAfter(text, lower, next, context, upper) {
|
|
1965
2013
|
const leads = () => {
|
|
1966
2014
|
if (context.leads === null || context.leads.leadEnd.length !== text.length + 1) context.leads = indexLeads(text);
|
|
1967
2015
|
return context.leads;
|
|
@@ -1976,7 +2024,7 @@ function capitalizeAfter(text, lower, next, context) {
|
|
|
1976
2024
|
if (cp === void 0) continue;
|
|
1977
2025
|
const ch = String.fromCodePoint(cp);
|
|
1978
2026
|
if (!lower.test(ch)) continue;
|
|
1979
|
-
out += text.slice(emitted, at) +
|
|
2027
|
+
out += text.slice(emitted, at) + upper(ch);
|
|
1980
2028
|
emitted = at + ch.length;
|
|
1981
2029
|
from = emitted;
|
|
1982
2030
|
}
|
|
@@ -2009,7 +2057,7 @@ var afterLineBreak = (text, from) => {
|
|
|
2009
2057
|
const at = text.indexOf("\n", from);
|
|
2010
2058
|
return at === -1 ? null : { leadStart: at + 1, resume: at + 1 };
|
|
2011
2059
|
};
|
|
2012
|
-
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
|
|
2060
|
+
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOTTOK", "DOM", "NUM", "ABBR"];
|
|
2013
2061
|
var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
|
|
2014
2062
|
function restore(text, input, placeholders) {
|
|
2015
2063
|
if (!input.includes("\0")) {
|
|
@@ -2044,7 +2092,8 @@ function restoreAsTheLoopDoes(text, placeholders) {
|
|
|
2044
2092
|
}
|
|
2045
2093
|
return out;
|
|
2046
2094
|
}
|
|
2047
|
-
function postProcess(input) {
|
|
2095
|
+
function postProcess(input, locale = "") {
|
|
2096
|
+
const upper = TURKIC_LANGS.has(normalizeBaseLang(locale)) ? upTurkic : up;
|
|
2048
2097
|
const placeholders = /* @__PURE__ */ new Map();
|
|
2049
2098
|
let counter = 0;
|
|
2050
2099
|
const store = (value, prefix) => {
|
|
@@ -2064,6 +2113,7 @@ function postProcess(input) {
|
|
|
2064
2113
|
(m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
|
|
2065
2114
|
);
|
|
2066
2115
|
text = shieldEmails(text, (m) => store(m, "EMAIL"));
|
|
2116
|
+
text = text.replace(DOT_TOKEN_RE, (m) => storeWithTrailingPunct(m, "DOTTOK"));
|
|
2067
2117
|
text = shieldDomains(text, (m) => store(m, "DOM"));
|
|
2068
2118
|
text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
|
|
2069
2119
|
text = text.replace(MULTI_ABBR_RE, (m) => store(m, "ABBR"));
|
|
@@ -2073,11 +2123,11 @@ function postProcess(input) {
|
|
|
2073
2123
|
text = text.replace(SPACE_AFTER_COMMA_RE, "$1 ");
|
|
2074
2124
|
text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
|
|
2075
2125
|
text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
|
|
2076
|
-
text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead +
|
|
2126
|
+
text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + upper(ch));
|
|
2077
2127
|
const leadContext = { leads: null };
|
|
2078
|
-
text = capitalizeAfter(text, LOWER_RE, afterSentenceEnd, leadContext);
|
|
2079
|
-
text = capitalizeAfter(text, LOWER_RE, afterBlockTag(), leadContext);
|
|
2080
|
-
text = capitalizeAfter(text, LOWER_RE, afterLineBreak, leadContext);
|
|
2128
|
+
text = capitalizeAfter(text, LOWER_RE, afterSentenceEnd, leadContext, upper);
|
|
2129
|
+
text = capitalizeAfter(text, LOWER_RE, afterBlockTag(), leadContext, upper);
|
|
2130
|
+
text = capitalizeAfter(text, LOWER_RE, afterLineBreak, leadContext, upper);
|
|
2081
2131
|
return restore(text, input, placeholders).trim();
|
|
2082
2132
|
}
|
|
2083
2133
|
|
|
@@ -2098,7 +2148,7 @@ function renderWith(input, rng, opts = {}) {
|
|
|
2098
2148
|
};
|
|
2099
2149
|
const ast = resolveAst(input);
|
|
2100
2150
|
let out = renderAst(ast, ctx);
|
|
2101
|
-
if (opts.postProcess !== false) out = postProcess(out);
|
|
2151
|
+
if (opts.postProcess !== false) out = postProcess(out, ctx.locale);
|
|
2102
2152
|
return safetyRestore(out);
|
|
2103
2153
|
}
|
|
2104
2154
|
function resolveAst(input) {
|