Spaces:
Running
Running
| // Faithful port of scanpositions.py's Positioner: the char-grid bookkeeping | |
| // that underlies both the classic spaced-out "x / | x" display string AND the | |
| // FeetAtPunctBounds check that _measureComplexity relies on. Deliberately | |
| // omits P.promCands/P.wordbounds -- scanpositions.py's own header comment | |
| // flags that bookkeeping as "SO-FAR UNUSED... requiring a few CPU cycles and | |
| // not contributing anything," i.e. dead in the original too. | |
| import { STRESS, SLACK, PROMOTED, FOOTDIV, SYLMARK, footDict } from './constants.js'; | |
| import { invertFootDict } from './utilities.js'; | |
| const SCANMARKS = 'x/%'; | |
| const footDictInverse = invertFootDict(footDict); | |
| export class Positioner { | |
| charlist: string[] = []; | |
| sylmids: number[] = []; | |
| footplace: number[] = [0]; | |
| possLexicals: string[] = []; | |
| punctAt: number[] = []; | |
| private scanMarkMoved = false; | |
| NewLine(linelength: number): void { | |
| this.charlist = new Array(linelength + 1).fill(' '); | |
| this.sylmids = []; | |
| this.footplace = [0]; | |
| this.possLexicals = []; | |
| this.punctAt = []; | |
| this.scanMarkMoved = false; | |
| } | |
| /** Mutates `syls` in place (strips a monosyllable's trailing '*'), exactly | |
| * as AddWord does; returns the advanced line-character index. */ | |
| AddWord(syls: string[], linePosIn: number): number { | |
| let linePos = linePosIn; | |
| let ambiguous = true; | |
| if (syls.length === 1) { | |
| if (!syls[0].endsWith('*')) ambiguous = false; | |
| else syls[0] = syls[0].slice(0, -1); | |
| } else { | |
| for (const s of syls) { | |
| if (isUpperSyl(s)) { ambiguous = false; break; } | |
| } | |
| } | |
| if (ambiguous) { | |
| if (this.possLexicals.length === 0) this.possLexicals.push(this.GetMarks()); | |
| const halfway = this.possLexicals.length; | |
| this.possLexicals = this.possLexicals.concat(this.possLexicals); | |
| for (let pL = 0; pL < halfway; pL++) { | |
| this.possLexicals[pL] += syls.length === 1 ? STRESS : STRESS + SLACK; | |
| } | |
| for (let pL = halfway; pL < this.possLexicals.length; pL++) { | |
| this.possLexicals[pL] += syls.length === 1 ? SLACK : SLACK + STRESS; | |
| } | |
| } | |
| for (const s of syls) { | |
| this.sylmids.push(linePos + Math.floor(s.length / 2)); | |
| const newmark = isUpperSyl(s) ? STRESS : SLACK; | |
| this.AddScanMark(newmark, this.sylmids.length - 1); | |
| if (!ambiguous) { | |
| for (let pL = 0; pL < this.possLexicals.length; pL++) this.possLexicals[pL] += newmark; | |
| } | |
| linePos += s.length; | |
| } | |
| return linePos; | |
| } | |
| LocateFootDivPositions(): void { | |
| for (let syl = 0; syl < this.sylmids.length - 1; syl++) { | |
| this.footplace.push(this.sylmids[syl] + Math.floor((this.sylmids[syl + 1] - this.sylmids[syl]) / 2)); | |
| } | |
| this.footplace.push(this.charlist.length - 1); | |
| } | |
| AddPunct(str: string, linePosIn: number): number { | |
| let linePos = linePosIn; | |
| for (const c of str) { | |
| if (!/\s/.test(c)) { | |
| this.charlist[linePos] = c; | |
| this.punctAt.push(linePos); | |
| } | |
| linePos += 1; | |
| } | |
| return linePos; | |
| } | |
| AddScanMark(mark: string, syllable: number): void { | |
| if (syllable > this.sylmids.length) return; | |
| this.charlist[this.sylmids[syllable]] = mark; | |
| } | |
| AddFootDivMark(syllable: number): void { | |
| if (syllable > this.sylmids.length) return; | |
| const pos = syllable === this.sylmids.length ? this.charlist.length - 1 : this.footplace[syllable]; | |
| const empty = this.findEmptyPosForMark(pos); | |
| this.charlist[empty] = FOOTDIV; | |
| } | |
| EraseFootDivMark(syllable: number): void { | |
| this.charlist[this.footplace[syllable]] = ' '; | |
| } | |
| private findEmptyPosForMark(posIn: number): number { | |
| let pos = posIn; | |
| if (!SCANMARKS.includes(this.charlist[pos])) return pos; | |
| if (pos === 0 && SCANMARKS.includes(this.charlist[0])) { | |
| this.charlist[1] = this.charlist[0]; | |
| this.scanMarkMoved = true; | |
| return pos; | |
| } | |
| if (pos < this.charlist.length - 1 && !SCANMARKS.includes(this.charlist[pos + 1])) return pos + 1; | |
| let blank = pos; | |
| while (blank > 0 && SCANMARKS.includes(this.charlist[blank])) blank -= 1; | |
| for (let s = blank; s < pos; s++) this.charlist[s] = this.charlist[s + 1]; | |
| return pos; | |
| } | |
| GetAmbiguities(): string[] { | |
| return this.possLexicals.length > 0 ? this.possLexicals : [this.GetMarks()]; | |
| } | |
| GetMarks(includeFeet = false): string { | |
| return this.GetScanString(includeFeet).replace(/\s+/g, ''); | |
| } | |
| GetScanString(feet = true, punct = false, sylsOnly = false): string { | |
| let s = this.charlist.join(''); | |
| if (!feet || sylsOnly) s = s.replace(/\|/g, ' '); | |
| if (!punct || sylsOnly) s = s.replace(/[-.,;:?!()"']/g, ' '); | |
| if (sylsOnly) s = s.replace(/[^ ]/g, SYLMARK); | |
| return s; | |
| } | |
| /** Correct charlist's x/% marks to match a chosen ambiguity-resolution string. */ | |
| AdjustMarks(scansion: string): void { | |
| let i = 0; | |
| for (let c = 0; c < this.charlist.length; c++) { | |
| if (this.charlist[c] === 'x' || this.charlist[c] === '/') { | |
| this.charlist[c] = scansion[i]; | |
| i += 1; | |
| } | |
| if (i >= scansion.length) break; | |
| } | |
| } | |
| RemoveEndFootMarks(): void { | |
| this.removeTailFootMark(); | |
| this.removeHeadFootMark(); | |
| } | |
| private removeTailFootMark(): void { | |
| const joined = this.charlist.join(''); | |
| const lastfootdiv = joined.lastIndexOf(FOOTDIV); | |
| if (lastfootdiv === -1) return; | |
| let islastmark = true; | |
| for (let i = lastfootdiv; i < this.charlist.length; i++) { | |
| if (SCANMARKS.includes(this.charlist[i])) { islastmark = false; break; } | |
| } | |
| if (islastmark) this.charlist[lastfootdiv] = ' '; | |
| } | |
| private removeHeadFootMark(): void { | |
| if (this.charlist[0] === FOOTDIV) { | |
| if (this.scanMarkMoved) { | |
| this.charlist[0] = this.charlist[1]; | |
| this.charlist[1] = ' '; | |
| this.scanMarkMoved = false; | |
| } else { | |
| this.charlist[0] = ' '; | |
| } | |
| } | |
| } | |
| /** Per completed foot, was its END at (or immediately after) a punctuation | |
| * mark, with no intervening scan mark on the way back? Always keyed by the | |
| * IAMBIC footDict (as in the original -- this check is never invoked for | |
| * the anapestic path). */ | |
| FeetAtPunctBounds(footlist: string[]): boolean[] { | |
| const retlist: boolean[] = [true]; | |
| let i = 0; | |
| for (const f of footlist) { | |
| const pattern = footDictInverse[f]; | |
| i += pattern.length; | |
| if (i >= this.footplace.length) return retlist; | |
| let ip = this.footplace[i]; | |
| while (ip) { | |
| if (this.punctAt.includes(ip)) { retlist.push(true); break; } | |
| else if (SCANMARKS.includes(this.charlist[ip])) { retlist.push(false); break; } | |
| ip -= 1; | |
| } | |
| } | |
| return retlist; | |
| } | |
| } | |
| function isUpperSyl(s: string): boolean { | |
| return /[A-Z]/.test(s) && s === s.toUpperCase(); | |
| } | |