calliope-ts 0.0.2 → 0.0.4

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.
Files changed (160) hide show
  1. package/README.md +32 -20
  2. package/dist/caesura.d.ts +33 -0
  3. package/dist/caesura.d.ts.map +1 -0
  4. package/dist/caesura.js +202 -0
  5. package/dist/calliope/boundaries.d.ts +19 -0
  6. package/dist/calliope/boundaries.d.ts.map +1 -0
  7. package/dist/calliope/boundaries.js +182 -0
  8. package/dist/calliope/bracketing.d.ts +11 -0
  9. package/dist/calliope/bracketing.d.ts.map +1 -0
  10. package/dist/calliope/bracketing.js +416 -0
  11. package/dist/calliope/deps.d.ts +4 -0
  12. package/dist/calliope/deps.d.ts.map +1 -0
  13. package/dist/calliope/deps.js +181 -0
  14. package/dist/calliope/engine.d.ts +3 -0
  15. package/dist/calliope/engine.d.ts.map +1 -0
  16. package/dist/calliope/engine.js +71 -0
  17. package/dist/calliope/feats.d.ts +9 -0
  18. package/dist/calliope/feats.d.ts.map +1 -0
  19. package/dist/calliope/feats.js +45 -0
  20. package/dist/calliope/names.d.ts +7 -0
  21. package/dist/calliope/names.d.ts.map +1 -0
  22. package/dist/calliope/names.js +42 -0
  23. package/dist/calliope/postag.d.ts +8 -0
  24. package/dist/calliope/postag.d.ts.map +1 -0
  25. package/dist/calliope/postag.js +250 -0
  26. package/dist/calliope/prosodic.d.ts +3 -0
  27. package/dist/calliope/prosodic.d.ts.map +1 -0
  28. package/dist/calliope/prosodic.js +275 -0
  29. package/dist/calliope/relstress.d.ts +4 -0
  30. package/dist/calliope/relstress.d.ts.map +1 -0
  31. package/dist/calliope/relstress.js +688 -0
  32. package/dist/calliope/stressrules.d.ts +28 -0
  33. package/dist/calliope/stressrules.d.ts.map +1 -0
  34. package/dist/calliope/stressrules.js +147 -0
  35. package/dist/calliope/syntax.d.ts +38 -0
  36. package/dist/calliope/syntax.d.ts.map +1 -0
  37. package/dist/calliope/syntax.js +234 -0
  38. package/dist/calliope/udpos.d.ts +13 -0
  39. package/dist/calliope/udpos.d.ts.map +1 -0
  40. package/dist/calliope/udpos.js +156 -0
  41. package/dist/clio/caesura.d.ts +27 -0
  42. package/dist/clio/caesura.d.ts.map +1 -0
  43. package/dist/clio/caesura.js +148 -0
  44. package/dist/clio/depfix.d.ts +13 -0
  45. package/dist/clio/depfix.d.ts.map +1 -0
  46. package/dist/clio/depfix.js +84 -0
  47. package/dist/clio/display.d.ts +32 -0
  48. package/dist/clio/display.d.ts.map +1 -0
  49. package/dist/clio/display.js +976 -0
  50. package/dist/clio/engine.d.ts +3 -0
  51. package/dist/clio/engine.d.ts.map +1 -0
  52. package/dist/clio/engine.js +28 -0
  53. package/dist/clio/parser.d.ts +10 -0
  54. package/dist/clio/parser.d.ts.map +1 -0
  55. package/dist/clio/parser.js +696 -0
  56. package/dist/clio/phonological.d.ts +41 -0
  57. package/dist/clio/phonological.d.ts.map +1 -0
  58. package/dist/clio/phonological.js +788 -0
  59. package/dist/clio/phrasestress.d.ts +6 -0
  60. package/dist/clio/phrasestress.d.ts.map +1 -0
  61. package/dist/clio/phrasestress.js +106 -0
  62. package/dist/clio/pipeline.d.ts +11 -0
  63. package/dist/clio/pipeline.d.ts.map +1 -0
  64. package/dist/clio/pipeline.js +147 -0
  65. package/dist/clio/rhyme.d.ts +65 -0
  66. package/dist/clio/rhyme.d.ts.map +1 -0
  67. package/dist/clio/rhyme.js +761 -0
  68. package/dist/clio/scandroid.d.ts +17 -0
  69. package/dist/clio/scandroid.d.ts.map +1 -0
  70. package/dist/clio/scandroid.js +435 -0
  71. package/dist/clio/scansion.d.ts +46 -0
  72. package/dist/clio/scansion.d.ts.map +1 -0
  73. package/dist/clio/scansion.js +1086 -0
  74. package/dist/clio/semantics.d.ts +44 -0
  75. package/dist/clio/semantics.d.ts.map +1 -0
  76. package/dist/clio/semantics.js +139 -0
  77. package/dist/clio/stress.d.ts +83 -0
  78. package/dist/clio/stress.d.ts.map +1 -0
  79. package/dist/clio/stress.js +1737 -0
  80. package/dist/clio/tagfix.d.ts +6 -0
  81. package/dist/clio/tagfix.d.ts.map +1 -0
  82. package/dist/clio/tagfix.js +101 -0
  83. package/dist/display.d.ts +0 -6
  84. package/dist/display.d.ts.map +1 -1
  85. package/dist/display.js +322 -141
  86. package/dist/engine.d.ts +9 -0
  87. package/dist/engine.d.ts.map +1 -0
  88. package/dist/engine.js +12 -0
  89. package/dist/index.d.ts +4 -3
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +169 -26
  92. package/dist/parser.d.ts +3 -3
  93. package/dist/parser.d.ts.map +1 -1
  94. package/dist/parser.js +320 -563
  95. package/dist/phonological.d.ts +1 -1
  96. package/dist/phonological.d.ts.map +1 -1
  97. package/dist/phonological.js +73 -4
  98. package/dist/phrasestress.d.ts +6 -0
  99. package/dist/phrasestress.d.ts.map +1 -0
  100. package/dist/phrasestress.js +106 -0
  101. package/dist/rhyme.d.ts +40 -1
  102. package/dist/rhyme.d.ts.map +1 -1
  103. package/dist/rhyme.js +435 -7
  104. package/dist/scansion.d.ts +9 -0
  105. package/dist/scansion.d.ts.map +1 -1
  106. package/dist/scansion.js +145 -18
  107. package/dist/semantics.d.ts +44 -0
  108. package/dist/semantics.d.ts.map +1 -0
  109. package/dist/semantics.js +139 -0
  110. package/dist/stress.d.ts +127 -4
  111. package/dist/stress.d.ts.map +1 -1
  112. package/dist/stress.js +834 -61
  113. package/dist/types.d.ts +15 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/package.json +5 -3
  116. package/src/caesura.ts +201 -0
  117. package/src/calliope/boundaries.ts +190 -0
  118. package/src/calliope/bracketing.ts +390 -0
  119. package/src/calliope/deps.ts +160 -0
  120. package/src/calliope/engine.ts +77 -0
  121. package/src/calliope/feats.ts +46 -0
  122. package/src/calliope/names.ts +44 -0
  123. package/src/calliope/postag.ts +253 -0
  124. package/src/calliope/prosodic.ts +262 -0
  125. package/src/calliope/relstress.ts +645 -0
  126. package/src/calliope/stressrules.ts +147 -0
  127. package/src/calliope/syntax.ts +218 -0
  128. package/src/calliope/udpos.ts +152 -0
  129. package/src/calliope_src_contents.md +19049 -0
  130. package/src/clio/caesura.ts +145 -0
  131. package/src/clio/depfix.ts +88 -0
  132. package/src/clio/display.ts +1042 -0
  133. package/src/clio/engine.ts +38 -0
  134. package/src/clio/parser.ts +845 -0
  135. package/src/clio/phonological.ts +849 -0
  136. package/src/clio/phrasestress.ts +108 -0
  137. package/src/clio/pipeline.ts +154 -0
  138. package/src/clio/rhyme.ts +740 -0
  139. package/src/clio/scandroid.ts +434 -0
  140. package/src/clio/scansion.ts +1130 -0
  141. package/src/clio/semantics.ts +134 -0
  142. package/src/clio/stress.ts +1731 -0
  143. package/src/clio/tagfix.ts +104 -0
  144. package/src/display.ts +321 -137
  145. package/src/engine.ts +22 -0
  146. package/src/index.ts +176 -32
  147. package/src/parser.ts +346 -682
  148. package/src/phonological.ts +72 -4
  149. package/src/phrasestress.ts +108 -0
  150. package/src/rhyme.ts +428 -8
  151. package/src/scansion.ts +136 -15
  152. package/src/semantics.ts +134 -0
  153. package/src/stress.ts +794 -64
  154. package/src/types.ts +39 -4
  155. package/tests/DataForHayesLinesOnly.txt +364 -0
  156. package/tests/DataForHayesStressSymbolsRevised.txt +728 -0
  157. package/tests/basic.test.ts +443 -37
  158. package/tests/bench-hayes.mjs +72 -0
  159. package/tests/wagner-stress.test.ts +188 -0
  160. package/vitest.config.ts +0 -15
