@spintax/core 0.1.4 → 0.1.6

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 CHANGED
@@ -3,6 +3,80 @@
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.1.6 — 2026-07-13
7
+
8
+ Two post-process fixes. **Supersedes 0.1.5** — upgrade straight past it. No API change.
9
+
10
+ ### Fixed
11
+
12
+ - **A run of sentence punctuation is no longer split from the inside.** This is not a Spanish issue
13
+ and it predates the 0.1.5 work: the "space after `.!?`" rule looked exactly one character ahead
14
+ and never at the rest of the run, so it fired *between* the marks.
15
+
16
+ ```
17
+ wait... what? → Wait. . . What? (0.1.5 and earlier)
18
+ wow!!! → Wow! ! !
19
+ really?! → Really? !
20
+ ```
21
+
22
+ The ASCII ellipsis is the common casualty — and the Unicode `…` is *not* in the `[.!?]` class,
23
+ which is exactly why the corpus's existing ellipsis fixture never caught it. A run is now matched
24
+ whole and required to be complete: `([.!?]+)(?![.!?])`. The guard is load-bearing — a greedy `+`
25
+ alone gives ground back *into* the run to satisfy the lookaheads and yields `Wow!! !`.
26
+
27
+ - **`¡¿Qué haces?!` keeps its capital.** The 0.1.5 opener rule allowed exactly *one* opener, so the
28
+ RAE form for a sentence that is both a question and an exclamation — the most Spanish sentence
29
+ there is — still lost its capital. An opener followed by markup (`¿<strong>cómo</strong> estás?`,
30
+ `<p>¿<a href="/ayuda">necesitas ayuda</a>?</p>`) failed for the same reason: the old lead only
31
+ allowed `tags → opener → letter`, never `opener → tags → letter`.
32
+
33
+ The lead is now any run of tags, sentence openers and whitespace, in any order. The opener set
34
+ stays deliberately **narrow** — quotes and brackets are still not openers, and the fixtures added
35
+ in 0.1.5 keep guarding that.
36
+
37
+ ### Notes
38
+
39
+ - Mirrored into the PHP plugin engine (released there as 2.3.3). Verified against the shared golden
40
+ corpus in both engines: TS 243 tests; the conformance runner against the real plugin engine 107
41
+ tests / 120 assertions. No performance regression — the pathological-input cost is unchanged from
42
+ 0.1.5 and comes from the URL/domain shields, not from these rules.
43
+
44
+ ## 0.1.5 — 2026-07-13
45
+
46
+ Spanish post-process fix. No API change.
47
+
48
+ ### Fixed
49
+
50
+ - **`postProcess` no longer strips the capital from every Spanish sentence.** Spanish is the only
51
+ European language whose punctuation *opens* a sentence, and the capitalization passes upper-cased
52
+ the first **character** after a sentence boundary — which, for `¿cómo estás?`, is `¿`. An inverted
53
+ mark has no uppercase form, so the pass was a no-op and the real first letter stayed lowercase.
54
+ The spacing pass had the mirror-image gap: it knew "no space *before* closing punctuation" but had
55
+ no rule for an opener, so `¿ qué tal ?` only half-collapsed.
56
+
57
+ ```
58
+ hola. ¿cómo estás? ¡genial! → Hola. ¿Cómo estás? ¡Genial!
59
+ Hola. ¿ qué tal ? → Hola. ¿Qué tal?
60
+ ```
61
+
62
+ A new `SENTENCE_OPENERS = '¿¡'` concept drives both: an opener binds to the word it opens (before
63
+ capitalization, deliberately), and the four capitalization sites — start of text, after `.!?…`,
64
+ after a block-level tag, and after a newline — allow an optional opener between the boundary and
65
+ the letter. HTML paragraphs and multi-line templates were broken exactly like the bare `. ¿` case.
66
+
67
+ The opener set is deliberately **narrow**: quotes and brackets both open *and* close, so
68
+ capitalizing after them would mangle list markers (`Elige una. (a) primero`). Golden-corpus
69
+ fixtures guard both the fix and its narrowness, in **both** engines.
70
+
71
+ ### Notes
72
+
73
+ - Mirrored into the PHP plugin engine to hold the post-process parity contract. Verified against the
74
+ shared golden corpus in both engines: TS 230 tests; the conformance runner against the real plugin
75
+ engine 99 tests / 112 assertions; the plugin's own suite 578 tests, no regressions.
76
+ - The plugin carries the same fix but ships it in a later release, so for a short window a published
77
+ plugin and this package can differ on Spanish output. The parity *contract* holds — the engines'
78
+ behavior agrees and the corpus proves it — only the release timing differs.
79
+
6
80
  ## 0.1.4 — 2026-07-13
