calliope-ts 0.0.2 → 0.0.3
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/README.md +3 -13
- package/dist/caesura.d.ts +33 -0
- package/dist/caesura.d.ts.map +1 -0
- package/dist/caesura.js +202 -0
- package/dist/calliope/boundaries.d.ts +19 -0
- package/dist/calliope/boundaries.d.ts.map +1 -0
- package/dist/calliope/boundaries.js +182 -0
- package/dist/calliope/bracketing.d.ts +11 -0
- package/dist/calliope/bracketing.d.ts.map +1 -0
- package/dist/calliope/bracketing.js +416 -0
- package/dist/calliope/deps.d.ts +4 -0
- package/dist/calliope/deps.d.ts.map +1 -0
- package/dist/calliope/deps.js +181 -0
- package/dist/calliope/engine.d.ts +3 -0
- package/dist/calliope/engine.d.ts.map +1 -0
- package/dist/calliope/engine.js +71 -0
- package/dist/calliope/feats.d.ts +9 -0
- package/dist/calliope/feats.d.ts.map +1 -0
- package/dist/calliope/feats.js +45 -0
- package/dist/calliope/names.d.ts +7 -0
- package/dist/calliope/names.d.ts.map +1 -0
- package/dist/calliope/names.js +42 -0
- package/dist/calliope/postag.d.ts +8 -0
- package/dist/calliope/postag.d.ts.map +1 -0
- package/dist/calliope/postag.js +250 -0
- package/dist/calliope/prosodic.d.ts +3 -0
- package/dist/calliope/prosodic.d.ts.map +1 -0
- package/dist/calliope/prosodic.js +275 -0
- package/dist/calliope/relstress.d.ts +4 -0
- package/dist/calliope/relstress.d.ts.map +1 -0
- package/dist/calliope/relstress.js +688 -0
- package/dist/calliope/stressrules.d.ts +28 -0
- package/dist/calliope/stressrules.d.ts.map +1 -0
- package/dist/calliope/stressrules.js +147 -0
- package/dist/calliope/syntax.d.ts +38 -0
- package/dist/calliope/syntax.d.ts.map +1 -0
- package/dist/calliope/syntax.js +234 -0
- package/dist/calliope/udpos.d.ts +13 -0
- package/dist/calliope/udpos.d.ts.map +1 -0
- package/dist/calliope/udpos.js +156 -0
- package/dist/clio/caesura.d.ts +27 -0
- package/dist/clio/caesura.d.ts.map +1 -0
- package/dist/clio/caesura.js +148 -0
- package/dist/clio/depfix.d.ts +13 -0
- package/dist/clio/depfix.d.ts.map +1 -0
- package/dist/clio/depfix.js +84 -0
- package/dist/clio/display.d.ts +32 -0
- package/dist/clio/display.d.ts.map +1 -0
- package/dist/clio/display.js +976 -0
- package/dist/clio/engine.d.ts +3 -0
- package/dist/clio/engine.d.ts.map +1 -0
- package/dist/clio/engine.js +28 -0
- package/dist/clio/parser.d.ts +10 -0
- package/dist/clio/parser.d.ts.map +1 -0
- package/dist/clio/parser.js +696 -0
- package/dist/clio/phonological.d.ts +41 -0
- package/dist/clio/phonological.d.ts.map +1 -0
- package/dist/clio/phonological.js +788 -0
- package/dist/clio/phrasestress.d.ts +6 -0
- package/dist/clio/phrasestress.d.ts.map +1 -0
- package/dist/clio/phrasestress.js +106 -0
- package/dist/clio/pipeline.d.ts +11 -0
- package/dist/clio/pipeline.d.ts.map +1 -0
- package/dist/clio/pipeline.js +147 -0
- package/dist/clio/rhyme.d.ts +65 -0
- package/dist/clio/rhyme.d.ts.map +1 -0
- package/dist/clio/rhyme.js +761 -0
- package/dist/clio/scandroid.d.ts +17 -0
- package/dist/clio/scandroid.d.ts.map +1 -0
- package/dist/clio/scandroid.js +435 -0
- package/dist/clio/scansion.d.ts +46 -0
- package/dist/clio/scansion.d.ts.map +1 -0
- package/dist/clio/scansion.js +1086 -0
- package/dist/clio/semantics.d.ts +44 -0
- package/dist/clio/semantics.d.ts.map +1 -0
- package/dist/clio/semantics.js +139 -0
- package/dist/clio/stress.d.ts +83 -0
- package/dist/clio/stress.d.ts.map +1 -0
- package/dist/clio/stress.js +1737 -0
- package/dist/clio/tagfix.d.ts +6 -0
- package/dist/clio/tagfix.d.ts.map +1 -0
- package/dist/clio/tagfix.js +101 -0
- package/dist/display.d.ts +0 -6
- package/dist/display.d.ts.map +1 -1
- package/dist/display.js +322 -141
- package/dist/engine.d.ts +9 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +12 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +169 -26
- package/dist/parser.d.ts +3 -3
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +320 -563
- package/dist/phonological.d.ts +1 -1
- package/dist/phonological.d.ts.map +1 -1
- package/dist/phonological.js +73 -4
- package/dist/phrasestress.d.ts +6 -0
- package/dist/phrasestress.d.ts.map +1 -0
- package/dist/phrasestress.js +106 -0
- package/dist/rhyme.d.ts +40 -1
- package/dist/rhyme.d.ts.map +1 -1
- package/dist/rhyme.js +435 -7
- package/dist/scansion.d.ts +9 -0
- package/dist/scansion.d.ts.map +1 -1
- package/dist/scansion.js +145 -18
- package/dist/semantics.d.ts +44 -0
- package/dist/semantics.d.ts.map +1 -0
- package/dist/semantics.js +139 -0
- package/dist/stress.d.ts +127 -4
- package/dist/stress.d.ts.map +1 -1
- package/dist/stress.js +834 -61
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/caesura.ts +201 -0
- package/src/calliope/boundaries.ts +190 -0
- package/src/calliope/bracketing.ts +390 -0
- package/src/calliope/deps.ts +160 -0
- package/src/calliope/engine.ts +77 -0
- package/src/calliope/feats.ts +46 -0
- package/src/calliope/names.ts +44 -0
- package/src/calliope/postag.ts +253 -0
- package/src/calliope/prosodic.ts +262 -0
- package/src/calliope/relstress.ts +645 -0
- package/src/calliope/stressrules.ts +147 -0
- package/src/calliope/syntax.ts +218 -0
- package/src/calliope/udpos.ts +152 -0
- package/src/clio/caesura.ts +145 -0
- package/src/clio/depfix.ts +88 -0
- package/src/clio/display.ts +1042 -0
- package/src/clio/engine.ts +38 -0
- package/src/clio/parser.ts +845 -0
- package/src/clio/phonological.ts +849 -0
- package/src/clio/phrasestress.ts +108 -0
- package/src/clio/pipeline.ts +154 -0
- package/src/clio/rhyme.ts +740 -0
- package/src/clio/scandroid.ts +434 -0
- package/src/clio/scansion.ts +1130 -0
- package/src/clio/semantics.ts +134 -0
- package/src/clio/stress.ts +1731 -0
- package/src/clio/tagfix.ts +104 -0
- package/src/display.ts +321 -137
- package/src/engine.ts +22 -0
- package/src/index.ts +176 -32
- package/src/parser.ts +346 -682
- package/src/phonological.ts +72 -4
- package/src/phrasestress.ts +108 -0
- package/src/rhyme.ts +428 -8
- package/src/scansion.ts +136 -15
- package/src/semantics.ts +134 -0
- package/src/stress.ts +794 -64
- package/src/types.ts +39 -4
- package/tests/DataForHayesLinesOnly.txt +364 -0
- package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
- package/tests/basic.test.ts +443 -37
- package/tests/bench-hayes.mjs +72 -0
- package/tests/wagner-stress.test.ts +188 -0
- package/vitest.config.ts +0 -15
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
// calliope/relstress.ts — per-phonological-phrase relative stress for Calliope.
|
|
2
|
+
//
|
|
3
|
+
// This is the corrected relativisation McAleese A2 step 1b prescribes —
|
|
4
|
+
// "determine the relative stresses in EACH PP (w / n / m / s)" — and the direct
|
|
5
|
+
// answer to the maintainer's critique of the old pipeline (everything defaulting
|
|
6
|
+
// high, per-word boosts stacking linearly into adjacent reds, function words
|
|
7
|
+
// flattened into long w-runs).
|
|
8
|
+
//
|
|
9
|
+
// Principles:
|
|
10
|
+
// • Default is LOW. Each word's prominence is RAISED only with motivation.
|
|
11
|
+
// • Within a PP there is ONE nuclear peak (s) — the φ's MOST PROMINENT word per the
|
|
12
|
+
// genuine phrase stress (lowest `phraseStress`, the Compound + Nuclear Stress
|
|
13
|
+
// Rules computed in bracketing.ts). Other content words are subordinate (m); the
|
|
14
|
+
// rest ramp DOWN — nothing is boosted past the peak.
|
|
15
|
+
// • FUNCTION words take a category gradient, never a flat run:
|
|
16
|
+
// preposition / article / coordinator / complementiser → x (zero-provision)
|
|
17
|
+
// possessive / personal pronoun / auxiliary / modal → w (overt weak)
|
|
18
|
+
// quantifier / demonstrative / wh-word / negator → n (some stress)
|
|
19
|
+
// so "of all my days" reads x · n · w · s (of < my < all < days).
|
|
20
|
+
// • Compound fore-stress (SLATE roof, ICE cream) needs NO special case here: the
|
|
21
|
+
// Compound Stress Rule already gave the compound's LEFT element the lower
|
|
22
|
+
// phraseStress, so it wins the φ nuclear automatically.
|
|
23
|
+
// • The clash invariant is preserved: no two adjacent syllables share n / m / s
|
|
24
|
+
// (w and x may repeat); the less-prominent of a clashing pair is demoted.
|
|
25
|
+
//
|
|
26
|
+
// This stage DERIVES the symbolic contour FROM the integer phrase stress, per φ — but
|
|
27
|
+
// the two remain SEPARATE material signals: the integer ranking is global (one
|
|
28
|
+
// utterance nuclear), the symbolic contour is local (one beat per φ) and then
|
|
29
|
+
// modified (clash resolution), so they legitimately diverge — never forced equal.
|
|
30
|
+
import { collectPPTokens } from '../phonological.js';
|
|
31
|
+
import { isTransitiveFunctionWord, isPronoun, isInherentlyGiven } from './syntax.js';
|
|
32
|
+
const RANK = { x: 0, w: 1, n: 2, m: 3, s: 4 };
|
|
33
|
+
const LEVELS = ['x', 'w', 'n', 'm', 's'];
|
|
34
|
+
const CONTENT = /^(NN|NNS|NNP|NNPS|JJ|JJR|JJS|VB|VBD|VBG|VBN|VBP|VBZ|RB|RBR|RBS|CD|UH)$/;
|
|
35
|
+
const ARTICLES = new Set(['a', 'an', 'the']);
|
|
36
|
+
const QUANTIFIERS = new Set([
|
|
37
|
+
'all', 'both', 'each', 'every', 'some', 'any', 'many', 'much', 'few', 'no',
|
|
38
|
+
'most', 'half', 'several', 'either', 'neither', 'enough',
|
|
39
|
+
]);
|
|
40
|
+
const DEMONSTRATIVES = new Set(['this', 'that', 'these', 'those']);
|
|
41
|
+
// "no" is a NEGATOR, not a reducible determiner: "made with NO loss of time" keeps
|
|
42
|
+
// a beat on "no". Listed here (ahead of the rel==='DET' → x floor in functionLevel)
|
|
43
|
+
// so the negative determiner is never flattened to the clitic tier.
|
|
44
|
+
const NEGATORS = new Set(['not', 'never', "n't", 'nor', 'no']);
|
|
45
|
+
// "be" reduces in the contour even as a copula ("the sky is BLUE"). "have"/"do"
|
|
46
|
+
// reduce only as AUXILIARIES — as main verbs ("the woods HAVE it", "DO your
|
|
47
|
+
// work") they are full content, so they are gated on the AUX relation below.
|
|
48
|
+
const BE_FORMS = new Set(['be', 'am', 'is', 'are', 'was', 'were', 'been', 'being']);
|
|
49
|
+
// Aphaeresis clitics: "'tis"/"'twas" = it+is / it+was, weak pronoun+copula clipped
|
|
50
|
+
// to a single proclitic syllable — never a stressed content peak (en-pos mis-tags
|
|
51
|
+
// the capitalised "'Tis" as NNP). Apostrophes already stripped by bare().
|
|
52
|
+
const APHAERESIS = new Set(['tis', 'twas', 'twere', 'twill', 'twould', 'tisnt']);
|
|
53
|
+
function bare(w) {
|
|
54
|
+
return w.word.toLowerCase().replace(/['’]/g, '');
|
|
55
|
+
}
|
|
56
|
+
function isAphaeresis(w) { return APHAERESIS.has(bare(w)); }
|
|
57
|
+
function isContent(w) {
|
|
58
|
+
return CONTENT.test(w.lexicalClass) && !isAphaeresis(w);
|
|
59
|
+
}
|
|
60
|
+
function isReducedVerb(w, words) {
|
|
61
|
+
if (isAphaeresis(w))
|
|
62
|
+
return true;
|
|
63
|
+
const rel = w.canonicalRel ?? '';
|
|
64
|
+
// "to be" (infinitive main verb) and ROOT be-forms ("Where WAS I?",
|
|
65
|
+
// "I have BEEN") carry a beat — NOT reduced. Must check BEFORE the AUX
|
|
66
|
+
// gate because UD maps cop→AUX, so "to be" has canonicalRel='AUX'.
|
|
67
|
+
if (BE_FORMS.has(bare(w)) && /^(VB|VBD|VBG|VBN|VBP|VBZ)$/.test(w.lexicalClass)) {
|
|
68
|
+
if (words) {
|
|
69
|
+
const prev = words
|
|
70
|
+
.filter(x => x.syllables.length > 0 && x.absoluteIndex < w.absoluteIndex)
|
|
71
|
+
.sort((a, b) => b.absoluteIndex - a.absoluteIndex)[0];
|
|
72
|
+
if (prev && (prev.canonicalRel === 'COMPMARK' || prev.lexicalClass === 'TO'))
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (rel === 'ROOT')
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (rel === 'AUX' || rel === 'AUXPASS')
|
|
79
|
+
return true; // true auxiliary reduces
|
|
80
|
+
// copular "be" after a function word with a predicate reduces; "have"/"do" as
|
|
81
|
+
// MAIN verbs do not
|
|
82
|
+
return BE_FORMS.has(bare(w)) && /^(VB|VBD|VBG|VBN|VBP|VBZ)$/.test(w.lexicalClass);
|
|
83
|
+
}
|
|
84
|
+
/** Intrinsic prominence of a FUNCTION word — the category gradient. `words` (the
|
|
85
|
+
* whole line) lets the transitivity test (Wagner §6.5.5) see a preposition's
|
|
86
|
+
* complement: only a COMPLEMENT-TAKING (transitive) preposition has the reduced
|
|
87
|
+
* 'x' allomorph; a STRANDED or adverbial-particle preposition keeps a beat. */
|
|
88
|
+
function functionLevel(w, words) {
|
|
89
|
+
const lemma = bare(w);
|
|
90
|
+
const pos = w.lexicalClass;
|
|
91
|
+
const rel = w.canonicalRel ?? '';
|
|
92
|
+
// A quantifier / demonstrative in an ATTRIBUTIVE determiner slot ("each street",
|
|
93
|
+
// "this cat", "that day") is a reduced determiner (x), NOT a standalone n-tier
|
|
94
|
+
// quantifier. Only the determiner USE reduces — a predeterminer ("ALL the",
|
|
95
|
+
// PDT/det:predet) and a standalone quantifier/demonstrative ("EACH of them",
|
|
96
|
+
// "THIS is…", rel ≠ det) keep their 'n'. This is what lets a ϕ-initial
|
|
97
|
+
// preposition's beat ("through", "in") outrank an interior "each".
|
|
98
|
+
if (rel === 'DET' && pos !== 'PDT' && !NEGATORS.has(lemma))
|
|
99
|
+
return 'x';
|
|
100
|
+
// n — quantifiers, demonstratives, wh-words, negators (carry real stress)
|
|
101
|
+
if (pos === 'PDT' || QUANTIFIERS.has(lemma) || DEMONSTRATIVES.has(lemma) ||
|
|
102
|
+
NEGATORS.has(lemma) || /^(WDT|WP|WP\$|WRB)$/.test(pos))
|
|
103
|
+
return 'n';
|
|
104
|
+
// "out of" is a compound preposition with fixed stress on "out" (OUT of, not
|
|
105
|
+
// out OF). "out" carries the directional content; "of" is the grammatical
|
|
106
|
+
// linker that floors to 'x'. Raise "out" to 'n' so it outranks a preceding
|
|
107
|
+
// unstressed syllable (the "-ter" of "laughter") and differentiates the contour
|
|
108
|
+
// ("laughter OUT of" → s w n x, not flat s w w x). Only fires when "of"
|
|
109
|
+
// immediately follows — a stranded/particle "out" ("coming OUT") is handled by
|
|
110
|
+
// the transitivity check below and keeps its own beat.
|
|
111
|
+
if (lemma === 'out') {
|
|
112
|
+
const next = words.find(x => x.absoluteIndex === w.absoluteIndex + 1);
|
|
113
|
+
if (next && bare(next) === 'of')
|
|
114
|
+
return 'n';
|
|
115
|
+
}
|
|
116
|
+
// A STRANDED / intransitive preposition ("what are you waiting FOR", "she walked
|
|
117
|
+
// IN") is NOT the reducible proclitic of "in the house" — Wagner §6.5.5: only
|
|
118
|
+
// transitive functors have the weak allomorph. It keeps an overt beat ('w'), so
|
|
119
|
+
// the metrical fitter can promote it; a TRANSITIVE preposition floors at 'x'.
|
|
120
|
+
if (pos === 'IN' || pos === 'TO' || rel === 'CASE') {
|
|
121
|
+
return isTransitiveFunctionWord(w, words) ? 'x' : 'w';
|
|
122
|
+
}
|
|
123
|
+
// x — pure clitics: articles, coordinators, complementisers
|
|
124
|
+
if (ARTICLES.has(lemma) || pos === 'CC' || rel === 'CC' || rel === 'COMPMARK')
|
|
125
|
+
return 'x';
|
|
126
|
+
// w — possessives, pronouns, auxiliaries, modals, leftover determiners
|
|
127
|
+
return 'w';
|
|
128
|
+
}
|
|
129
|
+
/** The peak (highest lexical-stress) syllable of a word. */
|
|
130
|
+
function peakSyllable(w) {
|
|
131
|
+
let best = null;
|
|
132
|
+
let bestV = -1;
|
|
133
|
+
for (const s of w.syllables) {
|
|
134
|
+
const v = s.lexicalStress ?? s.stress;
|
|
135
|
+
if (v > bestV) {
|
|
136
|
+
bestV = v;
|
|
137
|
+
best = s;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
142
|
+
/** Paint every syllable of a word given the level assigned to its peak.
|
|
143
|
+
*
|
|
144
|
+
* The within-word contour preserves the LEXICAL gradient primary > secondary >
|
|
145
|
+
* unstressed (Nounsing's CMU stress → lexicalStress 2 = primary, 1 = secondary,
|
|
146
|
+
* 0 = unstressed). Crucially a SECONDARY stress is a REAL stress and must land
|
|
147
|
+
* STRICTLY ABOVE this word's own unstressed floor — never collapsed flat onto it.
|
|
148
|
+
* Hayes' stress-maximum theory: a polysyllable's internal contour (which syllables
|
|
149
|
+
* bear primary, secondary, or no stress) is a fixed lexical fact, not a position to
|
|
150
|
+
* be overwritten — "ac·CEL·er·AT·ed" must keep its EY2 secondary distinct from the
|
|
151
|
+
* truly toneless syllables around it, in a non-nuclear word (peak = m) exactly as
|
|
152
|
+
* much as in a nuclear one.
|
|
153
|
+
* peak → level; a co-primary (another lexicalStress-2 syllable) sits just below the
|
|
154
|
+
* peak; a secondary (lexicalStress 1) sits ONE TIER ABOVE this word's floor — 'n' if
|
|
155
|
+
* the floor is 'w' (a content word: ti in for·TI·tude is 'w', so tude's secondary
|
|
156
|
+
* clears it at 'n'), or 'w' if the floor is 'x' (a reduced function word: der in
|
|
157
|
+
* UNDER·neath is 'x', so un's secondary clears it at 'w', not flattened to 'x') —
|
|
158
|
+
* capped below the peak and at 'n' (the canonical non-peak ceiling); unstressed
|
|
159
|
+
* syllables sit AT the floor. */
|
|
160
|
+
function paintWord(w, level) {
|
|
161
|
+
const peak = peakSyllable(w);
|
|
162
|
+
const peakRank = RANK[level];
|
|
163
|
+
const floorRank = w.isContent ? RANK.w : RANK.x; // unstressed floor for this word
|
|
164
|
+
for (const s of w.syllables) {
|
|
165
|
+
if (s === peak) {
|
|
166
|
+
s.relativeStress = level;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const lv = s.lexicalStress ?? s.stress;
|
|
170
|
+
let r;
|
|
171
|
+
if (lv >= 2) {
|
|
172
|
+
r = peakRank - 1; // co-primary: just below the peak
|
|
173
|
+
}
|
|
174
|
+
else if (lv === 1) {
|
|
175
|
+
// SECONDARY stress → one tier ABOVE this word's floor (never flattened onto
|
|
176
|
+
// it), never reaching the peak, never exceeding 'n'.
|
|
177
|
+
r = Math.min(floorRank + 1, peakRank - 1, RANK.n);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
r = Math.min(floorRank, peakRank); // unstressed (≤ peak for reduced words)
|
|
181
|
+
}
|
|
182
|
+
s.relativeStress = LEVELS[Math.max(0, Math.min(r, peakRank))];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Relativise one phonological phrase by READING the genuine phrase stress.
|
|
186
|
+
*
|
|
187
|
+
* The cyclic Compound + Nuclear Stress Rules (bracketing.ts) already ranked every
|
|
188
|
+
* word with an integer (1 = strongest). Within a φ:
|
|
189
|
+
* • the lowest-ranked content word is the NUCLEAR (s) — and because the Compound
|
|
190
|
+
* Stress Rule fore-stressed a compound's LEFT element (lower integer), the
|
|
191
|
+
* compound peak falls out here for free (SLATE roof, ICE cream);
|
|
192
|
+
* • every other content word is a SECONDARY BEAT (m) — kept flat, NOT ramped to a
|
|
193
|
+
* trough, so the alternating beats that metrical verse needs stay visible (a
|
|
194
|
+
* ramp here flattened Hiawatha's "shores"/"Gitche" into non-beats → mis-meter);
|
|
195
|
+
* • the FINE gradient the maintainer asked for (ICE > cream, not a flat tie) is
|
|
196
|
+
* produced where content ABUTS, by clash resolution that breaks the tie with the
|
|
197
|
+
* phrase-stress integer — the less-prominent (higher-integer) member of two
|
|
198
|
+
* adjacent beats is the one demoted (so ICE ps2 keeps the beat, cream ps3 drops).
|
|
199
|
+
*
|
|
200
|
+
* Two anchored beats per φ, per Kiparsky's "beginnings free, endings strict"
|
|
201
|
+
* (McAleese pp.34–35, Hayes & Kaun 1996 — the procedure reads the stresses at the
|
|
202
|
+
* END of each unit, but the LEFT edge carries a lighter beat too):
|
|
203
|
+
* • ENDING (strict): the rightmost lowest-ranked content word → the nuclear s.
|
|
204
|
+
* • BEGINNING (free, light): a φ-initial PREPOSITION rises x→w — the subtle
|
|
205
|
+
* left-edge beat the maintainer hears on "…working | IN the house" and that lets
|
|
206
|
+
* "…through | each…" read through(w) > each(x). Articles, pronouns, auxiliaries
|
|
207
|
+
* and coordinators do not rise. */
|
|
208
|
+
function relativisePP(toks, words) {
|
|
209
|
+
if (toks.length === 0)
|
|
210
|
+
return;
|
|
211
|
+
// A DISCOURSE-GIVEN content word (repeated from an earlier line of the stanza) is
|
|
212
|
+
// subordinated relative to its sister (Wagner Ch.7) — but only as an ANCHOR
|
|
213
|
+
// demotion, and never when it would empty the φ of content. A word given a higher
|
|
214
|
+
// (worse) effective phrase-stress is less likely to win the φ nuclear.
|
|
215
|
+
const ps = (w) => {
|
|
216
|
+
const base = w.phraseStress || Infinity;
|
|
217
|
+
// A given content word — discourse-given (repeated from an earlier line), an
|
|
218
|
+
// inherently-given indefinite pronoun ("something", Wagner §7.2.3), or
|
|
219
|
+
// anaphorically given via a shared-head coordinate structure ("young blood
|
|
220
|
+
// and high blood" → the second "blood") — is less likely to win the φ
|
|
221
|
+
// nuclear, so a heavier sister in the SAME φ takes the beat.
|
|
222
|
+
const given = isContent(w) && (w.discourseGiven || isInherentlyGiven(w) || w.coordinateGiven);
|
|
223
|
+
return given ? base + 2 : base;
|
|
224
|
+
};
|
|
225
|
+
// Pronoun promotion (Nuclear Stress Rule + trochaic inversion): a simple personal
|
|
226
|
+
// pronoun (PRP with no apostrophe) at the start of the φ gets promoted to an
|
|
227
|
+
// anchor if it is syntactically prominent (phraseStress === 1) and its following
|
|
228
|
+
// verb in the φ is a function/light verb (have, has, had, do, etc.) with weaker
|
|
229
|
+
// phraseStress (phraseStress > 1). This captures the trochaic inversion in
|
|
230
|
+
// "I have no right" (I=1, have=3 → I wins the beat, have reduces) while
|
|
231
|
+
// sparing contractions ("I'll", "I'm") and pronouns preceding lexical content
|
|
232
|
+
// verbs ("I would give" → give is lexical, so I does NOT promote).
|
|
233
|
+
const promotePronoun = (w) => {
|
|
234
|
+
if (w.lexicalClass !== 'PRP' || w.word.includes("'") || w.word.includes("’"))
|
|
235
|
+
return false;
|
|
236
|
+
if (w.phraseStress !== 1)
|
|
237
|
+
return false;
|
|
238
|
+
const idx = toks.indexOf(w);
|
|
239
|
+
if (idx !== 0)
|
|
240
|
+
return false;
|
|
241
|
+
const nextVerb = toks.slice(1).find(x => /^VB/.test(x.lexicalClass));
|
|
242
|
+
if (!nextVerb)
|
|
243
|
+
return false;
|
|
244
|
+
const FUNCTION_VERBS_SHORT = new Set(['have', 'has', 'had', 'do', 'does', 'did', 'get', 'got']);
|
|
245
|
+
if (!FUNCTION_VERBS_SHORT.has(nextVerb.word.toLowerCase()))
|
|
246
|
+
return false;
|
|
247
|
+
// Only the MAIN-verb use of have/do ("I HAVE no right" — possession) licenses
|
|
248
|
+
// the inversion. As an AUXILIARY ("i have SENE them") the beat belongs to the
|
|
249
|
+
// lexical verb further right; promoting the pronoun here handed it the φ
|
|
250
|
+
// nuclear over the participle ("I"(s) have sene(m) — the Wyatt L3 bug).
|
|
251
|
+
if ((nextVerb.canonicalRel ?? '') === 'AUX' || (nextVerb.canonicalRel ?? '') === 'AUXPASS')
|
|
252
|
+
return false;
|
|
253
|
+
if ((nextVerb.phraseStress || 0) > 1)
|
|
254
|
+
return true;
|
|
255
|
+
return false;
|
|
256
|
+
};
|
|
257
|
+
// The set the gradient is measured against: content words (non-reduced) first;
|
|
258
|
+
// for a content-less φ fall back to its prominent function words ("for THAT",
|
|
259
|
+
// "to YOU"), then to any content, then to anything — so every φ has an anchor.
|
|
260
|
+
let anchors = toks.filter(w => promotePronoun(w) || (isContent(w) && !isReducedVerb(w, words)));
|
|
261
|
+
if (anchors.length === 0)
|
|
262
|
+
anchors = toks.filter(w => !isReducedVerb(w, words) && functionLevel(w, words) !== 'x');
|
|
263
|
+
if (anchors.length === 0)
|
|
264
|
+
anchors = toks.filter(isContent);
|
|
265
|
+
if (anchors.length === 0)
|
|
266
|
+
anchors = toks.slice();
|
|
267
|
+
const anchorSet = new Set(anchors);
|
|
268
|
+
// Nuclear strength = the lowest phrase-stress integer among the anchors; the
|
|
269
|
+
// nuclear WORD is the RIGHTMOST anchor at that value (English resolves rightward
|
|
270
|
+
// toward the nuclear). Because the Compound Stress Rule already fore-stressed a
|
|
271
|
+
// compound's LEFT element (lower integer), the compound peak falls out here for
|
|
272
|
+
// free — SLATE roof, ICE cream — with no special case.
|
|
273
|
+
const localMin = Math.min(...anchors.map(ps));
|
|
274
|
+
let nuclear = null;
|
|
275
|
+
for (const w of anchors)
|
|
276
|
+
if (ps(w) === localMin)
|
|
277
|
+
nuclear = w; // last = rightmost
|
|
278
|
+
// Base level per token.
|
|
279
|
+
const levels = toks.map((w) => {
|
|
280
|
+
if (w === nuclear)
|
|
281
|
+
return 's'; // φ nuclear (right edge)
|
|
282
|
+
if (anchorSet.has(w))
|
|
283
|
+
return 'm'; // secondary beat (flat — the
|
|
284
|
+
// gradient among abutting beats is set by clash resolution)
|
|
285
|
+
// BE-FORM flooring — only fires for true AUX copulas now (isReducedVerb
|
|
286
|
+
// already excludes "to be" and ROOT be-forms, which are anchors with beats).
|
|
287
|
+
// 'x' reserved for the clearest copula+predicate case: be-form after a
|
|
288
|
+
// function word directly followed by an adjective/noun predicate.
|
|
289
|
+
// Everything else → 'w' (promotable).
|
|
290
|
+
if (isReducedVerb(w, words)) {
|
|
291
|
+
if (BE_FORMS.has(bare(w))) {
|
|
292
|
+
const i = toks.indexOf(w);
|
|
293
|
+
const prev = i > 0 ? toks[i - 1] : null;
|
|
294
|
+
const followingContent = toks.slice(i + 1).find(x => isContent(x));
|
|
295
|
+
if (prev && !isContent(prev) && followingContent &&
|
|
296
|
+
/^(JJ|NN)/.test(followingContent.lexicalClass))
|
|
297
|
+
return 'x';
|
|
298
|
+
}
|
|
299
|
+
return 'w';
|
|
300
|
+
}
|
|
301
|
+
// Function word → its category gradient; a POLYSYLLABIC one floors its peak at
|
|
302
|
+
// 'w' so its stressed syllable outranks the reduced one (be·CAUSE, u·PON).
|
|
303
|
+
let tier = functionLevel(w, words);
|
|
304
|
+
if (w.syllables.length > 1 && tier === 'x')
|
|
305
|
+
tier = 'w';
|
|
306
|
+
// A polysyllabic function word whose lexicon entry records a genuine PRIMARY
|
|
307
|
+
// stress ("O·ver", "AF·ter", "UN·der", "u·PON") is not a reduced clitic — its
|
|
308
|
+
// stressed syllable is a real (if light) beat that metrical verse freely uses
|
|
309
|
+
// ("O·ver many a quaint and curious volume…" opens The Raven's trochee chain).
|
|
310
|
+
// Raise the peak one tier to 'n' — still below every content beat, still
|
|
311
|
+
// promotable/demotable by the fitter — leaving true reduced polysyllables
|
|
312
|
+
// ("be·cause" 00, "where·fore" secondary-only) at 'w'.
|
|
313
|
+
if (w.syllables.length > 1 && tier === 'w' &&
|
|
314
|
+
w.syllables.some(s => (s.lexicalStress ?? s.stress) === 2)) {
|
|
315
|
+
tier = 'n';
|
|
316
|
+
}
|
|
317
|
+
return tier;
|
|
318
|
+
});
|
|
319
|
+
// Phrase-initial beat ("beginnings free" — a light left-edge beat): a φ-initial
|
|
320
|
+
// PREPOSITION rises x→w (the subtle beat on "IN the house"; lets "through" outrank
|
|
321
|
+
// an interior "each"), unless it is itself the nuclear. Also fires for the first
|
|
322
|
+
// IN/TO AFTER a coordinator at the φ's start ("And of the best" — "And" is toks[0]
|
|
323
|
+
// at 'x', "of" is toks[1]; "of" gets the raise so it reads 'w', not flat 'x').
|
|
324
|
+
const raiseFirstPrep = (idx) => {
|
|
325
|
+
if (idx < 0 || idx >= toks.length)
|
|
326
|
+
return;
|
|
327
|
+
const w = toks[idx];
|
|
328
|
+
if (w === nuclear)
|
|
329
|
+
return;
|
|
330
|
+
if ((w.lexicalClass === 'IN' || w.lexicalClass === 'TO') && levels[idx] === 'x') {
|
|
331
|
+
levels[idx] = 'w';
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
raiseFirstPrep(0);
|
|
335
|
+
// If toks[0] is a coordinator (CC), the first IN/TO after it also gets the raise
|
|
336
|
+
// — it is κ-initial (the start of the prepositional phrase after "and"), even if
|
|
337
|
+
// not ϕ-initial. "And of the best" → "of" rises to 'w'.
|
|
338
|
+
if (toks.length > 1 && (toks[0].lexicalClass === 'CC' || (toks[0].canonicalRel ?? '') === 'CC')) {
|
|
339
|
+
for (let i = 1; i < toks.length; i++) {
|
|
340
|
+
if (toks[i].lexicalClass === 'IN' || toks[i].lexicalClass === 'TO') {
|
|
341
|
+
raiseFirstPrep(i);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
// Skip over articles/determiners to find the first IN/TO
|
|
345
|
+
if (toks[i].isContent)
|
|
346
|
+
break; // hit content before any prep
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Givenness escape (Wagner §6.1.3, maintainer's no-flat-run-on directive): when a
|
|
350
|
+
// φ's nuclear is an inherently-given PRONOUN immediately preceded by a transitive
|
|
351
|
+
// PREPOSITION ("of HIM", "to THEE"), do NOT leave them on a par or stress the
|
|
352
|
+
// pronoun — subordinate the given pronoun and give the preposition the beat
|
|
353
|
+
// ("OF him"), the differentiated reading. The metrical fitter may re-promote the
|
|
354
|
+
// pronoun if the line demands it.
|
|
355
|
+
if (nuclear && isPronoun(nuclear)) {
|
|
356
|
+
const ni = toks.indexOf(nuclear);
|
|
357
|
+
const prep = ni > 0 ? toks[ni - 1] : null;
|
|
358
|
+
if (prep && (prep.lexicalClass === 'IN' || prep.lexicalClass === 'TO' ||
|
|
359
|
+
(prep.canonicalRel ?? '') === 'CASE')) {
|
|
360
|
+
const pi = toks.indexOf(prep);
|
|
361
|
+
levels[ni] = 'w'; // given pronoun subordinated
|
|
362
|
+
levels[pi] = 'n'; // preposition takes the differentiating beat
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
toks.forEach((w, i) => paintWord(w, levels[i]));
|
|
366
|
+
// NOTE: the clash invariant (no adjacent equal n/m/s) is enforced once, globally,
|
|
367
|
+
// by resolveStressClashes over TRUE adjacent syllables in computeRelativeStress —
|
|
368
|
+
// not here. A per-word peak comparison would wrongly treat two polysyllables'
|
|
369
|
+
// stressed syllables as adjacent ("TY·ger TY·ger", peaks two syllables apart) and
|
|
370
|
+
// spuriously flatten every falling trochaic/dactylic foot.
|
|
371
|
+
}
|
|
372
|
+
const CLASH_NOUN = /^(NN|NNS|NNP|NNPS)$/;
|
|
373
|
+
/** Two clashing words form a COMPOUND whose LEFT element fore-stresses (Compound
|
|
374
|
+
* Stress Rule): a NOMD modifier + its head, or two adjacent nouns (the N+N compound
|
|
375
|
+
* the tagger routinely mislabels). Only for such a pair does the clash keep the
|
|
376
|
+
* lower-phraseStress (fore-stressed) member; every OTHER clash uses the default
|
|
377
|
+
* rightward resolution. This is the narrow place the Compound Stress Rule needs to
|
|
378
|
+
* reach the contour when neither element is the φ nuclear (e.g. "ice cream" buried
|
|
379
|
+
* under a wrong root) — without disturbing ordinary verse, where forcing the
|
|
380
|
+
* lower-phraseStress member to win moved beats onto odd positions and mis-metered. */
|
|
381
|
+
function isCompoundPair(a, b) {
|
|
382
|
+
if (a === b)
|
|
383
|
+
return false;
|
|
384
|
+
if ((a.canonicalRel ?? '') === 'NOMD' && a.dependency?.governor === b)
|
|
385
|
+
return true;
|
|
386
|
+
if ((b.canonicalRel ?? '') === 'NOMD' && b.dependency?.governor === a)
|
|
387
|
+
return true;
|
|
388
|
+
return CLASH_NOUN.test(a.lexicalClass) && CLASH_NOUN.test(b.lexicalClass) &&
|
|
389
|
+
Math.abs(a.absoluteIndex - b.absoluteIndex) === 1;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Stress-clash resolution over the TRUE surface syllable sequence (McAleese A2
|
|
393
|
+
* step 3d.ii: "stress clashes (ss, ms) > s-s"). A BEAT is one per foot (at most
|
|
394
|
+
* one per phonological phrase) — so two ADJACENT strong syllables (both ≥ m, in
|
|
395
|
+
* any order: ss, ms, sm, mm) are a clash, NOT two beats. One member drops to 'n'
|
|
396
|
+
* (below the beat threshold), leaving a single beat with a demoted neighbour;
|
|
397
|
+
* adjacent equal 'n' likewise demotes one member to 'w'.
|
|
398
|
+
*
|
|
399
|
+
* WHICH member drops:
|
|
400
|
+
* • a COMPOUND pair (isCompoundPair) keeps the fore-stressed (lower-phraseStress)
|
|
401
|
+
* element and demotes the other — the Compound Stress Rule, "ICE cream";
|
|
402
|
+
* • an n-n tie is broken by LEXICAL stress (Hayes' stress-maximum principle):
|
|
403
|
+
* the syllable with the higher lexicalStress value (2=primary > 1=secondary
|
|
404
|
+
* > 0=unstressed) is protected; the lower yields to 'w'. This preserves a
|
|
405
|
+
* polysyllable's internal contour — a PRIMARY (lexicalStress 2) that was
|
|
406
|
+
* demoted to 'n' by a prior m/s clash still outranks a SECONDARY
|
|
407
|
+
* (lexicalStress 1), which in turn outranks an artifact 'n' (lexicalStress
|
|
408
|
+
* 0, reached via the function-word gradient or a prior demotion). Without
|
|
409
|
+
* this, a cascade can crush real lexical data — e.g. "before" (be=1, fore=2):
|
|
410
|
+
* "fore" loses an m/s clash (demoted to 'n'), then ties "be"(n) and was
|
|
411
|
+
* wrongly sacrificed (the old code only protected ===1 secondaries, treating
|
|
412
|
+
* the primary 2 as "not grounded"), corrupting "wn" into "nw". Equal-
|
|
413
|
+
* lexical-stress ties (both primary, both secondary, both unstressed) default
|
|
414
|
+
* left (Rhythm Rule);
|
|
415
|
+
* • every other clash demotes the lower symbolic level, ties to the LEFT (the
|
|
416
|
+
* Rhythm Rule / Iambic Reversal — an earlier stress backs off ahead of one that
|
|
417
|
+
* follows closely, not a general "endings are strict" license).
|
|
418
|
+
*
|
|
419
|
+
* Because it compares adjacent SYLLABLES (not word peaks), a falling foot whose
|
|
420
|
+
* stresses are separated by a weak syllable ("TY·ger TY·ger", "HARB·our") is left
|
|
421
|
+
* intact — only genuinely abutting beats collapse.
|
|
422
|
+
*/
|
|
423
|
+
function resolveStressClashes(words, sylWord) {
|
|
424
|
+
const syls = [];
|
|
425
|
+
for (const w of [...words].sort((a, b) => a.absoluteIndex - b.absoluteIndex)) {
|
|
426
|
+
for (const s of w.syllables)
|
|
427
|
+
syls.push(s);
|
|
428
|
+
}
|
|
429
|
+
// Lexical stress value of a syllable (2=primary, 1=secondary, 0=unstressed).
|
|
430
|
+
// Used to break n-n ties: a syllable with HIGHER lexical stress has greater
|
|
431
|
+
// phonological integrity (Hayes' stress-maximum principle) and is protected
|
|
432
|
+
// against demotion by a syllable with lower lexical stress. The prior code
|
|
433
|
+
// checked only ===1 (secondary), which inverted the hierarchy — a primary
|
|
434
|
+
// (lexicalStress===2) was treated as "not grounded" and sacrificed to a
|
|
435
|
+
// secondary, corrupting words like "before" (be=1/secondary, fore=2/primary)
|
|
436
|
+
// into "nw" instead of "wn".
|
|
437
|
+
const lexVal = (s) => s.lexicalStress ?? s.stress ?? 0;
|
|
438
|
+
let changed = true;
|
|
439
|
+
for (let guard = 0; changed && guard < 8; guard++) {
|
|
440
|
+
changed = false;
|
|
441
|
+
for (let i = 0; i + 1 < syls.length; i++) {
|
|
442
|
+
const a = syls[i], b = syls[i + 1];
|
|
443
|
+
const ra = RANK[a.relativeStress ?? 'w'];
|
|
444
|
+
const rb = RANK[b.relativeStress ?? 'w'];
|
|
445
|
+
if (ra >= RANK.m && rb >= RANK.m) {
|
|
446
|
+
const wa = sylWord.get(a), wb = sylWord.get(b);
|
|
447
|
+
let demoteA;
|
|
448
|
+
if (wa && wb && wa !== wb && isCompoundPair(wa, wb)) {
|
|
449
|
+
// compound fore-stress: demote the LESS prominent (higher phrase stress)
|
|
450
|
+
demoteA = (wa.phraseStress || Infinity) >= (wb.phraseStress || Infinity);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
demoteA = ra <= rb; // default: lower level, tie → left
|
|
454
|
+
}
|
|
455
|
+
if (demoteA)
|
|
456
|
+
a.relativeStress = 'n';
|
|
457
|
+
else
|
|
458
|
+
b.relativeStress = 'n';
|
|
459
|
+
changed = true;
|
|
460
|
+
}
|
|
461
|
+
else if (ra === RANK.n && rb === RANK.n) {
|
|
462
|
+
// Break the tie by LEXICAL stress: the syllable with higher lexical
|
|
463
|
+
// stress (primary > secondary > unstressed) is protected; the lower
|
|
464
|
+
// yields. This preserves a polysyllable's internal contour (the
|
|
465
|
+
// primary survives even if a prior clash demoted it to n) while still
|
|
466
|
+
// letting genuine secondaries (lexicalStress 1) beat artifact n's
|
|
467
|
+
// (lexicalStress 0, reached via function-word gradient or prior
|
|
468
|
+
// demotion). Equal-lexical-stress ties default left (Rhythm Rule).
|
|
469
|
+
const la = lexVal(a), lb = lexVal(b);
|
|
470
|
+
if (la > lb)
|
|
471
|
+
b.relativeStress = 'w';
|
|
472
|
+
else if (lb > la)
|
|
473
|
+
a.relativeStress = 'w';
|
|
474
|
+
else
|
|
475
|
+
a.relativeStress = 'w'; // tie → default left
|
|
476
|
+
changed = true;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** Inherent-givenness nuclear demotion (Wagner §7.2.3). After each φ has been
|
|
482
|
+
* relativised, an inherently-given indefinite pronoun that won its OWN φ's nuclear
|
|
483
|
+
* (e.g. a lone "Something" set off as its own ϕ) should still YIELD the utterance
|
|
484
|
+
* peak to a heavier element later in the same intonational unit — so when a LATER
|
|
485
|
+
* word in the IU also reaches 's', the given pronoun's peak is demoted s→m. It
|
|
486
|
+
* keeps a beat (its lexical contour is untouched below the peak) but no longer
|
|
487
|
+
* claims the main prominence: "Something(m) for the modern STAGE(s)".
|
|
488
|
+
*
|
|
489
|
+
* Guards keep it from crushing a FOCAL pronoun: it never fires when the given word
|
|
490
|
+
* is the IU's only nuclear (a lone "Something."), and skips a lemma repeated in the
|
|
491
|
+
* IU (focal anaphora). */
|
|
492
|
+
function demoteGivenNuclei(iuWords) {
|
|
493
|
+
const ordered = iuWords.filter(w => w.syllables.length > 0)
|
|
494
|
+
.sort((a, b) => a.absoluteIndex - b.absoluteIndex);
|
|
495
|
+
const peak = (w) => Math.max(0, ...w.syllables.map(s => RANK[s.relativeStress ?? 'w']));
|
|
496
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
497
|
+
const w = ordered[i];
|
|
498
|
+
if (!isInherentlyGiven(w) || peak(w) !== RANK.s)
|
|
499
|
+
continue;
|
|
500
|
+
const lemma = bare(w);
|
|
501
|
+
if (ordered.filter(x => bare(x) === lemma).length > 1)
|
|
502
|
+
continue; // focal repetition
|
|
503
|
+
if (!ordered.slice(i + 1).some(x => peak(x) === RANK.s))
|
|
504
|
+
continue; // no heavier later nuclear
|
|
505
|
+
for (const s of w.syllables)
|
|
506
|
+
if ((s.relativeStress ?? 'w') === 's')
|
|
507
|
+
s.relativeStress = 'm';
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/** Alternation promotion (Attridge 1982), gated by Hayes' stress-maximum
|
|
511
|
+
* monosyllable/polysyllable asymmetry (McAleese pp.49-57; Halle-Keyser, Kiparsky
|
|
512
|
+
* 1975): English does not sustain a trough of three-or-more weak syllables — its
|
|
513
|
+
* medial syllable is promoted to a light beat, McAleese's 'n' ("some stress"). This
|
|
514
|
+
* is the SAME promotion the meter-fitter applies internally (`scansion.ts`: a 'w'
|
|
515
|
+
* flanked by weakness realises a beat); writing it into the displayed contour
|
|
516
|
+
* breaks the long w-troughs the maintainer flagged.
|
|
517
|
+
*
|
|
518
|
+
* NOTE on grounding: "beginnings free, endings strict" (Kiparsky 1968) is a loose
|
|
519
|
+
* statistical tendency about WHERE stress clashes resolve in the line, not a
|
|
520
|
+
* license to promote by position alone — it says nothing about whether a given
|
|
521
|
+
* syllable is phonologically eligible to carry stress at all. The actual gate
|
|
522
|
+
* here is Hayes' stress-maximum asymmetry: a STRESSED MONOSYLLABLE is positionally
|
|
523
|
+
* free (it has no internal contour to violate, so context can promote it — "error
|
|
524
|
+
* AND upon", "not TO advance"), but an UNSTRESSED syllable inside a POLYSYLLABLE
|
|
525
|
+
* is not free — the lexicon's verdict that it carries no stress (lexicalStress
|
|
526
|
+
* === 0) is a fixed phonological fact, and promoting it fabricates a secondary
|
|
527
|
+
* stress Nounsing-Pro never recorded (e.g. promoting "mariner"'s toneless "-ner"
|
|
528
|
+
* just because it sits between two other weak syllables, one of which belongs to
|
|
529
|
+
* the unrelated following word). So:
|
|
530
|
+
* • only an overt-weak 'w' promotes; a reduced clitic 'x' (the/a/of) stays the
|
|
531
|
+
* genuine trough — a clitic is never a beat;
|
|
532
|
+
* • a candidate that is the toneless (lexicalStress === 0) syllable of a word with
|
|
533
|
+
* MORE THAN ONE syllable is excluded — its own word's lexical contour already
|
|
534
|
+
* settled the question, and position cannot overrule it; a true monosyllable, or
|
|
535
|
+
* a syllable that itself carries some lexical stress (primary or secondary) but
|
|
536
|
+
* is sitting low because its WHOLE WORD only earned a low φ-level, remains
|
|
537
|
+
* eligible;
|
|
538
|
+
* • it must sit between two weak (w/x) syllables — the interior of a real trough;
|
|
539
|
+
* • the LINE EDGES do not count as weak, so a line-initial/final weak is never
|
|
540
|
+
* promoted (the off-beat before the first beat, and the final cadence, are left);
|
|
541
|
+
* • promotions never abut (a promoted 'n' breaks the run), so the result alternates
|
|
542
|
+
* w·n·w and the clash invariant (no adjacent equal n) is preserved. */
|
|
543
|
+
function promoteWeakTroughs(words) {
|
|
544
|
+
const syls = [];
|
|
545
|
+
const sylWord = new Map();
|
|
546
|
+
for (const w of [...words].sort((a, b) => a.absoluteIndex - b.absoluteIndex)) {
|
|
547
|
+
for (const s of w.syllables) {
|
|
548
|
+
syls.push(s);
|
|
549
|
+
sylWord.set(s, w);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const isW = (i) => i >= 0 && i < syls.length && syls[i].relativeStress === 'w';
|
|
553
|
+
const isLexicallyToneless = (i) => {
|
|
554
|
+
const s = syls[i];
|
|
555
|
+
if ((s.lexicalStress ?? s.stress) !== 0)
|
|
556
|
+
return false; // carries some real stress
|
|
557
|
+
const w = sylWord.get(s);
|
|
558
|
+
return !!w && w.syllables.length > 1; // interior of a polysyllable
|
|
559
|
+
};
|
|
560
|
+
let lastProm = -2;
|
|
561
|
+
for (let i = 1; i < syls.length - 1; i++) {
|
|
562
|
+
if (!isW(i) || !isW(i - 1) || !isW(i + 1))
|
|
563
|
+
continue; // medial of a ≥3 literal-w run
|
|
564
|
+
if (isLexicallyToneless(i))
|
|
565
|
+
continue; // no fabricated secondaries
|
|
566
|
+
if (i - 1 === lastProm)
|
|
567
|
+
continue; // keep promotions non-adjacent
|
|
568
|
+
syls[i].relativeStress = 'n';
|
|
569
|
+
lastProm = i;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/** Lower a polysyllabic word's final 'w' syllable to 'x' when another 'w'
|
|
573
|
+
* immediately follows it in surface order. The unstressed ending of a longer
|
|
574
|
+
* word (even a stress-bearing word) carries fainter emphasis than even a weakly-
|
|
575
|
+
* stressed syllable or word immediately following it — so a w-w seam at a word
|
|
576
|
+
* boundary is differentiated by dropping the word-internal one to the clitic tier.
|
|
577
|
+
*
|
|
578
|
+
* Narrowly scoped: only fires on the FINAL syllable of a polysyllabic word (2+
|
|
579
|
+
* syllables) that is 'w' (not 'n', 'm', 's', or 'x'), AND only when the very next
|
|
580
|
+
* syllable in surface order is also 'w'. Does NOT affect any other syllable of
|
|
581
|
+
* the word, does NOT fire when the next syllable is 'x', 'n', 'm', or 's'. */
|
|
582
|
+
function demotePolysyllabicFinalTrough(words) {
|
|
583
|
+
const syls = [];
|
|
584
|
+
const sylWord = new Map();
|
|
585
|
+
for (const w of [...words].sort((a, b) => a.absoluteIndex - b.absoluteIndex)) {
|
|
586
|
+
for (const s of w.syllables) {
|
|
587
|
+
syls.push(s);
|
|
588
|
+
sylWord.set(s, w);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (let i = 0; i < syls.length - 1; i++) {
|
|
592
|
+
const s = syls[i];
|
|
593
|
+
const next = syls[i + 1];
|
|
594
|
+
if (s.relativeStress !== 'w')
|
|
595
|
+
continue;
|
|
596
|
+
if (next.relativeStress !== 'w')
|
|
597
|
+
continue;
|
|
598
|
+
const w = sylWord.get(s);
|
|
599
|
+
if (!w || w.syllables.length < 2)
|
|
600
|
+
continue; // polysyllabic only
|
|
601
|
+
if (s !== w.syllables[w.syllables.length - 1])
|
|
602
|
+
continue; // final syllable only
|
|
603
|
+
s.relativeStress = 'x';
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/** Anaphoric givenness in coordinate structures (Krifka §4, Wagner §7.2).
|
|
607
|
+
* In a coordinate structure "X blood and Y blood" the HEAD lemma ("blood") is
|
|
608
|
+
* repeated across conjuncts. Both occurrences are the shared contrastive
|
|
609
|
+
* background — the CATEGORY "blood" is what stays constant, and the modifiers
|
|
610
|
+
* ("Young", "high") carry the contrastive focus. So BOTH heads are demoted
|
|
611
|
+
* (`coordinateGiven`), letting the modifier win the φ nuclear in EACH conjunct
|
|
612
|
+
* ("YOUNG blood and HIGH blood"). This is distinct from a refrain (identical
|
|
613
|
+
* phrase repeated for emphasis = focal) and from cross-line discourse
|
|
614
|
+
* givenness (repetition across lines of a stanza).
|
|
615
|
+
*
|
|
616
|
+
* Detection: a content word whose `canonicalRel` is CONJ and whose governor
|
|
617
|
+
* shares the same lemma. BOTH the conjunct (second occurrence) AND its
|
|
618
|
+
* governor (first occurrence) are marked `coordinateGiven`. The relativiser's
|
|
619
|
+
* `ps()` then demotes both, so the modifier in each φ wins the beat.
|
|
620
|
+
*
|
|
621
|
+
* Guard: only fires when the conjuncts' modifiers DIFFER — if the entire phrase
|
|
622
|
+
* is identical ("nevermore and nevermore") it's a refrain, not a shared-head
|
|
623
|
+
* coordinate, and both occurrences stay focal. */
|
|
624
|
+
function markCoordinateGivenness(words) {
|
|
625
|
+
for (const w of words) {
|
|
626
|
+
if (!isContent(w) || (w.canonicalRel ?? '') !== 'CONJ')
|
|
627
|
+
continue;
|
|
628
|
+
const gov = w.dependency?.governor;
|
|
629
|
+
if (!gov || gov === w)
|
|
630
|
+
continue;
|
|
631
|
+
// Same lemma (head repeated across conjuncts)?
|
|
632
|
+
if (bare(w) !== bare(gov))
|
|
633
|
+
continue;
|
|
634
|
+
// Guard against a refrain: check that the conjuncts have DIFFERENT
|
|
635
|
+
// pre-head modifiers. Compare the AMOD/ADVMOD lemmas immediately before
|
|
636
|
+
// each head. If both heads have the same modifier (or neither has one),
|
|
637
|
+
// leave both focal — it's either a refrain or a plain coordination.
|
|
638
|
+
const modBefore = (head) => {
|
|
639
|
+
const idx = words.indexOf(head);
|
|
640
|
+
if (idx <= 0)
|
|
641
|
+
return '';
|
|
642
|
+
const prev = words[idx - 1];
|
|
643
|
+
if ((prev.canonicalRel ?? '') === 'AMOD' || (prev.canonicalRel ?? '') === 'ADVMOD')
|
|
644
|
+
return bare(prev);
|
|
645
|
+
return '';
|
|
646
|
+
};
|
|
647
|
+
if (modBefore(w) === modBefore(gov))
|
|
648
|
+
continue;
|
|
649
|
+
// Mark BOTH heads as coordinate-given: the shared head is the contrastive
|
|
650
|
+
// background in both conjuncts, so contrastive focus falls on BOTH modifiers.
|
|
651
|
+
w.coordinateGiven = true;
|
|
652
|
+
gov.coordinateGiven = true;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** Assign relative stress for the whole sentence, one PP at a time. */
|
|
656
|
+
export function computeRelativeStress(words, ius) {
|
|
657
|
+
// Mark anaphorically-given heads in coordinate structures before relativisation,
|
|
658
|
+
// so the relativiser's `ps()` can demote them and let the modifier win the beat.
|
|
659
|
+
markCoordinateGivenness(words);
|
|
660
|
+
for (const iu of ius) {
|
|
661
|
+
for (const pp of iu.phonologicalPhrases) {
|
|
662
|
+
const toks = collectPPTokens(pp)
|
|
663
|
+
.filter(w => w.syllables.length > 0)
|
|
664
|
+
.sort((a, b) => a.absoluteIndex - b.absoluteIndex);
|
|
665
|
+
relativisePP(toks, words);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Enforce the clash rule over the whole line's surface syllables (catches the
|
|
669
|
+
// cross-PP abutments the per-PP painting cannot see). Carry each syllable's word
|
|
670
|
+
// so the clash can recognise a compound pair and apply the Compound Stress Rule.
|
|
671
|
+
const sylWord = new Map();
|
|
672
|
+
for (const w of words)
|
|
673
|
+
for (const s of w.syllables)
|
|
674
|
+
sylWord.set(s, w);
|
|
675
|
+
resolveStressClashes(words, sylWord);
|
|
676
|
+
// Per-IU: an inherently-given indefinite pronoun yields its φ-nuclear to a heavier
|
|
677
|
+
// later element in the same IU (after clash resolution, so the demotion is final).
|
|
678
|
+
for (const iu of ius) {
|
|
679
|
+
const iuWords = iu.phonologicalPhrases.flatMap(pp => collectPPTokens(pp));
|
|
680
|
+
demoteGivenNuclei(iuWords);
|
|
681
|
+
}
|
|
682
|
+
// Final readability pass: promote the medial weak of a ≥3-weak trough to 'n'
|
|
683
|
+
// (Attridge alternation), breaking long w-runs and filling promotable beats.
|
|
684
|
+
promoteWeakTroughs(words);
|
|
685
|
+
// Differentiate w-w seams at polysyllabic word boundaries: lower a
|
|
686
|
+
// polysyllabic word's final 'w' to 'x' when another 'w' immediately follows.
|
|
687
|
+
demotePolysyllabicFinalTrough(words);
|
|
688
|
+
}
|