Spaces:
Running
Running
File size: 7,901 Bytes
5ea8612 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | // Top-level orchestration for the independent Scandroid engine. Public API:
// `scanTextNatively(rawLines)` takes ONLY the raw verse-line strings of a
// poem (or a single line) and returns one rich, self-contained result per
// line -- Hartman's own verdict, Hartman's own stress map, in Hartman's own
// symbols and foot names. Nothing here reads anything Calliope has already
// worked out about the same lines.
import { footDict, AnapSubs, lineLengthName, FOOTDIV } from './constants.js';
import { invertFootDict } from './utilities.js';
import { ScansionMachine } from './machine.js';
import { deduceParametersForPoem, deduceParametersForSingleLine, type ScandroidParams, type Metron } from './deduceParameters.js';
const footDictInverse = invertFootDict(footDict);
const anapSubsInverse = invertFootDict(AnapSubs);
export interface ScandroidNativeFootResult {
/** 'Corral the Weird' | 'Maximize the Normal' | 'Anapestic' */
algorithm: string;
footlist: string[];
/** Feet joined by FOOTDIV ('|'), each foot's own STRESS/SLACK/PROMOTED marks. */
scanString: string;
/** The same marks with no foot divisions -- one character per syllable. */
marksString: string;
substitutions: number;
/** false = FAIL: wrong foot count against a fixed line length, a pyrrhic
* before a trochee, an amphibrach inside an anapestic line, etc. */
ok: boolean;
failReason?: string;
}
export interface ScandroidNativeLineResult {
metron: Metron;
metronName: string; // 'IAMBIC' | 'ANAPESTIC', Hartman's own status-bar wording
lineFeet: number;
lineFeetSet: boolean;
lineLengthName: string; // e.g. 'PENTAMETER', or 'VARIABLE' when lineFeetSet is false
/** The ChooseAlgorithm / GetBestAnapLexes winner -- Scandroid's own verdict. */
verdict: ScandroidNativeFootResult | null;
/** Iambic lines only: each algorithm's own independent best, for side-by-side display. */
corralTheWeird?: ScandroidNativeFootResult | null;
maximizeTheNormal?: ScandroidNativeFootResult | null;
/** 0-based syllable indices where a pyrrhic/slack-run was promoted to a beat. */
promotions: number[];
}
// Display-only helper (not a port of anything in the 2005 source): walks the
// footlist in pattern-length chunks to insert FOOTDIV between feet. On a
// length-mismatch FAIL the footlist can run past the syllables actually
// available (CleanUpRE/TestLengthAndDice keep assembling feet from whatever
// state a wrong-length line leaves them in) -- stop once marks are exhausted
// rather than emit a trail of empty '||||' segments.
function renderFootDividedString(marks: string, footlist: string[], patternDict: Record<string, string>): string {
const parts: string[] = [];
let pos = 0;
for (const foot of footlist) {
if (pos >= marks.length) break;
const len = patternDict[foot]?.length ?? 0;
parts.push(marks.slice(pos, pos + len));
pos += len;
}
return parts.join(FOOTDIV);
}
function buildFootResult(
algorithm: string,
footlist: string[],
marks: string,
substitutions: number,
ok: boolean,
patternDict: Record<string, string>,
failReason?: string
): ScandroidNativeFootResult {
return {
algorithm,
footlist: footlist.slice(),
scanString: renderFootDividedString(marks, footlist, patternDict),
marksString: marks,
substitutions,
ok,
failReason,
};
}
function runIndividualIambic(
line: string,
params: ScandroidParams,
algorithm: 1 | 2,
candidate: { marks: string; complexity: number },
algLabel: string
): ScandroidNativeFootResult {
const m = new ScansionMachine();
m.SetLineFeet(params.lineFeet, params.lineFeetSet);
m.ParseLine(line);
// Mirror DoAlgorithm's own auto-length side effect (this candidate never ran
// DoAlgorithm on THIS fresh machine, since we're going straight to the
// step-by-step path) -- every candidate shares the same total syllable
// count, so this is exactly what DoAlgorithm would have set LD.lfeet to.
if (!params.lineFeetSet) m.LD.lfeet = Math.floor(candidate.marks.length / 2);
const stepOk = m.runIambicStepByStep(algorithm, candidate.marks);
const promote = m.PromotePyrrhics();
const howwedoing = m.HowWeDoing();
return buildFootResult(algLabel, m.LD.footlist, m.P.GetMarks(), howwedoing.substitutions, stepOk && promote.ok && howwedoing.ok, footDictInverse);
}
function lengthName(feet: number, feetSet: boolean): string {
return feetSet ? lineLengthName[feet] ?? '' : 'VARIABLE';
}
/** Scan one line under already-deduced poem-level (or single-line) parameters. */
export function scanLineNatively(line: string, params: ScandroidParams): ScandroidNativeLineResult {
const metronName = params.metron === 'iambic' ? 'IAMBIC' : 'ANAPESTIC';
if (params.metron === 'iambic') {
const machine = new ScansionMachine();
machine.SetLineFeet(params.lineFeet, params.lineFeetSet);
machine.ParseLine(line);
const chosen = machine.ChooseAlgorithm();
if (!chosen) {
return {
metron: 'iambic', metronName, lineFeet: params.lineFeet, lineFeetSet: params.lineFeetSet,
lineLengthName: lengthName(params.lineFeet, params.lineFeetSet),
verdict: null, corralTheWeird: null, maximizeTheNormal: null, promotions: [],
};
}
const promote = machine.PromotePyrrhics();
const howwedoing = machine.HowWeDoing();
const verdict = buildFootResult(
chosen.verdictAlgorithm === 1 ? 'Corral the Weird' : 'Maximize the Normal',
machine.LD.footlist,
machine.P.GetMarks(),
howwedoing.substitutions,
chosen.verdictOk && promote.ok && howwedoing.ok,
footDictInverse
);
const corral = chosen.alg1Best ? runIndividualIambic(line, params, 1, chosen.alg1Best, 'Corral the Weird') : null;
const maximize = chosen.alg2Best ? runIndividualIambic(line, params, 2, chosen.alg2Best, 'Maximize the Normal') : null;
return {
metron: 'iambic', metronName, lineFeet: machine.LD.lfeet, lineFeetSet: params.lineFeetSet,
lineLengthName: lengthName(machine.LD.lfeet, params.lineFeetSet),
verdict, corralTheWeird: corral, maximizeTheNormal: maximize, promotions: promote.promotions,
};
}
// Anapestic
const machine = new ScansionMachine();
machine.SetLineFeet(params.lineFeet, params.lineFeetSet);
machine.ParseLine(line);
const best = machine.GetBestAnapLexes();
if (!best) {
return {
metron: 'anapestic', metronName, lineFeet: params.lineFeet, lineFeetSet: params.lineFeetSet,
lineLengthName: lengthName(params.lineFeet, params.lineFeetSet),
verdict: null, promotions: [],
};
}
const cleanup = machine.AnapCleanUpAndReport();
const verdict = buildFootResult(
'Anapestic', machine.LD.footlist, machine.P.GetMarks(), cleanup.substitutions,
cleanup.ok, anapSubsInverse, cleanup.fail
);
return {
metron: 'anapestic', metronName, lineFeet: machine.LD.lfeet, lineFeetSet: params.lineFeetSet,
lineLengthName: lengthName(machine.LD.lfeet, params.lineFeetSet),
verdict, promotions: [],
};
}
/** Scan a whole poem (or a single line): deduces Metron/line-length exactly
* once over the full raw-line list -- matching Scandroid's own "loaded
* document" behaviour for 2+ lines, and its "typed single line" behaviour
* (variable length, per-line Metron) for exactly one line -- then scans
* every line under those shared parameters. `rawLines` should be every
* non-blank line of the poem in reading order, spanning stanza breaks: the
* original deduces Metron/length from up to a dozen lines of the whole
* loaded document, not per stanza. */
export function scanTextNatively(rawLines: string[]): ScandroidNativeLineResult[] {
if (rawLines.length === 0) return [];
const params = rawLines.length < 2 ? deduceParametersForSingleLine(rawLines[0]) : deduceParametersForPoem(rawLines);
return rawLines.map(line => scanLineNatively(line, params));
}
|