7
81
 
8
82
  A post-process bug fix (hit in production) plus docs. No API change.
package/dist/index.cjs CHANGED
@@ -913,13 +913,19 @@ var DECIMAL_RE = /\b\d+\.\d+\b/gu;
913
913
  var MULTI_ABBR_RE = new RegExp(`\\b(?:\\p{L}{1,2}\\.${S}*){2,}`, "gu");
914
914
  var SINGLE_ABBR_RE = new RegExp(`(?<![\\p{L}\\p{N}])(?:${SINGLE_ABBREVS.join("|")})\\.(?=${S}|$|<)`, "giu");
915
915
  var TRAILING_PUNCT_RE = /([.,;:!]+)$/u;
916
+ var SENTENCE_OPENERS = "\xBF\xA1";
917
+ var LEAD = `(?:<[^>]+>|[${SENTENCE_OPENERS}]|${S})*`;
916
918
  var SPACE_BEFORE_PUNCT_RE = new RegExp(`${S}+([,;:!?.])`, "gu");
917
919
  var SPACE_AFTER_COMMA_RE = new RegExp(`([,;:])(?!\\d)(?!${S}|$|<)`, "gu");
918
- var SPACE_AFTER_SENTENCE_RE = new RegExp(`([.!?])(?!\\d)(?!${S}|$|<)`, "gu");
919
- var CAP_FIRST_RE = new RegExp(`^(${S}*(?:<[^>]+>${S}*)*)(\\p{Ll})`, "u");
920
- var CAP_AFTER_SENTENCE_RE = new RegExp(`([.!?\u2026])(${S}*(?:<\\/?[^>]+>${S}*)*)(\\p{Ll})`, "gu");
921
- var CAP_AFTER_BLOCK_RE = new RegExp(`(<\\/?(?:p|h[1-6]|li|blockquote|div|td|th)[^>]*>${S}*)(\\p{Ll})`, "giu");
922
- var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${S}*)(\\p{Ll})`, "gu");
920
+ var SPACE_AFTER_SENTENCE_RE = new RegExp(`([.!?]+)(?![.!?])(?!\\d)(?!${S}|$|<)`, "gu");
921
+ var SPACE_AFTER_OPENER_RE = new RegExp(`([${SENTENCE_OPENERS}])${S}+`, "gu");
922
+ var CAP_FIRST_RE = new RegExp(`^(${LEAD})(\\p{Ll})`, "u");
923
+ var CAP_AFTER_SENTENCE_RE = new RegExp(`([.!?\u2026])(${LEAD})(\\p{Ll})`, "gu");
924
+ var CAP_AFTER_BLOCK_RE = new RegExp(
925
+ `(<\\/?(?:p|h[1-6]|li|blockquote|div|td|th)[^>]*>${LEAD})(\\p{Ll})`,
926
+ "giu"
927
+ );
928
+ var CAP_AFTER_BREAK_RE = new RegExp(`(\\n${LEAD})(\\p{Ll})`, "gu");
923
929
  var up = (ch) => ch.toUpperCase();
924
930
  function postProcess(input) {
925
931
  const placeholders = /* @__PURE__ */ new Map();
@@ -951,6 +957,7 @@ function postProcess(input) {
951
957
  text = text.replace(SPACE_BEFORE_PUNCT_RE, "$1");
952
958
  text = text.replace(SPACE_AFTER_COMMA_RE, "$1 ");
953
959
  text = text.replace(SPACE_AFTER_SENTENCE_RE, "$1 ");
960
+ text = text.replace(SPACE_AFTER_OPENER_RE, "$1");
954
961
  text = text.replace(CAP_FIRST_RE, (_m, lead, ch) => lead + up(ch));
955
962
  text = text.replace(CAP_AFTER_SENTENCE_RE, (_m, p, gap, ch) => p + gap + up(ch));
956
963
  text = text.replace(CAP_AFTER_BLOCK_RE, (_m, tag, ch) => tag + up(ch));