@spintax/core 0.3.1 → 0.3.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/CHANGELOG.md +64 -0
- package/dist/index.cjs +20 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,70 @@
|
|
|
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.2 — 2026-07-23
|
|
7
|
+
|
|
8
|
+
Post-process robustness and scaling: `postProcess()` no longer emits its own U+0000 sentinel, and
|
|
9
|
+
the placeholder restore is linear instead of quadratic. Output is unchanged on ordinary text; the
|
|
10
|
+
two behaviour changes are on shapes that previously produced invalid output (a raw U+0000).
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`postProcess()` no longer emits a raw U+0000** into the returned text — on input carrying
|
|
15
|
+
none. A URI body runs to the first delimiter, so the URL rule and the `mailto:`/`tel:` rule
|
|
16
|
+
overlap whenever one URI carries the other's scheme, and shielding them in two passes let the
|
|
17
|
+
second run into a placeholder the first had minted:
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
render('mailto:sales@example.com?body=see%20https://shop.example.com/cart');
|
|
21
|
+
// 'mailto:sales@example.com?body=see%20\x00URL_0\x00' ← before this release
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The swallowed key never restored, so the engine returned its own placeholder delimiter: illegal
|
|
25
|
+
in an XML document, replaced with U+FFFD by an HTML parser, rejected by PostgreSQL in
|
|
26
|
+
`text`/`varchar` — and a live key again as soon as an edit detaches it from the `mailto:` prefix
|
|
27
|
+
that was shielding it, at which point the next render substitutes an unrelated URL into the
|
|
28
|
+
contact link. It also silently disabled the linear restore below, whose guard is
|
|
29
|
+
`input.includes('\x00')`.
|
|
30
|
+
|
|
31
|
+
The two rules are now **one alternation**, so the leftmost match takes the whole token whichever
|
|
32
|
+
scheme it is. Reordering the passes was the other candidate and is not equivalent: it moves the
|
|
33
|
+
damage onto a URL whose path carries a `mailto:`, where the leading half then loses its trailing
|
|
34
|
+
dot to the punctuation pass. Both directions are fixtures now, the second one negative. U+0000 is
|
|
35
|
+
also excluded from the URI body class, for a caller-supplied one.
|
|
36
|
+
|
|
37
|
+
Mirrored into the WordPress plugin engine, and gated by three golden-corpus fixtures — the
|
|
38
|
+
cross-engine contract, so `spintax-php`, `spintax-py` and `spintax-win` inherit the gate.
|
|
39
|
+
([#53](https://github.com/investblog/spintax-js/issues/53))
|
|
40
|
+
|
|
41
|
+
### Performance
|
|
42
|
+
|
|
43
|
+
- **Post-process no longer goes quadratic on shield-heavy text.** The restore step replaced each
|
|
44
|
+
shielded placeholder across the whole text one key at a time — O(text × placeholders) — and
|
|
45
|
+
since URLs, `mailto:`/`tel:` URIs, emails, domains, decimals and abbreviations are all shielded,
|
|
46
|
+
the placeholder count grows with the input. The stage came to dominate the render:
|
|
47
|
+
|
|
48
|
+
| input | `postProcess: false` | before | after |
|
|
49
|
+
|--------|----------------------|------------|---------|
|
|
50
|
+
| 47 KB | 0.007 s | 0.18 s | 0.014 s |
|
|
51
|
+
| 189 KB | 0.006 s | 3.06 s | 0.057 s |
|
|
52
|
+
| 756 KB | 0.081 s | **43.9 s** | 0.25 s |
|
|
53
|
+
|
|
54
|
+
A single left-to-right pass replaces the loop, behind a guard on the input. The two are not the
|
|
55
|
+
same function: the loop is a repeated *substring* substitution, so it rewrites every occurrence
|
|
56
|
+
of a key rather than the one the shield placed. Most of the disagreement needs a literal `\x00`
|
|
57
|
+
from the caller, and the guard sends that input to the original loop.
|
|
58
|
+
|
|
59
|
+
**One shape survives the guard**, and on it the behaviour changed deliberately: two adjacent
|
|
60
|
+
placeholders can sandwich caller text that spells a key, so one token's closing delimiter, that
|
|
61
|
+
text, and the next token's opening delimiter form a third occurrence of a real key. Rendering
|
|
62
|
+
`https://a.io e.g. URL_0mailto:x@y.io` — no `\x00` anywhere in it — the loop substituted the
|
|
63
|
+
forgery, destroyed two real tokens and returned raw `\x00` bytes; the single pass returns the
|
|
64
|
+
text intact. Measured over 456 976 probes, 12 `\x00`-free inputs distinguish the two restores and
|
|
65
|
+
the loop emits a raw `\x00` on all 12. A corpus fixture now pins the surviving answer, since the
|
|
66
|
+
engines disagreed on it. `npm run bench:postprocess` records the scaling.
|
|
67
|
+
([#52](https://github.com/investblog/spintax-js/issues/52),
|
|
68
|
+
[#54](https://github.com/investblog/spintax-js/issues/54))
|
|
69
|
+
|
|
6
70
|
## 0.3.1 — 2026-07-21
|
|
7
71
|
|
|
8
72
|
Author markup is sanitised in one place, so a handle renders exactly like its source.
|
package/dist/index.cjs
CHANGED
|
@@ -1038,8 +1038,9 @@ var SINGLE_ABBREVS = [
|
|
|
1038
1038
|
var WS = " \\t\\r\\n\\f\\x0B";
|
|
1039
1039
|
var S = `[${WS}]`;
|
|
1040
1040
|
var DOMAIN_PART = "(?:(?:(?:xn--)?[\\p{L}\\p{N}]+(?:-[\\p{L}\\p{N}]+)*)\\.)+(?:xn--[a-z0-9\\-]{2,59}|[\\p{L}][\\p{L}\\p{N}-]{1,62})";
|
|
1041
|
-
var
|
|
1042
|
-
var
|
|
1041
|
+
var URI_BODY = `[^\\x00${WS}<>"')\\]]`;
|
|
1042
|
+
var URI_RE = new RegExp(`(?:(?:https?|ftp):\\/\\/|(?:mailto|tel):)${URI_BODY}+`, "giu");
|
|
1043
|
+
var MAILTEL_PREFIX_RE = /^(?:mailto|tel):/iu;
|
|
1043
1044
|
var EMAIL_RE = new RegExp(`[a-z0-9._%+\\-]+@${DOMAIN_PART}\\b`, "giu");
|
|
1044
1045
|
var DOMAIN_RE = new RegExp(`\\b${DOMAIN_PART}\\b`, "giu");
|
|
1045
1046
|
var DECIMAL_RE = /\b\d+\.\d+\b/gu;
|
|
@@ -1060,6 +1061,18 @@ var CAP_AFTER_BLOCK_RE = new RegExp(
|
|
|
1060
1061
|
);
|
|
1061
1062
|
var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${LEAD})(\\p{Ll})`, "gu");
|
|
1062
1063
|
var up = (ch) => ch.toUpperCase();
|
|
1064
|
+
var SHIELD_PREFIXES = ["URL", "URI", "EMAIL", "DOM", "NUM", "ABBR"];
|
|
1065
|
+
var RESTORE_RE2 = new RegExp(`\\x00(?:${SHIELD_PREFIXES.join("|")})_\\d+\\x00`, "gu");
|
|
1066
|
+
function restore(text, input, placeholders) {
|
|
1067
|
+
if (!input.includes("\0")) {
|
|
1068
|
+
return text.replace(RESTORE_RE2, (key) => placeholders.get(key) ?? key);
|
|
1069
|
+
}
|
|
1070
|
+
let out = text;
|
|
1071
|
+
for (const [key, value] of placeholders) {
|
|
1072
|
+
out = out.split(key).join(value);
|
|
1073
|
+
}
|
|
1074
|
+
return out;
|
|
1075
|
+
}
|
|
1063
1076
|
function postProcess(input) {
|
|
1064
1077
|
const placeholders = /* @__PURE__ */ new Map();
|
|
1065
1078
|
let counter = 0;
|
|
@@ -1079,8 +1092,10 @@ function postProcess(input) {
|
|
|
1079
1092
|
return store(value, prefix);
|
|
1080
1093
|
};
|
|
1081
1094
|
let text = input;
|
|
1082
|
-
text = text.replace(
|
|
1083
|
-
|
|
1095
|
+
text = text.replace(
|
|
1096
|
+
URI_RE,
|
|
1097
|
+
(m) => storeWithTrailingPunct(m, MAILTEL_PREFIX_RE.test(m) ? "URI" : "URL")
|
|
1098
|
+
);
|
|
1084
1099
|
text = text.replace(EMAIL_RE, (m) => store(m, "EMAIL"));
|
|
1085
1100
|
text = text.replace(DOMAIN_RE, (m) => store(m, "DOM"));
|
|
1086
1101
|
text = text.replace(DECIMAL_RE, (m) => store(m, "NUM"));
|
|
@@ -1095,10 +1110,7 @@ function postProcess(input) {
|
|
|
1095
1110
|
text = text.replace(CAP_AFTER_SENTENCE_RE, (_m, p, gap, ch) => p + gap + up(ch));
|
|
1096
1111
|
text = text.replace(CAP_AFTER_BLOCK_RE, (_m, tag, ch) => tag + up(ch));
|
|
1097
1112
|
text = text.replace(CAP_AFTER_BREAK_RE, (_m, br, ch) => br + up(ch));
|
|
1098
|
-
|
|
1099
|
-
text = text.split(key).join(value);
|
|
1100
|
-
}
|
|
1101
|
-
return text.trim();
|
|
1113
|
+
return restore(text, input, placeholders).trim();
|
|
1102
1114
|
}
|
|
1103
1115
|
|
|
1104
1116
|
// src/internal/pipeline.ts
|