@@ -0,0 +1,28 @@
1
+ import { ClsWord } from '../types.js';
2
+ type Side = 'left' | 'right';
3
+ /** Which element of an adjacent (modifier w1, head-noun w2) structure carries
4
+ * primary stress, per Scenarios A/B/C. */
5
+ export declare function scenarioModifierSide(w1: ClsWord, w2: ClsWord): Side;
6
+ /**
7
+ * Apply the nominal-modifier stress scenarios over a sentence's words. Mirrors
8
+ * the legacy `applyCompoundStress` iteration (adjacent content words, noun head)
9
+ * but decides via the canonical relations / name flags rather than POS alone, so
10
+ * the proper-modifier and lexicalised cases come out right. Replaces
11
+ * `applyCompoundStress` in the Calliope engine.
12
+ */
13
+ export declare function applyScenarioStress(words: ClsWord[]): void;
14
+ /**
15
+ * Make the LEFT-stress scenario decision DURABLE in the relative-stress contour.
16
+ *
17
+ * The lexical compound pass (`applyScenarioStress`) sets the modifier primary, but
18
+ * the nuclear pass + relativiser re-promote the rightmost word, so a *non-curated*
19
+ * fore-stressed compound (SLATE roof, REAGAN era, WALL Street, HOT dog) would still
20
+ * surface as end-stressed. The legacy `resolveCompoundForestress` repairs this only
21
+ * for the curated `isLeftStressedPair` set; this Calliope post-pass — run AFTER
22
+ * `assignRelativeStresses` — re-asserts the same demote-only fore-stress for EVERY
23
+ * scenario-left pair: the modifier rises to the pair's max prominence, the head is
24
+ * demoted one rung below (never raised), which also keeps the no-equal-clash rule.
25
+ */
26
+ export declare function enforceScenarioStress(words: ClsWord[]): void;
27
+ export {};
28
+ //# sourceMappingURL=stressrules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stressrules.d.ts","sourceRoot":"","sources":["../../src/calliope/stressrules.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AA8BtC,KAAK,IAAI,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7B;2CAC2C;AAC3C,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,IAAI,CAqBnE;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAgB1D;AAeD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAkB5D"}
@@ -0,0 +1,147 @@
1
+ // calliope/stressrules.ts — Scenario A–O relation-keyed stress (the maintainer's
2
+ // authoritative spec, each rule cross-checked against external linguistics).
3
+ // Operates over POS + word.canonicalRel + the person/place name flags.
4
+ //
5
+ // THIS FILE so far implements the nominal-modifier scenarios — the most directly
6
+ // testable, and where the canonical substrate first pays off:
7
+ // A N+N noun-adjunct (NOMD) → LEFT (SLATE roof, BIRD nest, MSNBC news,
8
+ // REAGAN era) — the ~⅔ baseline;
9
+ // food/temporal heads stay RIGHT.
10
+ // B Adj+N (AMOD) → RIGHT (a green ROOF); a LEXICALISED Adj+N
11
+ // compound fore-stresses (HOT dog).
12
+ // C Proper NAME (NNP+NNP) → RIGHT/head (New YORK, Bay BRIDGE) EXCEPT a
13
+ // de-accenting classifier head ("Street"
14
+ // → WALL Street).
15
+ // M is subsumed here (proper-name extension → rightmost head).
16
+ // Verbal/clausal/override scenarios (D–L, N, O) are added next.
17
+ //
18
+ // External-theory notes (binding): N+N left is only a ~⅔ baseline (Kunter & Plag),
19
+ // so the curated right-stress heads are first-class; lexicalised Adj+N fore-stress
20
+ // is a documented lexicalisation marker, not merely contrast; "Street" de-accents.
21
+ import { setPrimaryStress, isRightStressedHead } from '../stress.js';
22
+ const NOUN = /^(NN|NNS|NNP|NNPS)$/;
23
+ const PROPER = /^(NNP|NNPS)$/;
24
+ const ADJ = /^JJ/;
25
+ function bare(w) {
26
+ return w.word.toLowerCase().replace(/[^a-z]/g, '');
27
+ }
28
+ function isNoun(w) { return NOUN.test(w.lexicalClass); }
29
+ function isProper(w) { return PROPER.test(w.lexicalClass); }
30
+ function isAdjMod(w) { return ADJ.test(w.lexicalClass) || w.canonicalRel === 'AMOD'; }
31
+ // Scenario C exception: proper-name heads that de-accent the name LEFT. "Street"
32
+ // is the well-documented case (WALL Street, STATE Street) vs right-stressing
33
+ // Avenue/Road/Square/Bridge (Fifth AVENUE) — see the plan's external cross-check.
34
+ const PROPER_NAME_LEFT_HEADS = new Set(['street']);
35
+ // Scenario B exception: lexicalised Adj+N compounds that fore-stress despite the
36
+ // AMOD right-stress baseline (the stress shift is the lexicalisation marker:
37
+ // "HOT dog" the food vs "hot DOG" a dog that is hot). Small, curated, defensible.
38
+ const LEXICALIZED_FORESTRESS = new Set([
39
+ 'hot dog', 'hot dogs', 'high school', 'high schools', 'white house',
40
+ 'black board', 'blackboard', 'green house', 'blue blood', 'hot rod',
41
+ ]);
42
+ function isLexicalizedForestress(a, b) {
43
+ return LEXICALIZED_FORESTRESS.has(`${bare(a)} ${bare(b)}`);
44
+ }
45
+ /** Which element of an adjacent (modifier w1, head-noun w2) structure carries
46
+ * primary stress, per Scenarios A/B/C. */
47
+ export function scenarioModifierSide(w1, w2) {
48
+ // Participial/gerundial HYPHENATED compound modifier (book-carrying, law-abiding,
49
+ // well-known) is adjectival/phrasal, NOT a noun adjunct — so it end-stresses, the
50
+ // head keeping prominence: "a book-carrying MAN", "a law-abiding CITIZEN". (The
51
+ // table's VBN/VBG-participle → end-stress; only a single-word -ing GERUND like
52
+ // WAITING room fore-stresses, and those are not hyphenated.)
53
+ if (/-/.test(w1.word) && /(ing|ed)$/i.test(bare(w1)))
54
+ return 'right';
55
+ // C — proper NAME (both proper nouns): head/right, classifier "Street" → left.
56
+ if (isProper(w1) && isProper(w2)) {
57
+ return PROPER_NAME_LEFT_HEADS.has(bare(w2)) ? 'left' : 'right';
58
+ }
59
+ // B — Adj+N: right (compositional); lexicalised Adj+N compound → left.
60
+ if (isAdjMod(w1)) {
61
+ return isLexicalizedForestress(w1, w2) ? 'left' : 'right';
62
+ }
63
+ // A — N+N noun-adjunct (incl. proper-modifier + common head, REAGAN era / MSNBC
64
+ // news): left by default; food/temporal right-stress heads (apple PIE) → right.
65
+ if (isNoun(w1) && isNoun(w2)) {
66
+ return isRightStressedHead(w2.word) ? 'right' : 'left';
67
+ }
68
+ return 'right';
69
+ }
70
+ /**
71
+ * Apply the nominal-modifier stress scenarios over a sentence's words. Mirrors
72
+ * the legacy `applyCompoundStress` iteration (adjacent content words, noun head)
73
+ * but decides via the canonical relations / name flags rather than POS alone, so
74
+ * the proper-modifier and lexicalised cases come out right. Replaces
75
+ * `applyCompoundStress` in the Calliope engine.
76
+ */
77
+ export function applyScenarioStress(words) {
78
+ const content = words.filter(w => w.isContent);
79
+ for (let i = 0; i + 1 < content.length; i++) {
80
+ const w1 = content[i];
81
+ const w2 = content[i + 1];
82
+ if (w1.absoluteIndex + 1 !== w2.absoluteIndex)
83
+ continue; // surface-adjacent only
84
+ if (!isNoun(w2))
85
+ continue; // head of the structure is a noun
86
+ if (!isNoun(w1) && !isAdjMod(w1))
87
+ continue; // modifier is a noun or adjective
88
+ if (scenarioModifierSide(w1, w2) === 'left') {
89
+ setPrimaryStress(w1, 2);
90
+ setPrimaryStress(w2, 1);
91
+ }
92
+ else {
93
+ setPrimaryStress(w1, 1);
94
+ setPrimaryStress(w2, 2);
95
+ }
96
+ }
97
+ }
98
+ const RANK = { x: 0, w: 1, n: 2, m: 3, s: 4 };
99
+ const LEVELS = ['x', 'w', 'n', 'm', 's'];
100
+ function peakSyll(w) {
101
+ let best = null;
102
+ let bestR = -1;
103
+ for (const s of w.syllables) {
104
+ const r = RANK[s.relativeStress ?? 'w'];
105
+ if (r >= bestR) {
106
+ bestR = r;
107
+ best = s;
108
+ }
109
+ }
110
+ return best;
111
+ }
112
+ /**
113
+ * Make the LEFT-stress scenario decision DURABLE in the relative-stress contour.
114
+ *
115
+ * The lexical compound pass (`applyScenarioStress`) sets the modifier primary, but
116
+ * the nuclear pass + relativiser re-promote the rightmost word, so a *non-curated*
117
+ * fore-stressed compound (SLATE roof, REAGAN era, WALL Street, HOT dog) would still
118
+ * surface as end-stressed. The legacy `resolveCompoundForestress` repairs this only
119
+ * for the curated `isLeftStressedPair` set; this Calliope post-pass — run AFTER
120
+ * `assignRelativeStresses` — re-asserts the same demote-only fore-stress for EVERY
121
+ * scenario-left pair: the modifier rises to the pair's max prominence, the head is
122
+ * demoted one rung below (never raised), which also keeps the no-equal-clash rule.
123
+ */
124
+ export function enforceScenarioStress(words) {
125
+ const content = words.filter(w => w.isContent);
126
+ for (let i = 0; i + 1 < content.length; i++) {
127
+ const w1 = content[i];
128
+ const w2 = content[i + 1];
129
+ if (w1.absoluteIndex + 1 !== w2.absoluteIndex)
130
+ continue;
131
+ if (!isNoun(w2))
132
+ continue;
133
+ if (!isNoun(w1) && !isAdjMod(w1))
134
+ continue;
135
+ if (scenarioModifierSide(w1, w2) !== 'left')
136
+ continue;
137
+ const s1 = peakSyll(w1);
138
+ const s2 = peakSyll(w2);
139
+ if (!s1 || !s2)
140
+ continue;
141
+ const r1 = RANK[s1.relativeStress ?? 'w'];
142
+ const r2 = RANK[s2.relativeStress ?? 'w'];
143
+ const hi = Math.max(r1, r2);
144
+ s1.relativeStress = LEVELS[hi]; // modifier ≥ both
145
+ s2.relativeStress = LEVELS[Math.min(r2, Math.max(0, hi - 1))]; // demote head only
146
+ }
147
+ }
@@ -0,0 +1,38 @@
1
+ import { ClsWord } from '../types.js';
2
+ export declare function isPunct(w: ClsWord): boolean;
3
+ export declare function isNoun(w: ClsWord): boolean;
4
+ export declare function isVerb(w: ClsWord): boolean;
5
+ /** A PRONOUN — inherently given (Wagner §7.2.3): a personal/possessive pronoun by
6
+ * POS, or any word the parse marks PronType=Prs. Demonstratives (this/that),
7
+ * wh-words (Rel/Int), negative (nobody/nothing) and indefinite-as-focus pronouns
8
+ * are NOT treated as inherently given here. */
9
+ export declare function isPronoun(w: ClsWord): boolean;
10
+ export declare function isInherentlyGiven(w: ClsWord): boolean;
11
+ /** A LIGHT nominal head — a personal pronoun or an inherently-given indefinite —
12
+ * whose post-nominal PP modifier cannot be hosted inside the head's own group and
13
+ * so needs its OWN ϕ (the phrasing break the ear hears after "Something …"). */
14
+ export declare function isLightNominalHead(w: ClsWord): boolean;
15
+ export declare function verbHasArgChild(verb: ClsWord, children: Map<number, ClsWord[]>): boolean;
16
+ export declare function isUnaccusativeOrPassive(subj: ClsWord, verb: ClsWord): boolean;
17
+ /** The preposition lemma governing an oblique noun: its `case`/`CASE` dependent. */
18
+ export declare function prepLemmaOf(obl: ClsWord, words: ClsWord[]): string | null;
19
+ /** An oblique ARGUMENT (goal/source/recipient) — can integrate with the verb so
20
+ * the verb is subordinated and the oblique NP bears the single accent. */
21
+ export declare function isObliqueArgument(obl: ClsWord, words: ClsWord[]): boolean;
22
+ /** A LOW LOCATIVE / temporal adjunct (Larson 2005): a VP-final place/time oblique
23
+ * that gets its OWN accent regardless of position — NOT a functor of the VP. */
24
+ export declare function isLowLocative(obl: ClsWord, head: ClsWord, words?: ClsWord[]): boolean;
25
+ /** A FRAME-SETTING locative (Wagner §6.5.1): a sentence-initial temporal/spatial
26
+ * oblique that frames the proposition — IS a functor, gets its own domain. */
27
+ export declare function isFrameSetting(obl: ClsWord, words: ClsWord[]): boolean;
28
+ /** A TRANSITIVE function word governs a nominal complement (Wagner §6.5.5: only
29
+ * complement-taking functors have weak/stressless allomorphs). An INTRANSITIVE
30
+ * or STRANDED preposition/particle keeps stress — returns false. */
31
+ export declare function isTransitiveFunctionWord(w: ClsWord, words: ClsWord[]): boolean;
32
+ export type FunctorSide = 'dep' | 'head' | 'match';
33
+ /** Which sister of a (dep, head) UD edge is the FUNCTOR — or 'match' for an
34
+ * associative pairing (Wagner §2.2.2). This is the relation-keyed mapping in
35
+ * the plan's Gap-2 table: for some relations the GOVERNOR is the functor
36
+ * (DOBJ/OBL/CCOMP), for others the DEPENDENT is (AMOD/CASE/AUX/DET/ADVMOD). */
37
+ export declare function functorOf(dep: ClsWord): FunctorSide;
38
+ //# sourceMappingURL=syntax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"syntax.d.ts","sourceRoot":"","sources":["../../src/calliope/syntax.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAQtC,wBAAgB,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAE3C;AACD,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAsC;AACjF,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAsC;AAGjF;;;gDAGgD;AAChD,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAG7C;AAaD,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAIrD;AAED;;iFAEiF;AACjF,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAEtD;AAMD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,GAC9C,OAAO,CAQT;AASD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAI7E;AAmBD,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,GAAG,IAAI,CAQzE;AAED;2EAC2E;AAC3E,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAKzE;AAED;iFACiF;AACjF,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAYrF;AAED;+EAC+E;AAC/E,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAStE;AAkBD;;qEAEqE;AACrE,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAqB9E;AAGD,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAEnD;;;gFAGgF;AAChF,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,WAAW,CAcnD"}
@@ -0,0 +1,234 @@
1
+ // calliope/syntax.ts — Wagner/Krifka structural predicates shared by the cyclic
2
+ // stress (bracketing.ts) and the relativiser (relstress.ts).
3
+ //
4
+ // These encode the functor/argument geometry of Wagner (2005) Ch. 6 and the
5
+ // argument/adjunct/locative typology of Krifka (2001) §4.5 / Wagner §6.5.1, read
6
+ // off the canonical UD relations + Penn POS + morphological FEATS. No semantic
7
+ // role labeller is available, so the obl subtyping and transitivity tests are
8
+ // heuristic (preposition lemma + surface position + complement presence); every
9
+ // classification is approximate and, where uncertain, defaults to the reading
10
+ // that preserves prior behaviour (integration / transitive).
11
+ import { feat } from './feats.js';
12
+ const NOUN = /^(NN|NNS|NNP|NNPS)$/;
13
+ const PROPER = /^(NNP|NNPS)$/;
14
+ const VERB = /^VB/;
15
+ const PRON = /^(PRP|PRP\$)$/;
16
+ export function isPunct(w) {
17
+ return /^[^A-Za-z0-9]+$/.test(w.lexicalClass) || w.syllables.length === 0;
18
+ }
19
+ export function isNoun(w) { return NOUN.test(w.lexicalClass); }
20
+ export function isVerb(w) { return VERB.test(w.lexicalClass); }
21
+ function bare(w) { return w.word.toLowerCase().replace(/['’]/g, ''); }
22
+ /** A PRONOUN — inherently given (Wagner §7.2.3): a personal/possessive pronoun by
23
+ * POS, or any word the parse marks PronType=Prs. Demonstratives (this/that),
24
+ * wh-words (Rel/Int), negative (nobody/nothing) and indefinite-as-focus pronouns
25
+ * are NOT treated as inherently given here. */
26
+ export function isPronoun(w) {
27
+ if (PRON.test(w.lexicalClass))
28
+ return true;
29
+ return feat(w, 'PronType') === 'Prs';
30
+ }
31
+ /** A POSITIVE indefinite pronoun — inherently given (Wagner §7.2.3): its
32
+ * existential presupposition is trivially satisfied in any context, so it is
33
+ * subordinated by default and yields the utterance nuclear to a heavier sister
34
+ * ("Something for the modern STAGE"). Only the SOME-/ANY- existential series:
35
+ * NEGATIVE (nothing/nobody) and temporal/emphatic words are deliberately excluded
36
+ * — those are FOCAL, not given (Eliot's "Nothing", McCartney's "Yesterday"), and
37
+ * per the maintainer's directive must keep their prominence. UDPipe routinely
38
+ * mis-tags these as NN, so detection is by lemma (+ PronType guard). */
39
+ const INDEFINITE_GIVEN = new Set([
40
+ 'something', 'someone', 'somebody', 'somewhat', 'somewhere', 'someplace',
41
+ ]);
42
+ export function isInherentlyGiven(w) {
43
+ if (!INDEFINITE_GIVEN.has(bare(w)))
44
+ return false;
45
+ const pt = feat(w, 'PronType');
46
+ return pt !== 'Neg'; // never a negative pronoun
47
+ }
48
+ /** A LIGHT nominal head — a personal pronoun or an inherently-given indefinite —
49
+ * whose post-nominal PP modifier cannot be hosted inside the head's own group and
50
+ * so needs its OWN ϕ (the phrasing break the ear hears after "Something …"). */
51
+ export function isLightNominalHead(w) {
52
+ return isPronoun(w) || isInherentlyGiven(w);
53
+ }
54
+ /** A child relation that makes a verb BRANCH (carry an internal argument) — the
55
+ * specifier-restriction trigger (Wagner §6.3.2): a transitive/clausal VP whose
56
+ * branchingness blocks subordination of its subject. */
57
+ const ARG_CHILD = new Set(['DOBJ', 'IOBJ', 'OBJ', 'OBL', 'CCOMP', 'XCOMP']);
58
+ export function verbHasArgChild(verb, children) {
59
+ return (children.get(verb.absoluteIndex) ?? []).some(c => {
60
+ const rel = c.canonicalRel ?? '';
61
+ if (!ARG_CHILD.has(rel))
62
+ return false;
63
+ // A low locative oblique is an adjunct, not a branching argument.
64
+ if (rel === 'OBL' && isLowLocative(c, verb))
65
+ return false;
66
+ return true;
67
+ });
68
+ }
69
+ /** An unaccusative / passive subject is an UNDERLYING OBJECT, so it can bear the
70
+ * nuclear accent (Krifka §4.5.2): NSUBJPASS, or Voice=Pass on the verb, or a
71
+ * small set of unaccusative intransitives. */
72
+ const UNACCUSATIVE = new Set([
73
+ 'arrive', 'come', 'go', 'fall', 'rise', 'appear', 'die', 'happen', 'remain',
74
+ 'emerge', 'occur', 'vanish', 'grow', 'sink', 'flow', 'melt', 'wake',
75
+ ]);
76
+ export function isUnaccusativeOrPassive(subj, verb) {
77
+ if ((subj.canonicalRel ?? '') === 'NSUBJPASS')
78
+ return true;
79
+ if (feat(verb, 'Voice') === 'Pass')
80
+ return true;
81
+ return UNACCUSATIVE.has(bare(verb).replace(/(ed|s|ing)$/, ''));
82
+ }
83
+ // ─── oblique typology (Wagner §6.5.1 / Larson 2005 / Krifka §4.5.1) ──────────
84
+ const GOAL_PREPS = new Set(['to', 'into', 'onto', 'toward', 'towards', 'unto']);
85
+ const SOURCE_PREPS = new Set(['from', 'out']);
86
+ const ADJUNCT_PREPS = new Set([
87
+ 'in', 'on', 'at', 'by', 'for', 'during', 'throughout', 'within', 'amid',
88
+ 'amidst', 'among', 'amongst', 'beneath', 'beside', 'over', 'under', 'above',
89
+ ]);
90
+ const FRAME_PREPS = new Set([
91
+ 'in', 'on', 'at', 'by', 'during', 'after', 'before', 'since', 'until', 'upon',
92
+ ]);
93
+ const TIME_NOUNS = new Set([
94
+ 'hour', 'hours', 'day', 'days', 'week', 'weeks', 'month', 'months', 'year',
95
+ 'years', 'minute', 'minutes', 'moment', 'moments', 'century', 'centuries',
96
+ 'morning', 'evening', 'night', 'nights', 'spring', 'summer', 'autumn', 'winter',
97
+ 'dawn', 'dusk', 'noon', 'midnight', 'season', 'seasons',
98
+ ]);
99
+ /** The preposition lemma governing an oblique noun: its `case`/`CASE` dependent. */
100
+ export function prepLemmaOf(obl, words) {
101
+ for (const w of words) {
102
+ if (w.dependency?.governor === obl && (w.canonicalRel === 'CASE' ||
103
+ w.lexicalClass === 'IN' || w.lexicalClass === 'TO')) {
104
+ return bare(w);
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ /** An oblique ARGUMENT (goal/source/recipient) — can integrate with the verb so
110
+ * the verb is subordinated and the oblique NP bears the single accent. */
111
+ export function isObliqueArgument(obl, words) {
112
+ const p = prepLemmaOf(obl, words);
113
+ if (!p)
114
+ return false;
115
+ if (GOAL_PREPS.has(p) || SOURCE_PREPS.has(p))
116
+ return true;
117
+ return false;
118
+ }
119
+ /** A LOW LOCATIVE / temporal adjunct (Larson 2005): a VP-final place/time oblique
120
+ * that gets its OWN accent regardless of position — NOT a functor of the VP. */
121
+ export function isLowLocative(obl, head, words) {
122
+ // Detect by preposition + noun semantics; if we have no word list, fall back to
123
+ // the noun being a time word.
124
+ if (words) {
125
+ const p = prepLemmaOf(obl, words);
126
+ if (p && ADJUNCT_PREPS.has(p)) {
127
+ // VP-final: the oblique follows its verbal head.
128
+ if (obl.absoluteIndex > head.absoluteIndex)
129
+ return true;
130
+ }
131
+ }
132
+ if (TIME_NOUNS.has(bare(obl)))
133
+ return true;
134
+ return false;
135
+ }
136
+ /** A FRAME-SETTING locative (Wagner §6.5.1): a sentence-initial temporal/spatial
137
+ * oblique that frames the proposition — IS a functor, gets its own domain. */
138
+ export function isFrameSetting(obl, words) {
139
+ const p = prepLemmaOf(obl, words);
140
+ if (!p || !FRAME_PREPS.has(p))
141
+ return false;
142
+ // sentence-initial: the preposition is (near) the first content token.
143
+ let firstContent = -1;
144
+ for (let i = 0; i < words.length; i++) {
145
+ if (!isPunct(words[i])) {
146
+ firstContent = words[i].absoluteIndex;
147
+ break;
148
+ }
149
+ }
150
+ return obl.absoluteIndex <= firstContent + 2;
151
+ }
152
+ // ─── function-word transitivity (Wagner §6.5.5) ──────────────────────────────
153
+ const PARTICLE_LEMMAS = new Set([
154
+ 'in', 'out', 'up', 'down', 'off', 'away', 'back', 'forth', 'over', 'around',
155
+ 'along', 'through', 'apart', 'aside', 'onward', 'about',
156
+ ]);
157
+ /** Last non-punct token of the clause at/after `w`? (the canonical strand site). */
158
+ function isClauseFinal(w, words) {
159
+ const idx = words.findIndex(x => x === w);
160
+ if (idx < 0)
161
+ return false;
162
+ for (let k = idx + 1; k < words.length; k++) {
163
+ if (!isPunct(words[k]))
164
+ return false;
165
+ }
166
+ return true;
167
+ }
168
+ /** A TRANSITIVE function word governs a nominal complement (Wagner §6.5.5: only
169
+ * complement-taking functors have weak/stressless allomorphs). An INTRANSITIVE
170
+ * or STRANDED preposition/particle keeps stress — returns false. */
171
+ export function isTransitiveFunctionWord(w, words) {
172
+ const pos = w.lexicalClass;
173
+ const rel = w.canonicalRel ?? '';
174
+ if (pos === 'RP' || rel === 'VPRT')
175
+ return false; // particles are intransitive
176
+ if (pos === 'IN' || pos === 'TO' || rel === 'CASE') {
177
+ // Does it govern a noun (have a nominal complement)? In UD the preposition is
178
+ // a CASE dependent of its noun, so check both directions.
179
+ const gov = w.dependency?.governor;
180
+ if (gov && !isPunct(gov) && NOUN.test(gov.lexicalClass) &&
181
+ gov.absoluteIndex > w.absoluteIndex) {
182
+ return true; // prep → noun complement to its right
183
+ }
184
+ const hasNominalDep = words.some(d => d.dependency?.governor === w &&
185
+ (NOUN.test(d.lexicalClass) || d.canonicalRel === 'OBL' || d.canonicalRel === 'DOBJ'));
186
+ if (hasNominalDep)
187
+ return true;
188
+ // Clause-final adverbial particle/preposition with no complement → stranded.
189
+ if (PARTICLE_LEMMAS.has(bare(w)) && isClauseFinal(w, words))
190
+ return false;
191
+ if (isClauseFinal(w, words))
192
+ return false; // stranded
193
+ return true; // default: transitive
194
+ }
195
+ return true; // det/aux/cc/etc.: transitive
196
+ }
197
+ /** Which sister of a (dep, head) UD edge is the FUNCTOR — or 'match' for an
198
+ * associative pairing (Wagner §2.2.2). This is the relation-keyed mapping in
199
+ * the plan's Gap-2 table: for some relations the GOVERNOR is the functor
200
+ * (DOBJ/OBL/CCOMP), for others the DEPENDENT is (AMOD/CASE/AUX/DET/ADVMOD). */
201
+ export function functorOf(dep) {
202
+ switch (dep.canonicalRel ?? '') {
203
+ case 'DOBJ':
204
+ case 'IOBJ':
205
+ case 'OBJ':
206
+ case 'OBL':
207
+ case 'CCOMP':
208
+ case 'XCOMP':
209
+ case 'NSUBJ':
210
+ case 'NSUBJPASS':
211
+ return 'head'; // governor (verb/VP) is functor
212
+ case 'AMOD':
213
+ case 'ADVMOD':
214
+ case 'ACL':
215
+ case 'ADVCL':
216
+ case 'CASE':
217
+ case 'AUX':
218
+ case 'AUXPASS':
219
+ case 'DET':
220
+ case 'NUMMOD':
221
+ case 'CC':
222
+ case 'COMPMARK':
223
+ case 'ADVMARK':
224
+ case 'EXPL':
225
+ case 'VPRT':
226
+ return 'dep'; // dependent is functor
227
+ case 'CONJ':
228
+ case 'DISCOURSE':
229
+ case 'INTJ':
230
+ return 'match'; // associative / own accent
231
+ default:
232
+ return 'head';
233
+ }
234
+ }
@@ -0,0 +1,13 @@
1
+ import type { UDWord } from 'udpipe-node';
2
+ /** True if `xpos` is a Penn tag the pipeline consumes directly (EWT/GUM). */
3
+ export declare function isPennTag(xpos: string | undefined): boolean;
4
+ /** The Penn tag for a token: the raw XPOS when it is already Penn (EWT/GUM),
5
+ * otherwise derived from UPOS+FEATS (LinES/ParTUT, or a missing XPOS). */
6
+ export declare function pennTagOf(w: UDWord): string;
7
+ /**
8
+ * Convert one UDPipe token to a Penn Treebank tag from UPOS + FEATS (+ lemma /
9
+ * deprel for the cases morphology alone can't settle). Falls back to the raw XPOS
10
+ * only when UPOS is absent.
11
+ */
12
+ export declare function udToPenn(w: UDWord): string;
13
+ //# sourceMappingURL=udpos.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"udpos.d.ts","sourceRoot":"","sources":["../../src/calliope/udpos.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAa1C,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAE3D;AAED;2EAC2E;AAC3E,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAG3C;AAaD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAiG1C"}
@@ -0,0 +1,156 @@
1
+ // calliope/udpos.ts — UNIVERSAL POS → Penn Treebank conversion.
2
+ //
3
+ // WHY: the phonological pipeline keys on Penn XPOS (NN/VBZ/JJ/DT/IN…), but
4
+ // UDPipe's XPOS column is treebank-specific and INCONSISTENT across models —
5
+ // EWT/GUM emit Penn tags, but LinES emits its own morphological tagset
6
+ // (DEF/SG-NOM/PL-NOM/ING/REL…) and ParTUT an Italian-derived one (RD/S/V/E/A/PD…),
7
+ // neither of which the downstream understands. UPOS and FEATS, by contrast, are
8
+ // the Universal Dependencies standard and are consistent (and more accurate:
9
+ // ~94% UPOS vs ~93% XPOS) across ALL four models. So we DERIVE the Penn tag the
10
+ // pipeline needs from UPOS + morphological FEATS, making the parser model-agnostic
11
+ // AND giving us morphology en-parse never had (Number, Tense, Degree, PronType,
12
+ // VerbForm) to make finer, more reliable distinctions.
13
+ // The Penn Treebank tag set the downstream understands. EWT/GUM emit these
14
+ // directly (and reliably — a lexicalised PDT like "all" stays PDT even when the
15
+ // parse mislabels its relation); LinES/ParTUT do NOT, so for those we derive the
16
+ // Penn tag from UPOS+FEATS instead.
17
+ const PENN_TAGS = new Set([
18
+ 'NN', 'NNS', 'NNP', 'NNPS', 'JJ', 'JJR', 'JJS',
19
+ 'VB', 'VBD', 'VBG', 'VBN', 'VBP', 'VBZ', 'MD',
20
+ 'RB', 'RBR', 'RBS', 'PRP', 'PRP$', 'WP', 'WP$', 'WDT', 'WRB',
21
+ 'DT', 'PDT', 'CD', 'IN', 'TO', 'CC', 'RP', 'EX', 'POS', 'UH', 'FW', 'SYM', 'LS',
22
+ ]);
23
+ /** True if `xpos` is a Penn tag the pipeline consumes directly (EWT/GUM). */
24
+ export function isPennTag(xpos) {
25
+ return !!xpos && PENN_TAGS.has(xpos);
26
+ }
27
+ /** The Penn tag for a token: the raw XPOS when it is already Penn (EWT/GUM),
28
+ * otherwise derived from UPOS+FEATS (LinES/ParTUT, or a missing XPOS). */
29
+ export function pennTagOf(w) {
30
+ if (isPennTag(w.xpos))
31
+ return w.xpos;
32
+ return udToPenn(w);
33
+ }
34
+ const MODAL_LEMMAS = new Set([
35
+ 'can', 'could', 'will', 'would', 'shall', 'should', 'may', 'might', 'must',
36
+ 'ought', "'ll", "'d", 'wilt', 'shalt', 'canst', 'wouldst', 'shouldst', 'couldst',
37
+ ]);
38
+ // Pre-determiners ("ALL the books", "BOTH his hands", "such a day", "HALF the night").
39
+ const PREDET_LEMMAS = new Set(['all', 'both', 'half', 'such', 'quite', 'many']);
40
+ function feat(w, k) {
41
+ return w.featsMap ? w.featsMap[k] : undefined;
42
+ }
43
+ /**
44
+ * Convert one UDPipe token to a Penn Treebank tag from UPOS + FEATS (+ lemma /
45
+ * deprel for the cases morphology alone can't settle). Falls back to the raw XPOS
46
+ * only when UPOS is absent.
47
+ */
48
+ export function udToPenn(w) {
49
+ const upos = w.upos || '';
50
+ const lemma = (w.lemma || w.form || '').toLowerCase();
51
+ const deprel = (w.deprel || '').toLowerCase();
52
+ const num = feat(w, 'Number');
53
+ const degree = feat(w, 'Degree');
54
+ const pron = feat(w, 'PronType');
55
+ const poss = feat(w, 'Poss');
56
+ const vform = feat(w, 'VerbForm');
57
+ const tense = feat(w, 'Tense');
58
+ const person = feat(w, 'Person');
59
+ // Pre-determiner ("ALL the time", "BOTH his hands"): a stress-bearing quantifier
60
+ // (Penn PDT, content), regardless of whether the model calls it DET or PRON. UD
61
+ // marks it with the det:predet relation; the non-Penn models lose that, so back
62
+ // it up with the lemma list.
63
+ if (deprel === 'det:predet')
64
+ return 'PDT';
65
+ switch (upos) {
66
+ case 'NOUN':
67
+ return num === 'Plur' ? 'NNS' : 'NN';
68
+ case 'PROPN':
69
+ return num === 'Plur' ? 'NNPS' : 'NNP';
70
+ case 'ADJ':
71
+ if (degree === 'Cmp')
72
+ return 'JJR';
73
+ if (degree === 'Sup')
74
+ return 'JJS';
75
+ // Ordinal numerals tag JJ in UD but CD-like in Penn; keep JJ (attributive).
76
+ return 'JJ';
77
+ case 'ADV':
78
+ if (pron === 'Int' || pron === 'Rel')
79
+ return 'WRB'; // when/where/why/how
80
+ if (degree === 'Cmp')
81
+ return 'RBR';
82
+ if (degree === 'Sup')
83
+ return 'RBS';
84
+ return 'RB';
85
+ case 'VERB':
86
+ case 'AUX': {
87
+ if (upos === 'AUX' && (feat(w, 'VerbType') === 'Mod' || MODAL_LEMMAS.has(lemma)))
88
+ return 'MD';
89
+ if (vform === 'Ger')
90
+ return 'VBG';
91
+ if (vform === 'Part')
92
+ return tense === 'Past' ? 'VBN' : 'VBG';
93
+ if (vform === 'Inf')
94
+ return 'VB';
95
+ if (vform === 'Fin') {
96
+ if (tense === 'Past')
97
+ return 'VBD';
98
+ if (person === '3' && num === 'Sing')
99
+ return 'VBZ';
100
+ return 'VBP';
101
+ }
102
+ // No VerbForm feature: best-effort by tense/person.
103
+ if (tense === 'Past')
104
+ return 'VBD';
105
+ if (person === '3' && num === 'Sing')
106
+ return 'VBZ';
107
+ return upos === 'AUX' ? 'VBP' : 'VB';
108
+ }
109
+ case 'PRON':
110
+ if (poss === 'Yes')
111
+ return (pron === 'Rel' || pron === 'Int') ? 'WP$' : 'PRP$';
112
+ if (pron === 'Rel' || pron === 'Int')
113
+ return 'WP';
114
+ if (pron === 'Dem')
115
+ return 'DT'; // "this/that" pronominal
116
+ return 'PRP';
117
+ case 'DET':
118
+ if (pron === 'Rel' || pron === 'Int')
119
+ return 'WDT'; // which/that(rel)/what
120
+ if (poss === 'Yes')
121
+ return 'PRP$'; // my/your/their (UD DET)
122
+ if (PREDET_LEMMAS.has(lemma) && deprel === 'det:predet')
123
+ return 'PDT';
124
+ return 'DT'; // articles + demonstratives
125
+ case 'ADP':
126
+ // A particle of a phrasal verb ("came DOWN", "give UP") is stress-bearing RP;
127
+ // an ordinary preposition is the reducible IN. UD marks the particle by the
128
+ // compound:prt relation.
129
+ return deprel === 'compound:prt' ? 'RP' : 'IN';
130
+ case 'SCONJ':
131
+ return 'IN';
132
+ case 'CCONJ':
133
+ return 'CC';
134
+ case 'PART':
135
+ if (lemma === 'to')
136
+ return 'TO';
137
+ if (lemma === "'s" || lemma === '’s' || deprel === 'case')
138
+ return 'POS';
139
+ if (lemma === 'not' || lemma === "n't" || lemma === "n’t")
140
+ return 'RB';
141
+ return 'RB';
142
+ case 'NUM':
143
+ return 'CD';
144
+ case 'INTJ':
145
+ return 'UH';
146
+ case 'SYM':
147
+ return 'SYM';
148
+ case 'X':
149
+ return 'FW';
150
+ case 'PUNCT':
151
+ // EWT/GUM give the punctuation char as XPOS already; otherwise use the form.
152
+ return (w.xpos && /[^A-Za-z0-9]/.test(w.xpos)) ? w.xpos : (w.form || w.xpos || ':');
153
+ default:
154
+ return w.xpos || w.upos || 'NN';
155
+ }
156
+ }