@sharpee/lang-en-us 1.2.0 → 2.0.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/actions/about.d.ts.map +1 -1
- package/actions/about.js +8 -8
- package/actions/about.js.map +1 -1
- package/actions/again.d.ts.map +1 -1
- package/actions/again.js.map +1 -1
- package/actions/answering.d.ts.map +1 -1
- package/actions/answering.js.map +1 -1
- package/actions/asking.d.ts.map +1 -1
- package/actions/asking.js +14 -14
- package/actions/asking.js.map +1 -1
- package/actions/attacking.d.ts.map +1 -1
- package/actions/attacking.js +51 -51
- package/actions/attacking.js.map +1 -1
- package/actions/climbing.d.ts.map +1 -1
- package/actions/climbing.js +4 -4
- package/actions/climbing.js.map +1 -1
- package/actions/closing.d.ts.map +1 -1
- package/actions/closing.js +5 -5
- package/actions/closing.js.map +1 -1
- package/actions/drinking.d.ts.map +1 -1
- package/actions/drinking.js +20 -20
- package/actions/drinking.js.map +1 -1
- package/actions/dropping.d.ts.map +1 -1
- package/actions/dropping.js +3 -3
- package/actions/dropping.js.map +1 -1
- package/actions/eating.d.ts.map +1 -1
- package/actions/eating.js +20 -20
- package/actions/eating.js.map +1 -1
- package/actions/entering.d.ts.map +1 -1
- package/actions/entering.js +10 -10
- package/actions/entering.js.map +1 -1
- package/actions/examining.d.ts.map +1 -1
- package/actions/examining.js +28 -23
- package/actions/examining.js.map +1 -1
- package/actions/exiting.d.ts.map +1 -1
- package/actions/exiting.js +4 -4
- package/actions/exiting.js.map +1 -1
- package/actions/giving.d.ts.map +1 -1
- package/actions/giving.js +12 -12
- package/actions/giving.js.map +1 -1
- package/actions/going.d.ts.map +1 -1
- package/actions/going.js +7 -7
- package/actions/going.js.map +1 -1
- package/actions/help.d.ts.map +1 -1
- package/actions/help.js +2 -2
- package/actions/help.js.map +1 -1
- package/actions/hiding.d.ts.map +1 -1
- package/actions/hiding.js.map +1 -1
- package/actions/index.d.ts.map +1 -1
- package/actions/index.js.map +1 -1
- package/actions/inserting.d.ts.map +1 -1
- package/actions/inserting.js +8 -8
- package/actions/inserting.js.map +1 -1
- package/actions/inventory.d.ts.map +1 -1
- package/actions/inventory.js.map +1 -1
- package/actions/listening.d.ts.map +1 -1
- package/actions/listening.js +7 -7
- package/actions/listening.js.map +1 -1
- package/actions/locking.d.ts.map +1 -1
- package/actions/locking.js +8 -8
- package/actions/locking.js.map +1 -1
- package/actions/looking.d.ts.map +1 -1
- package/actions/looking.js +5 -5
- package/actions/looking.js.map +1 -1
- package/actions/lowering.d.ts.map +1 -1
- package/actions/lowering.js +2 -2
- package/actions/lowering.js.map +1 -1
- package/actions/opening.d.ts.map +1 -1
- package/actions/opening.js +7 -7
- package/actions/opening.js.map +1 -1
- package/actions/pulling.d.ts.map +1 -1
- package/actions/pulling.js +19 -19
- package/actions/pulling.js.map +1 -1
- package/actions/pushing.d.ts.map +1 -1
- package/actions/pushing.js +14 -14
- package/actions/pushing.js.map +1 -1
- package/actions/putting.d.ts.map +1 -1
- package/actions/putting.js +12 -12
- package/actions/putting.js.map +1 -1
- package/actions/quitting.d.ts.map +1 -1
- package/actions/quitting.js.map +1 -1
- package/actions/raising.d.ts.map +1 -1
- package/actions/raising.js +2 -2
- package/actions/raising.js.map +1 -1
- package/actions/reading.d.ts.map +1 -1
- package/actions/reading.js +7 -7
- package/actions/reading.js.map +1 -1
- package/actions/removing.d.ts.map +1 -1
- package/actions/removing.js +8 -8
- package/actions/removing.js.map +1 -1
- package/actions/restoring.d.ts.map +1 -1
- package/actions/restoring.js +6 -6
- package/actions/restoring.js.map +1 -1
- package/actions/saving.d.ts.map +1 -1
- package/actions/saving.js +6 -6
- package/actions/saving.js.map +1 -1
- package/actions/scoring.d.ts.map +1 -1
- package/actions/scoring.js.map +1 -1
- package/actions/searching.d.ts.map +1 -1
- package/actions/searching.js +7 -7
- package/actions/searching.js.map +1 -1
- package/actions/showing.d.ts.map +1 -1
- package/actions/showing.js +11 -11
- package/actions/showing.js.map +1 -1
- package/actions/sleeping.d.ts.map +1 -1
- package/actions/sleeping.js.map +1 -1
- package/actions/smelling.d.ts.map +1 -1
- package/actions/smelling.js +10 -10
- package/actions/smelling.js.map +1 -1
- package/actions/switching-off.d.ts.map +1 -1
- package/actions/switching-off.js +12 -12
- package/actions/switching-off.js.map +1 -1
- package/actions/switching-on.d.ts.map +1 -1
- package/actions/switching-on.js +12 -12
- package/actions/switching-on.js.map +1 -1
- package/actions/taking-off.d.ts.map +1 -1
- package/actions/taking-off.js +4 -4
- package/actions/taking-off.js.map +1 -1
- package/actions/taking.d.ts.map +1 -1
- package/actions/taking.js +5 -5
- package/actions/taking.js.map +1 -1
- package/actions/talking.d.ts.map +1 -1
- package/actions/talking.js +15 -15
- package/actions/talking.js.map +1 -1
- package/actions/telling.d.ts.map +1 -1
- package/actions/telling.js +13 -13
- package/actions/telling.js.map +1 -1
- package/actions/throwing.d.ts.map +1 -1
- package/actions/throwing.js +21 -21
- package/actions/throwing.js.map +1 -1
- package/actions/touching.d.ts.map +1 -1
- package/actions/touching.js +20 -20
- package/actions/touching.js.map +1 -1
- package/actions/turning.d.ts.map +1 -1
- package/actions/turning.js +24 -24
- package/actions/turning.js.map +1 -1
- package/actions/undoing.d.ts.map +1 -1
- package/actions/undoing.js.map +1 -1
- package/actions/unlocking.d.ts.map +1 -1
- package/actions/unlocking.js +8 -8
- package/actions/unlocking.js.map +1 -1
- package/actions/version.d.ts.map +1 -1
- package/actions/version.js +3 -3
- package/actions/version.js.map +1 -1
- package/actions/waiting.d.ts.map +1 -1
- package/actions/waiting.js +1 -1
- package/actions/waiting.js.map +1 -1
- package/actions/wearing.d.ts.map +1 -1
- package/actions/wearing.js +5 -5
- package/actions/wearing.js.map +1 -1
- package/assembler/english-assembler.d.ts +66 -0
- package/assembler/english-assembler.d.ts.map +1 -0
- package/assembler/english-assembler.js +806 -0
- package/assembler/english-assembler.js.map +1 -0
- package/assembler/errors.d.ts +23 -0
- package/assembler/errors.d.ts.map +1 -0
- package/assembler/errors.js +35 -0
- package/assembler/errors.js.map +1 -0
- package/assembler/index.d.ts +10 -0
- package/assembler/index.d.ts.map +1 -0
- package/assembler/index.js +17 -0
- package/assembler/index.js.map +1 -0
- package/data/events.d.ts.map +1 -1
- package/data/events.js +11 -11
- package/data/events.js.map +1 -1
- package/data/messages.d.ts.map +1 -1
- package/data/messages.js +1 -1
- package/data/messages.js.map +1 -1
- package/data/verbs.d.ts.map +1 -1
- package/data/verbs.js.map +1 -1
- package/data/words.d.ts.map +1 -1
- package/data/words.js.map +1 -1
- package/formatters/article.d.ts.map +1 -1
- package/formatters/article.js.map +1 -1
- package/formatters/index.d.ts.map +1 -1
- package/formatters/index.js.map +1 -1
- package/formatters/list.d.ts.map +1 -1
- package/formatters/list.js.map +1 -1
- package/formatters/registry.d.ts.map +1 -1
- package/formatters/registry.js.map +1 -1
- package/formatters/text.d.ts.map +1 -1
- package/formatters/text.js.map +1 -1
- package/formatters/types.d.ts.map +1 -1
- package/formatters/types.js.map +1 -1
- package/formatters/verb.d.ts.map +1 -1
- package/formatters/verb.js.map +1 -1
- package/grammar.d.ts.map +1 -1
- package/grammar.js.map +1 -1
- package/index.d.ts +2 -1
- package/index.d.ts.map +1 -1
- package/index.js +4 -1
- package/index.js.map +1 -1
- package/language-provider.d.ts +44 -24
- package/language-provider.d.ts.map +1 -1
- package/language-provider.js +83 -40
- package/language-provider.js.map +1 -1
- package/npc/conversation.d.ts +5 -0
- package/npc/conversation.d.ts.map +1 -1
- package/npc/conversation.js +24 -19
- package/npc/conversation.js.map +1 -1
- package/npc/index.d.ts.map +1 -1
- package/npc/index.js.map +1 -1
- package/npc/influence.d.ts.map +1 -1
- package/npc/influence.js +4 -4
- package/npc/influence.js.map +1 -1
- package/npc/npc.d.ts +6 -0
- package/npc/npc.d.ts.map +1 -1
- package/npc/npc.js +39 -33
- package/npc/npc.js.map +1 -1
- package/npc/propagation.d.ts.map +1 -1
- package/npc/propagation.js +6 -6
- package/npc/propagation.js.map +1 -1
- package/number-words.d.ts +18 -0
- package/number-words.d.ts.map +1 -1
- package/number-words.js +77 -0
- package/number-words.js.map +1 -1
- package/package.json +3 -3
- package/parser/index.d.ts +8 -0
- package/parser/index.d.ts.map +1 -0
- package/parser/index.js +13 -0
- package/parser/index.js.map +1 -0
- package/parser/parse-phrase-template.d.ts +62 -0
- package/parser/parse-phrase-template.d.ts.map +1 -0
- package/parser/parse-phrase-template.js +384 -0
- package/parser/parse-phrase-template.js.map +1 -0
- package/perspective/index.d.ts.map +1 -1
- package/perspective/index.js.map +1 -1
- package/perspective/placeholder-resolver.d.ts +1 -1
- package/perspective/placeholder-resolver.d.ts.map +1 -1
- package/perspective/placeholder-resolver.js +12 -1
- package/perspective/placeholder-resolver.js.map +1 -1
- package/pluralize.d.ts.map +1 -1
- package/pluralize.js.map +1 -1
- package/sound-messages.d.ts +8 -8
- package/sound-messages.d.ts.map +1 -1
- package/sound-messages.js +8 -8
- package/sound-messages.js.map +1 -1
|
@@ -0,0 +1,806 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @file English Assembler — realizes a phrase tree to text blocks (ADR-192 §4).
|
|
4
|
+
*
|
|
5
|
+
* Purpose: the single English-locale component that walks a `Phrase` tree and
|
|
6
|
+
* is the SOLE authority for every cross-cutting correctness concern — article,
|
|
7
|
+
* agreement, punctuation, whitespace, reference, and case. It replaces the
|
|
8
|
+
* left-to-right formatter chain (`applyFormatters`) whose early collapse to a
|
|
9
|
+
* bare string lost the metadata neighbours need to agree against.
|
|
10
|
+
*
|
|
11
|
+
* Public interface: `EnglishAssembler` (implements `Assembler`),
|
|
12
|
+
* `ASSEMBLER_DEFAULT_BLOCK_KEY`, and the standalone `capitalizeSentenceStart`
|
|
13
|
+
* case-authority helper.
|
|
14
|
+
*
|
|
15
|
+
* Owner context: `@sharpee/lang-en-us` — English realization. The ADR-190 list
|
|
16
|
+
* formatter logic is reproduced here in the `PhraseList` case (the old
|
|
17
|
+
* `formatters/list.ts` is retired in Phase 3, not called from here).
|
|
18
|
+
*
|
|
19
|
+
* INVARIANT (ADR-192 §7): `realize` is a pure function of `(tree, ctx)` GIVEN the
|
|
20
|
+
* `textState` snapshot — the same tree, context, and counters yield byte-identical
|
|
21
|
+
* output. No clocks, no `Math.random`. The one declared state transition is a
|
|
22
|
+
* `Choice` advancing its `textState` counter (ADR-196 §3) — seeded, deterministic.
|
|
23
|
+
*
|
|
24
|
+
* All if-domain kinds are realized here: the foundational kinds (Literal,
|
|
25
|
+
* NounPhrase, PhraseList, Sequence, Empty) plus the `Verb` (199), `Verbatim`
|
|
26
|
+
* (200), `Numeral` (198), `Pronoun` (197), `Contents` (194), `Slot` (195), and
|
|
27
|
+
* `Optional` / `Choice` (196) atoms. `PhraseNotImplementedError` is now only a
|
|
28
|
+
* defensive guard against a future unhandled kind.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.EnglishAssembler = exports.ASSEMBLER_DEFAULT_BLOCK_KEY = void 0;
|
|
32
|
+
exports.capitalizeSentenceStart = capitalizeSentenceStart;
|
|
33
|
+
const if_domain_1 = require("@sharpee/if-domain");
|
|
34
|
+
const text_blocks_1 = require("@sharpee/text-blocks");
|
|
35
|
+
const pluralize_js_1 = require("../pluralize.js");
|
|
36
|
+
const number_words_js_1 = require("../number-words.js");
|
|
37
|
+
const errors_js_1 = require("./errors.js");
|
|
38
|
+
/**
|
|
39
|
+
* Default channel key for a realized tree. Phase 2 emits one block; the report
|
|
40
|
+
* layer (ADR-192 §6, Phase 4) assigns real channel keys (room.name, …) as it
|
|
41
|
+
* wires per-event trees.
|
|
42
|
+
*/
|
|
43
|
+
exports.ASSEMBLER_DEFAULT_BLOCK_KEY = text_blocks_1.CORE_BLOCK_KEYS.ACTION_RESULT;
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
// Article authority — a/an/the/some/∅ agreed over the realized head
|
|
46
|
+
// ===========================================================================
|
|
47
|
+
/** Indefinite article for a head phrase, agreed over its leading sound. */
|
|
48
|
+
function indefiniteArticle(head) {
|
|
49
|
+
if (!head)
|
|
50
|
+
return 'a';
|
|
51
|
+
const lower = head.toLowerCase();
|
|
52
|
+
// Silent-h and vowel-sound exceptions (ported from formatters/article.ts).
|
|
53
|
+
if (lower.startsWith('hour'))
|
|
54
|
+
return 'an';
|
|
55
|
+
if (lower.startsWith('honest'))
|
|
56
|
+
return 'an';
|
|
57
|
+
if (lower.startsWith('heir'))
|
|
58
|
+
return 'an';
|
|
59
|
+
if (lower.startsWith('uni'))
|
|
60
|
+
return 'a'; // "a university"
|
|
61
|
+
if (lower.startsWith('one'))
|
|
62
|
+
return 'a'; // "a one-way street"
|
|
63
|
+
const vowels = ['a', 'e', 'i', 'o', 'u'];
|
|
64
|
+
return vowels.includes(lower[0]) ? 'an' : 'a';
|
|
65
|
+
}
|
|
66
|
+
/** The article surface for a noun phrase, agreed over its rendered head. */
|
|
67
|
+
function articleSurface(np, head) {
|
|
68
|
+
if (np.properName)
|
|
69
|
+
return ''; // proper names suppress the article
|
|
70
|
+
switch (np.articleType) {
|
|
71
|
+
case 'none':
|
|
72
|
+
return '';
|
|
73
|
+
case 'definite':
|
|
74
|
+
return 'the';
|
|
75
|
+
case 'some':
|
|
76
|
+
return 'some';
|
|
77
|
+
case 'indefinite':
|
|
78
|
+
if (np.number === 'plural')
|
|
79
|
+
return ''; // "swords", not "a swords"
|
|
80
|
+
if (np.number === 'mass')
|
|
81
|
+
return 'some'; // "some sand"
|
|
82
|
+
return indefiniteArticle(head);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// ===========================================================================
|
|
86
|
+
// Agreement authority — number / pluralization
|
|
87
|
+
// ===========================================================================
|
|
88
|
+
/**
|
|
89
|
+
* The noun's surface form for its grammatical number (Agreement authority).
|
|
90
|
+
*
|
|
91
|
+
* An intrinsically-plural NounPhrase (`number: 'plural'`) carries its plural
|
|
92
|
+
* surface directly in `name` — the producer maps it from `IdentityTrait.name`,
|
|
93
|
+
* which an author marking an entity `.plural()` writes already-plural ("pygmy
|
|
94
|
+
* goats", "direction signs"). So the name is used as-is; only an explicit
|
|
95
|
+
* `pluralForm` overrides it. Re-running `pluralize` here would double-pluralize
|
|
96
|
+
* ("goats" → "goatses"). The count-group path (N identical *singular* entities →
|
|
97
|
+
* "two goats") pluralizes singular names itself and does not pass through here.
|
|
98
|
+
*/
|
|
99
|
+
function nounSurface(np) {
|
|
100
|
+
if (np.number === 'plural')
|
|
101
|
+
return np.pluralForm ?? np.name;
|
|
102
|
+
return np.name; // singular and mass use the base name
|
|
103
|
+
}
|
|
104
|
+
/** The head (adjectives + noun) the article agrees over. */
|
|
105
|
+
function headWithAdjectives(np) {
|
|
106
|
+
const adjectives = np.adjectives && np.adjectives.length ? `${np.adjectives.join(' ')} ` : '';
|
|
107
|
+
return `${adjectives}${nounSurface(np)}`;
|
|
108
|
+
}
|
|
109
|
+
/** Realize a single noun phrase: article + adjectives + noun, agreed as a whole. */
|
|
110
|
+
function renderNoun(np) {
|
|
111
|
+
const head = headWithAdjectives(np);
|
|
112
|
+
const article = articleSurface(np, head);
|
|
113
|
+
const text = article ? `${article} ${head}` : head;
|
|
114
|
+
// Case authority: the {capitalize …} hint upper-cases the rendered head.
|
|
115
|
+
return np.capitalize ? capitalizeSentenceStart(text) : text;
|
|
116
|
+
}
|
|
117
|
+
// ===========================================================================
|
|
118
|
+
// Agreement authority — verb conjugation (ADR-199)
|
|
119
|
+
// ===========================================================================
|
|
120
|
+
/**
|
|
121
|
+
* Suppletive verbs whose plural / 1st-singular forms are not the regular `-s`
|
|
122
|
+
* strip. Keyed by the 3rd-person-singular `lemma` the author types. This is the
|
|
123
|
+
* table lifted out of the deleted `formatters/verb.ts`, extended with do/go.
|
|
124
|
+
*/
|
|
125
|
+
const IRREGULAR_VERBS = {
|
|
126
|
+
is: { plural: 'are', firstSingular: 'am' }, // be (present)
|
|
127
|
+
was: { plural: 'were', firstSingular: 'was' }, // be (past): I was / you were
|
|
128
|
+
has: { plural: 'have' }, // have
|
|
129
|
+
does: { plural: 'do' }, // do
|
|
130
|
+
goes: { plural: 'go' }, // go
|
|
131
|
+
// `-ie` stems (ADR-204): 3sg adds only `-s`, so they surface as `-ies` and would be
|
|
132
|
+
// mis-handled by the `ies`→`y` rule (which is correct for the common `-y` stems). This
|
|
133
|
+
// closed set is the exception; add longer derivatives (unties, belies) on demand.
|
|
134
|
+
dies: { plural: 'die' }, // die
|
|
135
|
+
lies: { plural: 'lie' }, // lie
|
|
136
|
+
ties: { plural: 'tie' }, // tie
|
|
137
|
+
vies: { plural: 'vie' }, // vie
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Regular rule: the 3rd-singular `lemma` ends in `-s`; the plain form strips it (ADR-199,
|
|
141
|
+
* refined ADR-204). The `-es` strip applies only to genuine `-es` inflections — a doubled
|
|
142
|
+
* sibilant (`ss`/`zz`) or `x`/`ch`/`sh` — NOT to a single `-se`/`-ze` stem, which added only
|
|
143
|
+
* `-s` (use→uses, refuse→refuses). Single-`s` stems (focus/bus/gas, a rare closed set) that
|
|
144
|
+
* legitimately take `-es` are the residual ambiguity; add them to `IRREGULAR_VERBS` on demand.
|
|
145
|
+
*/
|
|
146
|
+
function regularPluralVerb(lemma) {
|
|
147
|
+
if (lemma.endsWith('ies') && lemma.length > 3)
|
|
148
|
+
return `${lemma.slice(0, -3)}y`; // carries → carry
|
|
149
|
+
if (/(?:ss|zz|x|ch|sh)es$/.test(lemma))
|
|
150
|
+
return lemma.slice(0, -2); // kisses → kiss, boxes → box, watches → watch
|
|
151
|
+
if (lemma.endsWith('s'))
|
|
152
|
+
return lemma.slice(0, -1); // opens → open, uses → use, refuses → refuse
|
|
153
|
+
return lemma; // no -s to strip — leave as authored
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The grammatical person of a subject noun phrase. The player subject (matched
|
|
157
|
+
* by `referableId` against `narrative.playerId`) takes the narrative person
|
|
158
|
+
* ("you are" in 2nd-person narration); otherwise an explicit `person` stamp, or
|
|
159
|
+
* undefined (→ third). ADR-199 §4 B, resolved at realize time where the
|
|
160
|
+
* narrative context is reliably present.
|
|
161
|
+
*/
|
|
162
|
+
function nounPerson(np, ctx) {
|
|
163
|
+
if (np.referableId !== undefined && np.referableId === ctx.narrative.playerId) {
|
|
164
|
+
return ctx.narrative.person;
|
|
165
|
+
}
|
|
166
|
+
return np.person;
|
|
167
|
+
}
|
|
168
|
+
/** Read the agreement surface (number, optional person) off a bound subject value. */
|
|
169
|
+
function subjectAgreement(subject, ctx) {
|
|
170
|
+
if (subject !== null && typeof subject === 'object' && typeof subject.kind === 'string') {
|
|
171
|
+
const phrase = subject;
|
|
172
|
+
if ((0, if_domain_1.isNounPhrase)(phrase))
|
|
173
|
+
return { number: phrase.number, person: nounPerson(phrase, ctx) };
|
|
174
|
+
if ((0, if_domain_1.isPhraseList)(phrase)) {
|
|
175
|
+
const present = phrase.items.filter((item) => !(0, if_domain_1.isEmpty)(item));
|
|
176
|
+
if (present.length > 1)
|
|
177
|
+
return { number: 'plural' }; // "the troll and the goats" → are
|
|
178
|
+
const only = present[0];
|
|
179
|
+
if (present.length === 1 && only && (0, if_domain_1.isNounPhrase)(only))
|
|
180
|
+
return { number: only.number, person: nounPerson(only, ctx) };
|
|
181
|
+
return { number: 'singular' };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// No agreement surface (Literal, Empty, scalar, unbound) → unmarked default (§4 C).
|
|
185
|
+
return { number: 'singular' };
|
|
186
|
+
}
|
|
187
|
+
/** Conjugate a lemma for the resolved subject number/person (Agreement authority). */
|
|
188
|
+
function conjugateVerb(lemma, number, person) {
|
|
189
|
+
// 3rd-person singular (and mass, which agrees singular) is the authored lemma.
|
|
190
|
+
if (person === 'third' && number !== 'plural')
|
|
191
|
+
return lemma;
|
|
192
|
+
const irregular = IRREGULAR_VERBS[lemma];
|
|
193
|
+
// 1st-person singular suppletive ("I am", "I was").
|
|
194
|
+
if (number !== 'plural' && person === 'first' && irregular?.firstSingular)
|
|
195
|
+
return irregular.firstSingular;
|
|
196
|
+
// Everything else (plural, or 1st/2nd person) takes the non-3rd-singular form.
|
|
197
|
+
if (irregular)
|
|
198
|
+
return irregular.plural;
|
|
199
|
+
return regularPluralVerb(lemma);
|
|
200
|
+
}
|
|
201
|
+
/** Realize a `Numeral` (ADR-198): digits, spelled words, or numeric ordinal. */
|
|
202
|
+
function renderNumeral(value, format) {
|
|
203
|
+
if (Number.isNaN(value))
|
|
204
|
+
return ''; // bound to a non-number — authoring error
|
|
205
|
+
switch (format) {
|
|
206
|
+
case 'words':
|
|
207
|
+
return (0, number_words_js_1.numberToWords)(value);
|
|
208
|
+
case 'ordinal':
|
|
209
|
+
return (0, number_words_js_1.ordinalString)(value);
|
|
210
|
+
default:
|
|
211
|
+
return String(value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Realize a `Verb` by agreeing it with its referenced subject's resolved surface. */
|
|
215
|
+
function renderVerb(verb, ctx) {
|
|
216
|
+
const agreement = subjectAgreement(ctx.params[verb.subjectRef], ctx);
|
|
217
|
+
// The subject's own person wins; else the Verb's declared person; else third.
|
|
218
|
+
const person = agreement.person ?? verb.person ?? 'third';
|
|
219
|
+
return conjugateVerb(verb.lemma, agreement.number, person);
|
|
220
|
+
}
|
|
221
|
+
// ===========================================================================
|
|
222
|
+
// Punctuation authority — serial commas, final and/or, no dangling comma
|
|
223
|
+
// ===========================================================================
|
|
224
|
+
/** Join already-rendered parts with the conjunction, honouring the serial comma. */
|
|
225
|
+
function joinParts(parts, conj, serialComma) {
|
|
226
|
+
if (parts.length === 0)
|
|
227
|
+
return 'nothing';
|
|
228
|
+
if (parts.length === 1)
|
|
229
|
+
return parts[0];
|
|
230
|
+
if (parts.length === 2)
|
|
231
|
+
return `${parts[0]} ${conj} ${parts[1]}`;
|
|
232
|
+
const head = parts.slice(0, -1).join(', ');
|
|
233
|
+
const last = parts[parts.length - 1];
|
|
234
|
+
return serialComma ? `${head}, ${conj} ${last}` : `${head} ${conj} ${last}`;
|
|
235
|
+
}
|
|
236
|
+
/** A noun phrase groups with identical siblings only if indefinite, singular, common. */
|
|
237
|
+
function isGroupable(np) {
|
|
238
|
+
return np.articleType === 'indefinite' && np.number === 'singular' && !np.properName;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Realize a list: group identical indefinite common nouns ("two goats"),
|
|
242
|
+
* pluralize, absorb `Empty`, and join under the punctuation authority. This is
|
|
243
|
+
* the ADR-190 list formatter, ported to operate on phrases (not `EntityInfo`).
|
|
244
|
+
*/
|
|
245
|
+
function renderList(items, conj, ctx) {
|
|
246
|
+
const present = items.filter((item) => !(0, if_domain_1.isEmpty)(item)); // Empty absorbed — no dangling comma
|
|
247
|
+
if (present.length === 0)
|
|
248
|
+
return 'nothing';
|
|
249
|
+
const parts = [];
|
|
250
|
+
const groupIndex = new Map();
|
|
251
|
+
for (const item of present) {
|
|
252
|
+
if ((0, if_domain_1.isNounPhrase)(item) && isGroupable(item)) {
|
|
253
|
+
const key = JSON.stringify([item.name, item.adjectives ?? []]);
|
|
254
|
+
const existing = groupIndex.get(key);
|
|
255
|
+
if (existing !== undefined) {
|
|
256
|
+
parts[existing].count++;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
groupIndex.set(key, parts.length);
|
|
260
|
+
parts.push({ count: 1, np: item, phrase: item });
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
parts.push({ count: 1, phrase: item });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const serialComma = ctx.settings.serialComma ?? true;
|
|
267
|
+
const rendered = parts
|
|
268
|
+
.map((part) => {
|
|
269
|
+
if (part.count > 1 && part.np) {
|
|
270
|
+
const adjectives = part.np.adjectives?.length ? `${part.np.adjectives.join(' ')} ` : '';
|
|
271
|
+
const plural = part.np.pluralForm ?? (0, pluralize_js_1.pluralize)(part.np.name);
|
|
272
|
+
return `${(0, number_words_js_1.countWord)(part.count)} ${adjectives}${plural}`;
|
|
273
|
+
}
|
|
274
|
+
return renderToString(part.phrase, ctx);
|
|
275
|
+
})
|
|
276
|
+
// ADR-196: an Optional-absent / Choice→Empty item realizes to "" — absorb it
|
|
277
|
+
// like Empty so it leaves no dangling comma (extends ADR-192 AC-6 to modifiers).
|
|
278
|
+
.filter((s) => s.length > 0);
|
|
279
|
+
if (rendered.length === 0)
|
|
280
|
+
return 'nothing';
|
|
281
|
+
return joinParts(rendered, conj, serialComma);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Realize a `Contents` (ADR-194): read the container's live contents from the
|
|
285
|
+
* world, bridge each entity to a `NounPhrase`, and render as a grouped list.
|
|
286
|
+
* Graceful — an unresolved container or missing bridge renders "nothing".
|
|
287
|
+
*/
|
|
288
|
+
function renderContents(contents, ctx) {
|
|
289
|
+
const ref = ctx.params[contents.containerRef];
|
|
290
|
+
let containerId;
|
|
291
|
+
if (typeof ref === 'string') {
|
|
292
|
+
containerId = ref;
|
|
293
|
+
}
|
|
294
|
+
else if (ref !== null && typeof ref === 'object' && ref.kind === 'noun') {
|
|
295
|
+
containerId = ref.referableId;
|
|
296
|
+
}
|
|
297
|
+
const bridge = ctx.world.nounPhraseFor;
|
|
298
|
+
if (containerId === undefined || !bridge)
|
|
299
|
+
return 'nothing';
|
|
300
|
+
const items = ctx.world
|
|
301
|
+
.getEntityContents(containerId)
|
|
302
|
+
.map((entity) => bridge(entity.id))
|
|
303
|
+
.filter((np) => np !== undefined);
|
|
304
|
+
return renderList(items, contents.conj ?? 'and', ctx);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Realize a `Slot` (ADR-195 §4): peek the turn's contributions for `slotKey`,
|
|
308
|
+
* realize each, absorb any that render empty (AC-4), and join the survivors. The
|
|
309
|
+
* SLOT owns the connective grammar — the contribution is bare content:
|
|
310
|
+
*
|
|
311
|
+
* - zero survivors → `''` (the slot is `Empty`; the stem + its terminator stay
|
|
312
|
+
* clean, no dangling space/comma — AC-3);
|
|
313
|
+
* - `sentence` mode (default) → a leading space then the survivors space-joined,
|
|
314
|
+
* so they follow the stem's terminator as independent sentences (AC-1);
|
|
315
|
+
* - `clause` mode → a leading `", "` then the survivors joined through the
|
|
316
|
+
* punctuation authority (`joinParts`: serial comma + final `conj`), so they
|
|
317
|
+
* attach as clauses before the stem's terminator (AC-2).
|
|
318
|
+
*
|
|
319
|
+
* The accessor is optional (`?.`): a context that never wired the store yields no
|
|
320
|
+
* contributions, so the slot simply realizes empty (ADR-195 §2).
|
|
321
|
+
*/
|
|
322
|
+
function renderSlot(slot, ctx) {
|
|
323
|
+
const contributions = ctx.slotContributions?.(slot.slotKey) ?? [];
|
|
324
|
+
const surfaces = contributions
|
|
325
|
+
.map((phrase) => renderToString(phrase, ctx))
|
|
326
|
+
.filter((s) => s.length > 0); // absorb Empty / empty-rendering contributions (AC-4)
|
|
327
|
+
if (surfaces.length === 0)
|
|
328
|
+
return ''; // zero contributions → Empty, clean stem (AC-3)
|
|
329
|
+
if ((slot.mode ?? 'sentence') === 'clause') {
|
|
330
|
+
const serialComma = ctx.settings.serialComma ?? true;
|
|
331
|
+
return `, ${joinParts(surfaces, slot.conj ?? 'and', serialComma)}`;
|
|
332
|
+
}
|
|
333
|
+
return ` ${surfaces.join(' ')}`;
|
|
334
|
+
}
|
|
335
|
+
// ===========================================================================
|
|
336
|
+
// Reference authority — last-mentioned tracking (placeholder; ADR-197)
|
|
337
|
+
// ===========================================================================
|
|
338
|
+
/**
|
|
339
|
+
* Record a realized noun phrase as last-mentioned so a later `Pronoun` can refer
|
|
340
|
+
* to it. The real resolution lands in ADR-197; here the Assembler only feeds the
|
|
341
|
+
* seam so the contract is exercised end-to-end.
|
|
342
|
+
*/
|
|
343
|
+
function noteReference(np, ctx) {
|
|
344
|
+
if (np.referableId !== undefined) {
|
|
345
|
+
ctx.reference.note({ referableId: np.referableId, number: np.number, pronounSet: np.pronounSet });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ===========================================================================
|
|
349
|
+
// Pronoun authority — case × number × gender (ADR-197)
|
|
350
|
+
// ===========================================================================
|
|
351
|
+
/** he/she/it/they forms keyed by case. */
|
|
352
|
+
const PRONOUNS = {
|
|
353
|
+
he: { subject: 'he', object: 'him', possessive: 'his', 'possessive-pronoun': 'his', reflexive: 'himself' },
|
|
354
|
+
she: { subject: 'she', object: 'her', possessive: 'her', 'possessive-pronoun': 'hers', reflexive: 'herself' },
|
|
355
|
+
it: { subject: 'it', object: 'it', possessive: 'its', 'possessive-pronoun': 'its', reflexive: 'itself' },
|
|
356
|
+
they: { subject: 'they', object: 'them', possessive: 'their', 'possessive-pronoun': 'theirs', reflexive: 'themselves' },
|
|
357
|
+
};
|
|
358
|
+
/** Pick the gender row for a referent: explicit pronounSet, else by number. */
|
|
359
|
+
function genderOf(ref) {
|
|
360
|
+
const set = ref.pronounSet?.toLowerCase();
|
|
361
|
+
if (set === 'he' || set === 'masculine')
|
|
362
|
+
return 'he';
|
|
363
|
+
if (set === 'she' || set === 'feminine')
|
|
364
|
+
return 'she';
|
|
365
|
+
if (set === 'they' || set === 'plural' || set === 'nonbinary')
|
|
366
|
+
return 'they';
|
|
367
|
+
if (set === 'it' || set === 'neuter')
|
|
368
|
+
return 'it';
|
|
369
|
+
return ref.number === 'plural' ? 'they' : 'it'; // no set → by number (mass agrees singular)
|
|
370
|
+
}
|
|
371
|
+
/** Realize a `Pronoun` (ADR-197): the last-mentioned referent in the requested case. */
|
|
372
|
+
function renderPronoun(pronoun, ctx) {
|
|
373
|
+
const ref = ctx.reference.lastMentioned();
|
|
374
|
+
// Graceful: no antecedent → neuter singular for the case (no throw).
|
|
375
|
+
const gender = ref ? genderOf(ref) : 'it';
|
|
376
|
+
return PRONOUNS[gender][pronoun.case];
|
|
377
|
+
}
|
|
378
|
+
// ===========================================================================
|
|
379
|
+
// Whitespace authority — collapse, verbatim-exempt
|
|
380
|
+
// ===========================================================================
|
|
381
|
+
/**
|
|
382
|
+
* Collapse runs of whitespace in non-verbatim runs to a single space, drop
|
|
383
|
+
* whitespace at run boundaries, and trim the block ends. Verbatim runs pass
|
|
384
|
+
* through untouched (ADR-183 whitespace authority, now Assembler-owned).
|
|
385
|
+
*/
|
|
386
|
+
function collapseWhitespace(runs) {
|
|
387
|
+
const out = [];
|
|
388
|
+
let prevEndedWithSpace = true; // seeded true so leading whitespace is trimmed
|
|
389
|
+
for (const run of runs) {
|
|
390
|
+
if (run.verbatim) {
|
|
391
|
+
out.push(run);
|
|
392
|
+
if (run.text.length > 0)
|
|
393
|
+
prevEndedWithSpace = /\s$/.test(run.text);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
let text = run.text.replace(/\s+/g, ' ');
|
|
397
|
+
if (prevEndedWithSpace)
|
|
398
|
+
text = text.replace(/^ /, '');
|
|
399
|
+
if (text.length === 0)
|
|
400
|
+
continue;
|
|
401
|
+
out.push({ ...run, text });
|
|
402
|
+
prevEndedWithSpace = / $/.test(text);
|
|
403
|
+
}
|
|
404
|
+
// Trim trailing space on the final non-verbatim run.
|
|
405
|
+
for (let i = out.length - 1; i >= 0; i--) {
|
|
406
|
+
if (out[i].verbatim)
|
|
407
|
+
break;
|
|
408
|
+
out[i] = { ...out[i], text: out[i].text.replace(/ $/, '') };
|
|
409
|
+
if (out[i].text.length > 0)
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
return out.filter((r) => r.verbatim || r.text.length > 0);
|
|
413
|
+
}
|
|
414
|
+
// ===========================================================================
|
|
415
|
+
// Case authority — sentence-start capitalization
|
|
416
|
+
// ===========================================================================
|
|
417
|
+
/**
|
|
418
|
+
* Capitalize the first alphabetic character of a sentence. Exposed as the Case
|
|
419
|
+
* authority; the explicit `{capitalize …}` template hint is wired to it by the
|
|
420
|
+
* parser in Phase 3 (ADR-192 §5). Not auto-applied — realization preserves the
|
|
421
|
+
* author's case unless capitalization is requested.
|
|
422
|
+
*
|
|
423
|
+
* @param text the realized text
|
|
424
|
+
* @returns the text with its first letter upper-cased
|
|
425
|
+
*/
|
|
426
|
+
function capitalizeSentenceStart(text) {
|
|
427
|
+
const index = text.search(/[a-z]/i);
|
|
428
|
+
if (index < 0)
|
|
429
|
+
return text;
|
|
430
|
+
return text.slice(0, index) + text[index].toUpperCase() + text.slice(index + 1);
|
|
431
|
+
}
|
|
432
|
+
// ===========================================================================
|
|
433
|
+
// Choice selection (ADR-196 §2/§3) — deterministic, persistent variation
|
|
434
|
+
// ===========================================================================
|
|
435
|
+
/**
|
|
436
|
+
* FNV-1a 32-bit hash of a seed string. Deterministic; used only to seed the PRNG.
|
|
437
|
+
*/
|
|
438
|
+
function hashSeed(s) {
|
|
439
|
+
let h = 2166136261 >>> 0;
|
|
440
|
+
for (let i = 0; i < s.length; i++) {
|
|
441
|
+
h ^= s.charCodeAt(i);
|
|
442
|
+
h = Math.imul(h, 16777619);
|
|
443
|
+
}
|
|
444
|
+
return h >>> 0;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* `mulberry32` — a tiny deterministic PRNG. Seeded from `(entityId, messageKey,
|
|
448
|
+
* counter)`, never `Math.random`/`Date.now`, so `random`/`sticky` selection
|
|
449
|
+
* reproduces byte-identically across runs and after save/restore (ADR-196 §3).
|
|
450
|
+
*/
|
|
451
|
+
function seededUnitFloat(entityId, messageKey, counter) {
|
|
452
|
+
let a = hashSeed(`${entityId}\0${messageKey}\0${counter}`) >>> 0;
|
|
453
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
454
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
455
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
456
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Select a `Choice`'s alternative from its persisted counter and advance the
|
|
460
|
+
* counter (ADR-196 §2). This is the one place the Assembler writes `ctx.textState`
|
|
461
|
+
* — the declared realize-time mutation (ADR-192 §7 / ADR-196 §4).
|
|
462
|
+
*
|
|
463
|
+
* Stored-number encoding: a trigger count for cycling/stopping/firstTime/random;
|
|
464
|
+
* the chosen index + 1 (sentinel 0/undefined = unchosen) for sticky.
|
|
465
|
+
*
|
|
466
|
+
* @returns the selected alternative phrase (caller realizes it; may be Empty).
|
|
467
|
+
*/
|
|
468
|
+
function selectChoice(choice, ctx) {
|
|
469
|
+
const { alternatives, selector, entityId, messageKey } = choice;
|
|
470
|
+
const len = alternatives.length;
|
|
471
|
+
if (len === 0)
|
|
472
|
+
return { kind: 'empty' }; // defensive — the contract requires ≥ 1
|
|
473
|
+
const stored = ctx.textState.get(entityId, messageKey);
|
|
474
|
+
switch (selector) {
|
|
475
|
+
case 'cycling': {
|
|
476
|
+
const n = stored ?? 0;
|
|
477
|
+
ctx.textState.set(entityId, messageKey, n + 1);
|
|
478
|
+
return alternatives[n % len];
|
|
479
|
+
}
|
|
480
|
+
case 'stopping': {
|
|
481
|
+
const n = stored ?? 0;
|
|
482
|
+
ctx.textState.set(entityId, messageKey, n + 1);
|
|
483
|
+
return alternatives[Math.min(n, len - 1)];
|
|
484
|
+
}
|
|
485
|
+
case 'firstTime': {
|
|
486
|
+
const n = stored ?? 0;
|
|
487
|
+
ctx.textState.set(entityId, messageKey, Math.min(n + 1, 1));
|
|
488
|
+
return alternatives[n === 0 ? 0 : Math.min(1, len - 1)];
|
|
489
|
+
}
|
|
490
|
+
case 'random': {
|
|
491
|
+
const n = stored ?? 0;
|
|
492
|
+
ctx.textState.set(entityId, messageKey, n + 1);
|
|
493
|
+
return alternatives[Math.floor(seededUnitFloat(entityId, messageKey, n) * len)];
|
|
494
|
+
}
|
|
495
|
+
case 'sticky': {
|
|
496
|
+
// Already chosen — replay the persisted index (stored = index + 1).
|
|
497
|
+
if (stored && stored > 0)
|
|
498
|
+
return alternatives[Math.min(stored - 1, len - 1)];
|
|
499
|
+
const i = Math.floor(seededUnitFloat(entityId, messageKey, 0) * len);
|
|
500
|
+
ctx.textState.set(entityId, messageKey, i + 1);
|
|
501
|
+
return alternatives[i];
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
// ===========================================================================
|
|
506
|
+
// Realization core
|
|
507
|
+
// ===========================================================================
|
|
508
|
+
/** Push a phrase's own decorations onto the inherited stack. */
|
|
509
|
+
function extendDeco(base, decorations) {
|
|
510
|
+
if (!decorations || decorations.length === 0)
|
|
511
|
+
return base;
|
|
512
|
+
return [
|
|
513
|
+
...base,
|
|
514
|
+
...decorations.map((d) => (d.value !== undefined ? { className: d.className, value: d.value } : { className: d.className })),
|
|
515
|
+
];
|
|
516
|
+
}
|
|
517
|
+
// ===========================================================================
|
|
518
|
+
// Sentence/Quote edge metadata (ADR-201 §3) — emitted here, resolved by the
|
|
519
|
+
// reconciliation pass. All reads below are a NODE'S OWN realized surface
|
|
520
|
+
// (its child/utterance runs), never neighbours' or whole-output prose (ADR-202).
|
|
521
|
+
// ===========================================================================
|
|
522
|
+
/** A node's own last glyph already terminates the clause (so no auto-terminal). */
|
|
523
|
+
function endsWithTerminalOrEllipsis(text) {
|
|
524
|
+
const t = text.trimEnd();
|
|
525
|
+
// `.` already covers a `...` ellipsis; `…` is the single-glyph form.
|
|
526
|
+
return t.endsWith('.') || t.endsWith('?') || t.endsWith('!') || t.endsWith('…');
|
|
527
|
+
}
|
|
528
|
+
/** True if the last non-empty run of a realized child ends in terminal punctuation. */
|
|
529
|
+
function lastContentRunEndsTerminal(runs) {
|
|
530
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
531
|
+
if (runs[i].text.length > 0)
|
|
532
|
+
return endsWithTerminalOrEllipsis(runs[i].text);
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Flag the first non-empty run as sentence-initial + cap-eligible, so the
|
|
538
|
+
* reconciliation pass capitalizes a sentence's / quote's first word. An explicit
|
|
539
|
+
* `capEligible: false` (author opt-out) is preserved, not overridden.
|
|
540
|
+
*/
|
|
541
|
+
function markFirstSentenceInitial(runs) {
|
|
542
|
+
const i = runs.findIndex((r) => r.text.length > 0);
|
|
543
|
+
if (i < 0)
|
|
544
|
+
return runs;
|
|
545
|
+
const copy = runs.slice();
|
|
546
|
+
copy[i] = { ...copy[i], sentenceInitial: true, capEligible: copy[i].capEligible === false ? false : true };
|
|
547
|
+
return copy;
|
|
548
|
+
}
|
|
549
|
+
/** Mark the last non-empty run as owning a trailing terminal mark (Sentence close). */
|
|
550
|
+
function markLastTrailingTerminal(runs, terminal) {
|
|
551
|
+
for (let i = runs.length - 1; i >= 0; i--) {
|
|
552
|
+
if (runs[i].text.length > 0) {
|
|
553
|
+
const copy = runs.slice();
|
|
554
|
+
copy[i] = { ...copy[i], ownsTrailingPunct: terminal };
|
|
555
|
+
return copy;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return runs;
|
|
559
|
+
}
|
|
560
|
+
/** Realize a phrase to flat runs, threading the decoration stack through composition. */
|
|
561
|
+
function realizeToRuns(phrase, ctx, deco) {
|
|
562
|
+
if ((0, if_domain_1.isEmpty)(phrase))
|
|
563
|
+
return [];
|
|
564
|
+
if ((0, if_domain_1.isLiteral)(phrase)) {
|
|
565
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
566
|
+
return [{ text: phrase.text, verbatim: phrase.whitespace === 'verbatim', deco: own }];
|
|
567
|
+
}
|
|
568
|
+
if ((0, if_domain_1.isNounPhrase)(phrase)) {
|
|
569
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
570
|
+
noteReference(phrase, ctx);
|
|
571
|
+
return [{ text: renderNoun(phrase), verbatim: false, deco: own }];
|
|
572
|
+
}
|
|
573
|
+
if ((0, if_domain_1.isPhraseList)(phrase)) {
|
|
574
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
575
|
+
return [{ text: renderList(phrase.items, phrase.conj, ctx), verbatim: false, deco: own }];
|
|
576
|
+
}
|
|
577
|
+
if ((0, if_domain_1.isVerb)(phrase)) {
|
|
578
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
579
|
+
return [{ text: renderVerb(phrase, ctx), verbatim: false, deco: own }];
|
|
580
|
+
}
|
|
581
|
+
if ((0, if_domain_1.isVerbatim)(phrase)) {
|
|
582
|
+
// Opaque pass-through (ADR-200): exempt from whitespace collapse.
|
|
583
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
584
|
+
return [{ text: phrase.text, verbatim: true, deco: own }];
|
|
585
|
+
}
|
|
586
|
+
if ((0, if_domain_1.isNumeral)(phrase)) {
|
|
587
|
+
// Numeral (ADR-198): digits / spelled words / ordinal.
|
|
588
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
589
|
+
return [{ text: renderNumeral(phrase.value, phrase.format), verbatim: false, deco: own }];
|
|
590
|
+
}
|
|
591
|
+
if ((0, if_domain_1.isPronoun)(phrase)) {
|
|
592
|
+
// Pronoun (ADR-197): the last-mentioned referent in the requested case.
|
|
593
|
+
// ADR-201 §2 (Q1) capitalization: `true` ⇒ cap now (own-glyph rule); `false`
|
|
594
|
+
// ⇒ never (opt out of sentence-start cap); absent ⇒ defer to position — if it
|
|
595
|
+
// lands sentence-initial, `markFirstSentenceInitial` flags it for the pass.
|
|
596
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
597
|
+
const text = renderPronoun(phrase, ctx);
|
|
598
|
+
if (phrase.capitalize === true) {
|
|
599
|
+
return [{ text: capitalizeSentenceStart(text), verbatim: false, deco: own }];
|
|
600
|
+
}
|
|
601
|
+
if (phrase.capitalize === false) {
|
|
602
|
+
return [{ text, verbatim: false, deco: own, capEligible: false }];
|
|
603
|
+
}
|
|
604
|
+
return [{ text, verbatim: false, deco: own }];
|
|
605
|
+
}
|
|
606
|
+
if ((0, if_domain_1.isContents)(phrase)) {
|
|
607
|
+
// Contents (ADR-194): the container's live contents as a grouped list.
|
|
608
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
609
|
+
return [{ text: renderContents(phrase, ctx), verbatim: false, deco: own }];
|
|
610
|
+
}
|
|
611
|
+
if ((0, if_domain_1.isSlot)(phrase)) {
|
|
612
|
+
// Slot (ADR-195): the turn's contributions for this key, joined under the
|
|
613
|
+
// slot-owned connective grammar. Zero survivors → no run (absorbed as Empty).
|
|
614
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
615
|
+
const text = renderSlot(phrase, ctx);
|
|
616
|
+
return text ? [{ text, verbatim: false, deco: own }] : [];
|
|
617
|
+
}
|
|
618
|
+
if ((0, if_domain_1.isSequence)(phrase)) {
|
|
619
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
620
|
+
return phrase.parts.flatMap((part) => realizeToRuns(part, ctx, own));
|
|
621
|
+
}
|
|
622
|
+
if ((0, if_domain_1.isOptional)(phrase)) {
|
|
623
|
+
// Optional (ADR-196 §1): the producer resolved `present`; realize the child or
|
|
624
|
+
// nothing. Absent → no runs, absorbed by the enclosing combinator like Empty.
|
|
625
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
626
|
+
return phrase.present ? realizeToRuns(phrase.child, ctx, own) : [];
|
|
627
|
+
}
|
|
628
|
+
if ((0, if_domain_1.isChoice)(phrase)) {
|
|
629
|
+
// Choice (ADR-196 §2): select one alternative from the persisted counter,
|
|
630
|
+
// advance the counter, and realize the winner. A winner that realizes to
|
|
631
|
+
// Empty leaves no runs (once-only text).
|
|
632
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
633
|
+
return realizeToRuns(selectChoice(phrase, ctx), ctx, own);
|
|
634
|
+
}
|
|
635
|
+
if ((0, if_domain_1.isSentence)(phrase)) {
|
|
636
|
+
// Sentence (ADR-201 §2): realize the child as a sentence — cap its first word
|
|
637
|
+
// and emit a terminal mark at its close, suppressed if the child already ends
|
|
638
|
+
// in terminal punctuation or an ellipsis (no double-punctuation).
|
|
639
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
640
|
+
let runs = markFirstSentenceInitial(realizeToRuns(phrase.child, ctx, own));
|
|
641
|
+
if (!lastContentRunEndsTerminal(runs)) {
|
|
642
|
+
runs = markLastTrailingTerminal(runs, phrase.terminal ?? '.');
|
|
643
|
+
}
|
|
644
|
+
return runs;
|
|
645
|
+
}
|
|
646
|
+
if ((0, if_domain_1.isQuote)(phrase)) {
|
|
647
|
+
// Quote (ADR-201 §2): wrap the utterance in locale glyphs, cap its first word,
|
|
648
|
+
// and place terminal punctuation INSIDE the closing glyph (suppressed if the
|
|
649
|
+
// utterance already ends terminal/ellipsis). The attributive comma is the
|
|
650
|
+
// template's in v1 (explicit composition, ADR §5) — the Quote does not own it.
|
|
651
|
+
// An utterance that absorbs to nothing absorbs the whole quote (no stray `""`).
|
|
652
|
+
const own = extendDeco(deco, phrase.decorations);
|
|
653
|
+
const utterance = markFirstSentenceInitial(realizeToRuns(phrase.utterance, ctx, own));
|
|
654
|
+
if (!utterance.some((r) => r.text.length > 0))
|
|
655
|
+
return [];
|
|
656
|
+
const open = { text: ctx.settings.openQuote ?? '"', verbatim: false, deco: own, quoteOpen: true };
|
|
657
|
+
const close = { text: ctx.settings.closeQuote ?? '"', verbatim: false, deco: own, quoteClose: true };
|
|
658
|
+
if (!lastContentRunEndsTerminal(utterance)) {
|
|
659
|
+
close.ownsTrailingPunct = phrase.terminal ?? '.';
|
|
660
|
+
}
|
|
661
|
+
return [open, ...utterance, close];
|
|
662
|
+
}
|
|
663
|
+
// Defensive: every if-domain kind is realized above, so `phrase` narrows to
|
|
664
|
+
// `never` here — TypeScript proves exhaustiveness. The cast keeps the guard
|
|
665
|
+
// live at runtime: a future kind added without a case is refused loudly,
|
|
666
|
+
// naming it, rather than silently dropping text.
|
|
667
|
+
throw new errors_js_1.PhraseNotImplementedError(phrase.kind);
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* The single structural reconciliation pass (ADR-201 §3.2). Walks the realized
|
|
671
|
+
* runs once and resolves, using run metadata only (never prose scanning, ADR-202):
|
|
672
|
+
* 1. Capitalization — upper-case the first glyph of each `sentenceInitial`
|
|
673
|
+
* `capEligible` run (the case authority's glyph helper does the upper-casing).
|
|
674
|
+
* 2. Terminal punctuation — materialize each `ownsTrailingPunct`: a closing-quote
|
|
675
|
+
* run places it INSIDE the glyph (`."`); any other run appends it.
|
|
676
|
+
* Whitespace collapse (§3.2 step 4) stays the final per-segment step in `realize`.
|
|
677
|
+
*/
|
|
678
|
+
function reconciliationPass(runs) {
|
|
679
|
+
return runs.map((run) => {
|
|
680
|
+
let out = run;
|
|
681
|
+
if (out.sentenceInitial && out.capEligible === true && out.text.length > 0) {
|
|
682
|
+
out = { ...out, text: capitalizeSentenceStart(out.text) };
|
|
683
|
+
}
|
|
684
|
+
if (out.ownsTrailingPunct) {
|
|
685
|
+
out = out.quoteClose
|
|
686
|
+
? { ...out, text: out.ownsTrailingPunct + out.text } // terminal inside the closing glyph
|
|
687
|
+
: { ...out, text: out.text + out.ownsTrailingPunct }; // sentence terminal appended
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
/** Realize a phrase to a plain string (used for list items and nested phrases). */
|
|
693
|
+
function renderToString(phrase, ctx) {
|
|
694
|
+
return collapseWhitespace(reconciliationPass(realizeToRuns(phrase, ctx, [])))
|
|
695
|
+
.map((r) => r.text)
|
|
696
|
+
.join('');
|
|
697
|
+
}
|
|
698
|
+
/** Build text content from collapsed runs, nesting decorations where present. */
|
|
699
|
+
function runsToContent(runs) {
|
|
700
|
+
const content = [];
|
|
701
|
+
let buffer = '';
|
|
702
|
+
for (const run of runs) {
|
|
703
|
+
if (run.deco.length === 0) {
|
|
704
|
+
buffer += run.text;
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (buffer) {
|
|
708
|
+
content.push(buffer);
|
|
709
|
+
buffer = '';
|
|
710
|
+
}
|
|
711
|
+
content.push(wrapDecorations(run.text, run.deco));
|
|
712
|
+
}
|
|
713
|
+
if (buffer)
|
|
714
|
+
content.push(buffer);
|
|
715
|
+
if (content.length === 0)
|
|
716
|
+
content.push('');
|
|
717
|
+
return content;
|
|
718
|
+
}
|
|
719
|
+
/** Wrap text in nested decorations, outermost first. */
|
|
720
|
+
function wrapDecorations(text, stack) {
|
|
721
|
+
let node = text;
|
|
722
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
723
|
+
const layer = stack[i];
|
|
724
|
+
node =
|
|
725
|
+
layer.value !== undefined
|
|
726
|
+
? { className: layer.className, content: [node], value: layer.value }
|
|
727
|
+
: { className: layer.className, content: [node] };
|
|
728
|
+
}
|
|
729
|
+
return node;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Split a run stream into per-block segments at newline boundaries (Whitespace
|
|
733
|
+
* authority — the block-structure half ADR-183/`createBlocks` owned). A single
|
|
734
|
+
* `\n` makes the next block a `tight` continuation; a blank line (`\n\n+`) starts
|
|
735
|
+
* a fresh paragraph. Newlines split every run — verbatim runs keep their
|
|
736
|
+
* *horizontal* whitespace but still break into blocks, matching the legacy
|
|
737
|
+
* `createBlocks` behaviour. No block's content carries a `\n`.
|
|
738
|
+
*/
|
|
739
|
+
function splitRunsOnNewlines(runs) {
|
|
740
|
+
const segments = [];
|
|
741
|
+
let current = [];
|
|
742
|
+
let tight = false; // the first segment is never a tight continuation
|
|
743
|
+
for (const run of runs) {
|
|
744
|
+
const pieces = run.text.split(/(\n+)/); // keep the newline groups as separators
|
|
745
|
+
for (const piece of pieces) {
|
|
746
|
+
if (piece === '')
|
|
747
|
+
continue;
|
|
748
|
+
if (/^\n+$/.test(piece)) {
|
|
749
|
+
segments.push({ runs: current, tight });
|
|
750
|
+
current = [];
|
|
751
|
+
tight = piece.length === 1; // single \n → tight; blank line → paragraph
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
current.push({ ...run, text: piece });
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
segments.push({ runs: current, tight });
|
|
759
|
+
return segments;
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* The English Assembler. Realizes a phrase tree to text blocks under the default
|
|
763
|
+
* channel key; the report layer re-keys per channel.
|
|
764
|
+
*/
|
|
765
|
+
class EnglishAssembler {
|
|
766
|
+
/**
|
|
767
|
+
* Realize a phrase tree to text blocks.
|
|
768
|
+
*
|
|
769
|
+
* Newlines in the realized text are lifted to block boundaries (no block's
|
|
770
|
+
* content carries `\n`); horizontal whitespace is collapsed per block, except
|
|
771
|
+
* in verbatim runs. A single-line tree yields one block.
|
|
772
|
+
*
|
|
773
|
+
* @param tree the phrase tree to realize
|
|
774
|
+
* @param ctx the render context (world, params, settings, seams)
|
|
775
|
+
* @returns the realized text blocks
|
|
776
|
+
* @throws PhraseNotImplementedError when a reserved stub kind is encountered
|
|
777
|
+
*/
|
|
778
|
+
realize(tree, ctx) {
|
|
779
|
+
let runs = realizeToRuns(tree, ctx, []);
|
|
780
|
+
// Honor the optional position seam (ADR-201 §4): a context that declares it
|
|
781
|
+
// starts sentence-initial flags the first word for capitalization. Absent →
|
|
782
|
+
// not sentence-initial (today's behavior), so existing render paths are unaffected.
|
|
783
|
+
if (ctx.position?.sentenceInitial)
|
|
784
|
+
runs = markFirstSentenceInitial(runs);
|
|
785
|
+
const segments = splitRunsOnNewlines(reconciliationPass(runs));
|
|
786
|
+
const blocks = [];
|
|
787
|
+
for (const seg of segments) {
|
|
788
|
+
const content = runsToContent(collapseWhitespace(seg.runs));
|
|
789
|
+
// Drop blank lines — they leave no block (paragraph spacing comes from
|
|
790
|
+
// the `tight` flag on the following block).
|
|
791
|
+
if (content.length === 1 && content[0] === '')
|
|
792
|
+
continue;
|
|
793
|
+
blocks.push({
|
|
794
|
+
key: exports.ASSEMBLER_DEFAULT_BLOCK_KEY,
|
|
795
|
+
content,
|
|
796
|
+
...(seg.tight ? { tight: true } : {}),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (blocks.length === 0) {
|
|
800
|
+
blocks.push({ key: exports.ASSEMBLER_DEFAULT_BLOCK_KEY, content: [''] });
|
|
801
|
+
}
|
|
802
|
+
return blocks;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
exports.EnglishAssembler = EnglishAssembler;
|
|
806
|
+
//# sourceMappingURL=english-assembler.js.map
|