mirror of
https://gitlab.com/walljm/dynamicbible.git
synced 2025-07-25 08:19:50 -04:00
1409 lines
35 KiB
TypeScript
1409 lines
35 KiB
TypeScript
import { HttpClient } from '@angular/common/http';
|
|
import { Injectable } from '@angular/core';
|
|
import {
|
|
AppState,
|
|
SavedPage,
|
|
HashTable,
|
|
Paragraph,
|
|
BiblePassage,
|
|
BibleVerse,
|
|
Error,
|
|
BibleParagraph,
|
|
BibleParagraphPassage,
|
|
CardItem,
|
|
DictionaryType,
|
|
StrongsDefinition,
|
|
StrongsCrossReference,
|
|
RMACCrossReference,
|
|
RMACDefinition,
|
|
WordLookupResult,
|
|
WordToStem,
|
|
IndexResult,
|
|
NoteItem,
|
|
DisplaySettings,
|
|
} from '../models/app-state';
|
|
import { Section, BibleReference } from '../common/bible-reference';
|
|
import { PageTitles, PageIcons } from '../constants';
|
|
import { createStateService } from '../common/state-service';
|
|
import { UUID } from 'angular2-uuid';
|
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
|
|
|
const initialState: AppState = {
|
|
cards: [
|
|
{
|
|
qry: 'UUIDGOESHERE',
|
|
dict: 'n/a',
|
|
type: 'Note',
|
|
data: {
|
|
id: UUID.UUID(),
|
|
xref: null,
|
|
title: 'Title Here',
|
|
content: '# Content Here\nIn Markdown format.',
|
|
},
|
|
},
|
|
],
|
|
autocomplete: [],
|
|
currentPage: null,
|
|
savedPages: [],
|
|
savedPagesLoaded: false,
|
|
mainPages: [
|
|
{ title: PageTitles.Search, icon: PageIcons.Search, route: 'search' },
|
|
{ title: PageTitles.Help, icon: PageIcons.Help, route: 'help' },
|
|
],
|
|
error: null,
|
|
displaySettings: {
|
|
showStrongsAsModal: false,
|
|
appendCardToBottom: true,
|
|
insertCardNextToItem: true,
|
|
clearSearchAfterQuery: true,
|
|
fontSize: 12,
|
|
cardFont: 'PT Serif',
|
|
showVersesOnNewLine: false,
|
|
showVerseNumbers: false,
|
|
showParagraphs: true,
|
|
showParagraphHeadings: true,
|
|
syncCardsAcrossDevices: false,
|
|
},
|
|
cardIcons: {
|
|
words: 'font_download',
|
|
passage: 'menu_book',
|
|
strongs: 'speaker_notes',
|
|
note: 'text_snippet',
|
|
},
|
|
};
|
|
|
|
type AppAction =
|
|
| {
|
|
type: 'GET_SAVED_PAGE';
|
|
pageId: string;
|
|
}
|
|
| {
|
|
type: 'UPDATE_SAVED_PAGES';
|
|
savedPages: SavedPage[];
|
|
}
|
|
| {
|
|
type: 'ADD_CARD_TO_SAVED_PAGE';
|
|
card: CardItem;
|
|
pageId: string;
|
|
}
|
|
| {
|
|
type: 'ADD_CARD';
|
|
card: CardItem;
|
|
nextToItem: CardItem;
|
|
}
|
|
| {
|
|
type: 'UPDATE_CARD';
|
|
newCard: CardItem;
|
|
oldCard: CardItem;
|
|
}
|
|
| {
|
|
type: 'REMOVE_CARD';
|
|
card: CardItem;
|
|
}
|
|
| {
|
|
type: 'UPDATE_ERROR';
|
|
error: Error;
|
|
}
|
|
| {
|
|
type: 'UPDATE_FONT_SIZE';
|
|
size: number;
|
|
}
|
|
| {
|
|
type: 'UPDATE_FONT_FAMILY';
|
|
cardFont: string;
|
|
}
|
|
| {
|
|
type: 'UPDATE_AUTOCOMPLETE';
|
|
words: string[];
|
|
}
|
|
| {
|
|
type: 'UPDATE_DISPLAY_SETTINGS';
|
|
settings: DisplaySettings;
|
|
};
|
|
|
|
function reducer(state: AppState, action: AppAction): AppState {
|
|
// somtimes the state is null. lets not complain if that happens.
|
|
if (state === undefined) {
|
|
state = initialState;
|
|
}
|
|
|
|
switch (action.type) {
|
|
case 'UPDATE_DISPLAY_SETTINGS': {
|
|
return {
|
|
...state,
|
|
displaySettings: action.settings,
|
|
};
|
|
}
|
|
case 'UPDATE_AUTOCOMPLETE': {
|
|
return {
|
|
...state,
|
|
autocomplete: [...action.words],
|
|
};
|
|
}
|
|
case 'UPDATE_SAVED_PAGES': {
|
|
return {
|
|
...state,
|
|
savedPagesLoaded: true,
|
|
savedPages: action.savedPages,
|
|
};
|
|
}
|
|
case 'GET_SAVED_PAGE': {
|
|
const page = state.savedPages.find(
|
|
(o) => o.id.toString() === action.pageId
|
|
);
|
|
|
|
if (!page) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
...state,
|
|
currentPage: page,
|
|
cards: [...page.queries],
|
|
};
|
|
}
|
|
case 'ADD_CARD_TO_SAVED_PAGE': {
|
|
return {
|
|
...state,
|
|
savedPages: [
|
|
...state.savedPages.map((o) => {
|
|
if (o.id.toString() === action.pageId) {
|
|
let cards = [];
|
|
if (state.displaySettings.appendCardToBottom) {
|
|
cards = [...o.queries, action.card];
|
|
} else {
|
|
cards = [action.card, ...o.queries];
|
|
}
|
|
return {
|
|
...o,
|
|
queries: cards,
|
|
};
|
|
}
|
|
return o;
|
|
}),
|
|
],
|
|
};
|
|
}
|
|
case 'ADD_CARD': {
|
|
let cards = [];
|
|
|
|
if (action.nextToItem && state.displaySettings.insertCardNextToItem) {
|
|
const idx = state.cards.indexOf(action.nextToItem);
|
|
|
|
if (state.displaySettings.appendCardToBottom) {
|
|
const before = state.cards.slice(0, idx + 1);
|
|
const after = state.cards.slice(idx + 1);
|
|
cards = [...before, action.card, ...after];
|
|
} else {
|
|
const before = state.cards.slice(0, idx);
|
|
const after = state.cards.slice(idx);
|
|
cards = [...before, action.card, ...after];
|
|
}
|
|
} else {
|
|
if (state.displaySettings.appendCardToBottom) {
|
|
cards = [...state.cards, action.card];
|
|
} else {
|
|
cards = [action.card, ...state.cards];
|
|
}
|
|
}
|
|
return {
|
|
...state,
|
|
cards,
|
|
};
|
|
}
|
|
case 'UPDATE_CARD': {
|
|
return {
|
|
...state,
|
|
cards: state.cards.map((c) => {
|
|
if (c === action.oldCard) {
|
|
return action.newCard;
|
|
}
|
|
return c;
|
|
}),
|
|
};
|
|
}
|
|
case 'REMOVE_CARD': {
|
|
return {
|
|
...state,
|
|
cards: [...state.cards.filter((c) => c !== action.card)],
|
|
};
|
|
}
|
|
case 'UPDATE_FONT_SIZE': {
|
|
return {
|
|
...state,
|
|
displaySettings: {
|
|
...state.displaySettings,
|
|
fontSize: action.size,
|
|
},
|
|
};
|
|
}
|
|
case 'UPDATE_FONT_FAMILY': {
|
|
return {
|
|
...state,
|
|
displaySettings: {
|
|
...state.displaySettings,
|
|
cardFont: action.cardFont,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root',
|
|
})
|
|
export class AppService extends createStateService(reducer, initialState) {
|
|
private wordToStem: Map<string, string>;
|
|
private paragraphs: HashTable<Paragraph>;
|
|
private searchIndexArray: string[];
|
|
private autocomplete: string[];
|
|
|
|
private readonly dataPath = 'assets/data';
|
|
|
|
constructor(
|
|
private http: HttpClient,
|
|
private localStorageService: StorageMap
|
|
) {
|
|
super();
|
|
|
|
this.searchIndexArray = this.buildIndexArray().sort();
|
|
}
|
|
|
|
removeCard(card: CardItem) {
|
|
this.dispatch({
|
|
type: 'REMOVE_CARD',
|
|
card,
|
|
});
|
|
}
|
|
|
|
private dispatchError(msg: string) {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
msg,
|
|
},
|
|
});
|
|
console.log(msg);
|
|
}
|
|
|
|
changeFont(cardFont: string) {
|
|
this.dispatch({
|
|
type: 'UPDATE_FONT_FAMILY',
|
|
cardFont,
|
|
});
|
|
}
|
|
|
|
//#region Saved Pages
|
|
|
|
async initSavedPages() {
|
|
const exists = await this.localStorageService.has('savedPages').toPromise();
|
|
|
|
if (exists) {
|
|
const savedPages = (await this.localStorageService
|
|
.get('savedPages')
|
|
.toPromise()) as SavedPage[];
|
|
|
|
this.dispatch({
|
|
type: 'UPDATE_SAVED_PAGES',
|
|
savedPages,
|
|
});
|
|
}
|
|
}
|
|
|
|
getSavedPage(pageid: string) {
|
|
this.dispatch({
|
|
type: 'GET_SAVED_PAGE',
|
|
pageId: pageid,
|
|
});
|
|
}
|
|
|
|
savePage(title: string) {
|
|
const state = this.getState();
|
|
|
|
const savedPages = [
|
|
...state.savedPages,
|
|
{
|
|
// create a new saved page object
|
|
title,
|
|
id: UUID.UUID().toString(),
|
|
queries: [...state.cards],
|
|
},
|
|
];
|
|
|
|
this.localStorageService.set('savedPages', savedPages).subscribe(
|
|
// success
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_SAVED_PAGES',
|
|
savedPages,
|
|
});
|
|
},
|
|
// error
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
// tslint:disable-next-line: quotemark
|
|
msg: "Something went wrong and the page wasn't saved. :(",
|
|
},
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
updatePage() {
|
|
const state = this.getState();
|
|
|
|
const savedPages = [
|
|
...state.savedPages.filter((o) => o.id === state.currentPage.id),
|
|
{
|
|
id: state.currentPage.id,
|
|
title: state.currentPage.title,
|
|
queries: [...state.cards],
|
|
},
|
|
];
|
|
|
|
this.localStorageService.set('savedPages', savedPages).subscribe(
|
|
// success
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_SAVED_PAGES',
|
|
savedPages,
|
|
});
|
|
},
|
|
// error
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
// tslint:disable-next-line: quotemark
|
|
msg: "Something went wrong and the page wasn't saved. :(",
|
|
},
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
addCardToSavedPage(pageId: string, card: CardItem) {
|
|
this.dispatch({
|
|
type: 'ADD_CARD_TO_SAVED_PAGE',
|
|
card,
|
|
pageId,
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Display Settings
|
|
|
|
updateDisplaySettings(settings: DisplaySettings) {
|
|
this.saveSettingsApi(settings).subscribe(
|
|
// success
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_DISPLAY_SETTINGS',
|
|
settings,
|
|
});
|
|
},
|
|
// error
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
// tslint:disable-next-line: quotemark
|
|
msg: "Something went wrong and the settings weren't saved. :(",
|
|
},
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async initDisplaySettings() {
|
|
const hasDisplaySettings = await this.localStorageService
|
|
.has('displaySettings')
|
|
.toPromise();
|
|
|
|
if (hasDisplaySettings) {
|
|
const settings = await this.getSettingsApi();
|
|
|
|
this.dispatch({
|
|
type: 'UPDATE_DISPLAY_SETTINGS',
|
|
settings,
|
|
});
|
|
}
|
|
}
|
|
|
|
private saveSettingsApi(settings: DisplaySettings) {
|
|
return this.localStorageService.set('displaySettings', settings);
|
|
}
|
|
|
|
private async getSettingsApi() {
|
|
return (await this.localStorageService
|
|
.get('displaySettings')
|
|
.toPromise()) as DisplaySettings;
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Notes
|
|
|
|
async getNote(qry: string, nextToItem: CardItem = null) {
|
|
const note = (await this.localStorageService
|
|
.get('notes/' + qry)
|
|
.toPromise()) as NoteItem;
|
|
|
|
const card = {
|
|
qry,
|
|
dict: 'n/a',
|
|
type: 'Note',
|
|
data: note,
|
|
};
|
|
|
|
this.dispatch({
|
|
type: 'ADD_CARD',
|
|
card,
|
|
nextToItem,
|
|
});
|
|
}
|
|
|
|
async createNote(card: CardItem, nextToItem: CardItem = null) {
|
|
this.saveNoteApi(card.data as NoteItem).subscribe(
|
|
// success
|
|
() => {
|
|
this.dispatch({
|
|
type: 'ADD_CARD',
|
|
card,
|
|
nextToItem,
|
|
});
|
|
},
|
|
// error
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
// tslint:disable-next-line: quotemark
|
|
msg: "Something went wrong and the note wasn't saved. :(",
|
|
},
|
|
});
|
|
}
|
|
);
|
|
}
|
|
async editNote(newCard: CardItem, oldCard: CardItem) {
|
|
this.saveNoteApi(newCard.data as NoteItem).subscribe(
|
|
// success
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_CARD',
|
|
newCard,
|
|
oldCard,
|
|
});
|
|
},
|
|
// error
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
// tslint:disable-next-line: quotemark
|
|
msg: "Something went wrong and the note wasn't saved. :(",
|
|
},
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
async deleteNote(noteCard: CardItem) {
|
|
this.deleteNoteApi(noteCard.data as NoteItem).subscribe(
|
|
// success
|
|
() => {
|
|
this.removeCard(noteCard);
|
|
},
|
|
// error
|
|
() => {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
// tslint:disable-next-line: quotemark
|
|
msg: "Something went wrong and the note wasn't saved. :(",
|
|
},
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
private deleteNoteApi(note: NoteItem) {
|
|
return this.localStorageService.delete('notes/' + note.id);
|
|
}
|
|
|
|
private saveNoteApi(note: NoteItem) {
|
|
return this.localStorageService.set('notes/' + note.id, note);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Strongs
|
|
|
|
async getStrongs(
|
|
strongsNumber: string,
|
|
dict: DictionaryType,
|
|
nextToItem: CardItem = null
|
|
) {
|
|
const card = await this.getStrongsCard(strongsNumber, dict);
|
|
|
|
this.dispatch({
|
|
type: 'ADD_CARD',
|
|
card,
|
|
nextToItem,
|
|
});
|
|
}
|
|
|
|
async getStrongsCard(strongsNumber: string, dict: string) {
|
|
const result = await this.getStrongsFromApi(strongsNumber, dict);
|
|
const d = dict === 'grk' ? 'G' : 'H';
|
|
|
|
const card = {
|
|
qry: `${d}${strongsNumber}`,
|
|
dict: d,
|
|
type: 'Strongs',
|
|
data: result,
|
|
};
|
|
return card;
|
|
}
|
|
|
|
private async getStrongsFromApi(strongsNumber: string, dict: string) {
|
|
const sn = parseInt(strongsNumber, 10);
|
|
const result = {
|
|
prefix: '',
|
|
sn,
|
|
def: null,
|
|
rmac: null,
|
|
crossrefs: null,
|
|
rmaccode: '',
|
|
};
|
|
|
|
if (dict === 'grk') {
|
|
result.prefix = 'G';
|
|
if (sn > 5624 || sn < 1) {
|
|
this.dispatchError(
|
|
`Strong's Number G${sn} is out of range. Strong's numbers range from 1 - 5624 in the New Testament.`
|
|
);
|
|
return;
|
|
}
|
|
} else {
|
|
result.prefix = 'H';
|
|
if (sn > 8674 || sn < 1) {
|
|
this.dispatchError(
|
|
`Strong's Number H${sn} is out of range. Strong's numbers range from 1 - 8674 in the Old Testament.`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let url = `${dict}${Math.ceil(sn / 100)}.json`;
|
|
try {
|
|
const d = await this.http
|
|
.get<StrongsDefinition[]>(`${this.dataPath}/strongs/${url}`)
|
|
.toPromise();
|
|
result.def = d.find((el) => el.i === result.prefix + result.sn);
|
|
} catch (err) {
|
|
this.dispatchError(
|
|
`Unable to retrieve Strong's Data for ${result.prefix}${result.sn}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const d = await this.http
|
|
.get<StrongsCrossReference[]>(`${this.dataPath}/strongscr/cr${url}`)
|
|
.toPromise();
|
|
|
|
result.crossrefs = d.find(
|
|
(o) => o.id.toUpperCase() === result.prefix + result.sn
|
|
);
|
|
} catch (err) {
|
|
this.dispatchError(
|
|
`Unable to retrieve Strong\'s Cross References for ${result.prefix}${result.sn}`
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
if (dict === 'grk') {
|
|
url = `${this.dataPath}/rmac/rs${Math.ceil(sn / 1000)}.json`;
|
|
|
|
// rmac is a two get process.
|
|
// first, get the rmac code.
|
|
try {
|
|
const rmacCrossReferences = await this.http
|
|
.get<RMACCrossReference[]>(url)
|
|
.toPromise();
|
|
|
|
// deal with RMAC
|
|
const referencesForThisStrongsNumber = rmacCrossReferences.filter(
|
|
(el, i) => {
|
|
return el.i === sn.toString();
|
|
}
|
|
);
|
|
|
|
if (referencesForThisStrongsNumber.length === 0) {
|
|
return result;
|
|
}
|
|
result.rmaccode = referencesForThisStrongsNumber[0].r;
|
|
} catch (err) {
|
|
this.dispatchError('Unable to retrieve RMAC');
|
|
return;
|
|
}
|
|
|
|
// if you were able to get the rmac code, then get the related definition.
|
|
if (result.rmaccode !== undefined) {
|
|
url = `${this.dataPath}/rmac/r-${result.rmaccode.substring(0, 1)}.json`;
|
|
|
|
try {
|
|
const rmacDefinitions = await this.http
|
|
.get<RMACDefinition[]>(url)
|
|
.toPromise();
|
|
result.rmac = rmacDefinitions.find(
|
|
(o) => o.id.toLowerCase() === result.rmaccode
|
|
);
|
|
} catch (err) {
|
|
this.dispatchError('Unable to retrieve RMAC');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
//#endregion
|
|
|
|
//#region Bible Passages
|
|
|
|
async getPassage(ref: BibleReference, nextToItem: CardItem = null) {
|
|
const card = await this.composeBiblePassageCardItem(ref);
|
|
this.dispatch({
|
|
type: 'ADD_CARD',
|
|
card,
|
|
nextToItem,
|
|
});
|
|
}
|
|
|
|
async updatePassage(oldCard: CardItem, ref: BibleReference) {
|
|
const newCard = await this.composeBiblePassageCardItem(ref);
|
|
this.dispatch({
|
|
type: 'UPDATE_CARD',
|
|
oldCard,
|
|
newCard,
|
|
});
|
|
}
|
|
|
|
private async composeBiblePassageCardItem(ref: BibleReference) {
|
|
const result = await this.getPassageFromApi(ref.Section);
|
|
return {
|
|
qry: ref.toString(),
|
|
dict: ref.Section.start.book.book_number > 39 ? 'G' : 'H',
|
|
type: 'Passage',
|
|
data: result,
|
|
};
|
|
}
|
|
|
|
private async getPassageFromApi(section: Section) {
|
|
try {
|
|
const chapters = []; // the verses from the chapter.
|
|
const result = {
|
|
cs: [],
|
|
testament: '',
|
|
ref: BibleReference.toString(section),
|
|
};
|
|
|
|
if (Number(section.start.chapter) > section.start.book.last_chapter) {
|
|
this.dispatchError(
|
|
`The requested chapter ${section.start.book.name} is out of range. Please pick a chapter between 1 and ${section.end.book.last_chapter}.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (Number(section.end.chapter) > section.end.book.last_chapter) {
|
|
this.dispatchError(
|
|
`The requested chapter ${section.end.book.name} is out of range. Please pick a chapter between 1 and ${section.end.book.last_chapter}.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
for (
|
|
let i = Number(section.start.chapter);
|
|
i <= Number(section.end.chapter);
|
|
i++
|
|
) {
|
|
try {
|
|
const d = await this.http
|
|
.get<BiblePassage>(
|
|
`${this.dataPath}/bibles/kjv_strongs/${section.start.book.book_number}-${i}.json`
|
|
)
|
|
.toPromise();
|
|
chapters.push(d);
|
|
} catch (err) {
|
|
this.dispatchError(`Unable to retrieve bible passage ${result.ref}.`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
const passages: BiblePassage[] = [];
|
|
|
|
// prep the passages
|
|
for (let j = 0; j < chapters.length; j++) {
|
|
const vss: BibleVerse[] = [];
|
|
let start: number;
|
|
let end: string | number;
|
|
|
|
// figure out the start verse.
|
|
if (j === 0) {
|
|
if (section.start.verse.indexOf('*') !== -1) {
|
|
// you sometimes use this as a shortcut to the last verse
|
|
// replace the * with the last verse
|
|
// e.g jn 1:* - 3:4
|
|
|
|
// update the section and the ref.
|
|
section.start.verse = chapters[j].vss.length.toString();
|
|
result.ref = BibleReference.toString(section);
|
|
} else {
|
|
start = parseInt(section.start.verse, 10);
|
|
}
|
|
} else {
|
|
start = 1;
|
|
}
|
|
|
|
// figure out the end verse
|
|
if (j + 1 === chapters.length) {
|
|
end = section.end.verse;
|
|
} else {
|
|
end = '*';
|
|
}
|
|
|
|
// get the verses requested.
|
|
const tvs = chapters[j].vss.length;
|
|
if (end === '*' || parseInt(end, 10) > tvs) {
|
|
end = tvs;
|
|
}
|
|
|
|
// we're using c based indexes here, so the index is 1 less than the verse #.
|
|
for (let i = start; i <= end; i++) {
|
|
vss.push(chapters[j].vss[i - 1]);
|
|
}
|
|
|
|
passages.push({
|
|
ch: chapters[j].ch,
|
|
vss,
|
|
});
|
|
}
|
|
|
|
// convert into paragraphs.
|
|
result.cs = await this.convertToParagraphPassages(passages, section);
|
|
|
|
if (section.start.book.book_number >= 40) {
|
|
result.testament = 'new';
|
|
} else {
|
|
result.testament = 'old';
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
this.dispatchError(`An unknown error occurred: ${error}.`);
|
|
}
|
|
}
|
|
|
|
private async convertToParagraphPassages(
|
|
chapters: BiblePassage[],
|
|
section: Section
|
|
) {
|
|
// get the paragraphs the first time if you haven't already.
|
|
if (!this.paragraphs) {
|
|
this.paragraphs = await this.http
|
|
.get<HashTable<Paragraph>>(`${this.dataPath}/bibles/paras.json`)
|
|
.toPromise();
|
|
}
|
|
|
|
const passages: BibleParagraphPassage[] = [];
|
|
for (const ch of chapters) {
|
|
const passage = {
|
|
ch: ch.ch,
|
|
paras: this.convertToParagraphs(ch, section, this.paragraphs),
|
|
};
|
|
|
|
passages.push(passage);
|
|
}
|
|
|
|
return passages;
|
|
}
|
|
|
|
private convertToParagraphs(
|
|
ch: BiblePassage,
|
|
section: Section,
|
|
paragraphMarkers: HashTable<Paragraph>
|
|
): BibleParagraph[] {
|
|
// group the verses into paragraphs.
|
|
|
|
// create an initial paragraph to hold verses that might come before a paragraph.
|
|
let para = { p: { h: '', p: 0 }, vss: [] };
|
|
const paras = [];
|
|
const vss: BibleVerse[] = [];
|
|
|
|
// for each verse in the chapter, break them into paragraphs.
|
|
for (const v of ch.vss) {
|
|
if (this.getRefKey(v, section) in paragraphMarkers) {
|
|
paras.push(para);
|
|
para = {
|
|
p: paragraphMarkers[this.getRefKey(v, section)],
|
|
vss: [v],
|
|
};
|
|
|
|
if (para.p === undefined) {
|
|
para.p = { h: '', p: 0 };
|
|
} // just in case you can't find a paragraph.
|
|
} else {
|
|
para.vss.push(v);
|
|
}
|
|
}
|
|
|
|
// add the final paragraph if it has verses.
|
|
if (para.vss.length > 0) {
|
|
paras.push(para);
|
|
}
|
|
|
|
return paras;
|
|
}
|
|
|
|
private getRefKey(vs: BibleVerse, section: Section) {
|
|
return BibleReference.formatReferenceKey(
|
|
section.start.book.book_number,
|
|
section.start.chapter,
|
|
vs.v
|
|
);
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Word Search
|
|
|
|
async getWords(qry: string, nextToItem: CardItem = null) {
|
|
const result = await this.getWordsFromApi(qry);
|
|
|
|
const card = {
|
|
qry,
|
|
dict: 'n/a',
|
|
type: 'Words',
|
|
data: result,
|
|
};
|
|
|
|
this.dispatch({
|
|
type: 'ADD_CARD',
|
|
card,
|
|
nextToItem,
|
|
});
|
|
}
|
|
|
|
private async getWordsFromApi(qry: string): Promise<WordLookupResult> {
|
|
// its possible that this might get called before the word to stem map is build. first time, check and populate...
|
|
if (!this.wordToStem) {
|
|
await this.getStemWordIndex();
|
|
}
|
|
|
|
// now carry on...
|
|
const qs = this.normalizeQueryString(qry);
|
|
const results: (readonly string[])[] = [];
|
|
|
|
// Loop through each query term.
|
|
for (const q of qs) {
|
|
if (!this.wordToStem.has(q)) {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
msg: `Unable to find the word "${q}" in any passages.`,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const stem =
|
|
q.startsWith('"') && q.endsWith('"')
|
|
? q.split('"').join('') // if its quoted, search the exact term.
|
|
: this.wordToStem.get(q); // otherwise use the stem to get a broader set of results.
|
|
|
|
// handle the first case.
|
|
if (stem <= this.searchIndexArray[0]) {
|
|
results.unshift(
|
|
await this.getSearchReferences(
|
|
`${this.dataPath}/index/${this.searchIndexArray[0]}idx.json`,
|
|
stem
|
|
)
|
|
);
|
|
break;
|
|
}
|
|
|
|
// For each query term, figure out which file it is in, and get it.
|
|
for (let w = 1; w < this.searchIndexArray.length; w++) {
|
|
// If we are at the end of the array, we want to use a different test.
|
|
if (
|
|
stem <= this.searchIndexArray[w] &&
|
|
stem > this.searchIndexArray[w - 1]
|
|
) {
|
|
results.unshift(
|
|
await this.getSearchReferences(
|
|
`${this.dataPath}/index/${this.searchIndexArray[w]}idx.json`,
|
|
stem
|
|
)
|
|
);
|
|
}
|
|
}
|
|
} // End of loop through query terms
|
|
|
|
// Now we need to test results. If there is more than one item in the array, we need to find the set
|
|
// that is shared by all of them. IF not, we can just return those refs.
|
|
if (results.length === 0) {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
msg: `No passages found for query: ${qry}.`,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (results.length === 1) {
|
|
// we go down this path if only one word was searched for
|
|
return {
|
|
refs: results[0],
|
|
word: qry,
|
|
};
|
|
} else {
|
|
// we go down this path if more than one word was searched for
|
|
return {
|
|
refs: this.findSharedSet(results),
|
|
word: qry,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the references a given word is found in.
|
|
* Returns a string[].
|
|
* @param url - The url of the word index
|
|
* @param query - The word to lookup.
|
|
*/
|
|
private async getStemWordIndex() {
|
|
this.wordToStem = new Map<string, string>();
|
|
try {
|
|
const r = await this.http
|
|
.get<WordToStem[]>(`${this.dataPath}/index/word_to_stem_idx.json`)
|
|
.toPromise();
|
|
|
|
// find the right word
|
|
for (const i of r) {
|
|
this.wordToStem.set(i.w, i.s);
|
|
}
|
|
} catch (err) {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
msg: err,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the references a given word is found in.
|
|
* Returns a string[].
|
|
* @param url - The url of the word index
|
|
* @param query - The word to lookup.
|
|
*/
|
|
private async getSearchReferences(url: string, query: string) {
|
|
// getSearchRefs takes a url and uses ajax to retrieve the references and returns an array of references.
|
|
let r: IndexResult[];
|
|
|
|
try {
|
|
r = await this.http.get<IndexResult[]>(url).toPromise();
|
|
} catch (err) {
|
|
this.dispatch({
|
|
type: 'UPDATE_ERROR',
|
|
error: {
|
|
msg: err,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
// find the right word
|
|
const refs = r.filter((o) => o.w === query);
|
|
if (refs.length > 0) {
|
|
return refs[0].r;
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private buildIndexArray() {
|
|
const words: string[] = [];
|
|
words.unshift('abishur');
|
|
words.unshift('achor');
|
|
words.unshift('adoni');
|
|
words.unshift('afterward');
|
|
words.unshift('ahishahar');
|
|
words.unshift('alleg');
|
|
words.unshift('ambush');
|
|
words.unshift('ancestor');
|
|
words.unshift('aphik');
|
|
words.unshift('arbah');
|
|
words.unshift('arodi');
|
|
words.unshift('ashkenaz');
|
|
words.unshift('ate');
|
|
words.unshift('azaniah');
|
|
words.unshift('backbiteth');
|
|
words.unshift('barbarian');
|
|
words.unshift('beard');
|
|
words.unshift('begettest');
|
|
words.unshift('benefactor');
|
|
words.unshift('bethel');
|
|
words.unshift('bilshan');
|
|
words.unshift('blindeth');
|
|
words.unshift('booti');
|
|
words.unshift('breaketh');
|
|
words.unshift('bucket');
|
|
words.unshift('cabbon');
|
|
words.unshift('caphtor');
|
|
words.unshift('causeless');
|
|
words.unshift('chapmen');
|
|
words.unshift('chese');
|
|
words.unshift('chrysoprasus');
|
|
words.unshift('cloth');
|
|
words.unshift('common');
|
|
words.unshift('confess');
|
|
words.unshift('contendeth');
|
|
words.unshift('coucheth');
|
|
words.unshift('crept');
|
|
words.unshift('curseth');
|
|
words.unshift('darius');
|
|
words.unshift('decketh');
|
|
words.unshift('dema');
|
|
words.unshift('devil');
|
|
words.unshift('directeth');
|
|
words.unshift('disposit');
|
|
words.unshift('doth');
|
|
words.unshift('drowsi');
|
|
words.unshift('ebe');
|
|
words.unshift('elead');
|
|
words.unshift('elkoshit');
|
|
words.unshift('encourag');
|
|
words.unshift('entreat');
|
|
words.unshift('eschew');
|
|
words.unshift('ever');
|
|
words.unshift('expert');
|
|
words.unshift('fallest');
|
|
words.unshift('feedeth');
|
|
words.unshift('filthi');
|
|
words.unshift('fleeth');
|
|
words.unshift('forborn');
|
|
words.unshift('forsookest');
|
|
words.unshift('fretteth');
|
|
words.unshift('gahar');
|
|
words.unshift('gazzam');
|
|
words.unshift('gibea');
|
|
words.unshift('glister');
|
|
words.unshift('got');
|
|
words.unshift('grope');
|
|
words.unshift('hadlai');
|
|
words.unshift('hammon');
|
|
words.unshift('harbona');
|
|
words.unshift('hasrah');
|
|
words.unshift('hazezon');
|
|
words.unshift('heinous');
|
|
words.unshift('herebi');
|
|
words.unshift('highest');
|
|
words.unshift('holdeth');
|
|
words.unshift('hosanna');
|
|
words.unshift('huri');
|
|
words.unshift('ill');
|
|
words.unshift('inexcus');
|
|
words.unshift('intend');
|
|
words.unshift('ishui');
|
|
words.unshift('jaazaniah');
|
|
words.unshift('jaminit');
|
|
words.unshift('jecoliah');
|
|
words.unshift('jeopard');
|
|
words.unshift('jethro');
|
|
words.unshift('joiarib');
|
|
words.unshift('juda');
|
|
words.unshift('kelaiah');
|
|
words.unshift('kishion');
|
|
words.unshift('laden');
|
|
words.unshift('laughter');
|
|
words.unshift('lehi');
|
|
words.unshift('lift');
|
|
words.unshift('loatheth');
|
|
words.unshift('lucius');
|
|
words.unshift('madmen');
|
|
words.unshift('malachi');
|
|
words.unshift('march');
|
|
words.unshift('maul');
|
|
words.unshift('melchizedek');
|
|
words.unshift('merrili');
|
|
words.unshift('midianit');
|
|
words.unshift('miri');
|
|
words.unshift('modest');
|
|
words.unshift('move');
|
|
words.unshift('naashon');
|
|
words.unshift('nazareth');
|
|
words.unshift('nephishesim');
|
|
words.unshift('nisan');
|
|
words.unshift('obadiah');
|
|
words.unshift('oliveyard');
|
|
words.unshift('oren');
|
|
words.unshift('overrun');
|
|
words.unshift('pallu');
|
|
words.unshift('pas');
|
|
words.unshift('peel');
|
|
words.unshift('pernici');
|
|
words.unshift('philip');
|
|
words.unshift('pison');
|
|
words.unshift('plucketh');
|
|
words.unshift('pour');
|
|
words.unshift('price');
|
|
words.unshift('proport');
|
|
words.unshift('purg');
|
|
words.unshift('rabboni');
|
|
words.unshift('ravish');
|
|
words.unshift('redeemedst');
|
|
words.unshift('remainest');
|
|
words.unshift('reput');
|
|
words.unshift('revers');
|
|
words.unshift('rissah');
|
|
words.unshift('ruddi');
|
|
words.unshift('said');
|
|
words.unshift('sapphir');
|
|
words.unshift('scepter');
|
|
words.unshift('secundus');
|
|
words.unshift('separ');
|
|
words.unshift('shachia');
|
|
words.unshift('sharar');
|
|
words.unshift('sheepshear');
|
|
words.unshift('sheva');
|
|
words.unshift('shishak');
|
|
words.unshift('shroud');
|
|
words.unshift('signifi');
|
|
words.unshift('sittest');
|
|
words.unshift('slow');
|
|
words.unshift('soft');
|
|
words.unshift('sowedst');
|
|
words.unshift('spoil');
|
|
words.unshift('station');
|
|
words.unshift('stoop');
|
|
words.unshift('strongest');
|
|
words.unshift('sum');
|
|
words.unshift('sweep');
|
|
words.unshift('tahapan');
|
|
words.unshift('tast');
|
|
words.unshift('ten');
|
|
words.unshift('thereat');
|
|
words.unshift('threaten');
|
|
words.unshift('timbrel');
|
|
words.unshift('tongu');
|
|
words.unshift('travailest');
|
|
words.unshift('trust');
|
|
words.unshift('uncircumcis');
|
|
words.unshift('unprepar');
|
|
words.unshift('urg');
|
|
words.unshift('vat');
|
|
words.unshift('visiteth');
|
|
words.unshift('wash');
|
|
words.unshift('wed');
|
|
words.unshift('wherewith');
|
|
words.unshift('winepress');
|
|
words.unshift('won');
|
|
words.unshift('written');
|
|
words.unshift('zalmonah');
|
|
words.unshift('zenan');
|
|
words.unshift('ziphim');
|
|
words.unshift('zuzim');
|
|
|
|
return words;
|
|
}
|
|
|
|
/*
|
|
* Returns a list of references in string form as a string[] that are shared
|
|
* given a list of lists of references in string form.
|
|
*/
|
|
private findSharedSet(referenceSet: (readonly string[])[]) {
|
|
const results = [];
|
|
// FindSharedSet takes an array of reference arrays, and figures out
|
|
// which references are shared by all arrays/sets, then returns a single
|
|
// array of references.
|
|
// tslint:disable-next-line: prefer-for-of
|
|
for (let j = 0; j < referenceSet.length; j++) {
|
|
const refs = referenceSet[j];
|
|
if (refs != null) {
|
|
for (let i = 0; i < refs.length; i++) {
|
|
const r = refs[i].split(':');
|
|
// convert references to single integers.
|
|
// Book * 100000000, Chapter * 10000, Verse remains same, add all together.
|
|
results[j][i] = this.toReferenceNumber(r);
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// get the first result
|
|
let first = results[0];
|
|
|
|
// for each additional result, get the shared set
|
|
for (let i = 1; i < results.length; i++) {
|
|
first = this.returnSharedSet(results[i], first);
|
|
}
|
|
|
|
// convert the references back into book, chapter and verse.
|
|
for (let i = 0; i < first.length; i++) {
|
|
const ref = first[i];
|
|
first[i] = this.toReferenceString(ref);
|
|
}
|
|
|
|
return first;
|
|
}
|
|
|
|
private toReferenceString(ref: number) {
|
|
return BibleReference.formatReferenceKey(
|
|
Math.floor(ref / 100000000),
|
|
Math.floor((ref % 100000000) / 10000),
|
|
Math.floor((ref % 100000000) % 10000)
|
|
);
|
|
}
|
|
|
|
private toReferenceNumber(r: string[]) {
|
|
let ref = parseInt(r[0], 10) * 100000000;
|
|
ref = ref + parseInt(r[1], 10) * 10000;
|
|
ref = ref + parseInt(r[2], 10) * 1;
|
|
return ref;
|
|
}
|
|
|
|
private returnSharedSet(x, y) {
|
|
/// <summary>
|
|
/// Takes two javascript arrays and returns an array
|
|
/// containing a set of values shared by arrays.
|
|
/// </summary>
|
|
// declare iterator
|
|
let i = 0;
|
|
// declare terminator
|
|
let t = x.length < y.length ? x.length : y.length;
|
|
// sort the arrays
|
|
x.sort((a, b) => a - b);
|
|
y.sort((a, b) => a - b);
|
|
// in this loop, we remove from the arrays, the
|
|
// values that aren't shared between them.
|
|
while (i < t) {
|
|
if (x[i] === y[i]) {
|
|
i++;
|
|
}
|
|
|
|
if (x[i] < y[i]) {
|
|
x.splice(i, 1);
|
|
}
|
|
|
|
if (x[i] > y[i]) {
|
|
y.splice(i, 1);
|
|
}
|
|
|
|
t = x.length < y.length ? x.length : y.length;
|
|
// we have to make sure to remove any extra values
|
|
// at the end of an array when we reach the end of
|
|
// the other.
|
|
if (t === i && t < x.length) {
|
|
x.splice(i, x.length - i);
|
|
}
|
|
|
|
if (t === i && t < y.length) {
|
|
y.splice(i, x.length - i);
|
|
}
|
|
}
|
|
// we could return y, because at this time, both arrays
|
|
// are identical.
|
|
return x;
|
|
}
|
|
|
|
private normalizeQueryString(qry: string): string[] {
|
|
qry = qry.toLowerCase();
|
|
return qry.replace(/'/g, '').replace(/\s+/g, ' ').split(' ');
|
|
}
|
|
|
|
//#endregion
|
|
|
|
//#region Auto Complete
|
|
|
|
async getAutoComplete(keyword: string) {
|
|
if (!this.autocomplete) {
|
|
// if you have't populated the word list yet, do so...
|
|
const data = await this.http
|
|
.get<WordToStem[]>(`${this.dataPath}/index/word_to_stem_idx.json`)
|
|
.toPromise();
|
|
this.autocomplete = data.map((o) => o.w);
|
|
}
|
|
|
|
let qry = keyword;
|
|
let prefix = '';
|
|
const idx = qry.lastIndexOf(';');
|
|
const words: string[] = [];
|
|
|
|
if (idx > -1) {
|
|
qry = keyword.substr(idx + 1).trim();
|
|
prefix = keyword.substr(0, idx).trim() + '; ';
|
|
}
|
|
|
|
qry = qry.split('"').join(''); // remove any quotes, just in case
|
|
|
|
if (qry.search(/[0-9]/i) === -1) {
|
|
// its a word
|
|
for (const item of BibleReference.Books) {
|
|
if (
|
|
item.name !== 'Unknown' &&
|
|
(item.name.toLowerCase().indexOf(qry.toLowerCase()) > -1 ||
|
|
item.short_name.toLowerCase().indexOf(qry.toLowerCase()) > -1)
|
|
) {
|
|
words.push(prefix + item.name);
|
|
if (words.length > 2) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const item of this.autocomplete) {
|
|
if (item.toLowerCase().indexOf(qry.toLowerCase()) > -1) {
|
|
words.push(prefix + item);
|
|
if (words.length > 6) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.dispatch({
|
|
type: 'UPDATE_AUTOCOMPLETE',
|
|
words,
|
|
});
|
|
} else if (qry.search(/(H|G)[0-9]/i) !== -1) {
|
|
// its a strongs lookup
|
|
if (qry.substr(0, 1).toUpperCase() === 'H') {
|
|
const num = parseInt(qry.substr(1), 10);
|
|
for (let x = num; x < num + 10 && x < 8675; x++) {
|
|
words.push('H' + x);
|
|
}
|
|
} else if (qry.substr(0, 1).toUpperCase() === 'G') {
|
|
const num = parseInt(qry.substr(1), 10);
|
|
for (let x = num; x < num + 10 && x < 5625; x++) {
|
|
words.push('G' + x);
|
|
}
|
|
}
|
|
|
|
this.dispatch({
|
|
type: 'UPDATE_AUTOCOMPLETE',
|
|
words,
|
|
});
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
}
|