calliope-ts 0.0.4 → 0.1.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/README.md +69 -3
- package/dist/calliope/bracketing.d.ts.map +1 -1
- package/dist/calliope/bracketing.js +59 -5
- package/dist/calliope/deps.d.ts +5 -1
- package/dist/calliope/deps.d.ts.map +1 -1
- package/dist/calliope/deps.js +53 -3
- package/dist/calliope/engine.d.ts.map +1 -1
- package/dist/calliope/engine.js +6 -1
- package/dist/calliope/relstress.d.ts.map +1 -1
- package/dist/calliope/relstress.js +411 -15
- package/dist/calliope/syntax.d.ts +33 -0
- package/dist/calliope/syntax.d.ts.map +1 -1
- package/dist/calliope/syntax.js +199 -0
- package/dist/data/en_common_words.json +1 -0
- package/dist/display.d.ts +15 -1
- package/dist/display.d.ts.map +1 -1
- package/dist/display.js +112 -10
- package/dist/fabbhalle.d.ts +54 -0
- package/dist/fabbhalle.d.ts.map +1 -0
- package/dist/fabbhalle.js +404 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -33
- package/dist/phonological.d.ts.map +1 -1
- package/dist/phonological.js +7 -1
- package/dist/rewriteEn.d.ts +14 -0
- package/dist/rewriteEn.d.ts.map +1 -0
- package/dist/rewriteEn.js +392 -0
- package/dist/rhyme.d.ts +35 -3
- package/dist/rhyme.d.ts.map +1 -1
- package/dist/rhyme.js +176 -11
- package/dist/russian/accentuator.d.ts +31 -0
- package/dist/russian/accentuator.d.ts.map +1 -0
- package/dist/russian/accentuator.js +546 -0
- package/dist/russian/collocations.d.ts +7 -0
- package/dist/russian/collocations.d.ts.map +1 -0
- package/dist/russian/collocations.js +38 -0
- package/dist/russian/compounds.d.ts +6 -0
- package/dist/russian/compounds.d.ts.map +1 -0
- package/dist/russian/compounds.js +81 -0
- package/dist/russian/download.d.ts +23 -0
- package/dist/russian/download.d.ts.map +1 -0
- package/dist/russian/download.js +117 -0
- package/dist/russian/engine.d.ts +6 -0
- package/dist/russian/engine.d.ts.map +1 -0
- package/dist/russian/engine.js +1424 -0
- package/dist/russian/parser.d.ts +8 -0
- package/dist/russian/parser.d.ts.map +1 -0
- package/dist/russian/parser.js +92 -0
- package/dist/russian/paths.d.ts +55 -0
- package/dist/russian/paths.d.ts.map +1 -0
- package/dist/russian/paths.js +114 -0
- package/dist/russian/rewrite.d.ts +6 -0
- package/dist/russian/rewrite.d.ts.map +1 -0
- package/dist/russian/rewrite.js +400 -0
- package/dist/russian/rhyme.d.ts +18 -0
- package/dist/russian/rhyme.d.ts.map +1 -0
- package/dist/russian/rhyme.js +230 -0
- package/dist/russian/syllabifier.d.ts +14 -0
- package/dist/russian/syllabifier.d.ts.map +1 -0
- package/dist/russian/syllabifier.js +475 -0
- package/dist/russian/types.d.ts +94 -0
- package/dist/russian/types.d.ts.map +1 -0
- package/dist/russian/types.js +27 -0
- package/dist/scandroidNative/constants.d.ts +16 -0
- package/dist/scandroidNative/constants.d.ts.map +1 -0
- package/dist/scandroidNative/constants.js +60 -0
- package/dist/scandroidNative/deduceParameters.d.ts +16 -0
- package/dist/scandroidNative/deduceParameters.d.ts.map +1 -0
- package/dist/scandroidNative/deduceParameters.js +69 -0
- package/dist/scandroidNative/dictionary-data.d.ts +2 -0
- package/dist/scandroidNative/dictionary-data.d.ts.map +1 -0
- package/dist/scandroidNative/dictionary-data.js +744 -0
- package/dist/scandroidNative/dictionary.d.ts +12 -0
- package/dist/scandroidNative/dictionary.d.ts.map +1 -0
- package/dist/scandroidNative/dictionary.js +50 -0
- package/dist/scandroidNative/engine.d.ts +41 -0
- package/dist/scandroidNative/engine.d.ts.map +1 -0
- package/dist/scandroidNative/engine.js +119 -0
- package/dist/scandroidNative/machine.d.ts +93 -0
- package/dist/scandroidNative/machine.d.ts.map +1 -0
- package/dist/scandroidNative/machine.js +795 -0
- package/dist/scandroidNative/positioner.d.ts +32 -0
- package/dist/scandroidNative/positioner.d.ts.map +1 -0
- package/dist/scandroidNative/positioner.js +208 -0
- package/dist/scandroidNative/syllabizer.d.ts +3 -0
- package/dist/scandroidNative/syllabizer.d.ts.map +1 -0
- package/dist/scandroidNative/syllabizer.js +215 -0
- package/dist/scandroidNative/utilities.d.ts +37 -0
- package/dist/scandroidNative/utilities.d.ts.map +1 -0
- package/dist/scandroidNative/utilities.js +86 -0
- package/dist/scansion.d.ts.map +1 -1
- package/dist/scansion.js +118 -14
- package/dist/stress.d.ts +1 -0
- package/dist/stress.d.ts.map +1 -1
- package/dist/stress.js +502 -42
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +27 -6
- package/tools/fetch-russian-data.mjs +30 -0
- package/webapp/public/app.js +2418 -0
- package/webapp/public/assets/GithubLogo.png +0 -0
- package/webapp/public/assets/NPM2.png +0 -0
- package/webapp/public/assets/fonts/MurenaBold-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/MurenaItalic-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/MurenaRegular-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/Myra_4F_Caps_Bold-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/SinkinSans-700Bold.otf +0 -0
- package/webapp/public/assets/fonts/butterflies.ttf +0 -0
- package/webapp/public/assets/fonts/midcase_blackline-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/neueral-bold-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/neueral-regular-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/popelka-webfont.woff2 +0 -0
- package/webapp/public/assets/fonts/vremenagrotesk-webfont.woff +0 -0
- package/webapp/public/assets/fonts/wicky_javick_bold-webfont.woff2 +0 -0
- package/webapp/public/assets/logos/Logo_Var_10_Celtic_Knot_SVG.svg +116 -0
- package/webapp/public/assets/logos/Logo_Var_11_Graph_SVG.svg +313 -0
- package/webapp/public/assets/logos/Logo_Var_12_Limacon_Knot_SVG.svg +3942 -0
- package/webapp/public/assets/logos/Logo_Var_13_Crescents_SVG.svg +1030 -0
- package/webapp/public/assets/logos/Logo_Var_1_Spiral_SVG.svg +1491 -0
- package/webapp/public/assets/logos/Logo_Var_2_Tunnel_SVG.svg +12 -0
- package/webapp/public/assets/logos/Logo_Var_3_Triskelion_SVG.svg +10 -0
- package/webapp/public/assets/logos/Logo_Var_4_Quinquetra_PNG.png +0 -0
- package/webapp/public/assets/logos/Logo_Var_5_Penrose_Pentagram_SVG.svg +37 -0
- package/webapp/public/assets/logos/Logo_Var_6_Triskelion2_SVG.svg +10 -0
- package/webapp/public/assets/logos/Logo_Var_7_SixfoldSpirals_SVG.svg +20 -0
- package/webapp/public/assets/logos/Logo_Var_8_Radial_Labyrinth_SVG.svg +163 -0
- package/webapp/public/assets/logos/Logo_Var_9_Radial_Labyrinth_2_SVG.svg +106 -0
- package/webapp/public/index.html +316 -0
- package/webapp/public/styles.css +1114 -0
- package/webapp/server.mjs +680 -0
- package/src/caesura.ts +0 -201
- package/src/calliope/boundaries.ts +0 -190
- package/src/calliope/bracketing.ts +0 -390
- package/src/calliope/deps.ts +0 -160
- package/src/calliope/engine.ts +0 -77
- package/src/calliope/feats.ts +0 -46
- package/src/calliope/names.ts +0 -44
- package/src/calliope/postag.ts +0 -253
- package/src/calliope/prosodic.ts +0 -262
- package/src/calliope/relstress.ts +0 -645
- package/src/calliope/stressrules.ts +0 -147
- package/src/calliope/syntax.ts +0 -218
- package/src/calliope/udpos.ts +0 -152
- package/src/calliope_src_contents.md +0 -19049
- package/src/clio/caesura.ts +0 -145
- package/src/clio/depfix.ts +0 -88
- package/src/clio/display.ts +0 -1042
- package/src/clio/engine.ts +0 -38
- package/src/clio/parser.ts +0 -845
- package/src/clio/phonological.ts +0 -849
- package/src/clio/phrasestress.ts +0 -108
- package/src/clio/pipeline.ts +0 -154
- package/src/clio/rhyme.ts +0 -740
- package/src/clio/scandroid.ts +0 -434
- package/src/clio/scansion.ts +0 -1130
- package/src/clio/semantics.ts +0 -134
- package/src/clio/stress.ts +0 -1731
- package/src/clio/tagfix.ts +0 -104
- package/src/depfix.ts +0 -88
- package/src/display.ts +0 -1138
- package/src/engine.ts +0 -22
- package/src/index.ts +0 -690
- package/src/parser.ts +0 -501
- package/src/phonological.ts +0 -917
- package/src/phrasestress.ts +0 -108
- package/src/rhyme.ts +0 -748
- package/src/scandroid.ts +0 -434
- package/src/scansion.ts +0 -1174
- package/src/semantics.ts +0 -134
- package/src/stress.ts +0 -2111
- package/src/tagfix.ts +0 -104
- package/src/types.ts +0 -265
- package/tests/DataForHayesLinesOnly.txt +0 -364
- package/tests/DataForHayesStressSymbolsRevised.txt +0 -728
- package/tests/basic.test.ts +0 -1102
- package/tests/bench-hayes.mjs +0 -72
- package/tests/epg64.meter.train.txt +0 -8139
- package/tests/litlab-sample-2016.txt +0 -1738
- package/tests/prosodic.meter.train.txt +0 -13192
- package/tests/wagner-stress.test.ts +0 -188
- package/tsconfig.json +0 -29
- package/vitest.config.d.ts +0 -2
- package/vitest.config.js +0 -14
package/dist/display.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { ClsWord, LineResult } from './types.js';
|
|
2
|
-
|
|
2
|
+
import { type FHSyllable } from './fabbhalle.js';
|
|
3
|
+
/** The Fabb–Halle projection of a line — one FHSyllable per syllable of every
|
|
4
|
+
* non-punctuation word, carrying LEXICAL stress only. Exported so the CLI
|
|
5
|
+
* can run F&H's poem-level rule-set discovery over a whole document before
|
|
6
|
+
* rendering (only lex/poly matter for discovery; text is cosmetic). */
|
|
7
|
+
export declare function fabbHalleSyllablesOf(result: LineResult): FHSyllable[];
|
|
8
|
+
export interface RenderOptions {
|
|
9
|
+
/** Rule set discovered by F&H's own poem-level grid construction
|
|
10
|
+
* (buildFabbHalleGridsForPoem over the whole document). When present,
|
|
11
|
+
* the F&H section scans under it and reports agreement with the engine's
|
|
12
|
+
* verdict; when absent, the section falls back to stipulating the
|
|
13
|
+
* engine's meter (legacy single-line behaviour). */
|
|
14
|
+
fhSchema?: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare function renderUnifiedDisplay(result: LineResult, rawLine?: string, opts?: RenderOptions): string;
|
|
3
17
|
export declare function renderLegend(): string;
|
|
4
18
|
/**
|
|
5
19
|
* The long-form legend triggered from the main menu's "Display Legend" option:
|
package/dist/display.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EAMP,UAAU,EAGX,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"display.d.ts","sourceRoot":"","sources":["../src/display.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,OAAO,EAMP,UAAU,EAGX,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAsB,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AA8MrE;;;wEAGwE;AACxE,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,EAAE,CAerE;AAED,MAAM,WAAW,aAAa;IAC5B;;;;yDAIqD;IACrD,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,aAAa,GAAG,MAAM,CAwXvG;AAMD,wBAAgB,YAAY,IAAI,MAAM,CAUrC;AAuHD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAsBzC;AAqCD,4DAA4D;AAC5D,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,WAAW,EAAE,CAAC;CACtB;AAmCD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAuC/E;AA2OD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,CA0FlE"}
|
package/dist/display.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { isPunctuation } from './parser.js';
|
|
5
5
|
import { syllabifyWord, syllableVowelLengths } from './phonological.js';
|
|
6
|
+
import { buildFabbHalleGrid } from './fabbhalle.js';
|
|
6
7
|
import { computeCaesurae } from './caesura.js';
|
|
7
8
|
import { computeBoundaries } from './calliope/boundaries.js';
|
|
8
9
|
import { summarizePoem, analyzePhonopoetics } from './rhyme.js';
|
|
@@ -171,7 +172,28 @@ function buildColSyls(words, ius) {
|
|
|
171
172
|
// ═══════════════════════════════════════════════════════════════════════
|
|
172
173
|
// UNIFIED DISPLAY — All layers integrated
|
|
173
174
|
// ═══════════════════════════════════════════════════════════════════════
|
|
174
|
-
|
|
175
|
+
/** The Fabb–Halle projection of a line — one FHSyllable per syllable of every
|
|
176
|
+
* non-punctuation word, carrying LEXICAL stress only. Exported so the CLI
|
|
177
|
+
* can run F&H's poem-level rule-set discovery over a whole document before
|
|
178
|
+
* rendering (only lex/poly matter for discovery; text is cosmetic). */
|
|
179
|
+
export function fabbHalleSyllablesOf(result) {
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const w of result.sentence.words) {
|
|
182
|
+
if (isPunctuation(w.lexicalClass))
|
|
183
|
+
continue;
|
|
184
|
+
const poly = w.syllables.length > 1;
|
|
185
|
+
// Monosyllabic FUNCTION words are clitics with no lexical stress in
|
|
186
|
+
// F&H's sense (condition (16): a maximum's neighbours must "not carry
|
|
187
|
+
// lexical stress" — "were", "in", "like" never block a maximum, while
|
|
188
|
+
// content monosyllables like "wolf" legitimately do).
|
|
189
|
+
const cliticMono = !poly && !w.isContent;
|
|
190
|
+
for (const s of w.syllables) {
|
|
191
|
+
out.push({ text: s.text, lex: cliticMono ? 0 : (s.lexicalStress ?? s.stress ?? 0), poly });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
export function renderUnifiedDisplay(result, rawLine, opts) {
|
|
175
197
|
const words = result.sentence.words;
|
|
176
198
|
const ius = result.phonologicalHierarchy;
|
|
177
199
|
const detail = result.phonologicalScansion;
|
|
@@ -445,18 +467,98 @@ export function renderUnifiedDisplay(result, rawLine) {
|
|
|
445
467
|
lines.push(' ' + H2('Scansion: ') + detail.scansion);
|
|
446
468
|
lines.push(' ' + H2('Summary: ') + detail.summary);
|
|
447
469
|
lines.push('');
|
|
448
|
-
// ── Layer 9: Scandroid
|
|
449
|
-
|
|
450
|
-
|
|
470
|
+
// ── Layer 9: Scandroid — Hartman's own, fully independent second opinion ──
|
|
471
|
+
// Its own dictionary, its own syllabifier, its own algorithms; reads only
|
|
472
|
+
// the raw line text. Never touches or is touched by anything above.
|
|
473
|
+
if (result.scandroidNative) {
|
|
474
|
+
const sn = result.scandroidNative;
|
|
475
|
+
lines.push(H1('Scandroid (independent)'));
|
|
451
476
|
lines.push('');
|
|
452
|
-
|
|
453
|
-
|
|
477
|
+
const lengthTxt = sn.lineFeetSet ? sn.lineLengthName : `${sn.lineLengthName} (variable length)`;
|
|
478
|
+
lines.push(' ' + H2('Metron: ') + `${sn.metronName} ${lengthTxt}`);
|
|
479
|
+
if (sn.verdict) {
|
|
480
|
+
const failTxt = sn.verdict.ok ? '' : chalk.red(' FAIL' + (sn.verdict.failReason ? ` (${sn.verdict.failReason})` : ''));
|
|
481
|
+
lines.push(' ' + H2('Verdict: ') + `${sn.verdict.algorithm} — ${sn.verdict.scanString}` + failTxt);
|
|
482
|
+
lines.push(' ' + H2('Marks: ') + sn.verdict.marksString + ' ' + chalk.dim(`${sn.verdict.substitutions} substitution(s)`));
|
|
454
483
|
}
|
|
455
|
-
|
|
456
|
-
lines.push(' ' + H2('
|
|
484
|
+
else {
|
|
485
|
+
lines.push(' ' + H2('Verdict: ') + chalk.red('FAIL (no scannable resolution)'));
|
|
457
486
|
}
|
|
487
|
+
if (sn.corralTheWeird)
|
|
488
|
+
lines.push(' ' + H2('CW: ') + sn.corralTheWeird.scanString + (sn.corralTheWeird.ok ? '' : chalk.red(' FAIL')));
|
|
489
|
+
if (sn.maximizeTheNormal)
|
|
490
|
+
lines.push(' ' + H2('MN: ') + sn.maximizeTheNormal.scanString + (sn.maximizeTheNormal.ok ? '' : chalk.red(' FAIL')));
|
|
458
491
|
lines.push('');
|
|
459
492
|
}
|
|
493
|
+
// ── Layer 9b: Fabb–Halle bracketed grid (independent second opinion) ──
|
|
494
|
+
// When the caller supplies opts.fhSchema (the rule set F&H's OWN poem-level
|
|
495
|
+
// grid construction discovered — see buildFabbHalleGridsForPoem), the line
|
|
496
|
+
// is scanned under that; otherwise it falls back to stipulating the
|
|
497
|
+
// engine's final meter name. Never fed back into the main scansion.
|
|
498
|
+
{
|
|
499
|
+
const fhSyls = linearSyls.map(ls => ({
|
|
500
|
+
text: ls.chunk,
|
|
501
|
+
lex: 0,
|
|
502
|
+
poly: ls.wordRef.syllables.length > 1,
|
|
503
|
+
}));
|
|
504
|
+
// Lexical stresses assigned positionally, walking words in surface order
|
|
505
|
+
// (mirrors how linearSyls itself was built). Monosyllabic function
|
|
506
|
+
// words stay lex 0 — clitics carry no lexical stress in F&H's sense.
|
|
507
|
+
{
|
|
508
|
+
let k = 0;
|
|
509
|
+
for (const w of words) {
|
|
510
|
+
if (isPunctuation(w.lexicalClass))
|
|
511
|
+
continue;
|
|
512
|
+
const cliticMono = w.syllables.length === 1 && !w.isContent;
|
|
513
|
+
for (const s of w.syllables) {
|
|
514
|
+
if (k < fhSyls.length)
|
|
515
|
+
fhSyls[k].lex = cliticMono ? 0 : (s.lexicalStress ?? s.stress ?? 0);
|
|
516
|
+
k++;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
let fh;
|
|
521
|
+
let discoveredNote = '';
|
|
522
|
+
if (opts?.fhSchema) {
|
|
523
|
+
const size = opts.fhSchema === 'loose' ? 0
|
|
524
|
+
: opts.fhSchema === 'iambic' || opts.fhSchema === 'trochaic' ? 2 : 3;
|
|
525
|
+
fh = buildFabbHalleGrid(fhSyls, opts.fhSchema, size > 0 ? Math.floor(fhSyls.length / size) : undefined);
|
|
526
|
+
const engineMeter = detail.meter || '';
|
|
527
|
+
const agrees = engineMeter.includes(opts.fhSchema)
|
|
528
|
+
|| (opts.fhSchema === 'loose' && /accentual|loose|dolnik|free/i.test(engineMeter));
|
|
529
|
+
discoveredNote = agrees
|
|
530
|
+
? ' · poem-discovered by grid construction, agreeing with the engine'
|
|
531
|
+
: ` · poem-discovered by grid construction (the engine read ${engineMeter || 'nothing'})`;
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
fh = buildFabbHalleGrid(fhSyls, detail.meter || '', detail.footCount);
|
|
535
|
+
}
|
|
536
|
+
if (fh) {
|
|
537
|
+
lines.push(H1('Fabb–Halle Grid') + chalk.dim(' — bracketed-grid scansion (Meter in Poetry, 2008); * projects, ( groups rightward, ) leftward'));
|
|
538
|
+
lines.push('');
|
|
539
|
+
lines.push(' ' + chalk.dim(fh.ruleLabel + discoveredNote));
|
|
540
|
+
const maximaSet = new Set(fh.maxima);
|
|
541
|
+
const violSet = new Set(fh.violations);
|
|
542
|
+
const colW = fhSyls.map((s, i) => Math.max(s.text.length, ...fh.rows.map(r => (r[i] || '').length)) + 1);
|
|
543
|
+
const pad = (t, wd) => t + ' '.repeat(Math.max(0, wd - t.length));
|
|
544
|
+
lines.push(' ' + chalk.dim('Syl: ') + fhSyls.map((s, i) => {
|
|
545
|
+
const t = pad(s.text, colW[i]);
|
|
546
|
+
return violSet.has(i) ? chalk.red(t) : maximaSet.has(i) ? chalk.yellow(t) : t;
|
|
547
|
+
}).join(''));
|
|
548
|
+
fh.rows.forEach((row, g) => {
|
|
549
|
+
lines.push(' ' + chalk.dim(`G${g}: `) + row.map((m, i) => pad(m, colW[i])).join(''));
|
|
550
|
+
});
|
|
551
|
+
const verdict = fh.looseFeet != null
|
|
552
|
+
? `${fh.looseFeet} feet — in a loose meter the foot count IS the meter; maxima anchor the feet, unfooted syllables are free`
|
|
553
|
+
: fh.maxima.length === 0
|
|
554
|
+
? 'no maxima in this line — vacuously metrical'
|
|
555
|
+
: fh.violations.length === 0
|
|
556
|
+
? `${fh.maxima.length} maxim${fh.maxima.length === 1 ? 'um' : 'a'}, all project to gridline 1 → metrical`
|
|
557
|
+
: `${fh.violations.length} of ${fh.maxima.length} maxima fail to project → unmetrical under these rules`;
|
|
558
|
+
lines.push(' ' + (fh.metrical ? chalk.green(verdict) : chalk.red(verdict)));
|
|
559
|
+
lines.push('');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
460
562
|
// ── Layer 10: Reading projection (stress gradient over the input) ──
|
|
461
563
|
// A reading-view-style colourisation of the verbatim input, so the finalised
|
|
462
564
|
// analysis always shows "something that looks like the input". Falls back to
|
|
@@ -657,7 +759,7 @@ function renderLineGlossary(words) {
|
|
|
657
759
|
}
|
|
658
760
|
/** Surface form reduced to bare lowercase letters (drops apostrophes/hyphens). */
|
|
659
761
|
function normWordForm(s) {
|
|
660
|
-
return s.toLowerCase().replace(/[^a-z]/g, '');
|
|
762
|
+
return s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z]/g, '');
|
|
661
763
|
}
|
|
662
764
|
/** Colour each orthographic syllable of an original token by its relative stress. */
|
|
663
765
|
function colourToken(tokenText, word) {
|
|
@@ -702,7 +804,7 @@ function colourToken(tokenText, word) {
|
|
|
702
804
|
* the rest of the line. No original character is ever dropped.
|
|
703
805
|
*/
|
|
704
806
|
export function projectStressOntoLine(rawLine, words) {
|
|
705
|
-
const tokenRe = /[A-Za-z]+(?:['’\-][A-Za-z]+)*/g;
|
|
807
|
+
const tokenRe = /[A-Za-z\u00C0-\u024F]+(?:['’\-][A-Za-z\u00C0-\u024F]+)*/g; // accented Latin included (Milésien)
|
|
706
808
|
let out = '';
|
|
707
809
|
let cursor = 0;
|
|
708
810
|
let wi = 0;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface FHSyllable {
|
|
2
|
+
text: string;
|
|
3
|
+
lex: number;
|
|
4
|
+
poly: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface FabbHalleResult {
|
|
7
|
+
ruleLabel: string;
|
|
8
|
+
rows: string[][];
|
|
9
|
+
maxima: number[];
|
|
10
|
+
violations: number[];
|
|
11
|
+
metrical: boolean;
|
|
12
|
+
complete: boolean;
|
|
13
|
+
looseFeet?: number;
|
|
14
|
+
}
|
|
15
|
+
/** How maxima are defined. 'flanked' is the English strict-meter condition
|
|
16
|
+
* (F&H 2006 (16)): primary stress in a polysyllable, preceded AND followed
|
|
17
|
+
* in the line by syllables without lexical stress — line-edge syllables
|
|
18
|
+
* never qualify. 'polysyllabic' drops the flanking clause: any polysyllable
|
|
19
|
+
* primary stress is a maximum. That is the correct parameter for RUSSIAN
|
|
20
|
+
* strict meters, where the empirical law (Taranovsky/Jakobson; F&H treat
|
|
21
|
+
* Russian with the same parametric latitude their fn. 12 grants) is that a
|
|
22
|
+
* polysyllabic word's stress may fall only on an ictus, with no adjacency
|
|
23
|
+
* escape hatch — Russian classical verse simply does not license the
|
|
24
|
+
* clash-adjacent inversions English does. */
|
|
25
|
+
export type MaximaMode = 'flanked' | 'polysyllabic';
|
|
26
|
+
export interface FabbHalleOptions {
|
|
27
|
+
maximaMode?: MaximaMode;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the bracketed grid for one line. `meterName` selects the rule set —
|
|
31
|
+
* a single derivation VERIFIES a line against a stipulated rule set; meter
|
|
32
|
+
* DISCOVERY is a poem-level matter (see buildFabbHalleGridsForPoem below).
|
|
33
|
+
* `footCount`, when the caller supplies it, fixes the expected G0 count so
|
|
34
|
+
* that OVERLONG lines exercise
|
|
35
|
+
* F&H's optional NON-PROJECTION (their underlined syllables: synaloepha,
|
|
36
|
+
* "Heav'n"-type syncope, feminine endings — §1.1 example (2)). Only
|
|
37
|
+
* lexically stressless syllables may fail to project; the choice minimizes
|
|
38
|
+
* maximum-condition violations, ties resolving to the EARLIEST position
|
|
39
|
+
* (King James VI's observation that projection is likelier near the line's
|
|
40
|
+
* end). ACCENTUAL / dol'nik verdicts route to the LOOSE-meter rules
|
|
41
|
+
* instead. Returns null when the line's meter has no F&H rule set.
|
|
42
|
+
*/
|
|
43
|
+
export declare function buildFabbHalleGrid(syls: FHSyllable[], meterName: string, footCount?: number, options?: FabbHalleOptions): FabbHalleResult | null;
|
|
44
|
+
export interface FabbHallePoemResult {
|
|
45
|
+
schema: string;
|
|
46
|
+
grids: (FabbHalleResult | null)[];
|
|
47
|
+
cost: number;
|
|
48
|
+
runnerUp?: {
|
|
49
|
+
schema: string;
|
|
50
|
+
cost: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export declare function buildFabbHalleGridsForPoem(lines: FHSyllable[][], options?: FabbHalleOptions): FabbHallePoemResult | null;
|
|
54
|
+
//# sourceMappingURL=fabbhalle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fabbhalle.d.ts","sourceRoot":"","sources":["../src/fabbhalle.ts"],"names":[],"mappings":"AAkCA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0FD;;;;;;;;;8CAS8C;AAC9C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,cAAc,CAAC;AAEpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB;AA6FD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,gBAAgB,GACpF,eAAe,GAAG,IAAI,CAkGxB;AAoBD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,eAAe,GAAG,IAAI,CAAC,EAAE,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAC7C;AAiBD,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAChD,mBAAmB,GAAG,IAAI,CAiC5B"}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
// fabbhalle.ts — Fabb & Halle (2008, *Meter in Poetry*) Bracketed-Grid
|
|
2
|
+
// scansion: an INDEPENDENT second-opinion module beside Scandroid.
|
|
3
|
+
//
|
|
4
|
+
// F&H hold that the fundamental principle of metrical verse is COUNTING, not
|
|
5
|
+
// rhythm: each syllable projects an asterisk on gridline 0; iterative rules
|
|
6
|
+
// insert parentheses (a left paren groups rightward, a right paren leftward),
|
|
7
|
+
// each group projects its HEAD (edgemost asterisk) to the next gridline, and
|
|
8
|
+
// a well-formed grid terminates with EXACTLY ONE asterisk on the bottom
|
|
9
|
+
// gridline (their condition (11)). Rhythm falls out as a condition stated
|
|
10
|
+
// over the grid — for English strict meters, condition (7)/(8):
|
|
11
|
+
//
|
|
12
|
+
// A MAXIMUM — a syllable carrying primary stress in a polysyllabic word,
|
|
13
|
+
// preceded AND followed in the same line by syllables without lexical
|
|
14
|
+
// stress — must project to gridline 1.
|
|
15
|
+
//
|
|
16
|
+
// Monosyllables are unregulated (their placement is a *tendency*, which Fabb
|
|
17
|
+
// 2004 argues is implied form, explained pragmatically — not constitutive
|
|
18
|
+
// form, not part of the generative rules). This is deliberately a far more
|
|
19
|
+
// LINEAR and textocentric method than the Calliope pipeline (McAleese's
|
|
20
|
+
// critique); it is offered as an extra signal, never feeding the main
|
|
21
|
+
// scansion.
|
|
22
|
+
//
|
|
23
|
+
// Rule sets follow the worked examples: iambic pentameter (F&H 2006 (5)-(14):
|
|
24
|
+
// G0 R→L binary heads-right; G1 R→L ternary, final metron binary; all higher
|
|
25
|
+
// gridlines likewise R→L — rules (11) and (13) both begin "at the right
|
|
26
|
+
// edge", and the head is always the edgemost asterisk in the direction of
|
|
27
|
+
// iteration), trochaic tetrameter (Keats 'Fancy': all gridlines L→R binary,
|
|
28
|
+
// heads left), anapestic tetrameter (Byron 'Sennacherib': G0 R→L ternary
|
|
29
|
+
// heads-right, initial group may be incomplete). Amphibrachic lines fall
|
|
30
|
+
// out of the ANAPESTIC rules for free: R→L ternary grouping of an
|
|
31
|
+
// 11-syllable amphibrachic tetrameter yields groups |2|3|3|3| whose right
|
|
32
|
+
// heads sit at positions 2·5·8·11 — exactly the amphibrach's medial beats
|
|
33
|
+
// (F&H have no medial-headed groups; none are needed).
|
|
34
|
+
/** Group the asterisks at `cols` per `spec`; return groups in left-to-right
|
|
35
|
+
* order, each an array of member columns. The group at the far end of the
|
|
36
|
+
* iteration may be incomplete (F&H allow non-maximal edge groups). */
|
|
37
|
+
function group(cols, spec) {
|
|
38
|
+
const groups = [];
|
|
39
|
+
if (spec.dir === 'RL') {
|
|
40
|
+
for (let end = cols.length; end > 0; end -= spec.size) {
|
|
41
|
+
const start = Math.max(0, end - spec.size);
|
|
42
|
+
groups.unshift(cols.slice(start, end));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
for (let start = 0; start < cols.length; start += spec.size) {
|
|
47
|
+
groups.push(cols.slice(start, Math.min(cols.length, start + spec.size)));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return groups;
|
|
51
|
+
}
|
|
52
|
+
/** Head column of a group: rightmost for RL ')' groups, leftmost for LR '(' . */
|
|
53
|
+
function headOf(g, spec) {
|
|
54
|
+
return spec.dir === 'RL' ? g[g.length - 1] : g[0];
|
|
55
|
+
}
|
|
56
|
+
/** Pick the grouping rule for a HIGHER gridline holding `n` asterisks,
|
|
57
|
+
* following the paper's worked examples where they exist. Pentameter (F&H
|
|
58
|
+
* 2006 rule (11)/(12)): G1 ternary R→L with the final (last-constructed,
|
|
59
|
+
* leftmost) metron binary. Trimeter ("Up-Hill" (33), "Yes, to the very
|
|
60
|
+
* end"): G1 BINARY — `*) * *)`, two metra {1},{2,3} — NOT one ternary
|
|
61
|
+
* group. Hexameter (fn. 9): two full ternary metra. Tetrameter: binary.
|
|
62
|
+
* Direction: every gridline of a meter iterates the same way as its G0 —
|
|
63
|
+
* rules (11) and (13) both begin "at the right edge" for iambic, and the
|
|
64
|
+
* head is the edgemost asterisk in the direction of iteration ("the
|
|
65
|
+
* rightmost asterisk in each group projects"), so heads-right throughout
|
|
66
|
+
* R→L meters and, mirror-image, heads-left throughout L→R meters. */
|
|
67
|
+
function higherSpec(n, g0dir) {
|
|
68
|
+
const dir = g0dir;
|
|
69
|
+
if (n <= 4)
|
|
70
|
+
return { dir, size: 2 }; // incl. n=3 → {1},{2,3} per (33)
|
|
71
|
+
if (n % 3 === 0)
|
|
72
|
+
return { dir, size: 3 };
|
|
73
|
+
if (n % 2 === 0)
|
|
74
|
+
return { dir, size: 2 };
|
|
75
|
+
return { dir, size: 3 }; // odd ≥ 5 → ternary w/ final binary
|
|
76
|
+
}
|
|
77
|
+
/** G0 rule set from the main engine's meter family name. Returns null for
|
|
78
|
+
* meters outside F&H's English inventory (accentual, free…). */
|
|
79
|
+
function g0Spec(meterName) {
|
|
80
|
+
const m = meterName.toLowerCase();
|
|
81
|
+
if (m.includes('iamb'))
|
|
82
|
+
return { spec: { dir: 'RL', size: 2 }, label: 'iambic: G0 Right-to-Left binary, heads right' };
|
|
83
|
+
if (m.includes('troch'))
|
|
84
|
+
return { spec: { dir: 'LR', size: 2 }, label: 'trochaic: G0 Left-to-Right binary, heads left' };
|
|
85
|
+
if (m.includes('amph'))
|
|
86
|
+
return { spec: { dir: 'LR', size: 3 }, label: 'amphibrachic: G0 Left-to-Right ternary, heads left (initial ∆ unprojected)', dropFirst: true };
|
|
87
|
+
if (m.includes('anap'))
|
|
88
|
+
return { spec: { dir: 'RL', size: 3 }, label: 'anapestic: G0 Right-to-Left ternary, heads right' };
|
|
89
|
+
if (m.includes('dact'))
|
|
90
|
+
return { spec: { dir: 'LR', size: 3 }, label: 'dactylic: G0 Left-to-Right ternary, heads left' };
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/** Construct rows + G0 head set for a given projection (the columns that
|
|
94
|
+
* project to gridline 0), leaving unprojected columns blank on every row. */
|
|
95
|
+
function construct(syls, projected, g0) {
|
|
96
|
+
const rows = [];
|
|
97
|
+
let cols = projected.slice();
|
|
98
|
+
let g1cols = new Set();
|
|
99
|
+
let spec = g0.spec;
|
|
100
|
+
for (let g = 0; g < 4 && cols.length > 1; g++) {
|
|
101
|
+
if (g > 0)
|
|
102
|
+
spec = higherSpec(cols.length, g0.spec.dir);
|
|
103
|
+
const groups = group(cols, spec);
|
|
104
|
+
const row = syls.map(() => '');
|
|
105
|
+
for (const c of cols)
|
|
106
|
+
row[c] = '*';
|
|
107
|
+
for (const grp of groups) {
|
|
108
|
+
if (spec.dir === 'RL')
|
|
109
|
+
row[grp[grp.length - 1]] = '*)';
|
|
110
|
+
else
|
|
111
|
+
row[grp[0]] = '(*';
|
|
112
|
+
}
|
|
113
|
+
rows.push(row);
|
|
114
|
+
const heads = groups.map(grp => headOf(grp, spec));
|
|
115
|
+
if (g === 0)
|
|
116
|
+
g1cols = new Set(heads);
|
|
117
|
+
cols = heads;
|
|
118
|
+
}
|
|
119
|
+
const bottom = syls.map(() => '');
|
|
120
|
+
if (cols.length === 1)
|
|
121
|
+
bottom[cols[0]] = '*';
|
|
122
|
+
rows.push(bottom);
|
|
123
|
+
return { rows, g1cols, complete: cols.length === 1 };
|
|
124
|
+
}
|
|
125
|
+
/** Maxima per condition (8)/(16), or the flanking-free Russian variant. */
|
|
126
|
+
function findMaxima(syls, mode) {
|
|
127
|
+
const maxima = [];
|
|
128
|
+
if (mode === 'polysyllabic') {
|
|
129
|
+
for (let i = 0; i < syls.length; i++) {
|
|
130
|
+
if (syls[i].poly && syls[i].lex >= 2)
|
|
131
|
+
maxima.push(i);
|
|
132
|
+
}
|
|
133
|
+
return maxima;
|
|
134
|
+
}
|
|
135
|
+
for (let i = 1; i < syls.length - 1; i++) {
|
|
136
|
+
if (!syls[i].poly || syls[i].lex < 2)
|
|
137
|
+
continue;
|
|
138
|
+
if (syls[i - 1].lex === 0 && syls[i + 1].lex === 0)
|
|
139
|
+
maxima.push(i);
|
|
140
|
+
}
|
|
141
|
+
return maxima;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* LOOSE iambic meter (F&H 2006 §3, "Spring Quiet"): in loose meters maxima
|
|
145
|
+
* DETERMINE grid construction rather than being checked against it.
|
|
146
|
+
* (21) maximum = any syllable carrying primary stress in a polysyllable
|
|
147
|
+
* (no flanking condition);
|
|
148
|
+
* (22) insert a right parenthesis after each maximum's asterisk, FIRST;
|
|
149
|
+
* (23) then iterate R→L: skip one asterisk, insert a LEFT parenthesis to
|
|
150
|
+
* the left of the next — but an already-parenthesized asterisk
|
|
151
|
+
* refuses the insertion and the procedure reverts to the skip step
|
|
152
|
+
* (the (27) walkthrough: "Arching high over" → *) * (* *) *).
|
|
153
|
+
* Feet are asterisk runs preceded by "(" or followed by ")" (definition (6),
|
|
154
|
+
* Idsardi's one-boundary feet); syllables between feet stay UNFOOTED (the
|
|
155
|
+
* "-ching" of "Arching"). Line length = FEET, so 4–7-syllable lines all
|
|
156
|
+
* count as dimeter — the meter of folk verse, ballads, dol'nik. Heads
|
|
157
|
+
* project right. Condition (17) does not apply: metricality is the foot
|
|
158
|
+
* count itself.
|
|
159
|
+
*/
|
|
160
|
+
function buildLooseGrid(syls) {
|
|
161
|
+
const n = syls.length;
|
|
162
|
+
const rightP = new Array(n).fill(false);
|
|
163
|
+
const leftP = new Array(n).fill(false);
|
|
164
|
+
const maxima = [];
|
|
165
|
+
for (let i = 0; i < n; i++) {
|
|
166
|
+
if (syls[i].poly && syls[i].lex >= 2) {
|
|
167
|
+
maxima.push(i);
|
|
168
|
+
rightP[i] = true;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// Iterative footing (23): skip-first, R→L.
|
|
172
|
+
let p = n - 1;
|
|
173
|
+
let state = 'skip';
|
|
174
|
+
while (p >= 0) {
|
|
175
|
+
if (state === 'skip') {
|
|
176
|
+
p--;
|
|
177
|
+
state = 'insert';
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (rightP[p] || leftP[p]) {
|
|
181
|
+
state = 'skip';
|
|
182
|
+
continue;
|
|
183
|
+
} // insertion refused
|
|
184
|
+
leftP[p] = true;
|
|
185
|
+
p--;
|
|
186
|
+
state = 'skip';
|
|
187
|
+
}
|
|
188
|
+
// Feet: "(…" runs and bare "…)" singletons; unfooted syllables in neither.
|
|
189
|
+
const feet = [];
|
|
190
|
+
let open = null;
|
|
191
|
+
for (let i = 0; i < n; i++) {
|
|
192
|
+
if (leftP[i]) {
|
|
193
|
+
if (open)
|
|
194
|
+
feet.push(open);
|
|
195
|
+
open = [i];
|
|
196
|
+
}
|
|
197
|
+
else if (open) {
|
|
198
|
+
open.push(i);
|
|
199
|
+
}
|
|
200
|
+
if (rightP[i]) {
|
|
201
|
+
if (open) {
|
|
202
|
+
feet.push(open);
|
|
203
|
+
open = null;
|
|
204
|
+
}
|
|
205
|
+
else
|
|
206
|
+
feet.push([i]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (open)
|
|
210
|
+
feet.push(open);
|
|
211
|
+
// Display: G0 marks + G1 heads (rightmost per foot) — the loose grid stops
|
|
212
|
+
// at the foot count, which IS the meter (F&H display no deeper gridlines
|
|
213
|
+
// for Spring Quiet).
|
|
214
|
+
const rows = [];
|
|
215
|
+
rows.push(syls.map((_, i) => {
|
|
216
|
+
if (leftP[i] && rightP[i])
|
|
217
|
+
return '(*)';
|
|
218
|
+
if (leftP[i])
|
|
219
|
+
return '(*';
|
|
220
|
+
if (rightP[i])
|
|
221
|
+
return '*)';
|
|
222
|
+
return '*';
|
|
223
|
+
}));
|
|
224
|
+
const g1 = syls.map(() => '');
|
|
225
|
+
for (const f of feet)
|
|
226
|
+
g1[f[f.length - 1]] = '*';
|
|
227
|
+
rows.push(g1);
|
|
228
|
+
return {
|
|
229
|
+
ruleLabel: `loose iambic (maxima-anchored, F&H 2006 §3): pre-paren maxima, then Right-to-Left skip-first left parens — ${feet.length} feet`,
|
|
230
|
+
rows,
|
|
231
|
+
maxima,
|
|
232
|
+
violations: [],
|
|
233
|
+
metrical: true,
|
|
234
|
+
complete: true,
|
|
235
|
+
looseFeet: feet.length,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Build the bracketed grid for one line. `meterName` selects the rule set —
|
|
240
|
+
* a single derivation VERIFIES a line against a stipulated rule set; meter
|
|
241
|
+
* DISCOVERY is a poem-level matter (see buildFabbHalleGridsForPoem below).
|
|
242
|
+
* `footCount`, when the caller supplies it, fixes the expected G0 count so
|
|
243
|
+
* that OVERLONG lines exercise
|
|
244
|
+
* F&H's optional NON-PROJECTION (their underlined syllables: synaloepha,
|
|
245
|
+
* "Heav'n"-type syncope, feminine endings — §1.1 example (2)). Only
|
|
246
|
+
* lexically stressless syllables may fail to project; the choice minimizes
|
|
247
|
+
* maximum-condition violations, ties resolving to the EARLIEST position
|
|
248
|
+
* (King James VI's observation that projection is likelier near the line's
|
|
249
|
+
* end). ACCENTUAL / dol'nik verdicts route to the LOOSE-meter rules
|
|
250
|
+
* instead. Returns null when the line's meter has no F&H rule set.
|
|
251
|
+
*/
|
|
252
|
+
export function buildFabbHalleGrid(syls, meterName, footCount, options) {
|
|
253
|
+
if (syls.length < 2)
|
|
254
|
+
return null;
|
|
255
|
+
if (meterName === 'auto') {
|
|
256
|
+
// Fallback only — F&H's parse is relative to a STIPULATED meter, so
|
|
257
|
+
// callers that know the line's meter must pass it. Kept for lines whose
|
|
258
|
+
// verdict names no meter at all.
|
|
259
|
+
const schemas = ['iambic', 'trochaic', 'dactylic', 'amphibrachic', 'anapestic', 'loose'];
|
|
260
|
+
let bestRes = null;
|
|
261
|
+
let bestScore = -Infinity;
|
|
262
|
+
for (const schema of schemas) {
|
|
263
|
+
const res = buildFabbHalleGrid(syls, schema, footCount, options);
|
|
264
|
+
if (!res)
|
|
265
|
+
continue;
|
|
266
|
+
// Score: heavily penalize violations; loose meter gets a constant penalty so strict meters win if perfect.
|
|
267
|
+
let score = 0;
|
|
268
|
+
if (schema === 'loose') {
|
|
269
|
+
score = -5; // Loose meter base penalty
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
score -= res.violations.length * 10;
|
|
273
|
+
if (!res.metrical)
|
|
274
|
+
score -= 100;
|
|
275
|
+
}
|
|
276
|
+
if (score > bestScore) {
|
|
277
|
+
bestScore = score;
|
|
278
|
+
bestRes = res;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return bestRes;
|
|
282
|
+
}
|
|
283
|
+
if (/accentual|dolnik|dol'nik|loose/i.test(meterName))
|
|
284
|
+
return buildLooseGrid(syls);
|
|
285
|
+
const g0 = g0Spec(meterName);
|
|
286
|
+
if (!g0)
|
|
287
|
+
return null;
|
|
288
|
+
const maxima = findMaxima(syls, options?.maximaMode ?? 'flanked');
|
|
289
|
+
const all = syls.map((_, i) => i);
|
|
290
|
+
if (g0.dropFirst && all.length > 0) {
|
|
291
|
+
all.shift();
|
|
292
|
+
}
|
|
293
|
+
// Candidate projections:
|
|
294
|
+
// F&H allow dropping syllables that don't project to G0 (e.g. feminine endings, synaloepha).
|
|
295
|
+
// If footCount is provided, we use it to calculate exactly how many to drop.
|
|
296
|
+
// Otherwise, we guess by trying to drop up to 2 final stressless syllables.
|
|
297
|
+
let candidates = [all];
|
|
298
|
+
const expected = footCount && footCount > 0 ? footCount * g0.spec.size : 0;
|
|
299
|
+
if (expected > 0) {
|
|
300
|
+
const extra = all.length - expected;
|
|
301
|
+
if (extra === 1 || extra === 2) {
|
|
302
|
+
const droppable = all.filter(i => syls[i].lex === 0);
|
|
303
|
+
const sets = [];
|
|
304
|
+
if (extra === 1) {
|
|
305
|
+
for (const d of droppable)
|
|
306
|
+
sets.push(all.filter(i => i !== d));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
for (let a = 0; a < droppable.length; a++) {
|
|
310
|
+
for (let b = a + 1; b < droppable.length; b++) {
|
|
311
|
+
sets.push(all.filter(i => i !== droppable[a] && i !== droppable[b]));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// The FULL projection stays a candidate alongside the drop sets: a
|
|
316
|
+
// "long" line may equally be a CATALECTIC line of the next foot count
|
|
317
|
+
// (amphibrachic 11 = 4 feet with a short final group, not 3 feet + 2
|
|
318
|
+
// extrametricals), and F&H's incomplete-final-group allowance handles
|
|
319
|
+
// it without non-projection. Violation-minimisation picks per line.
|
|
320
|
+
candidates = [all, ...sets];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
// No expected count: try dropping 1 or 2 final stressless syllables
|
|
325
|
+
// (feminine/dactylic endings) alongside the full projection.
|
|
326
|
+
if (all.length > 0 && syls[all[all.length - 1]].lex === 0) {
|
|
327
|
+
const c2 = all.slice(0, -1);
|
|
328
|
+
candidates.push(c2);
|
|
329
|
+
if (c2.length > 0 && syls[c2[c2.length - 1]].lex === 0) {
|
|
330
|
+
candidates.push(all.slice(0, -2));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
let best = null;
|
|
335
|
+
let bestViol = Infinity;
|
|
336
|
+
for (const proj of candidates) {
|
|
337
|
+
const c = construct(syls, proj, g0);
|
|
338
|
+
const viol = maxima.filter(i => proj.includes(i) && !c.g1cols.has(i)).length
|
|
339
|
+
+ (c.complete ? 0 : 10);
|
|
340
|
+
if (viol < bestViol) {
|
|
341
|
+
bestViol = viol;
|
|
342
|
+
best = c;
|
|
343
|
+
}
|
|
344
|
+
if (bestViol === 0)
|
|
345
|
+
break; // earliest zero-violation wins
|
|
346
|
+
}
|
|
347
|
+
if (!best)
|
|
348
|
+
return null;
|
|
349
|
+
const violations = maxima.filter(i => !best.g1cols.has(i));
|
|
350
|
+
return {
|
|
351
|
+
ruleLabel: g0.label,
|
|
352
|
+
rows: best.rows,
|
|
353
|
+
maxima,
|
|
354
|
+
violations,
|
|
355
|
+
metrical: violations.length === 0 && best.complete,
|
|
356
|
+
complete: best.complete,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
const DISCOVERY_SCHEMAS = ['iambic', 'trochaic', 'anapestic', 'amphibrachic', 'dactylic'];
|
|
360
|
+
// The condition on maxima is INVIOLABLE in a strict meter ("MAXIMA must
|
|
361
|
+
// project to gridline 1") — so a violation costs far more than the loose
|
|
362
|
+
// reading's flat per-line rate: a poem whose lines keep violating under
|
|
363
|
+
// every strict setting (a dol'nik) tips to loose, while sporadic violations
|
|
364
|
+
// (tagger noise on a homograph, a genuinely transgressive line) don't flip
|
|
365
|
+
// an otherwise-clean strict poem.
|
|
366
|
+
const VIOLATION_COST = 2.0;
|
|
367
|
+
const INCOMPLETE_COST = 6.0;
|
|
368
|
+
const LOOSE_COST_PER_LINE = 0.9;
|
|
369
|
+
function schemaFootSize(schema) {
|
|
370
|
+
return schema === 'iambic' || schema === 'trochaic' ? 2 : 3;
|
|
371
|
+
}
|
|
372
|
+
export function buildFabbHalleGridsForPoem(lines, options) {
|
|
373
|
+
const usable = lines.filter(l => l.length >= 2);
|
|
374
|
+
if (usable.length === 0)
|
|
375
|
+
return null;
|
|
376
|
+
const ranked = [];
|
|
377
|
+
for (const schema of DISCOVERY_SCHEMAS) {
|
|
378
|
+
const size = schemaFootSize(schema);
|
|
379
|
+
let cost = 0;
|
|
380
|
+
for (const syls of usable) {
|
|
381
|
+
const fh = buildFabbHalleGrid(syls, schema, Math.floor(syls.length / size), options);
|
|
382
|
+
if (!fh) {
|
|
383
|
+
cost += 8;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
cost += fh.violations.length * VIOLATION_COST + (fh.complete ? 0 : INCOMPLETE_COST);
|
|
387
|
+
}
|
|
388
|
+
ranked.push({ schema, cost });
|
|
389
|
+
}
|
|
390
|
+
ranked.push({ schema: 'loose', cost: usable.length * LOOSE_COST_PER_LINE });
|
|
391
|
+
// Stable sort: ties resolve to the DISCOVERY_SCHEMAS order (a frequency
|
|
392
|
+
// prior over the tradition's meters — iambic first), loose last.
|
|
393
|
+
ranked.sort((a, b) => a.cost - b.cost);
|
|
394
|
+
const winner = ranked[0];
|
|
395
|
+
const size = winner.schema === 'loose' ? 0 : schemaFootSize(winner.schema);
|
|
396
|
+
const grids = lines.map(syls => syls.length < 2 ? null :
|
|
397
|
+
buildFabbHalleGrid(syls, winner.schema, size > 0 ? Math.floor(syls.length / size) : undefined, options));
|
|
398
|
+
return {
|
|
399
|
+
schema: winner.schema,
|
|
400
|
+
grids,
|
|
401
|
+
cost: winner.cost,
|
|
402
|
+
runnerUp: ranked[1] ? { schema: ranked[1].schema, cost: ranked[1].cost } : undefined,
|
|
403
|
+
};
|
|
404
|
+
}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsB5C,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAKV,UAAU,EAMX,MAAM,YAAY,CAAC;AAyLpB,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,EAAE,MAAM,GAAE,aAA4B,GAAG,UAAU,EAAE,EAAE,CAuDtH;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,UAAO,EAAE,MAAM,GAAE,aAA4B,GAAG,UAAU,EAAE,CAGjH;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,GAAE,aAA4B,GAAG,aAAa,EAAE,CAkC1G;AAqDD,eAAO,MAAM,UAAU,MAAM,CAAC;AAE9B,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC1C;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,GAClB;IAAE,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAEpB,MAAM,WAAW,OAAO;IAAG,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;CAAE;AAC7E,MAAM,MAAM,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExD,wBAAgB,UAAU,IAAI,OAAO,CAAoD;AAMzF;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,GAAG,QAAQ,CAoCrE"}
|