diff --git a/app/db/src/app/app.module.ts b/app/db/src/app/app.module.ts index d8ed9af1..8c102df2 100644 --- a/app/db/src/app/app.module.ts +++ b/app/db/src/app/app.module.ts @@ -10,6 +10,8 @@ import { HttpClientModule } from '@angular/common/http'; import { SearchPage } from './search/components/search-page/search.page'; import { PassageComponent } from './search/components/passage/passage.component'; import { StrongsComponent } from './search/components/strongs/strongs.component'; +import { WordsComponent } from './search/components/words/words.component'; + import { VersePickerModalComponent } from './search/components/verse-picker/verse-picker-modal.component'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -55,6 +57,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; SearchPage, PassageComponent, StrongsComponent, + WordsComponent, VersePickerModalComponent, ], imports: [ diff --git a/app/db/src/app/common/card.component.ts b/app/db/src/app/common/card.component.ts index 133f6dad..71ecf580 100644 --- a/app/db/src/app/common/card.component.ts +++ b/app/db/src/app/common/card.component.ts @@ -6,6 +6,7 @@ import { Component, } from '@angular/core'; import { CardItem, OpenData } from '../models/app-state'; +import { BibleReference } from './bible-reference'; @Component({ template: '', @@ -55,4 +56,14 @@ export class CardComponent { this.onClose.emit(this.cardItem); }, d); } + + makePassage(p: string) { + return new BibleReference( + BibleReference.bookName(parseInt(p.split(':')[0], 10)).name + + ' ' + + p.split(':')[1] + + ':' + + p.split(':')[2] + ); + } } diff --git a/app/db/src/app/common/subscriber.component.ts b/app/db/src/app/common/subscriber.component.ts new file mode 100644 index 00000000..9b4a1fa4 --- /dev/null +++ b/app/db/src/app/common/subscriber.component.ts @@ -0,0 +1,22 @@ +import { OnDestroy, Injectable } from '@angular/core'; +import { Subscription } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class SubscriberComponent implements OnDestroy { + protected subscriptions: Subscription[] = []; + + public ngOnDestroy(): void { + for (const subscription of this.subscriptions) { + subscription.unsubscribe(); + } + } + + protected addSubscriptions(...subs: Subscription[]) { + this.subscriptions.push(...subs); + } + protected addSubscription(sub: Subscription) { + this.subscriptions.push(sub); + } +} diff --git a/app/db/src/app/models/app-state.ts b/app/db/src/app/models/app-state.ts index 3028ac76..27f223ca 100644 --- a/app/db/src/app/models/app-state.ts +++ b/app/db/src/app/models/app-state.ts @@ -2,6 +2,7 @@ export interface AppState { readonly savedPages: readonly SavedPage[]; readonly mainPages: readonly Page[]; readonly cards: readonly CardItem[]; + readonly autocomplete: readonly string[]; readonly error: Error; readonly paragraphs: HashTable; readonly displaySettings: DisplaySettings; @@ -11,7 +12,7 @@ export interface Error { readonly msg: string; } -export type Data = BiblePassageResult | StrongsResult; +export type Data = BiblePassageResult | StrongsResult | WordLookupResult; export interface DisplaySettings { readonly showStrongsAsModal: boolean; @@ -151,3 +152,22 @@ export interface RMACCrossReference { } //#endregion + +//#region Word Search + +export interface WordLookupResult { + readonly refs: readonly string[]; + readonly word: string; +} + +export interface IndexResult { + readonly r: readonly string[]; + readonly w: string; +} + +export interface WordToStem { + readonly w: string; + readonly s: string; +} + +//#endregion diff --git a/app/db/src/app/search/components/passage/passage.component.ts b/app/db/src/app/search/components/passage/passage.component.ts index ee204521..0141c08f 100644 --- a/app/db/src/app/search/components/passage/passage.component.ts +++ b/app/db/src/app/search/components/passage/passage.component.ts @@ -152,7 +152,7 @@ export class PassageComponent extends CardComponent implements OnInit { const dict = this.cardItem.dict === 'H' ? 'heb' : 'grk'; const numbers = q.split(' '); for (const sn of numbers) { - this.appService.getNewStrongs(sn, dict, this.cardItem); + this.appService.getStrongs(sn, dict, this.cardItem); } } diff --git a/app/db/src/app/search/components/search-page/search.page.html b/app/db/src/app/search/components/search-page/search.page.html index bf5af1c2..74b9c62a 100644 --- a/app/db/src/app/search/components/search-page/search.page.html +++ b/app/db/src/app/search/components/search-page/search.page.html @@ -12,6 +12,7 @@ type="search" autocomplete="off" matInput + #autoCompleteInput [formControl]="searchControl" [matAutocomplete]="auto" (keyup.enter)="search($event.target.value)" @@ -34,8 +35,8 @@
- - + + + - +   diff --git a/app/db/src/app/search/components/search-page/search.page.ts b/app/db/src/app/search/components/search-page/search.page.ts index 347e9cf7..6bff31ef 100644 --- a/app/db/src/app/search/components/search-page/search.page.ts +++ b/app/db/src/app/search/components/search-page/search.page.ts @@ -1,32 +1,43 @@ -import { Component, OnInit, HostListener } from '@angular/core'; +import { Component, OnInit, ViewChild, AfterViewInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable } from 'rxjs'; import { FormControl } from '@angular/forms'; -import { map } from 'rxjs/operators'; import { AppService } from 'src/app/services/app.service'; import { NavService } from 'src/app/services/nav.service'; import { OpenData, CardItem } from 'src/app/models/app-state'; import { BibleReference } from 'src/app/common/bible-reference'; import { MatDialog } from '@angular/material/dialog'; import { VersePickerModalComponent } from '../verse-picker/verse-picker-modal.component'; +import { SubscriberComponent } from '../../../common/subscriber.component'; + +import { + MatAutocompleteTrigger, + MatAutocomplete, +} from '@angular/material/autocomplete'; @Component({ selector: 'app-search-page', templateUrl: './search.page.html', styleUrls: ['./search.page.scss'], }) -export class SearchPage implements OnInit { +export class SearchPage extends SubscriberComponent + implements OnInit, AfterViewInit { cards$ = this.appService.select((state) => state.cards); - suggestions$: Observable; - + suggestions$ = this.appService.select((state) => state.autocomplete); searchControl = new FormControl(); + @ViewChild(MatAutocomplete) + autoComplete: MatAutocomplete; + @ViewChild('autoCompleteInput', { read: MatAutocompleteTrigger }) + autoCompleteTrigger: MatAutocompleteTrigger; + constructor( private activatedRoute: ActivatedRoute, private appService: AppService, public navService: NavService, public dialog: MatDialog - ) {} + ) { + super(); + } ngOnInit() { // if a route was passed in, perform a search. @@ -35,9 +46,25 @@ export class SearchPage implements OnInit { this.search(term); } - this.suggestions$ = this.searchControl.valueChanges.pipe( - map((value) => this.getSearchItems(value)) + // subscribe to autocomplete input control's changes + this.addSubscription( + this.searchControl.valueChanges.subscribe((value: string) => + this.appService.getAutoComplete(value.toLowerCase()) + ) ); + + this.cards$.subscribe((state) => { + console.log('Cards updated...'); + }); + } + + ngAfterViewInit(): void { + // this.autoComplete.opened.subscribe((v) => { + // if (this.searchControl.value === '') { + // // close autocompet if the search control is empty... + // this.autoCompleteTrigger.closePanel(); + // } + // }); } launchPicker() { @@ -55,6 +82,9 @@ export class SearchPage implements OnInit { async search(search: string) { // clear search box. this.searchControl.setValue(''); + if (this.autoCompleteTrigger) { + this.autoCompleteTrigger.closePanel(); + } try { const terms = search.split(';'); @@ -70,17 +100,17 @@ export class SearchPage implements OnInit { // }); } else if (q.search(/[0-9]/i) === -1) { // // its a search term. - // list.push({ qry: q, dict: 'na', type: 'Words' }); + await this.appService.getWords(q); } else if (q.search(/(H|G)[0-9]/i) !== -1) { // its a strongs lookup const dict = q.substring(0, 1).search(/h/i) !== -1 ? 'heb' : 'grk'; const strongsNumber = q.substring(1, q.length); - this.appService.getNewStrongs(strongsNumber, dict); + await this.appService.getStrongs(strongsNumber, dict); } else { // its a verse reference. if (q !== '') { const myref = new BibleReference(q.trim()); - this.appService.getNewPassage(myref); + await this.appService.getPassage(myref); } } } @@ -109,28 +139,10 @@ export class SearchPage implements OnInit { //#endregion //#region Typeahead/Autocomplete - private getSearchItems(value: string): string[] { - const filterValue = value.toLowerCase(); - - return ['One', 'Two', 'Three'].filter((option) => - option.toLowerCase().includes(filterValue) - ); - } select(selection: any): void { this.search(selection); } - @HostListener('document:click', ['$event']) - private documentClickHandler(event) { - // if (this.searchbarElem) { - // this.searchbarElem.getInputElement().then((el) => { - // if (el.contains(event.target)) { - // this.showList = false; - // } - // }); - // } - } - //#endregion } diff --git a/app/db/src/app/search/components/strongs/strongs.component.html b/app/db/src/app/search/components/strongs/strongs.component.html index db963920..c222ad79 100644 --- a/app/db/src/app/search/components/strongs/strongs.component.html +++ b/app/db/src/app/search/components/strongs/strongs.component.html @@ -1,4 +1,4 @@ -
+
article diff --git a/app/db/src/app/search/components/strongs/strongs.component.scss b/app/db/src/app/search/components/strongs/strongs.component.scss index 0bb9318f..0d3503dd 100644 --- a/app/db/src/app/search/components/strongs/strongs.component.scss +++ b/app/db/src/app/search/components/strongs/strongs.component.scss @@ -1,4 +1,4 @@ -.passage-title { +.strongs-title { background-color: var(--strongs-color-primary); } diff --git a/app/db/src/app/search/components/strongs/strongs.component.ts b/app/db/src/app/search/components/strongs/strongs.component.ts index 99a30530..4fcfb533 100644 --- a/app/db/src/app/search/components/strongs/strongs.component.ts +++ b/app/db/src/app/search/components/strongs/strongs.component.ts @@ -1,5 +1,4 @@ import { Component, ElementRef, ViewChild } from '@angular/core'; -import { BibleReference } from '../../../common/bible-reference'; import { AppService } from '../../../services/app.service'; import { CardComponent } from '../../../common/card.component'; @@ -33,18 +32,8 @@ export class StrongsComponent extends CardComponent { }); } - makePassage(p: string) { - return new BibleReference( - BibleReference.bookName(parseInt(p.split(';')[0], 10)).name + - ' ' + - p.split(';')[1] + - ':' + - p.split(';')[2] - ); - } - openPassage(p: string) { const ref = this.makePassage(p); - this.appService.getNewPassage(ref, this.cardItem); + this.appService.getPassage(ref, this.cardItem); } } diff --git a/app/db/src/app/search/components/verse-picker/verse-picker-modal.component.ts b/app/db/src/app/search/components/verse-picker/verse-picker-modal.component.ts index e2ac2ac2..930d2ceb 100644 --- a/app/db/src/app/search/components/verse-picker/verse-picker-modal.component.ts +++ b/app/db/src/app/search/components/verse-picker/verse-picker-modal.component.ts @@ -33,7 +33,7 @@ export class VersePickerModalComponent { setChapter(chapter: number) { // close the control, trigger the passage event. - this.appService.getNewPassage( + this.appService.getPassage( new BibleReference(this.book.name + ' ' + chapter) ); this.dialogRef.close(); diff --git a/app/db/src/app/search/components/words/words.component.html b/app/db/src/app/search/components/words/words.component.html new file mode 100644 index 00000000..f3c6ea57 --- /dev/null +++ b/app/db/src/app/search/components/words/words.component.html @@ -0,0 +1,47 @@ +
+ font_download + {{ cardItem.qry }} + +
+ +
+ + + + + + + + + +
diff --git a/app/db/src/app/search/components/words/words.component.scss b/app/db/src/app/search/components/words/words.component.scss new file mode 100644 index 00000000..38305573 --- /dev/null +++ b/app/db/src/app/search/components/words/words.component.scss @@ -0,0 +1,43 @@ +.words-title { + background-color: var(--words-color-primary); +} + +.card-close-button { + color: var(--words-color-accent); +} + +.card-actions { + color: var(--words-color-primary); +} + +.card-content { + overflow-y: auto; + max-height: 25rem; + font-family: var(--card-font); + font-size: var(--font-size); + padding: 0.5rem; +} +.passage-button-wrapper { + width: 33.3%; + min-width: 13rem; + display: inline-flex; +} + +.passage-button { + padding: 0.5em; + margin: 0.3rem; + background-color: var(--words-color-button); + color: #fff; + text-align: center; + cursor: pointer; + width: 100%; +} +.passage-button:hover { + background-color: var(--words-color-primary); +} + +@media screen and (max-width: 687px) { + .passage-button-wrapper { + width: 50%; + } +} diff --git a/app/db/src/app/search/components/words/words.component.ts b/app/db/src/app/search/components/words/words.component.ts new file mode 100644 index 00000000..bbdc62d1 --- /dev/null +++ b/app/db/src/app/search/components/words/words.component.ts @@ -0,0 +1,40 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { AppService } from '../../../services/app.service'; +import { CardComponent } from '../../../common/card.component'; + +@Component({ + selector: 'app-words', + templateUrl: 'words.component.html', + styleUrls: ['./words.component.scss'], + preserveWhitespaces: true, +}) +export class WordsComponent extends CardComponent { + @ViewChild('words') wordsElement; + + constructor( + protected elementRef: ElementRef, + private appService: AppService + ) { + super(elementRef); + console.log('rendering word search...'); + } + + copy() { + const html = this.wordsElement.nativeElement.innerHTML; + const text = this.wordsElement.nativeElement.innerText; + this.copyToClip(text, html); + } + + openItem(p: string) { + this.onItemClicked.emit({ + card: this.cardItem, + qry: p, + from_search_bar: false, + }); + } + + openPassage(p: string) { + const ref = this.makePassage(p); + this.appService.getPassage(ref, this.cardItem); + } +} diff --git a/app/db/src/app/services/app.service.ts b/app/db/src/app/services/app.service.ts index 2585feab..eef025cd 100644 --- a/app/db/src/app/services/app.service.ts +++ b/app/db/src/app/services/app.service.ts @@ -7,17 +7,18 @@ import { Paragraph, BiblePassage, BibleVerse, - Data, Error, BibleParagraph, BibleParagraphPassage, CardItem, DictionaryType, - StrongsResult, StrongsDefinition, StrongsCrossReference, RMACCrossReference, RMACDefinition, + WordLookupResult, + WordToStem, + IndexResult, } from '../models/app-state'; import { Section, BibleReference } from '../common/bible-reference'; import { PageTitles } from '../constants'; @@ -26,6 +27,7 @@ import * as math from 'mathjs'; const initialState: AppState = { cards: [], + autocomplete: [], savedPages: [], mainPages: [ { title: PageTitles.Search, icon: 'search' }, @@ -81,6 +83,10 @@ type AppAction = | { type: 'UPDATE_FONT_FAMILY'; family: string; + } + | { + type: 'UPDATE_AUTOCOMPLETE'; + words: string[]; }; function reducer(state: AppState, action: AppAction): AppState { @@ -90,6 +96,12 @@ function reducer(state: AppState, action: AppAction): AppState { } switch (action.type) { + case 'UPDATE_AUTOCOMPLETE': { + return { + ...state, + autocomplete: [...action.words], + }; + } case 'UPDATE_PAGES': { return { ...state, @@ -173,9 +185,17 @@ function reducer(state: AppState, action: AppAction): AppState { providedIn: 'root', }) export class AppService extends createStateService(reducer, initialState) { + private wordToStem: Map; + + private searchIndexArray: string[]; + private autocomplete: string[]; + constructor(private http: HttpClient) { super(); + + this.searchIndexArray = this.buildIndexArray().sort(); } + async getSavedPages() { this.dispatch({ type: 'UPDATE_PAGES', @@ -188,17 +208,6 @@ export class AppService extends createStateService(reducer, initialState) { }); } - async getParagraphMarkers(): Promise> { - const paras = await this.http - .get>('assets/data/bibles/paras.json') - .toPromise(); - this.dispatch({ - type: 'UPDATE_PARAGRAPHS', - paragraphs: paras, - }); - return paras; - } - removeCard(card: CardItem) { this.dispatch({ type: 'REMOVE_CARD', @@ -206,9 +215,26 @@ export class AppService extends createStateService(reducer, initialState) { }); } + private dispatchError(msg: string) { + this.dispatch({ + type: 'UPDATE_ERROR', + error: { + msg, + }, + }); + console.log(msg); + } + + private formatReferenceKey( + book: number | string, + chapter: number | string, + vs: number | string + ) { + return `${book}:${chapter}:${vs}`; + } //#region Strongs - async getNewStrongs( + async getStrongs( strongsNumber: string, dict: DictionaryType, nextToItem: CardItem = null @@ -244,23 +270,17 @@ export class AppService extends createStateService(reducer, initialState) { if (dict === 'grk') { result.prefix = 'G'; if (sn > 5624 || sn < 1) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `Strong's Number G${sn} is out of range. Strong's numbers range from 1 - 5624 in the New Testament.`, - }, - }); + 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.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `Strong's Number H${sn} is out of range. Strong's numbers range from 1 - 8674 in the Old Testament.`, - }, - }); + this.dispatchError( + `Strong's Number H${sn} is out of range. Strong's numbers range from 1 - 8674 in the Old Testament.` + ); return; } } @@ -272,12 +292,9 @@ export class AppService extends createStateService(reducer, initialState) { .toPromise(); result.def = d.find((el) => el.i === result.prefix + result.sn); } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `Unable to retrieve Strong's Data for ${result.prefix}${result.sn}`, - }, - }); + this.dispatchError( + `Unable to retrieve Strong's Data for ${result.prefix}${result.sn}` + ); return; } @@ -292,12 +309,10 @@ export class AppService extends createStateService(reducer, initialState) { } } } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `Unable to retrieve Strong\'s Cross References for ${result.prefix}${result.sn}`, - }, - }); + this.dispatchError( + `Unable to retrieve Strong\'s Cross References for ${result.prefix}${result.sn}` + ); + return; } @@ -311,12 +326,7 @@ export class AppService extends createStateService(reducer, initialState) { const d = await this.http.get(url).toPromise(); rmacCrossReferences = d; } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: 'Unable to retrieve RMAC', - }, - }); + this.dispatchError('Unable to retrieve RMAC'); return; } @@ -342,12 +352,7 @@ export class AppService extends createStateService(reducer, initialState) { } } } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: 'Unable to retrieve RMAC', - }, - }); + this.dispatchError('Unable to retrieve RMAC'); return; } } @@ -358,7 +363,7 @@ export class AppService extends createStateService(reducer, initialState) { //#region Bible Passages - async getNewPassage(ref: BibleReference, nextToItem: CardItem = null) { + async getPassage(ref: BibleReference, nextToItem: CardItem = null) { const card = await this.composeBiblePassageCardItem(ref); this.dispatch({ type: 'ADD_CARD', @@ -396,22 +401,16 @@ export class AppService extends createStateService(reducer, initialState) { }; if (Number(section.start.chapter) > section.start.book.last_chapter) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `The requested chapter ${section.start.book.name} is out of range. Please pick a chapter between 1 and ${section.end.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.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `The requested chapter ${section.end.book.name} is out of range. Please pick a chapter between 1 and ${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; } @@ -428,12 +427,7 @@ export class AppService extends createStateService(reducer, initialState) { .toPromise(); chapters.push(d); } catch (err) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `Unable to retrieve bible passage ${result.ref}.`, - }, - }); + this.dispatchError(`Unable to retrieve bible passage ${result.ref}.`); return; } } @@ -503,13 +497,7 @@ export class AppService extends createStateService(reducer, initialState) { return result; } catch (error) { - this.dispatch({ - type: 'UPDATE_ERROR', - error: { - msg: `An unknown error occurred: ${error}.`, - }, - }); - console.log(error); + this.dispatchError(`An unknown error occurred: ${error}.`); } } @@ -538,6 +526,17 @@ export class AppService extends createStateService(reducer, initialState) { return passages; } + private async getParagraphMarkers(): Promise> { + const paras = await this.http + .get>('assets/data/bibles/paras.json') + .toPromise(); + this.dispatch({ + type: 'UPDATE_PARAGRAPHS', + paragraphs: paras, + }); + return paras; + } + private convertToParagraphs( ch: BiblePassage, section: Section, @@ -576,10 +575,534 @@ export class AppService extends createStateService(reducer, initialState) { } private getRefKey(vs: BibleVerse, section: Section) { - return ( - section.start.book.book_number + ';' + section.start.chapter + ';' + vs.v + return this.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 { + // 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 = this.wordToStem.get(q); + + // handle the first case. + if (stem <= this.searchIndexArray[0]) { + results.unshift( + await this.getSearchReferences( + 'assets/data/index/' + this.searchIndexArray[0] + 'idx.json', + stem + ) + ); + break; + } + + // For each query term, figure out which xml file it is in, and get it. + // getSearchRefs returns an array of references. + 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( + 'assets/data/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(); + try { + const r = await this.http + .get('assets/data/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(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 this.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) { + /// + /// Takes two javascript arrays and returns an array + /// containing a set of values shared by arrays. + /// + // 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('assets/data/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() + '; '; + } + + 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 } diff --git a/app/db/src/styles/app.scss b/app/db/src/styles/app.scss index a55dbd6b..2c9bcfed 100644 --- a/app/db/src/styles/app.scss +++ b/app/db/src/styles/app.scss @@ -16,6 +16,11 @@ html { --strongs-color-primary: rgb(17, 70, 29); --strongs-color-accent: rgb(122, 206, 143); --strongs-heading-font-family: "Roboto Condensed"; + + --words-color-primary: rgb(0, 85, 85); + --words-color-accent: rgb(9, 172, 172); + --words-color-button: rgb(27, 133, 133); + --words-heading-font-family: "Roboto Condensed"; } body { @@ -38,8 +43,8 @@ body { } span { - line-height: 100%; - vertical-align: text-top; + line-height: 110%; + vertical-align: top; padding-left: 1rem; } }