UNPKG

@gracious.tech/fetch-client

Version:

Interact with a fetch(bible) collection in an API-like way

4 lines 155 kB
{ "version": 3, "sources": ["../../references/src/utils.ts", "../../references/src/data.ts", "../../references/src/last_verse.ts", "../../references/src/passage.ts", "../../references/src/detect.ts", "../../references/src/stats.ts", "../src/utils.ts", "../src/book.ts", "../src/licenses.ts", "../src/translation.ts", "../src/collection.ts", "../src/crossref.ts", "../src/client.ts"], "sourcesContent": ["\n// Safer integer parsing that returns null for invalid instead of NaN\nexport function parse_int(input:string, min?:number, max?:number):number|null{\n let int = parseInt(input, 10)\n if (Number.isNaN(int)){\n return null\n }\n if (min !== undefined){\n int = Math.max(int, min)\n }\n if (max !== undefined){\n int = Math.min(int, max)\n }\n return int\n}\n", "\n// Bible book ids in traditional order\nexport const books_ordered:readonly string[] = Object.freeze([\n 'gen', 'exo', 'lev', 'num', 'deu', 'jos', 'jdg', 'rut', '1sa', '2sa', '1ki', '2ki', '1ch',\n '2ch', 'ezr', 'neh', 'est', 'job', 'psa', 'pro', 'ecc', 'sng', 'isa', 'jer', 'lam', 'ezk',\n 'dan', 'hos', 'jol', 'amo', 'oba', 'jon', 'mic', 'nam', 'hab', 'zep', 'hag', 'zec', 'mal',\n 'mat', 'mrk', 'luk', 'jhn', 'act', 'rom', '1co', '2co', 'gal', 'eph', 'php', 'col', '1th',\n '2th', '1ti', '2ti', 'tit', 'phm', 'heb', 'jas', '1pe', '2pe', '1jn', '2jn', '3jn', 'jud',\n 'rev',\n])\n\n\n// Usual English names of Bible books\nexport const book_names_english:Readonly<Record<string, string>> = Object.freeze({\n 'gen': \"Genesis\",\n 'exo': \"Exodus\",\n 'lev': \"Leviticus\",\n 'num': \"Numbers\",\n 'deu': \"Deuteronomy\",\n 'jos': \"Joshua\",\n 'jdg': \"Judges\",\n 'rut': \"Ruth\",\n '1sa': \"1 Samuel\",\n '2sa': \"2 Samuel\",\n '1ki': \"1 Kings\",\n '2ki': \"2 Kings\",\n '1ch': \"1 Chronicles\",\n '2ch': \"2 Chronicles\",\n 'ezr': \"Ezra\",\n 'neh': \"Nehemiah\",\n 'est': \"Esther\",\n 'job': \"Job\",\n 'psa': \"Psalms\",\n 'pro': \"Proverbs\",\n 'ecc': \"Ecclesiastes\",\n 'sng': \"Song of Songs\",\n 'isa': \"Isaiah\",\n 'jer': \"Jeremiah\",\n 'lam': \"Lamentations\",\n 'ezk': \"Ezekiel\",\n 'dan': \"Daniel\",\n 'hos': \"Hosea\",\n 'jol': \"Joel\",\n 'amo': \"Amos\",\n 'oba': \"Obadiah\",\n 'jon': \"Jonah\",\n 'mic': \"Micah\",\n 'nam': \"Nahum\",\n 'hab': \"Habakkuk\",\n 'zep': \"Zephaniah\",\n 'hag': \"Haggai\",\n 'zec': \"Zechariah\",\n 'mal': \"Malachi\",\n 'mat': \"Matthew\",\n 'mrk': \"Mark\",\n 'luk': \"Luke\",\n 'jhn': \"John\",\n 'act': \"Acts\",\n 'rom': \"Romans\",\n '1co': \"1 Corinthians\",\n '2co': \"2 Corinthians\",\n 'gal': \"Galatians\",\n 'eph': \"Ephesians\",\n 'php': \"Philippians\",\n 'col': \"Colossians\",\n '1th': \"1 Thessalonians\",\n '2th': \"2 Thessalonians\",\n '1ti': \"1 Timothy\",\n '2ti': \"2 Timothy\",\n 'tit': \"Titus\",\n 'phm': \"Philemon\",\n 'heb': \"Hebrews\",\n 'jas': \"James\",\n '1pe': \"1 Peter\",\n '2pe': \"2 Peter\",\n '1jn': \"1 John\",\n '2jn': \"2 John\",\n '3jn': \"3 John\",\n 'jud': \"Jude\",\n 'rev': \"Revelation\",\n})\n\n\n// Usual English abbreviations of Bible books\n// NOTE Aiming for short but also easily recognisable\nexport const book_abbrev_english:Readonly<Record<string, string>> = Object.freeze({\n 'gen': \"Gen\",\n 'exo': \"Exo\",\n 'lev': \"Lev\",\n 'num': \"Num\",\n 'deu': \"Deut\",\n 'jos': \"Josh\",\n 'jdg': \"Judg\",\n 'rut': \"Ruth\",\n '1sa': \"1 Sam\",\n '2sa': \"2 Sam\",\n '1ki': \"1 King\",\n '2ki': \"2 King\",\n '1ch': \"1 Chr\",\n '2ch': \"2 Chr\",\n 'ezr': \"Ezra\",\n 'neh': \"Neh\",\n 'est': \"Est\",\n 'job': \"Job\",\n 'psa': \"Psalm\",\n 'pro': \"Prov\",\n 'ecc': \"Ecc\",\n 'sng': \"Song\",\n 'isa': \"Isa\",\n 'jer': \"Jer\",\n 'lam': \"Lam\",\n 'ezk': \"Ezek\",\n 'dan': \"Dan\",\n 'hos': \"Hos\",\n 'jol': \"Joel\",\n 'amo': \"Amos\",\n 'oba': \"Obad\",\n 'jon': \"Jonah\",\n 'mic': \"Micah\",\n 'nam': \"Nahum\",\n 'hab': \"Hab\",\n 'zep': \"Zeph\",\n 'hag': \"Hag\",\n 'zec': \"Zech\",\n 'mal': \"Mal\",\n 'mat': \"Matt\",\n 'mrk': \"Mark\",\n 'luk': \"Luke\",\n 'jhn': \"John\",\n 'act': \"Acts\",\n 'rom': \"Rom\",\n '1co': \"1 Cor\",\n '2co': \"2 Cor\",\n 'gal': \"Gal\",\n 'eph': \"Eph\",\n 'php': \"Phil\",\n 'col': \"Col\",\n '1th': \"1 Thes\",\n '2th': \"2 Thes\",\n '1ti': \"1 Tim\",\n '2ti': \"2 Tim\",\n 'tit': \"Titus\",\n 'phm': \"Phil\",\n 'heb': \"Heb\",\n 'jas': \"James\",\n '1pe': \"1 Pet\",\n '2pe': \"2 Pet\",\n '1jn': \"1 John\",\n '2jn': \"2 John\",\n '3jn': \"3 John\",\n 'jud': \"Jude\",\n 'rev': \"Rev\",\n})\n\n\n// Special English abbreviations of book names\n// These could in theory abbreviate multiple books, and are only specified because of convention\n// See https://www.logos.com/bible-book-abbreviations\n// These are hard-coded so that they will result in a correct match if English default is kept\nexport const english_abbrev_include:readonly [string, string][] = Object.freeze([\n // [code, abbrev]\n ['num', \"nm\"],\n ['ezr', \"ez\"],\n ['mic', \"mc\"],\n ['hab', \"hb\"],\n ['jhn', \"jn\"],\n ['php', \"phil\"],\n ['phm', \"pm\"],\n ['jas', \"jm\"],\n ['jud', \"jud\"],\n ['jud', \"jd\"],\n])\n\n\n// Abbreviations that should be ignored for being too vague\n// Words are only added if (1) common and (2) could actually match a book\n// E.g. \"So. 1\" is ok but not \"So 1 cat\"\nexport const english_abbrev_exclude:readonly string[] =\n Object.freeze([\"is\", \"so\", \"at\", \"am\", \"me\", \"he\", \"hi\"])\n", "\n// The number of verses for every chapter of the Bible\n// WARN Chapters are zero-indexed (i.e chapter 1 is at index 0)\nexport const last_verse:Readonly<Record<string, number[]>> = Object.freeze({\n '1ch': [\n 54,\n 55,\n 24,\n 43,\n 26,\n 81,\n 40,\n 40,\n 44,\n 14,\n 47,\n 40,\n 14,\n 17,\n 29,\n 43,\n 27,\n 17,\n 19,\n 8,\n 30,\n 19,\n 32,\n 31,\n 31,\n 32,\n 34,\n 21,\n 30,\n ],\n '1co': [\n 31,\n 16,\n 23,\n 21,\n 13,\n 20,\n 40,\n 13,\n 27,\n 33,\n 34,\n 31,\n 13,\n 40,\n 58,\n 24,\n ],\n '1jn': [\n 10,\n 29,\n 24,\n 21,\n 21,\n ],\n '1ki': [\n 53,\n 46,\n 28,\n 34,\n 18,\n 38,\n 51,\n 66,\n 28,\n 29,\n 43,\n 33,\n 34,\n 31,\n 34,\n 34,\n 24,\n 46,\n 21,\n 43,\n 29,\n 53,\n ],\n '1pe': [\n 25,\n 25,\n 22,\n 19,\n 14,\n ],\n '1sa': [\n 28,\n 36,\n 21,\n 22,\n 12,\n 21,\n 17,\n 22,\n 27,\n 27,\n 15,\n 25,\n 23,\n 52,\n 35,\n 23,\n 58,\n 30,\n 24,\n 42,\n 15,\n 23,\n 29,\n 22,\n 44,\n 25,\n 12,\n 25,\n 11,\n 31,\n 13,\n ],\n '1th': [\n 10,\n 20,\n 13,\n 18,\n 28,\n ],\n '1ti': [\n 20,\n 15,\n 16,\n 16,\n 25,\n 21,\n ],\n '2ch': [\n 17,\n 18,\n 17,\n 22,\n 14,\n 42,\n 22,\n 18,\n 31,\n 19,\n 23,\n 16,\n 22,\n 15,\n 19,\n 14,\n 19,\n 34,\n 11,\n 37,\n 20,\n 12,\n 21,\n 27,\n 28,\n 23,\n 9,\n 27,\n 36,\n 27,\n 21,\n 33,\n 25,\n 33,\n 27,\n 23,\n ],\n '2co': [\n 24,\n 17,\n 18,\n 18,\n 21,\n 18,\n 16,\n 24,\n 15,\n 18,\n 33,\n 21,\n 14,\n ],\n '2jn': [\n 13,\n ],\n '2ki': [\n 18,\n 25,\n 27,\n 44,\n 27,\n 33,\n 20,\n 29,\n 37,\n 36,\n 21,\n 21,\n 25,\n 29,\n 38,\n 20,\n 41,\n 37,\n 37,\n 21,\n 26,\n 20,\n 37,\n 20,\n 30,\n ],\n '2pe': [\n 21,\n 22,\n 18,\n ],\n '2sa': [\n 27,\n 32,\n 39,\n 12,\n 25,\n 23,\n 29,\n 18,\n 13,\n 19,\n 27,\n 31,\n 39,\n 33,\n 37,\n 23,\n 29,\n 33,\n 43,\n 26,\n 22,\n 51,\n 39,\n 25,\n ],\n '2th': [\n 12,\n 17,\n 18,\n ],\n '2ti': [\n 18,\n 26,\n 17,\n 22,\n ],\n '3jn': [\n 15,\n ],\n 'act': [\n 26,\n 47,\n 26,\n 37,\n 42,\n 15,\n 60,\n 40,\n 43,\n 48,\n 30,\n 25,\n 52,\n 28,\n 41,\n 40,\n 34,\n 28,\n 41,\n 38,\n 40,\n 30,\n 35,\n 27,\n 27,\n 32,\n 44,\n 31,\n ],\n 'amo': [\n 15,\n 16,\n 15,\n 13,\n 27,\n 14,\n 17,\n 14,\n 15,\n ],\n 'col': [\n 29,\n 23,\n 25,\n 18,\n ],\n 'dan': [\n 21,\n 49,\n 30,\n 37,\n 31,\n 28,\n 28,\n 27,\n 27,\n 21,\n 45,\n 13,\n ],\n 'deu': [\n 46,\n 37,\n 29,\n 49,\n 33,\n 25,\n 26,\n 20,\n 29,\n 22,\n 32,\n 32,\n 18,\n 29,\n 23,\n 22,\n 20,\n 22,\n 21,\n 20,\n 23,\n 30,\n 25,\n 22,\n 19,\n 19,\n 26,\n 68,\n 29,\n 20,\n 30,\n 52,\n 29,\n 12,\n ],\n 'ecc': [\n 18,\n 26,\n 22,\n 16,\n 20,\n 12,\n 29,\n 17,\n 18,\n 20,\n 10,\n 14,\n ],\n 'eph': [\n 23,\n 22,\n 21,\n 32,\n 33,\n 24,\n ],\n 'est': [\n 22,\n 23,\n 15,\n 17,\n 14,\n 14,\n 10,\n 17,\n 32,\n 3,\n ],\n 'exo': [\n 22,\n 25,\n 22,\n 31,\n 23,\n 30,\n 25,\n 32,\n 35,\n 29,\n 10,\n 51,\n 22,\n 31,\n 27,\n 36,\n 16,\n 27,\n 25,\n 26,\n 36,\n 31,\n 33,\n 18,\n 40,\n 37,\n 21,\n 43,\n 46,\n 38,\n 18,\n 35,\n 23,\n 35,\n 35,\n 38,\n 29,\n 31,\n 43,\n 38,\n ],\n 'ezk': [\n 28,\n 10,\n 27,\n 17,\n 17,\n 14,\n 27,\n 18,\n 11,\n 22,\n 25,\n 28,\n 23,\n 23,\n 8,\n 63,\n 24,\n 32,\n 14,\n 49,\n 32,\n 31,\n 49,\n 27,\n 17,\n 21,\n 36,\n 26,\n 21,\n 26,\n 18,\n 32,\n 33,\n 31,\n 15,\n 38,\n 28,\n 23,\n 29,\n 49,\n 26,\n 20,\n 27,\n 31,\n 25,\n 24,\n 23,\n 35,\n ],\n 'ezr': [\n 11,\n 70,\n 13,\n 24,\n 17,\n 22,\n 28,\n 36,\n 15,\n 44,\n ],\n 'gal': [\n 24,\n 21,\n 29,\n 31,\n 26,\n 18,\n ],\n 'gen': [\n 31,\n 25,\n 24,\n 26,\n 32,\n 22,\n 24,\n 22,\n 29,\n 32,\n 32,\n 20,\n 18,\n 24,\n 21,\n 16,\n 27,\n 33,\n 38,\n 18,\n 34,\n 24,\n 20,\n 67,\n 34,\n 35,\n 46,\n 22,\n 35,\n 43,\n 55,\n 32,\n 20,\n 31,\n 29,\n 43,\n 36,\n 30,\n 23,\n 23,\n 57,\n 38,\n 34,\n 34,\n 28,\n 34,\n 31,\n 22,\n 33,\n 26,\n ],\n 'hab': [\n 17,\n 20,\n 19,\n ],\n 'hag': [\n 15,\n 23,\n ],\n 'heb': [\n 14,\n 18,\n 19,\n 16,\n 14,\n 20,\n 28,\n 13,\n 28,\n 39,\n 40,\n 29,\n 25,\n ],\n 'hos': [\n 11,\n 23,\n 5,\n 19,\n 15,\n 11,\n 16,\n 14,\n 17,\n 15,\n 12,\n 14,\n 16,\n 9,\n ],\n 'isa': [\n 31,\n 22,\n 26,\n 6,\n 30,\n 13,\n 25,\n 22,\n 21,\n 34,\n 16,\n 6,\n 22,\n 32,\n 9,\n 14,\n 14,\n 7,\n 25,\n 6,\n 17,\n 25,\n 18,\n 23,\n 12,\n 21,\n 13,\n 29,\n 24,\n 33,\n 9,\n 20,\n 24,\n 17,\n 10,\n 22,\n 38,\n 22,\n 8,\n 31,\n 29,\n 25,\n 28,\n 28,\n 25,\n 13,\n 15,\n 22,\n 26,\n 11,\n 23,\n 15,\n 12,\n 17,\n 13,\n 12,\n 21,\n 14,\n 21,\n 22,\n 11,\n 12,\n 19,\n 12,\n 25,\n 24,\n ],\n 'jas': [\n 27,\n 26,\n 18,\n 17,\n 20,\n ],\n 'jdg': [\n 36,\n 23,\n 31,\n 24,\n 31,\n 40,\n 25,\n 35,\n 57,\n 18,\n 40,\n 15,\n 25,\n 20,\n 20,\n 31,\n 13,\n 31,\n 30,\n 48,\n 25,\n ],\n 'jer': [\n 19,\n 37,\n 25,\n 31,\n 31,\n 30,\n 34,\n 22,\n 26,\n 25,\n 23,\n 17,\n 27,\n 22,\n 21,\n 21,\n 27,\n 23,\n 15,\n 18,\n 14,\n 30,\n 40,\n 10,\n 38,\n 24,\n 22,\n 17,\n 32,\n 24,\n 40,\n 44,\n 26,\n 22,\n 19,\n 32,\n 21,\n 28,\n 18,\n 16,\n 18,\n 22,\n 13,\n 30,\n 5,\n 28,\n 7,\n 47,\n 39,\n 46,\n 64,\n 34,\n ],\n 'jhn': [\n 51,\n 25,\n 36,\n 54,\n 47,\n 71,\n 53,\n 59,\n 41,\n 42,\n 57,\n 50,\n 38,\n 31,\n 27,\n 33,\n 26,\n 40,\n 42,\n 31,\n 25,\n ],\n 'job': [\n 22,\n 13,\n 26,\n 21,\n 27,\n 30,\n 21,\n 22,\n 35,\n 22,\n 20,\n 25,\n 28,\n 22,\n 35,\n 22,\n 16,\n 21,\n 29,\n 29,\n 34,\n 30,\n 17,\n 25,\n 6,\n 14,\n 23,\n 28,\n 25,\n 31,\n 40,\n 22,\n 33,\n 37,\n 16,\n 33,\n 24,\n 41,\n 30,\n 24,\n 34,\n 17,\n ],\n 'jol': [\n 20,\n 32,\n 21,\n ],\n 'jon': [\n 17,\n 10,\n 10,\n 11,\n ],\n 'jos': [\n 18,\n 24,\n 17,\n 24,\n 15,\n 27,\n 26,\n 35,\n 27,\n 43,\n 23,\n 24,\n 33,\n 15,\n 63,\n 10,\n 18,\n 28,\n 51,\n 9,\n 45,\n 34,\n 16,\n 33,\n ],\n 'jud': [\n 25,\n ],\n 'lam': [\n 22,\n 22,\n 66,\n 22,\n 22,\n ],\n 'lev': [\n 17,\n 16,\n 17,\n 35,\n 19,\n 30,\n 38,\n 36,\n 24,\n 20,\n 47,\n 8,\n 59,\n 57,\n 33,\n 34,\n 16,\n 30,\n 37,\n 27,\n 24,\n 33,\n 44,\n 23,\n 55,\n 46,\n 34,\n ],\n 'luk': [\n 80,\n 52,\n 38,\n 44,\n 39,\n 49,\n 50,\n 56,\n 62,\n 42,\n 54,\n 59,\n 35,\n 35,\n 32,\n 31,\n 37,\n 43,\n 48,\n 47,\n 38,\n 71,\n 56,\n 53,\n ],\n 'mal': [\n 14,\n 17,\n 18,\n 6,\n ],\n 'mat': [\n 25,\n 23,\n 17,\n 25,\n 48,\n 34,\n 29,\n 34,\n 38,\n 42,\n 30,\n 50,\n 58,\n 36,\n 39,\n 28,\n 27,\n 35,\n 30,\n 34,\n 46,\n 46,\n 39,\n 51,\n 46,\n 75,\n 66,\n 20,\n ],\n 'mic': [\n 16,\n 13,\n 12,\n 13,\n 15,\n 16,\n 20,\n ],\n 'mrk': [\n 45,\n 28,\n 35,\n 41,\n 43,\n 56,\n 37,\n 38,\n 50,\n 52,\n 33,\n 44,\n 37,\n 72,\n 47,\n // Mark's ending: 1-8 common, 9-20 long ending\n // Some also add the alternate short ending to end of long ending with 21-22\n // Not supporting 21-22 as whether ending abrupt/long/short it definitely wasn't both\n 20,\n ],\n 'nam': [\n 15,\n 13,\n 19,\n ],\n 'neh': [\n 11,\n 20,\n 32,\n 23,\n 19,\n 19,\n 73,\n 18,\n 38,\n 39,\n 36,\n 47,\n 31,\n ],\n 'num': [\n 54,\n 34,\n 51,\n 49,\n 31,\n 27,\n 89,\n 26,\n 23,\n 36,\n 35,\n 16,\n 33,\n 45,\n 41,\n 50,\n 13,\n 32,\n 22,\n 29,\n 35,\n 41,\n 30,\n 25,\n 18,\n 65,\n 23,\n 31,\n 40,\n 16,\n 54,\n 42,\n 56,\n 29,\n 34,\n 13,\n ],\n 'oba': [\n 21,\n ],\n 'phm': [\n 25,\n ],\n 'php': [\n 30,\n 30,\n 21,\n 23,\n ],\n 'pro': [\n 33,\n 22,\n 35,\n 27,\n 23,\n 35,\n 27,\n 36,\n 18,\n 32,\n 31,\n 28,\n 25,\n 35,\n 33,\n 33,\n 28,\n 24,\n 29,\n 30,\n 31,\n 29,\n 35,\n 34,\n 28,\n 28,\n 27,\n 28,\n 27,\n 33,\n 31,\n ],\n 'psa': [\n 6,\n 12,\n 8,\n 8,\n 12,\n 10,\n 17,\n 9,\n 20,\n 18,\n 7,\n 8,\n 6,\n 7,\n 5,\n 11,\n 15,\n 50,\n 14,\n 9,\n 13,\n 31,\n 6,\n 10,\n 22,\n 12,\n 14,\n 9,\n 11,\n 12,\n 24,\n 11,\n 22,\n 22,\n 28,\n 12,\n 40,\n 22,\n 13,\n 17,\n 13,\n 11,\n 5,\n 26,\n 17,\n 11,\n 9,\n 14,\n 20,\n 23,\n 19,\n 9,\n 6,\n 7,\n 23,\n 13,\n 11,\n 11,\n 17,\n 12,\n 8,\n 12,\n 11,\n 10,\n 13,\n 20,\n 7,\n 35,\n 36,\n 5,\n 24,\n 20,\n 28,\n 23,\n 10,\n 12,\n 20,\n 72,\n 13,\n 19,\n 16,\n 8,\n 18,\n 12,\n 13,\n 17,\n 7,\n 18,\n 52,\n 17,\n 16,\n 15,\n 5,\n 23,\n 11,\n 13,\n 12,\n 9,\n 9,\n 5,\n 8,\n 28,\n 22,\n 35,\n 45,\n 48,\n 43,\n 13,\n 31,\n 7,\n 10,\n 10,\n 9,\n 8,\n 18,\n 19,\n 2,\n 29,\n 176,\n 7,\n 8,\n 9,\n 4,\n 8,\n 5,\n 6,\n 5,\n 6,\n 8,\n 8,\n 3,\n 18,\n 3,\n 3,\n 21,\n 26,\n 9,\n 8,\n 24,\n 13,\n 10,\n 7,\n 12,\n 15,\n 21,\n 10,\n 20,\n 14,\n 9,\n 6,\n ],\n 'rev': [\n 20,\n 29,\n 22,\n 11,\n 14,\n 17,\n 17,\n 13,\n 21,\n 11,\n 19,\n 18, // 12:18 Many translations append 12:18 to 12:17 as the final verse\n 18,\n 20,\n 8,\n 21,\n 18,\n 24,\n 21,\n 15,\n 27,\n 21,\n ],\n 'rom': [\n 32,\n 29,\n 31,\n 25,\n 21,\n 23,\n 25,\n 39,\n 33,\n 21,\n 36,\n 21,\n 14,\n 26,\n 33,\n 27,\n ],\n 'rut': [\n 22,\n 23,\n 18,\n 22,\n ],\n 'sng': [\n 17,\n 17,\n 11,\n 16,\n 16,\n 13,\n 13,\n 14,\n ],\n 'tit': [\n 16,\n 15,\n 15,\n ],\n 'zec': [\n 21,\n 13,\n 10,\n 14,\n 11,\n 15,\n 14,\n 23,\n 17,\n 12,\n 17,\n 14,\n 9,\n 21,\n ],\n 'zep': [\n 18,\n 15,\n 20,\n ],\n})\n", "\nimport {parse_int} from './utils.js'\nimport {books_ordered, book_names_english, english_abbrev_include,\n english_abbrev_exclude} from './data.js'\nimport {last_verse} from './last_verse.js'\n\n\nexport interface PassageArgs {\n book:string\n start_chapter?:number|undefined|null\n start_verse?:number|undefined|null\n end_chapter?:number|undefined|null\n end_verse?:number|undefined|null\n}\n\nexport type ReferenceType = 'book'|'chapter'|'verse'|'range_verses'|'range_chapters'|'range_multi'\n\nexport type BookNamesArg = Record<string, string>|[string, string][]\n\n\n// Parse verses reference string into an object (but does not validate numbers)\nexport function _verses_str_to_obj(ref:string){\n\n // Clean ref\n ref = ref.replace(/ /g, '') // Remove spaces\n .replace(/\\./g, ':').replace(/\uFF1A/gu, ':') // Normalise chap/verse seperators to common colon\n .replace(/\\p{Dash}/gu, '-') // Normalise range separators to common hyphen\n\n // Init props\n let start_chapter:number|undefined\n let start_verse:number|undefined\n let end_chapter:number|undefined\n let end_verse:number|undefined\n\n if (!ref.includes(':')){\n // Dealing with chapters only\n const parts = ref.split('-')\n start_chapter = parse_int(parts[0]!) ?? undefined\n end_chapter = parse_int(parts[1] ?? '') ?? undefined\n } else {\n // Includes verses\n const parts = ref.split('-')\n const start_parts = parts[0]!.split(':')\n start_chapter = parse_int(start_parts[0]!) ?? undefined\n start_verse = parse_int(start_parts[1] ?? '') ?? undefined\n if (parts[1]){\n // Is a range\n const end_parts = parts[1].split(':')\n if (end_parts.length > 1){\n // Specifies end chapter\n end_chapter = parse_int(end_parts[0]!) ?? undefined\n end_verse = parse_int(end_parts[1]!) ?? undefined\n } else {\n // End verse is in same chapter\n end_verse = parse_int(end_parts[0]!) ?? undefined\n }\n }\n }\n\n return {start_chapter, start_verse, end_chapter, end_verse}\n}\n\n\n// Get book USX code from the book name or an abbreviation of it\n// This should be language neutral (though some English special cases are included)\n// `book_names` is an array so that multiple names for same book can be provided\nexport function _detect_book(input:string, book_names:[string, string][],\n exclude_book_names:string[]=[], match_from_start=true):string|null{\n\n // Clean util to be used for both input and book names\n const clean = (string:string) => {\n return string\n .trim().toLowerCase()\n .replace(/^i /, '1').replace('1st ', '1').replace('first ', '1')\n .replace(/^ii /, '2').replace('2nd ', '2').replace('second ', '2')\n .replace(/^iii /, '3').replace('3rd ', '3').replace('third ', '3')\n .replace(/[^\\d\\p{Letter}]/gui, '')\n }\n\n // Clean the input\n input = clean(input)\n if (!input){\n return null // So know have at least 1 char\n }\n\n // Ignore if excluded\n exclude_book_names = exclude_book_names.map(name => clean(name))\n if (exclude_book_names.includes(input)){\n return null\n }\n\n // If a direct match to a code, just return it\n // This allows passing a book code when the human name is not available\n if (books_ordered.includes(input)){\n return input\n }\n\n // Normalise book names and ensure no empty strings\n const normalised = book_names.map(([code, name]) => ([code, clean(name)] as [string, string]))\n .filter(([code, name]) => name)\n\n // See if input matches or abbreviates any book name\n const matches:[string, string][] = []\n for (const [code, name] of normalised){\n if (input === name){\n return code // Return straight away if an exact match (even if could prefix multiple)\n } else if (name.startsWith(input)){\n matches.push([code, name])\n }\n }\n\n // Only return if unique match\n if (matches.length === 1){\n return matches[0]![0]\n } else if (matches.length){\n return null // Multiple matches so input must be too vague\n }\n\n // Try fuzzy regex, since vowels are often removed in abbreviations\n // NOTE Constructed regex should be safe as only digits and letters are allowed in input\n let input_regex_str = input.split('').join('.{0,4}')\n if (match_from_start){\n if (['1', '2', '3'].includes(input[0]!)){\n // Must match first two chars from start if first char is a number\n input_regex_str = '^' + input[0]! + input.slice(1).split('').join('.{0,4}')\n } else {\n input_regex_str = '^' + input_regex_str\n }\n }\n const input_regex = new RegExp(input_regex_str)\n const fuzzy_matches = normalised.filter(([code, name]) => input_regex.test(name))\n if (fuzzy_matches.length === 1){\n return fuzzy_matches[0]![0]\n }\n\n return null\n}\n\n\nexport class PassageReference {\n\n readonly type:ReferenceType\n readonly range:boolean\n readonly book:string\n readonly ot:boolean\n readonly nt:boolean\n readonly start_chapter:number\n readonly start_verse:number\n readonly end_chapter:number\n readonly end_verse:number\n readonly args_valid:boolean // Whether the original input was valid or not\n readonly _args:PassageArgs // The args originally given when creating this ref\n\n /* Force a given passage reference to be valid (providing as much or as little as desired)\n Chapter and verse numbers will be forced to their closest valid equivalent.\n All properties are returned and `type`/`range` signifies what kind of reference it is.\n */\n constructor(book:string, chapter?:number, verse?:number)\n constructor(reference:PassageArgs)\n constructor(book_or_obj:string|PassageArgs, chapter?:number, verse?:number){\n\n // Normalize args\n if (typeof book_or_obj !== 'string'){\n // Destructure so extraneous properties aren't preserved in `this._args`\n this._args = {\n book: book_or_obj.book,\n start_chapter: book_or_obj.start_chapter,\n start_verse: book_or_obj.start_verse,\n end_chapter: book_or_obj.end_chapter,\n end_verse: book_or_obj.end_verse,\n }\n } else {\n this._args = {\n book: book_or_obj,\n start_chapter: chapter,\n start_verse: verse,\n }\n }\n\n // Detect what props are provided (will use later)\n const chapters_given = typeof this._args.start_chapter === 'number'\n || typeof this._args.end_chapter === 'number'\n const verses_given = typeof this._args.start_verse === 'number'\n || typeof this._args.end_verse === 'number'\n\n // Provide defaults\n this.book = this._args.book\n this.start_chapter = this._args.start_chapter ?? 1\n this.start_verse = this._args.start_verse ?? 1\n this.end_chapter = this._args.end_chapter ?? this._args.start_chapter ?? 1\n // If end_chapter given then dealing with whole chapters, otherwise a non-range\n this.end_verse = this._args.end_verse ?? (this._args.end_chapter ? 999 : 1)\n\n // Validate book\n if (books_ordered.indexOf(this.book) === -1){\n this.book = 'gen'\n }\n\n // Ensure start chapter is valid\n const last_verse_book = last_verse[this.book]!\n if (this.start_chapter < 1){\n this.start_chapter = 1\n this.start_verse = 1\n } else if (this.start_chapter > last_verse_book.length){\n this.start_chapter = last_verse_book.length\n this.start_verse = last_verse_book[last_verse_book.length-1]!\n }\n\n // Ensure start verse is valid\n this.start_verse = Math.min(Math.max(this.start_verse, 1),\n last_verse_book[this.start_chapter-1]!)\n\n // Ensure end is not before start\n if (this.end_chapter < this.start_chapter ||\n (this.end_chapter === this.start_chapter && this.end_verse < this.start_verse)){\n this.end_chapter = this.start_chapter\n this.end_verse = this.start_verse\n }\n\n // Ensure end chapter is not invalid (already know is same or later than start)\n if (this.end_chapter > last_verse_book.length){\n this.end_chapter = last_verse_book.length\n this.end_verse = last_verse_book[last_verse_book.length-1]!\n }\n\n // Ensure end verse is valid\n this.end_verse = Math.min(Math.max(this.end_verse, 1), last_verse_book[this.end_chapter-1]!)\n\n // Determine type\n const chapters_same = this.start_chapter === this.end_chapter\n const verses_same = this.start_verse === this.end_verse\n if (chapters_same && verses_same){\n this.type = chapters_given ? (verses_given ? 'verse' : 'chapter') : 'book'\n } else {\n this.type = chapters_same ? 'range_verses' :\n (verses_given ? 'range_multi': 'range_chapters')\n }\n\n // Identify if range completes chapters\n if (this.type === 'range_multi' && this.start_verse === 1\n && this.end_verse === last_verse_book[this.end_chapter-1]){\n this.type = 'range_chapters'\n }\n\n // Determine range and testament properties\n this.range = this.type.startsWith('range_')\n this.ot = books_ordered.indexOf(this.book) < 39\n this.nt = !this.ot\n\n // Determine if original input was valid\n const determine_args_valid = () => {\n\n // Verify parts stayed same, but only those provided (ignoring undefined)\n if (this._args.book !== this.book){\n return false\n }\n const props = ['start_chapter', 'start_verse', 'end_chapter', 'end_verse'] as const\n for (const prop of props){\n if (Number.isInteger(this._args[prop]) && this._args[prop] !== this[prop]){\n return false\n }\n }\n\n // Also fail if provided args don't make sense\n // NOTE Already know that no numbers in ref are 0/false due to above\n if (!this._args.start_chapter && (this._args.end_chapter || this._args.start_verse\n || this._args.end_verse)){\n return false // e.g. Matt :1\n }\n if (this._args.end_verse && !this._args.start_verse){\n return false // e.g. Matt 1:-1\n }\n if (this._args.start_verse && this._args.end_chapter && !this._args.end_verse){\n return false // e.g. Matt 1:1-2:\n }\n\n return true\n }\n this.args_valid = determine_args_valid()\n }\n\n // Parse passage reference string\n // book_names can be a list if a single book has multiple names [[\"gen\", \"Genesis\"], ...]\n static from_string(reference:string, book_names?:BookNamesArg, exclude_book_names?:string[],\n min_chars=2, match_from_start=true):PassageReference|null{\n\n // Default to English book names if none given\n // NOTE Don't always include in case creates false positives in some languages\n if (!book_names){\n book_names = [...Object.entries(book_names_english), ...english_abbrev_include]\n if (!exclude_book_names){\n exclude_book_names = [...english_abbrev_exclude]\n }\n }\n\n // Conform book_names to a list\n const book_names_list = Array.isArray(book_names) ? book_names : Object.entries(book_names)\n\n // Trim\n reference = reference.trim()\n\n // Find start of first digit, except if start of string (e.g. 1 Sam)\n let verses_start = reference.slice(1).search(/\\d/) + 1\n if (verses_start === 0){ // NOTE +1 above means never -1 and 0 is a no match\n verses_start = reference.length\n }\n\n // Fail if book string is less than min chars\n // Check before cleaning the value so that \"So. 1\" works but \"So 1\" doesn't (if min were 3)\n const book_str = reference.slice(0, verses_start).trim()\n if (book_str.length < min_chars){\n return null\n }\n\n // If book can be parsed, ref is valid even if verse range can't be parsed\n const book_code = _detect_book(book_str, book_names_list, exclude_book_names,\n match_from_start)\n if (!book_code){\n return null\n }\n\n // Parse verses\n const verses_str = reference.slice(verses_start)\n let verses = _verses_str_to_obj(verses_str)\n\n // Interpret single digits as verses for single chapter books\n const single_chapter = ['2jn', '3jn', 'jud', 'oba', 'phm'].includes(book_code)\n if (single_chapter && verses.start_chapter\n && verses.start_verse === undefined && verses.end_verse === undefined){\n verses = _verses_str_to_obj('1:' + verses_str)\n }\n\n return new PassageReference({book: book_code, ...verses})\n }\n\n // Return a new reference that extends from start of first ref to end of second ref\n static from_refs(start:PassageReference, end:PassageReference){\n return new PassageReference({\n book: start.book,\n start_chapter: start.start_chapter,\n start_verse: start.start_verse,\n end_chapter: end.end_chapter,\n end_verse: end.end_verse,\n })\n }\n\n // Get name for book (defaults to English when book names not provided)\n get_book_string(book_names:Record<string, string>={}):string{\n // WARN A value may be undefined or empty string\n return book_names[this.book] || book_names_english[this.book]!\n }\n\n // Get string representation of verses\n get_verses_string(verse_sep=':', range_sep='-'):string{\n\n if (this.type === 'book'){\n return ''\n } else if (this.type === 'chapter'){\n return `${this.start_chapter}`\n } else if (this.type === 'range_chapters'){\n return `${this.start_chapter}${range_sep}${this.end_chapter}`\n } else if (this.type === 'verse'){\n return `${this.start_chapter}${verse_sep}${this.start_verse}`\n }\n\n // Other ranges\n let out = `${this.start_chapter}${verse_sep}${this.start_verse}${range_sep}`\n if (this.end_chapter !== this.start_chapter){\n out += `${this.end_chapter}${verse_sep}`\n }\n return out + `${this.end_verse}`\n }\n\n // Format passage reference to a readable string\n toString(book_names:Record<string, string>={}, verse_sep=':', range_sep='-'):string{\n const out = this.get_book_string(book_names) + ' '\n + this.get_verses_string(verse_sep, range_sep)\n return out.trim()\n }\n\n // Whether this reference ends before the given chapter/verse or not\n is_before(chapter:number, verse:number):boolean{\n return this.end_chapter < chapter ||\n (this.end_chapter === chapter && this.end_verse < verse)\n }\n\n // Whether this reference starts after the given chapter/verse or not\n is_after(chapter:number, verse:number):boolean{\n return this.start_chapter > chapter ||\n (this.start_chapter === chapter && this.start_verse > verse)\n }\n\n // Whether this reference includes the given chapter/verse or not\n includes(chapter:number, verse:number):boolean{\n return !this.is_before(chapter, verse) && !this.is_after(chapter, verse)\n }\n\n // Get a reference for just the start verse of this reference (no effect if single verse)\n get_start(){\n return new PassageReference({\n book: this.book,\n start_chapter: this.start_chapter,\n start_verse: this.start_verse,\n })\n }\n\n // Get a reference for just the end verse of this reference (no effect if single verse)\n get_end(){\n return new PassageReference({\n book: this.book,\n start_chapter: this.end_chapter,\n start_verse: this.end_verse,\n })\n }\n\n // Get a reference for the verse previous to this one (accounting for chapters)\n // It can optionally be relative to the end verse, but a range is never returned (single verse)\n get_prev_verse(prev_to_end=false):PassageReference|null{\n\n // Optionally relative to end rather than start\n let chapter = prev_to_end ? this.end_chapter : this.start_chapter\n let verse = prev_to_end ? this.end_verse : this.start_verse\n\n // Ensure action possible\n if (chapter === 1 && verse === 1){\n return null\n }\n\n // Go back a verse\n if (verse === 1){\n chapter -= 1\n verse = last_verse[this.book]![chapter-1]!\n } else {\n verse -= 1\n }\n\n return new PassageReference({\n book: this.book,\n start_chapter: chapter,\n start_verse: verse,\n })\n }\n\n // Get a reference for the verse after this one (accounting for chapters)\n // It can optionally be relative to the end verse, but a range is never returned (single verse)\n get_next_verse(after_end=false):PassageReference|null{\n\n // Optionally relative to end rather than start\n let chapter = after_end ? this.end_chapter : this.start_chapter\n let verse = after_end ? this.end_verse : this.start_verse\n\n // Ensure action possible\n const last_verse_book = last_verse[this.book]!\n if (chapter === last_verse_book.length\n && verse === last_verse_book[last_verse_book.length-1]){\n return null\n }\n\n // Go forward a verse\n if (verse === last_verse_book[chapter-1]){\n chapter += 1\n verse = 1\n } else {\n verse += 1\n }\n\n return new PassageReference({\n book: this.book,\n start_chapter: chapter,\n start_verse: verse,\n })\n }\n}\n", "\nimport {PassageReference, BookNamesArg} from './passage.js'\n\n\nexport interface PassageReferenceMatch {\n ref:PassageReference\n text:string\n index:number\n index_from_prev_match:number\n}\n\n\n// Regex strings used for identifying passage references in blocks of text\n// NOTE Allow two spaces but no more, to be forgiving but not match weird text\nconst regex_verse_sep = '[:\uFF1A\\\\.]'\nconst regex_book_num_prefix = '(?:(?:[123]|I{1,3}) ? ?)?'\nconst regex_book_name_tmpl = '\\\\p{Letter}[\\\\p{Letter}\\\\p{Dash} ]{MIN_MID,16}END_LETTER\\\\.? ? ?'\nconst regex_integer_with_opt_sep =\n '\\\\d{1,3}[abc]?(?: ? ?' + regex_verse_sep + ' ? ?\\\\d{1,3}[abc]?)?'\nconst regex_verse_range = regex_integer_with_opt_sep + '(?: ? ?\\\\p{Dash} ? ?'\n + regex_integer_with_opt_sep + ')?'\nconst regex_trailing = '(?![\\\\d\\\\p{Letter}@#$%])' // Doesn't make sense to be followed by these\n\nconst regex_between_ranges = ' ? ?[,;] ? ?'\nconst regex_additional_range = regex_between_ranges + '(' + regex_verse_range + ')' + regex_trailing\n\n\n// Detect the text and position of passage references in a block of text\n// Whole books aren't detected (e.g. Philemon) only references with a range (e.g. Philemon 1)\nexport function* detect_references(text:string, book_names?:BookNamesArg,\n exclude_book_names?:string[], min_chars=2, match_from_start=true)\n :Generator<PassageReferenceMatch, null, undefined>{\n\n // Shortcut for calling from_string\n const from_string = (value:string) => {\n return PassageReference.from_string(value, book_names, exclude_book_names, min_chars,\n match_from_start)\n }\n\n // Generate regexs with dynamic value based on min_chars for book name\n // MIN_MID is -2 as first and last char already specified\n // END_LETTER is not present if min_chars is 1\n const regex_book_name = regex_book_name_tmpl\n .replace('MIN_MID', String(Math.max(0, min_chars - 2)))\n .replace('END_LETTER', min_chars > 1 ? '\\\\p{Letter}' : '')\n const regex_complete =\n regex_book_num_prefix + regex_book_name + regex_verse_range + regex_trailing\n const regex_book_check =\n regex_between_ranges + '(' + regex_book_num_prefix + regex_book_name + ')'\n\n // Create regex (will manually manipulate lastIndex property of it)\n const regex = new RegExp(regex_complete, 'uig')\n\n // Keep track of end of last match\n // This is useful for callers to know if they modify the text as they go (changing its length)\n let end_of_prev_match = 0\n\n // Loop until find a valid ref (not all regex matches will be valid)\n while (true){\n const match = regex.exec(text)\n if (!match){\n return null // Either no matches or no valid matches...\n }\n\n // Confirm match is actually a valid ref\n const ref = from_string(match[0])\n if (ref && ref.args_valid){\n yield {\n ref,\n text: match[0],\n index: match.index,\n index_from_prev_match: match.index - end_of_prev_match,\n }\n end_of_prev_match = match.index + match[0].length\n\n // See if additional ranges immediately after this ref\n // WARN Sticky flag 'y' needed to ensure match is at start of lastIndex\n const add_regex = new RegExp(regex_additional_range, 'uiy')\n add_regex.lastIndex = regex.lastIndex // Move up to where main regex is up to\n while (true){\n\n // If followed by a valid book name, skip check for additional ranges\n // E.g. (John 1:1,3, 3 John 1)\n // WARN Sticky flag 'y' needed to ensure match is at start of lastIndex\n const book_look_ahead = new RegExp(regex_book_check, 'uiy')\n book_look_ahead.lastIndex = add_regex.lastIndex\n const possible_book = book_look_ahead.exec(text)\n if (possible_book && from_string(possible_book[1]!)){\n break\n }\n\n const add_match = add_regex.exec(text)\n if (!add_match){\n break\n }\n\n // Since this regex uses a capture group, need to get index of capture\n const add_match_real_index = add_match.index + add_match[0].indexOf(add_match[1]!)\n\n // Confirm valid ref, prefixing with book (and opt end chapter) from main ref\n let prefix = ref.book\n const has_verse_sep = new RegExp(regex_verse_sep).test(add_match[1]!)\n if (!has_verse_sep && ['verse', 'range_verses', 'range_multi'].includes(ref.type)){\n prefix += `${ref.end_chapter}:`\n }\n const add_ref = from_string(prefix + add_match[1]!)\n if (!add_ref || !add_ref.args_valid){\n break\n }\n yield {\n ref: add_ref,\n text: add_match[1]!,\n index: add_match_real_index,\n index_from_prev_match: add_match_real_index - end_of_prev_match,\n }\n end_of_prev_match = add_match_real_index + add_match[1]!.length\n\n // Move main regex up to where successful additional ranges regex is up to\n // WARN Only if larger as lastIndex will reset to 0 at end of string\n if (add_regex.lastIndex > regex.lastIndex){\n regex.lastIndex = add_regex.lastIndex\n }\n }\n\n } else {\n // If invalid, try next word as match might still have included a partial ref\n // e.g. \"in 1 Corinthians 9\" -> \"in 1\" -> \"1 Corinthians 9\"\n const chars_to_next_word = match[0].indexOf(' ', 1)\n if (chars_to_next_word >= 1){\n // Backtrack to exclude just first word of previous match\n regex.lastIndex -= (match[0].length - chars_to_next_word - 1)\n }\n }\n }\n}\n", "\nimport {last_verse} from './last_verse.js'\n\n\n// Get chapter numbers for a book\nexport function get_chapters(book:string):number[]{\n // NOTE Need to +1 since chapter numbers are derived from place in last_verse array\n return [...Array(last_verse[book]!.length).keys()].map(i => i + 1)\n}\n\n\n// Get verse numbers for a chapter\nexport function get_verses(book:string, chapter:number):number[]{\n // WARN Position of each chapter is chapter-1 due to starting from 0\n return [...Array(last_verse[book]![chapter-1]).keys()].map(i => i + 1)\n}\n", "\n// @internal\nexport async function request(url:string):Promise<string>{\n // Request the text contents of a URL\n // TODO Waiting on fetch types: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/60924\n /* eslint-disable */\n // @ts-ignore Node 18 does have fetch but types not updated yet\n const resp = await fetch(url, {mode: 'cors'})\n if (resp.ok){\n return await resp.text()\n }\n throw new Error(`${resp.status} ${resp.statusText}: ${url}`)\n /* eslint-enable */\n}\n\n\n// @internal\nexport function deep_copy<T extends object>(source:T):T{\n // Deep copy an object that originated from JSON (doesn't handle other edge cases)\n const copy = (Array.isArray(source) ? [] : {}) as T\n for (const key in source){\n const value = source[key]\n // @ts-ignore This util is only for simple JSON objects where as TS accounts for all cases\n copy[key] = (typeof value === 'object' && value !== null) ? deep_copy(value) : value\n }\n return copy\n}\n\n\n// @internal\nexport function rm_diacritics(string:string):string{\n // Remove diacritics from a string\n // See https://stackoverflow.com/a/37511463/10262211\n return string.normalize('NFKD').replace(/\\p{Diacritic}/gu, '')\n}\n\n\n// @internal\nfunction _fuzzy_match(input:string, candidate:string):number{\n // Simple fuzzy match algorithm for matching input to a single word candidate\n // Returns 0 for perfect mat