@spintax/core 0.3.0 → 0.3.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/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/dist/index.cjs +29 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +29 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
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.3.1 — 2026-07-21
|
|
7
|
+
|
|
8
|
+
Author markup is sanitised in one place, so a handle renders exactly like its source.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **`render(parse(src))` no longer diverges from `render(src)`** when the template contains an
|
|
13
|
+
author-typed engine sentinel (the reserved range U+E000–U+E005). The strip that keeps stray
|
|
14
|
+
sentinels out of a tree lived at the render entry points, so `parse()` and `analyze(str)` — two
|
|
15
|
+
of the three doors into the parser — skipped it, and the mandatory safety-restore then rewrote
|
|
16
|
+
the author's character into a structural glyph they never wrote:
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const src = `a${String.fromCharCode(0xe000)}b`;
|
|
20
|
+
render(src, { postProcess: false }); // "ab"
|
|
21
|
+
render(parse(src), { postProcess: false }); // "a{b" ← before this release
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The strip now lives in `parseTemplate`, the single door from author source into an AST, so all
|
|
25
|
+
three entry points agree. `parseSequence` is deliberately **not** sanitised — it re-parses a
|
|
26
|
+
variable's *value*, where sentinels a host `neutralize()`d are legitimate and must survive to the
|
|
27
|
+
restore — and `ParsedAst.source` still keeps the original bytes so diagnostics point at what was
|
|
28
|
+
typed. ([#51](https://github.com/investblog/spintax-js/issues/51))
|
|
29
|
+
|
|
30
|
+
Templates that contain no reserved-range characters are unaffected: the strip is a no-op on them.
|
|
31
|
+
The Python port fixed the same defect the same way; the PHP engines never had it (no `parse()`
|
|
32
|
+
handle, no PUA sentinels).
|
|
33
|
+
|
|
6
34
|
## 0.3.0 — 2026-07-19
|
|
7
35
|
|
|
8
36
|
`#set` goes back to being a macro and a new `#def` carries roll-once. Breaking: it changes what
|
package/README.md
CHANGED
|
@@ -153,7 +153,7 @@ render('%bio%', { context: { bio: neutralize('Save {50|60}% today') }, postProce
|
|
|
153
153
|
|
|
154
154
|
With a `seed`, `render` is reproducible within this engine. Cross-engine RNG-sequence parity
|
|
155
155
|
with the PHP plugin is a **non-goal** — only the *deterministic* behavior (validation verdicts,
|
|
156
|
-
plural buckets, conditional truthiness, `#set`
|
|
156
|
+
plural buckets, conditional truthiness, `#set`/`#def` semantics, post-process output) is parity-gated.
|
|
157
157
|
|
|
158
158
|
## Links
|
|
159
159
|
|
package/dist/index.cjs
CHANGED
|
@@ -23,13 +23,38 @@ function isParsedAst(value) {
|
|
|
23
23
|
return typeof value === "object" && value !== null && value.astVersion === AST_VERSION && typeof value.source === "string" && Array.isArray(value.nodes);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// src/internal/neutralize.ts
|
|
27
|
+
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
28
|
+
var SENTINEL_BASE = 57344;
|
|
29
|
+
var SHIELD = /* @__PURE__ */ new Map();
|
|
30
|
+
var RESTORE = /* @__PURE__ */ new Map();
|
|
31
|
+
STRUCTURAL.forEach((ch, i) => {
|
|
32
|
+
const sentinel = String.fromCharCode(SENTINEL_BASE + i);
|
|
33
|
+
SHIELD.set(ch, sentinel);
|
|
34
|
+
RESTORE.set(sentinel, ch);
|
|
35
|
+
});
|
|
36
|
+
var SHIELD_RE = /[{}[\]%#]/gu;
|
|
37
|
+
var RESTORE_RE = new RegExp(
|
|
38
|
+
`[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
|
|
39
|
+
"gu"
|
|
40
|
+
);
|
|
41
|
+
function neutralize(value) {
|
|
42
|
+
return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
|
|
43
|
+
}
|
|
44
|
+
function safetyRestore(text) {
|
|
45
|
+
return text.replace(RESTORE_RE, (ch) => RESTORE.get(ch) ?? ch);
|
|
46
|
+
}
|
|
47
|
+
function stripSentinels(text) {
|
|
48
|
+
return text.replace(RESTORE_RE, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
26
51
|
// src/internal/parser.ts
|
|
27
52
|
var VARIABLE_RE = /^%(\w+)%/;
|
|
28
53
|
var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)[ \t]*\r?$/gmu;
|
|
29
54
|
var CONDITIONAL_NAME_RE = /^[A-Za-z_]\w*/;
|
|
30
55
|
var PLURAL_PREFIX = "plural ";
|
|
31
56
|
function parseTemplate(src) {
|
|
32
|
-
const { body, setDefs, defDefs } = extractDirectives(stripComments(src));
|
|
57
|
+
const { body, setDefs, defDefs } = extractDirectives(stripComments(stripSentinels(src)));
|
|
33
58
|
return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
|
|
34
59
|
}
|
|
35
60
|
function extractDirectives(text) {
|
|
@@ -702,31 +727,6 @@ var NotImplementedError = class extends SpintaxError {
|
|
|
702
727
|
}
|
|
703
728
|
};
|
|
704
729
|
|
|
705
|
-
// src/internal/neutralize.ts
|
|
706
|
-
var STRUCTURAL = ["{", "}", "[", "]", "%", "#"];
|
|
707
|
-
var SENTINEL_BASE = 57344;
|
|
708
|
-
var SHIELD = /* @__PURE__ */ new Map();
|
|
709
|
-
var RESTORE = /* @__PURE__ */ new Map();
|
|
710
|
-
STRUCTURAL.forEach((ch, i) => {
|
|
711
|
-
const sentinel = String.fromCharCode(SENTINEL_BASE + i);
|
|
712
|
-
SHIELD.set(ch, sentinel);
|
|
713
|
-
RESTORE.set(sentinel, ch);
|
|
714
|
-
});
|
|
715
|
-
var SHIELD_RE = /[{}[\]%#]/gu;
|
|
716
|
-
var RESTORE_RE = new RegExp(
|
|
717
|
-
`[${String.fromCharCode(SENTINEL_BASE)}-${String.fromCharCode(SENTINEL_BASE + STRUCTURAL.length - 1)}]`,
|
|
718
|
-
"gu"
|
|
719
|
-
);
|
|
720
|
-
function neutralize(value) {
|
|
721
|
-
return value.replace(SHIELD_RE, (ch) => SHIELD.get(ch) ?? ch);
|
|
722
|
-
}
|
|
723
|
-
function safetyRestore(text) {
|
|
724
|
-
return text.replace(RESTORE_RE, (ch) => RESTORE.get(ch) ?? ch);
|
|
725
|
-
}
|
|
726
|
-
function stripSentinels(text) {
|
|
727
|
-
return text.replace(RESTORE_RE, "");
|
|
728
|
-
}
|
|
729
|
-
|
|
730
730
|
// src/internal/render.ts
|
|
731
731
|
var MAX_VARIABLE_DEPTH = 50;
|
|
732
732
|
var INCLUDE_LINE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
@@ -753,7 +753,7 @@ function resolveIncludes(text, ctx) {
|
|
|
753
753
|
throw new IncludeResolverError(`includeResolver threw for "${ref}"`, { cause });
|
|
754
754
|
}
|
|
755
755
|
if (included === null) return "";
|
|
756
|
-
return renderAst(parseTemplate(
|
|
756
|
+
return renderAst(parseTemplate(included), {
|
|
757
757
|
...ctx,
|
|
758
758
|
runtimeContext: ctx.runtimeContext,
|
|
759
759
|
// child inherits runtime, not parent #set
|
|
@@ -879,7 +879,7 @@ function renderPlural(node, opts) {
|
|
|
879
879
|
if (/[{}[\]]/u.test(formsRaw)) {
|
|
880
880
|
report({
|
|
881
881
|
code: "plural.nested-brackets",
|
|
882
|
-
message: "Plural form slot contains nested spintax brackets; extract via #set
|
|
882
|
+
message: "Plural form slot contains nested spintax brackets; extract via #def first \u2014 a #set is substituted verbatim and would put the brackets straight back.",
|
|
883
883
|
construct: rawConstruct(countRaw, formsRaw),
|
|
884
884
|
locale: base
|
|
885
885
|
});
|
|
@@ -1113,7 +1113,7 @@ function renderWith(input, rng, opts = {}) {
|
|
|
1113
1113
|
includeStack: [],
|
|
1114
1114
|
onPluralError: opts.onPluralError
|
|
1115
1115
|
};
|
|
1116
|
-
const ast = resolveAst(
|
|
1116
|
+
const ast = resolveAst(input);
|
|
1117
1117
|
let out = renderAst(ast, ctx);
|
|
1118
1118
|
if (opts.postProcess !== false) out = postProcess(out);
|
|
1119
1119
|
return safetyRestore(out);
|