@spintax/core 0.11.0 → 0.12.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 +40 -0
- package/README.md +1 -1
- package/dist/index.cjs +50 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +50 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,46 @@
|
|
|
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.12.0 — 2026-10-06
|
|
7
|
+
|
|
8
|
+
A minor: rendered text moves for permutation separators in Arabic, Hebrew, Thai and Lao. No verdict
|
|
9
|
+
changes and no API change; `render()` already took the `locale` it now also reads here.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **An Arabic list can be written (#90).** The conjunction و ("and") — and ف ("and then") — is written
|
|
14
|
+
attached to the next word: `الكازينو والبث`, never `الكازينو و البث`. A letter-only separator was always
|
|
15
|
+
padded on both sides, whatever spaces the author wrote, so no shape of `lastsep` could produce it. Now,
|
|
16
|
+
under `locale: 'ar'`, a separator that is exactly و or ف keeps the space before it and none after when
|
|
17
|
+
the next element starts with an Arabic letter: `[<sep="، ";lastsep="و">…]` renders
|
|
18
|
+
`الكازينو، المراهنات الرياضية والبث المباشر`. Before anything else — a Latin brand name, a digit — both
|
|
19
|
+
spaces stay: `و Evolution`. Hebrew ו under `locale: 'he'` works the same way. The rule is keyed by
|
|
20
|
+
LANGUAGE, not script: Persian and Urdu write the same letter و as a word of its own, so under `fa` and
|
|
21
|
+
`ur` it is padded as before, and so is every word conjunction (أو, ثم, או) and و with no locale.
|
|
22
|
+
- **Thai and Lao separators join bare (#90),** as Han and kana have since 0.11.0: `[<lastsep="และ">แมว|สุนัข]`
|
|
23
|
+
renders `แมวและสุนัข`. Khmer and Myanmar take the same rule. A separator carrying a combining mark —
|
|
24
|
+
Thai `หรือ`, most Khmer and Myanmar words — is not all letters, so it was never padded and still renders
|
|
25
|
+
exactly as written, spaces included.
|
|
26
|
+
|
|
27
|
+
No template in the family's stored content uses any of these separators (measured over casino-platform,
|
|
28
|
+
content-gen, both sites and the plugin: the only hits were the probes that found the issue), so no
|
|
29
|
+
existing text changes.
|
|
30
|
+
|
|
31
|
+
### Corpus
|
|
32
|
+
|
|
33
|
+
408 cases (+22), all in `render-semantics.json`. `perm/sep-arabic-*` (و and ف attached, the author's
|
|
34
|
+
spaces, a regional locale, before a Latin word, a digit, an astral Arabic letter and U+0640, a word
|
|
35
|
+
conjunction, the issue's whole list with the post-process on), `perm/sep-persian-waw-padded`,
|
|
36
|
+
`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-*`,
|
|
37
|
+
`perm/sep-lao-not-padded`, `perm/sep-khmer-not-padded`, `perm/sep-myanmar-not-padded`, and
|
|
38
|
+
`perm/sep-script-not-script-extensions`.
|
|
39
|
+
|
|
40
|
+
That last one pins a divergence 0.11.0 shipped in both PHP engines without anyone seeing it: PCRE2 10.40
|
|
41
|
+
and later read `\p{Han}` as Script_Extensions, so U+3006 〆, U+303C 〼, the kana repeat marks U+3031–3035
|
|
42
|
+
and the halfwidth sound marks U+FF9E/FF9F joined bare there and were padded here and in every port. The
|
|
43
|
+
PHP engines now exclude them, which is Script on any PCRE2 version. The same trap sat under the new Arabic
|
|
44
|
+
test (U+0640, tatweel); the next element is read by code point, not by UTF-16 unit.
|
|
45
|
+
|
|
6
46
|
## 0.11.0 — 2026-10-03
|
|
7
47
|
|
|
8
48
|
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 408 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
|
}
|