@sahabaplus/mushaf-engine
Version:
TypeScript implementation of a Quran Mushaf navigation engine
380 lines (379 loc) • 16.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseMushafEngine = void 0;
const mushaf_1 = require("../mushaf");
const navigation_1 = require("../navigation");
/**
* Basic implementation of the Mushaf navigation engine
*/
class BaseMushafEngine {
/**
* Create a new BaseMushafEngine with the given Mushaf
*
* @param mushaf - The Mushaf to navigate through
*/
constructor(mushaf) {
this.mushaf = mushaf;
this.quranMetadata = mushaf_1.QuranMetadata.fromMushaf(mushaf);
}
/**
* Create a navigator with the given settings and direction
*/
createNavigator(settings, direction) {
return new navigation_1.VersesNavigator(this.mushaf, this.quranMetadata, settings, direction);
}
/**
* Get metadata about a specific Sura
*
* @param suraNumber - Sura number (1-114)
* @returns Information about the specified Sura, or undefined if not found
*/
getSuraInfo(suraNumber) {
return this.quranMetadata.getSuraInfo(suraNumber);
}
/**
* Find the next verse from a given verse in the specified direction
*
* @param from - Verse position to start from
* @param direction - Direction to navigate
* @param settings - Navigation settings
* @returns The next verse or undefined if at the end
*/
nextVerse(from, direction, settings) {
const navigator = this.createNavigator(settings, direction);
navigator.resetPosition(from);
return navigator.nextVerse();
}
/**
* Find the previous verse from a given verse in the specified direction
*
* @param from - Verse position to start from
* @param direction - Direction to navigate
* @param settings - Navigation settings
* @returns The previous verse or undefined if at the start
*/
previousVerse(from, direction, settings) {
const navigator = this.createNavigator(settings, direction);
navigator.resetPosition(from);
return navigator.previousVerse();
}
/**
* Check if a verse position is out of the current navigation bounds
*/
isOutOfBounds(verse, direction, settings) {
const navigator = this.createNavigator(settings, direction);
return navigator.isOutOfBounds(verse);
}
/**
* Get the effective start of the navigation range for the given direction and settings
*/
getStartBound(direction, settings) {
const navigator = this.createNavigator(settings, direction);
return navigator.getStartBound();
}
/**
* Get the effective end of the navigation range for the given direction and settings
*/
getEndBound(direction, settings) {
const navigator = this.createNavigator(settings, direction);
return navigator.getEndBound();
}
/**
* Check if navigation direction is wrong for given start and end positions.
*
* This is a thin wrapper around `VersesNavigator.isWrongDirection` so that
* consumers of the engine can reuse the same ordering rules.
*/
isWrongDirection(start, end, direction) {
return navigation_1.VersesNavigator.isWrongDirection(start, end, direction);
}
/**
* Find a verse in the mushaf and return its location (Verse, page, index_of_verse)
*
* @param verse - Verse position to find
* @returns The verse and its location, or undefined if not found
*/
findVerseLocation(verse) {
const sura = this.quranMetadata.getSuraInfo(verse.getSura());
if (sura) {
const pages = this.mushaf.pages;
for (let i = sura.startPage; i <= sura.endPage; i++) {
const page = pages[i - 1];
for (let j = 0; j < page.verses().length; j++) {
const verseObj = page.verses()[j];
if (verseObj.sura === verse.getSura() &&
verseObj.number === verse.getVerse()) {
return {
verse: verseObj,
pageNumber: page.number,
verseIndexOnPage: j,
};
}
}
}
}
return undefined;
}
/**
* Resolve a (sura, verse) position to the full Verse object in the mushaf
*/
findVerse(position) {
const result = this.findVerseLocation(position);
return result?.verse;
}
/**
* Resolve a (sura, verse) pair directly to a Verse object.
*/
resolvePosition(sura, verse) {
return this.findVerse(new navigation_1.VersePosition(sura, verse));
}
/**
* Calculate the direct distance in lines between start and end verses
*
* This calculates the simple A→B distance without accounting for cycles.
* When bounded navigation produces cycles, callers can use the `cycleDistance`
* field from `NavigationResult` to compute total distance:
*
* ```text
* total_distance ≈ (cycles_completed - 1) * cycle_distance + direct_distance
* ```
*
* where `direct_distance` is obtained from this method.
*
* @param start - Start verse position
* @param end - End verse position
* @param direction - Direction of navigation
* @param settings - Navigation settings
* @returns Direct distance in lines between the two verses
* @throws Error if the verses are unreachable or not found
*/
calculateLines(start, end, direction, settings) {
const isWrongDirection = navigation_1.VersesNavigator.isWrongDirection(start, end, direction);
const navigator = this.createNavigator(settings, direction);
const outOfBounds = navigator.isOutOfBounds(start);
if (isWrongDirection && outOfBounds) {
throw new Error("Invalid direction for verse ordering");
}
const startVerseResult = this.findVerseLocation(start);
const endVerseResult = this.findVerseLocation(end);
if (!startVerseResult || !endVerseResult) {
throw new Error("Verse not found");
}
const startVerseObj = startVerseResult.verse;
const endVerseObj = endVerseResult.verse;
const resetResult = navigator.resetPosition(start);
if (!resetResult) {
throw new Error("Verse not found or out of bounds");
}
// Calculate direct distance from start to end
let directLines = 0.0;
let maxIterations = 10000; // Prevent infinite loops
let iterations = 0;
while (true) {
const verseLines = navigator.calculateVerseLines(navigator.currentVerse());
directLines += verseLines;
if (navigator.currentVerse().sura === endVerseObj.sura &&
navigator.currentVerse().number === endVerseObj.number) {
break;
}
// Check if nextVerse() returns undefined to prevent infinite loop
const nextVerse = navigator.nextVerse();
if (!nextVerse) {
// If we couldn't reach the end verse and we're not cycling, return an error
// But if start is within bounds, allow cycling to try to reach the end
if (outOfBounds || iterations >= maxIterations) {
throw new Error("Invalid direction for verse ordering");
}
// Reset to start bound to allow cycling
const startBound = navigator.getStartBound();
navigator.resetPosition(startBound);
iterations++;
continue;
}
iterations++;
if (iterations >= maxIterations) {
throw new Error("Invalid direction for verse ordering");
}
}
// Round lines to 2 decimal places
const lines = Math.round(directLines * 100) / 100;
return lines;
}
/**
* Calculate the lines taken by a verse including any sura headers, using the
* same rules as the internal navigator.
*/
calculateVerseLines(verse, settings) {
const navigator = this.createNavigator(settings, navigation_1.Direction.Downwards);
return navigator.calculateVerseLines(verse);
}
/**
* Determine which last of sura result to prefer
*
* @param lastOfSura - Existing last of sura result, if any
* @param currentVerse - Current verse being processed
* @param direction - Direction of navigation
* @returns The preferred last of sura result
*/
preferLastOfSura(lastOfSura, currentVerse, direction) {
if (lastOfSura && currentVerse.sura === lastOfSura.lastVerse.sura) {
return lastOfSura;
}
const sura = this.quranMetadata.getSuraInfo(currentVerse.sura);
if (!sura) {
throw new Error(`Sura info not found for sura ${currentVerse.sura}`);
}
const lastVerseResult = this.findVerseLocation(new navigation_1.VersePosition(sura.number, sura.totalVerses));
if (!lastVerseResult) {
throw new Error(`Last verse not found for sura ${sura.number}`);
}
const lastVerse = lastVerseResult.verse;
const linesDistance = this.calculateLines(navigation_1.VersePosition.fromVerse(currentVerse), navigation_1.VersePosition.fromVerse(lastVerse), direction, navigation_1.NavigationSettings.builder().build()) - currentVerse.lines;
if (lastOfSura &&
Math.abs(lastOfSura.linesDistance) + 3.0 < Math.abs(linesDistance)) {
// Added 3 to prefer the new last
return lastOfSura;
}
else {
return new navigation_1.LastVerseResult(linesDistance, lastVerse);
}
}
/**
* Determine which last of page result to prefer
*
* @param lastOfPage - Existing last of page result, if any
* @param currentVerse - Current verse being processed
* @param direction - Direction of navigation
* @returns The preferred last of page result
*/
preferLastOfPage(lastOfPage, currentVerse, direction) {
if (lastOfPage && currentVerse.equals(lastOfPage.lastVerse)) {
return lastOfPage;
}
const verseInfo = this.findVerseLocation(navigation_1.VersePosition.fromVerse(currentVerse));
if (!verseInfo) {
throw new Error(`Verse info not found for sura ${currentVerse.sura}, verse ${currentVerse.number}`);
}
const page = this.mushaf.getPage(verseInfo.pageNumber);
if (!page) {
throw new Error(`Page not found for page number ${verseInfo.pageNumber}`);
}
const newLastOfPage = page.verses()[page.verses().length - 1];
if (navigation_1.VersesNavigator.isWrongDirection(navigation_1.VersePosition.fromVerse(currentVerse), navigation_1.VersePosition.fromVerse(newLastOfPage), direction)) {
return lastOfPage;
}
const linesDistance = this.calculateLines(navigation_1.VersePosition.fromVerse(currentVerse), navigation_1.VersePosition.fromVerse(newLastOfPage), direction, navigation_1.NavigationSettings.builder().build());
if (lastOfPage &&
Math.abs(lastOfPage.linesDistance) + 1.0 < Math.abs(linesDistance)) {
// Added 1 to prefer the new last
return lastOfPage;
}
else {
return new navigation_1.LastVerseResult(linesDistance, newLastOfPage);
}
}
/**
* Navigate from a verse by a specified number of lines in the given direction
*
* @param params - Navigation parameters
* @returns Navigation result containing the verse at the destination after navigation
*/
navigate(params) {
const { lines, from, direction, settings } = params;
// Validate inputs
if (lines < 0) {
throw new Error("Lines should be positive or zero");
}
// If no lines to navigate, find and return the current verse
if (lines === 0) {
const verseResult = this.findVerseLocation(from);
if (verseResult) {
return navigation_1.NavigationResult.newNormal(verseResult.verse, 0);
}
throw new Error("Verse Not Found!");
}
// Find starting position
const verseResult = this.findVerseLocation(from);
if (!verseResult) {
throw new Error("Verse Not Found!");
}
let remainingLines = lines;
const navigator = this.createNavigator(settings, direction);
const resetResult = navigator.resetPosition(from);
if (!resetResult) {
throw new Error("Verse Not Found or Out of Bounds!");
}
let overflow;
let previousVerse = navigator.currentVerse();
let lastOfPage;
let lastOfSura;
// Track cycles (passes through initial verse) and boundary crossings
const initialVerse = navigator.currentVerse();
let cyclesCompleted = 0;
let crossedBoundaries = false;
let hasMoved = false;
// Track cycle distance (lines accumulated in the current cycle)
let linesInCurrentCycle = 0.0;
let cycleDistance = 0.0; // Set when first cycle completes
while (true) {
const currentVerse = navigator.currentVerse();
// We looped twice and stuck at the same verse
if (previousVerse.sura === currentVerse.sura &&
previousVerse.number === currentVerse.number &&
Math.abs(lines - remainingLines) > Number.EPSILON) {
break;
}
// Track cycles: count passes through the initial verse (after at least one move)
// Note: When a cycle is detected, we continue the loop and count this verse's lines
// This ensures consistency with calculateLines cycle distance calculation
if (hasMoved &&
currentVerse.sura === initialVerse.sura &&
currentVerse.number === initialVerse.number) {
cyclesCompleted += 1;
if (cycleDistance === 0.0) {
// Capture cycle distance on first cycle completion
cycleDistance = linesInCurrentCycle;
}
linesInCurrentCycle = 0.0; // Reset for next cycle
}
const currentSuraInfo = this.quranMetadata.getSuraInfo(currentVerse.sura);
if (!currentSuraInfo)
throw new Error(`Sura info not found for sura ${currentVerse.sura}`);
const verseLines = navigator.calculateVerseLines(currentVerse);
// Accumulate lines for cycle distance tracking
linesInCurrentCycle += verseLines;
const diff = Math.round((remainingLines - verseLines) * 100) / 100;
if (currentVerse.isLastOfPage()) {
lastOfPage = new navigation_1.LastVerseResult(-1.0 * diff, currentVerse);
}
if (currentSuraInfo.totalVerses === currentVerse.number) {
lastOfSura = new navigation_1.LastVerseResult(-1.0 * diff, currentVerse);
}
if (diff < 0.0) {
overflow = new navigation_1.OverflowResult(-1.0 * diff, currentVerse);
break;
}
remainingLines = diff;
previousVerse = currentVerse;
if (remainingLines > Number.EPSILON) {
// Check return value - break if boundary reached
const nextVerse = navigator.nextVerse();
if (!nextVerse) {
break;
}
if (navigator.crossedBoundaries) {
crossedBoundaries = true;
}
hasMoved = true;
}
else {
break;
}
}
const finalLastOfSura = this.preferLastOfSura(lastOfSura, previousVerse, direction);
const finalLastOfPage = this.preferLastOfPage(lastOfPage, previousVerse, direction);
const cycleInfo = new navigation_1.CycleInfo(cyclesCompleted, crossedBoundaries, cycleDistance);
return new navigation_1.NavigationResult(previousVerse, overflow, finalLastOfPage, finalLastOfSura, lines - remainingLines, cycleInfo);
}
}
exports.BaseMushafEngine = BaseMushafEngine;