calliope / src /scandroidNative /positioner.ts
AlekseyCalvin's picture
Upload 9 files
1c65d29 verified
Raw
History Blame Contribute Delete
6.75 kB
// 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();
}