@youversion/platform-core 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +10 -0
- package/dist/index.cjs +70 -2
- package/dist/index.d.cts +16 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +68 -1
- package/package.json +1 -1
- package/src/getAdjacentChapter.test.ts +153 -0
- package/src/getAdjacentChapter.ts +121 -0
- package/src/index.ts +1 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @youversion/platform-core@1.
|
|
2
|
+
> @youversion/platform-core@1.17.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/core
|
|
3
3
|
> tsup src/index.ts --format cjs,esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mCJS[39m Build start
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[
|
|
12
|
-
[
|
|
13
|
-
[
|
|
14
|
-
[
|
|
11
|
+
[32mCJS[39m [1mdist/index.cjs [22m[32m56.12 KB[39m
|
|
12
|
+
[32mCJS[39m ⚡️ Build success in 44ms
|
|
13
|
+
[32mESM[39m [1mdist/index.js [22m[32m53.00 KB[39m
|
|
14
|
+
[32mESM[39m ⚡️ Build success in 45ms
|
|
15
15
|
[34mDTS[39m Build start
|
|
16
|
-
[32mDTS[39m ⚡️ Build success in
|
|
17
|
-
[32mDTS[39m [1mdist/index.d.cts [22m[
|
|
18
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
16
|
+
[32mDTS[39m ⚡️ Build success in 1914ms
|
|
17
|
+
[32mDTS[39m [1mdist/index.d.cts [22m[32m34.06 KB[39m
|
|
18
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m34.06 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @youversion/platform-core
|
|
2
2
|
|
|
3
|
+
## 1.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- c3d673e: added error ui for faild verses
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- a5f91bf: Add cross-book chapter navigation to Bible Reader toolbar with prev/next buttons, intro chapter support, and accessible aria-labels
|
|
12
|
+
|
|
3
13
|
## 1.16.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,8 @@ __export(index_exports, {
|
|
|
34
34
|
YouVersionAPI: () => YouVersionAPI,
|
|
35
35
|
YouVersionAPIUsers: () => YouVersionAPIUsers,
|
|
36
36
|
YouVersionPlatformConfiguration: () => YouVersionPlatformConfiguration,
|
|
37
|
-
YouVersionUserInfo: () => YouVersionUserInfo
|
|
37
|
+
YouVersionUserInfo: () => YouVersionUserInfo,
|
|
38
|
+
getAdjacentChapter: () => getAdjacentChapter
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(index_exports);
|
|
40
41
|
|
|
@@ -1663,6 +1664,72 @@ var YouVersionAPI = class {
|
|
|
1663
1664
|
return request;
|
|
1664
1665
|
}
|
|
1665
1666
|
};
|
|
1667
|
+
|
|
1668
|
+
// src/getAdjacentChapter.ts
|
|
1669
|
+
function isIntroChapter(chapterId) {
|
|
1670
|
+
return chapterId.toLowerCase().includes("intro");
|
|
1671
|
+
}
|
|
1672
|
+
function getAdjacentChapter(books, currentBookId, currentChapterId, direction) {
|
|
1673
|
+
if (books.length === 0) return null;
|
|
1674
|
+
const bookIndex = books.findIndex((b) => b.id === currentBookId);
|
|
1675
|
+
if (bookIndex === -1) return null;
|
|
1676
|
+
const book = books[bookIndex];
|
|
1677
|
+
const chapters = book.chapters ?? [];
|
|
1678
|
+
return direction === "next" ? getNext(books, bookIndex, book, chapters, currentChapterId) : getPrevious(books, bookIndex, book, chapters, currentChapterId);
|
|
1679
|
+
}
|
|
1680
|
+
function getNext(books, bookIndex, book, chapters, currentChapterId) {
|
|
1681
|
+
if (isIntroChapter(currentChapterId)) {
|
|
1682
|
+
const firstCanonical2 = chapters.find((c) => !isIntroChapter(c.id));
|
|
1683
|
+
if (firstCanonical2) {
|
|
1684
|
+
return { bookId: book.id, chapterId: firstCanonical2.id };
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
if (!isIntroChapter(currentChapterId)) {
|
|
1688
|
+
const idx = chapters.findIndex((c) => c.id === currentChapterId);
|
|
1689
|
+
if (idx !== -1 && idx < chapters.length - 1) {
|
|
1690
|
+
const next = chapters[idx + 1];
|
|
1691
|
+
if (!isIntroChapter(next.id)) {
|
|
1692
|
+
return { bookId: book.id, chapterId: next.id };
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
const nextBook = books[bookIndex + 1];
|
|
1697
|
+
if (!nextBook) return null;
|
|
1698
|
+
const nextChapters = nextBook.chapters ?? [];
|
|
1699
|
+
const firstCanonical = nextChapters.find((c) => !isIntroChapter(c.id));
|
|
1700
|
+
return firstCanonical ? { bookId: nextBook.id, chapterId: firstCanonical.id } : null;
|
|
1701
|
+
}
|
|
1702
|
+
function getPrevious(books, bookIndex, book, chapters, currentChapterId) {
|
|
1703
|
+
if (isIntroChapter(currentChapterId)) {
|
|
1704
|
+
return lastCanonicalOfPreviousBook(books, bookIndex);
|
|
1705
|
+
}
|
|
1706
|
+
const idx = chapters.findIndex((c) => c.id === currentChapterId);
|
|
1707
|
+
if (idx === -1) return null;
|
|
1708
|
+
const firstCanonicalIdx = chapters.findIndex((c) => !isIntroChapter(c.id));
|
|
1709
|
+
const isFirstCanonical = idx === firstCanonicalIdx;
|
|
1710
|
+
if (isFirstCanonical) {
|
|
1711
|
+
if (book.intro) {
|
|
1712
|
+
return { bookId: book.id, chapterId: book.intro.id };
|
|
1713
|
+
}
|
|
1714
|
+
return lastCanonicalOfPreviousBook(books, bookIndex);
|
|
1715
|
+
}
|
|
1716
|
+
if (idx > 0) {
|
|
1717
|
+
return { bookId: book.id, chapterId: chapters[idx - 1].id };
|
|
1718
|
+
}
|
|
1719
|
+
return null;
|
|
1720
|
+
}
|
|
1721
|
+
function lastCanonicalOfPreviousBook(books, bookIndex) {
|
|
1722
|
+
const prevBook = books[bookIndex - 1];
|
|
1723
|
+
if (!prevBook) return null;
|
|
1724
|
+
const prevChapters = prevBook.chapters ?? [];
|
|
1725
|
+
for (let i = prevChapters.length - 1; i >= 0; i--) {
|
|
1726
|
+
const ch = prevChapters[i];
|
|
1727
|
+
if (!isIntroChapter(ch.id)) {
|
|
1728
|
+
return { bookId: prevBook.id, chapterId: ch.id };
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
return null;
|
|
1732
|
+
}
|
|
1666
1733
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1667
1734
|
0 && (module.exports = {
|
|
1668
1735
|
ApiClient,
|
|
@@ -1679,5 +1746,6 @@ var YouVersionAPI = class {
|
|
|
1679
1746
|
YouVersionAPI,
|
|
1680
1747
|
YouVersionAPIUsers,
|
|
1681
1748
|
YouVersionPlatformConfiguration,
|
|
1682
|
-
YouVersionUserInfo
|
|
1749
|
+
YouVersionUserInfo,
|
|
1750
|
+
getAdjacentChapter
|
|
1683
1751
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -731,4 +731,19 @@ declare const BOOK_IDS: readonly ["GEN", "EXO", "LEV", "NUM", "DEU", "JOS", "JDG
|
|
|
731
731
|
*/
|
|
732
732
|
declare const BOOK_CANON: Record<BookUsfm, Canon>;
|
|
733
733
|
|
|
734
|
-
|
|
734
|
+
type AdjacentChapterResult = {
|
|
735
|
+
bookId: string;
|
|
736
|
+
chapterId: string;
|
|
737
|
+
} | null;
|
|
738
|
+
/**
|
|
739
|
+
* Computes the next or previous chapter across book boundaries.
|
|
740
|
+
*
|
|
741
|
+
* @param books - Ordered array of Bible books with populated chapters
|
|
742
|
+
* @param currentBookId - USFM book ID (e.g., "JHN")
|
|
743
|
+
* @param currentChapterId - Chapter ID string (e.g., "1", "INTRO")
|
|
744
|
+
* @param direction - Navigation direction
|
|
745
|
+
* @returns Target book and chapter IDs, or null at Bible boundaries
|
|
746
|
+
*/
|
|
747
|
+
declare function getAdjacentChapter(books: BibleBook[], currentBookId: string, currentChapterId: string, direction: 'next' | 'previous'): AdjacentChapterResult;
|
|
748
|
+
|
|
749
|
+
export { ApiClient, type ApiConfig, type AuthenticationScopes, type AuthenticationState, BOOK_CANON, BOOK_IDS, type BibleBook, type BibleBookIntro, type BibleChapter, BibleClient, type BibleIndex, type BibleIndexBook, type BibleIndexChapter, type BibleIndexVerse, type BiblePassage, type BibleVerse, type BibleVersion, type CANON, type Collection, type CreateHighlight, DEFAULT_LICENSE_FREE_BIBLE_VERSION, type DeleteHighlightOptions, type GetHighlightsOptions, type GetLanguagesOptions, type Highlight, type HighlightColor, HighlightsClient, type Language, LanguagesClient, MemoryStorageStrategy, SessionStorageStrategy, SignInWithYouVersionPermission, type SignInWithYouVersionPermissionValues, SignInWithYouVersionResult, type StorageStrategy, type User, type VOTD, YouVersionAPI, YouVersionAPIUsers, YouVersionPlatformConfiguration, YouVersionUserInfo, type YouVersionUserInfoJSON, getAdjacentChapter };
|
package/dist/index.d.ts
CHANGED
|
@@ -731,4 +731,19 @@ declare const BOOK_IDS: readonly ["GEN", "EXO", "LEV", "NUM", "DEU", "JOS", "JDG
|
|
|
731
731
|
*/
|
|
732
732
|
declare const BOOK_CANON: Record<BookUsfm, Canon>;
|
|
733
733
|
|
|
734
|
-
|
|
734
|
+
type AdjacentChapterResult = {
|
|
735
|
+
bookId: string;
|
|
736
|
+
chapterId: string;
|
|
737
|
+
} | null;
|
|
738
|
+
/**
|
|
739
|
+
* Computes the next or previous chapter across book boundaries.
|
|
740
|
+
*
|
|
741
|
+
* @param books - Ordered array of Bible books with populated chapters
|
|
742
|
+
* @param currentBookId - USFM book ID (e.g., "JHN")
|
|
743
|
+
* @param currentChapterId - Chapter ID string (e.g., "1", "INTRO")
|
|
744
|
+
* @param direction - Navigation direction
|
|
745
|
+
* @returns Target book and chapter IDs, or null at Bible boundaries
|
|
746
|
+
*/
|
|
747
|
+
declare function getAdjacentChapter(books: BibleBook[], currentBookId: string, currentChapterId: string, direction: 'next' | 'previous'): AdjacentChapterResult;
|
|
748
|
+
|
|
749
|
+
export { ApiClient, type ApiConfig, type AuthenticationScopes, type AuthenticationState, BOOK_CANON, BOOK_IDS, type BibleBook, type BibleBookIntro, type BibleChapter, BibleClient, type BibleIndex, type BibleIndexBook, type BibleIndexChapter, type BibleIndexVerse, type BiblePassage, type BibleVerse, type BibleVersion, type CANON, type Collection, type CreateHighlight, DEFAULT_LICENSE_FREE_BIBLE_VERSION, type DeleteHighlightOptions, type GetHighlightsOptions, type GetLanguagesOptions, type Highlight, type HighlightColor, HighlightsClient, type Language, LanguagesClient, MemoryStorageStrategy, SessionStorageStrategy, SignInWithYouVersionPermission, type SignInWithYouVersionPermissionValues, SignInWithYouVersionResult, type StorageStrategy, type User, type VOTD, YouVersionAPI, YouVersionAPIUsers, YouVersionPlatformConfiguration, YouVersionUserInfo, type YouVersionUserInfoJSON, getAdjacentChapter };
|
package/dist/index.js
CHANGED
|
@@ -1623,6 +1623,72 @@ var YouVersionAPI = class {
|
|
|
1623
1623
|
return request;
|
|
1624
1624
|
}
|
|
1625
1625
|
};
|
|
1626
|
+
|
|
1627
|
+
// src/getAdjacentChapter.ts
|
|
1628
|
+
function isIntroChapter(chapterId) {
|
|
1629
|
+
return chapterId.toLowerCase().includes("intro");
|
|
1630
|
+
}
|
|
1631
|
+
function getAdjacentChapter(books, currentBookId, currentChapterId, direction) {
|
|
1632
|
+
if (books.length === 0) return null;
|
|
1633
|
+
const bookIndex = books.findIndex((b) => b.id === currentBookId);
|
|
1634
|
+
if (bookIndex === -1) return null;
|
|
1635
|
+
const book = books[bookIndex];
|
|
1636
|
+
const chapters = book.chapters ?? [];
|
|
1637
|
+
return direction === "next" ? getNext(books, bookIndex, book, chapters, currentChapterId) : getPrevious(books, bookIndex, book, chapters, currentChapterId);
|
|
1638
|
+
}
|
|
1639
|
+
function getNext(books, bookIndex, book, chapters, currentChapterId) {
|
|
1640
|
+
if (isIntroChapter(currentChapterId)) {
|
|
1641
|
+
const firstCanonical2 = chapters.find((c) => !isIntroChapter(c.id));
|
|
1642
|
+
if (firstCanonical2) {
|
|
1643
|
+
return { bookId: book.id, chapterId: firstCanonical2.id };
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
if (!isIntroChapter(currentChapterId)) {
|
|
1647
|
+
const idx = chapters.findIndex((c) => c.id === currentChapterId);
|
|
1648
|
+
if (idx !== -1 && idx < chapters.length - 1) {
|
|
1649
|
+
const next = chapters[idx + 1];
|
|
1650
|
+
if (!isIntroChapter(next.id)) {
|
|
1651
|
+
return { bookId: book.id, chapterId: next.id };
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
const nextBook = books[bookIndex + 1];
|
|
1656
|
+
if (!nextBook) return null;
|
|
1657
|
+
const nextChapters = nextBook.chapters ?? [];
|
|
1658
|
+
const firstCanonical = nextChapters.find((c) => !isIntroChapter(c.id));
|
|
1659
|
+
return firstCanonical ? { bookId: nextBook.id, chapterId: firstCanonical.id } : null;
|
|
1660
|
+
}
|
|
1661
|
+
function getPrevious(books, bookIndex, book, chapters, currentChapterId) {
|
|
1662
|
+
if (isIntroChapter(currentChapterId)) {
|
|
1663
|
+
return lastCanonicalOfPreviousBook(books, bookIndex);
|
|
1664
|
+
}
|
|
1665
|
+
const idx = chapters.findIndex((c) => c.id === currentChapterId);
|
|
1666
|
+
if (idx === -1) return null;
|
|
1667
|
+
const firstCanonicalIdx = chapters.findIndex((c) => !isIntroChapter(c.id));
|
|
1668
|
+
const isFirstCanonical = idx === firstCanonicalIdx;
|
|
1669
|
+
if (isFirstCanonical) {
|
|
1670
|
+
if (book.intro) {
|
|
1671
|
+
return { bookId: book.id, chapterId: book.intro.id };
|
|
1672
|
+
}
|
|
1673
|
+
return lastCanonicalOfPreviousBook(books, bookIndex);
|
|
1674
|
+
}
|
|
1675
|
+
if (idx > 0) {
|
|
1676
|
+
return { bookId: book.id, chapterId: chapters[idx - 1].id };
|
|
1677
|
+
}
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
function lastCanonicalOfPreviousBook(books, bookIndex) {
|
|
1681
|
+
const prevBook = books[bookIndex - 1];
|
|
1682
|
+
if (!prevBook) return null;
|
|
1683
|
+
const prevChapters = prevBook.chapters ?? [];
|
|
1684
|
+
for (let i = prevChapters.length - 1; i >= 0; i--) {
|
|
1685
|
+
const ch = prevChapters[i];
|
|
1686
|
+
if (!isIntroChapter(ch.id)) {
|
|
1687
|
+
return { bookId: prevBook.id, chapterId: ch.id };
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1626
1692
|
export {
|
|
1627
1693
|
ApiClient,
|
|
1628
1694
|
BOOK_CANON,
|
|
@@ -1638,5 +1704,6 @@ export {
|
|
|
1638
1704
|
YouVersionAPI,
|
|
1639
1705
|
YouVersionAPIUsers,
|
|
1640
1706
|
YouVersionPlatformConfiguration,
|
|
1641
|
-
YouVersionUserInfo
|
|
1707
|
+
YouVersionUserInfo,
|
|
1708
|
+
getAdjacentChapter
|
|
1642
1709
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { getAdjacentChapter } from './getAdjacentChapter';
|
|
3
|
+
import type { BibleBook } from './schemas/book';
|
|
4
|
+
|
|
5
|
+
function makeBook(
|
|
6
|
+
id: string,
|
|
7
|
+
chapterCount: number,
|
|
8
|
+
opts?: { hasIntro?: boolean; introInChapters?: boolean },
|
|
9
|
+
): BibleBook {
|
|
10
|
+
const chapters = [];
|
|
11
|
+
|
|
12
|
+
if (opts?.introInChapters) {
|
|
13
|
+
chapters.push({ id: 'INTRO', passage_id: `${id}.INTRO`, title: 'Intro' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (let i = 1; i <= chapterCount; i++) {
|
|
17
|
+
chapters.push({ id: i.toString(), passage_id: `${id}.${i}`, title: i.toString() });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
title: id,
|
|
23
|
+
full_title: id,
|
|
24
|
+
canon: 'old_testament',
|
|
25
|
+
chapters,
|
|
26
|
+
...(opts?.hasIntro
|
|
27
|
+
? { intro: { id: 'INTRO', passage_id: `${id}.INTRO`, title: 'Intro' } }
|
|
28
|
+
: {}),
|
|
29
|
+
} as BibleBook;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const GEN = makeBook('GEN', 50, { hasIntro: true });
|
|
33
|
+
const EXO = makeBook('EXO', 40, { hasIntro: true });
|
|
34
|
+
const LEV = makeBook('LEV', 27);
|
|
35
|
+
const REV = makeBook('REV', 22);
|
|
36
|
+
const books: BibleBook[] = [GEN, EXO, LEV, REV];
|
|
37
|
+
|
|
38
|
+
describe('getAdjacentChapter', () => {
|
|
39
|
+
it('next chapter within same book', () => {
|
|
40
|
+
expect(getAdjacentChapter(books, 'GEN', '1', 'next')).toEqual({
|
|
41
|
+
bookId: 'GEN',
|
|
42
|
+
chapterId: '2',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('next chapter cross-book, skipping intro', () => {
|
|
47
|
+
expect(getAdjacentChapter(books, 'GEN', '50', 'next')).toEqual({
|
|
48
|
+
bookId: 'EXO',
|
|
49
|
+
chapterId: '1',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('next chapter at Bible end returns null', () => {
|
|
54
|
+
expect(getAdjacentChapter(books, 'REV', '22', 'next')).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('next from intro → chapter 1 of same book', () => {
|
|
58
|
+
expect(getAdjacentChapter(books, 'GEN', 'INTRO', 'next')).toEqual({
|
|
59
|
+
bookId: 'GEN',
|
|
60
|
+
chapterId: '1',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('previous chapter within same book', () => {
|
|
65
|
+
expect(getAdjacentChapter(books, 'GEN', '3', 'previous')).toEqual({
|
|
66
|
+
bookId: 'GEN',
|
|
67
|
+
chapterId: '2',
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('previous from chapter 1 when book has intro → intro of same book', () => {
|
|
72
|
+
expect(getAdjacentChapter(books, 'GEN', '1', 'previous')).toEqual({
|
|
73
|
+
bookId: 'GEN',
|
|
74
|
+
chapterId: 'INTRO',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('previous from chapter 1 when book has no intro → last canonical of previous book', () => {
|
|
79
|
+
expect(getAdjacentChapter(books, 'LEV', '1', 'previous')).toEqual({
|
|
80
|
+
bookId: 'EXO',
|
|
81
|
+
chapterId: '40',
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('previous from intro → last canonical of previous book', () => {
|
|
86
|
+
expect(getAdjacentChapter(books, 'EXO', 'INTRO', 'previous')).toEqual({
|
|
87
|
+
bookId: 'GEN',
|
|
88
|
+
chapterId: '50',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('previous at Bible start returns null', () => {
|
|
93
|
+
expect(getAdjacentChapter(books, 'GEN', 'INTRO', 'previous')).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('previous at Bible start (chapter 1, no intro)', () => {
|
|
97
|
+
const booksNoIntro = [makeBook('GEN', 50), makeBook('EXO', 40)];
|
|
98
|
+
expect(getAdjacentChapter(booksNoIntro, 'GEN', '1', 'previous')).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('empty books array returns null', () => {
|
|
102
|
+
expect(getAdjacentChapter([], 'GEN', '1', 'next')).toBeNull();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('current book not found returns null', () => {
|
|
106
|
+
expect(getAdjacentChapter(books, 'FAKE', '1', 'next')).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('handles intro chapter present in chapters array for next', () => {
|
|
110
|
+
const booksWithIntroInArray = [
|
|
111
|
+
makeBook('GEN', 50, { hasIntro: true, introInChapters: true }),
|
|
112
|
+
makeBook('EXO', 40),
|
|
113
|
+
];
|
|
114
|
+
expect(getAdjacentChapter(booksWithIntroInArray, 'GEN', 'INTRO', 'next')).toEqual({
|
|
115
|
+
bookId: 'GEN',
|
|
116
|
+
chapterId: '1',
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('handles intro chapter present in chapters array for previous', () => {
|
|
121
|
+
const booksWithIntroInArray = [
|
|
122
|
+
makeBook('GEN', 50, { hasIntro: true, introInChapters: true }),
|
|
123
|
+
makeBook('EXO', 40, { hasIntro: true, introInChapters: true }),
|
|
124
|
+
];
|
|
125
|
+
expect(getAdjacentChapter(booksWithIntroInArray, 'EXO', '1', 'previous')).toEqual({
|
|
126
|
+
bookId: 'EXO',
|
|
127
|
+
chapterId: 'INTRO',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('cross-book next skips intro in chapters array of next book', () => {
|
|
132
|
+
const booksWithIntroInArray = [
|
|
133
|
+
makeBook('GEN', 50),
|
|
134
|
+
makeBook('EXO', 40, { hasIntro: true, introInChapters: true }),
|
|
135
|
+
];
|
|
136
|
+
expect(getAdjacentChapter(booksWithIntroInArray, 'GEN', '50', 'next')).toEqual({
|
|
137
|
+
bookId: 'EXO',
|
|
138
|
+
chapterId: '1',
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('cross-book previous from book without intro goes to last chapter of previous book', () => {
|
|
143
|
+
const mixed = [
|
|
144
|
+
makeBook('GEN', 50, { hasIntro: true }),
|
|
145
|
+
makeBook('EXO', 40),
|
|
146
|
+
makeBook('LEV', 27, { hasIntro: true }),
|
|
147
|
+
];
|
|
148
|
+
expect(getAdjacentChapter(mixed, 'EXO', '1', 'previous')).toEqual({
|
|
149
|
+
bookId: 'GEN',
|
|
150
|
+
chapterId: '50',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { BibleBook } from './schemas/book';
|
|
2
|
+
import type { BibleChapter } from './schemas/chapter';
|
|
3
|
+
|
|
4
|
+
type AdjacentChapterResult = { bookId: string; chapterId: string } | null;
|
|
5
|
+
|
|
6
|
+
function isIntroChapter(chapterId: string): boolean {
|
|
7
|
+
return chapterId.toLowerCase().includes('intro');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Computes the next or previous chapter across book boundaries.
|
|
12
|
+
*
|
|
13
|
+
* @param books - Ordered array of Bible books with populated chapters
|
|
14
|
+
* @param currentBookId - USFM book ID (e.g., "JHN")
|
|
15
|
+
* @param currentChapterId - Chapter ID string (e.g., "1", "INTRO")
|
|
16
|
+
* @param direction - Navigation direction
|
|
17
|
+
* @returns Target book and chapter IDs, or null at Bible boundaries
|
|
18
|
+
*/
|
|
19
|
+
export function getAdjacentChapter(
|
|
20
|
+
books: BibleBook[],
|
|
21
|
+
currentBookId: string,
|
|
22
|
+
currentChapterId: string,
|
|
23
|
+
direction: 'next' | 'previous',
|
|
24
|
+
): AdjacentChapterResult {
|
|
25
|
+
if (books.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
const bookIndex = books.findIndex((b) => b.id === currentBookId);
|
|
28
|
+
if (bookIndex === -1) return null;
|
|
29
|
+
|
|
30
|
+
const book = books[bookIndex]!;
|
|
31
|
+
const chapters: BibleChapter[] = book.chapters ?? [];
|
|
32
|
+
|
|
33
|
+
return direction === 'next'
|
|
34
|
+
? getNext(books, bookIndex, book, chapters, currentChapterId)
|
|
35
|
+
: getPrevious(books, bookIndex, book, chapters, currentChapterId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getNext(
|
|
39
|
+
books: BibleBook[],
|
|
40
|
+
bookIndex: number,
|
|
41
|
+
book: BibleBook,
|
|
42
|
+
chapters: BibleChapter[],
|
|
43
|
+
currentChapterId: string,
|
|
44
|
+
): AdjacentChapterResult {
|
|
45
|
+
// Next from intro → chapter 1 of same book
|
|
46
|
+
if (isIntroChapter(currentChapterId)) {
|
|
47
|
+
const firstCanonical = chapters.find((c) => !isIntroChapter(c.id));
|
|
48
|
+
if (firstCanonical) {
|
|
49
|
+
return { bookId: book.id, chapterId: firstCanonical.id };
|
|
50
|
+
}
|
|
51
|
+
// No canonical chapters — fall through to cross-book
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!isIntroChapter(currentChapterId)) {
|
|
55
|
+
const idx = chapters.findIndex((c) => c.id === currentChapterId);
|
|
56
|
+
if (idx !== -1 && idx < chapters.length - 1) {
|
|
57
|
+
const next = chapters[idx + 1]!;
|
|
58
|
+
if (!isIntroChapter(next.id)) {
|
|
59
|
+
return { bookId: book.id, chapterId: next.id };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Cross-book: first non-intro chapter of next book
|
|
65
|
+
const nextBook = books[bookIndex + 1];
|
|
66
|
+
if (!nextBook) return null;
|
|
67
|
+
|
|
68
|
+
const nextChapters: BibleChapter[] = nextBook.chapters ?? [];
|
|
69
|
+
const firstCanonical = nextChapters.find((c) => !isIntroChapter(c.id));
|
|
70
|
+
return firstCanonical ? { bookId: nextBook.id, chapterId: firstCanonical.id } : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getPrevious(
|
|
74
|
+
books: BibleBook[],
|
|
75
|
+
bookIndex: number,
|
|
76
|
+
book: BibleBook,
|
|
77
|
+
chapters: BibleChapter[],
|
|
78
|
+
currentChapterId: string,
|
|
79
|
+
): AdjacentChapterResult {
|
|
80
|
+
// Previous from intro → last canonical chapter of previous book
|
|
81
|
+
if (isIntroChapter(currentChapterId)) {
|
|
82
|
+
return lastCanonicalOfPreviousBook(books, bookIndex);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const idx = chapters.findIndex((c) => c.id === currentChapterId);
|
|
86
|
+
if (idx === -1) return null;
|
|
87
|
+
|
|
88
|
+
// Determine if we're on the first canonical chapter
|
|
89
|
+
const firstCanonicalIdx = chapters.findIndex((c) => !isIntroChapter(c.id));
|
|
90
|
+
const isFirstCanonical = idx === firstCanonicalIdx;
|
|
91
|
+
|
|
92
|
+
if (isFirstCanonical) {
|
|
93
|
+
// If book has an intro, go to intro
|
|
94
|
+
if (book.intro) {
|
|
95
|
+
return { bookId: book.id, chapterId: book.intro.id };
|
|
96
|
+
}
|
|
97
|
+
// No intro → cross-book
|
|
98
|
+
return lastCanonicalOfPreviousBook(books, bookIndex);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Not at first canonical — go to previous chapter in same book
|
|
102
|
+
if (idx > 0) {
|
|
103
|
+
return { bookId: book.id, chapterId: chapters[idx - 1]!.id };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function lastCanonicalOfPreviousBook(books: BibleBook[], bookIndex: number): AdjacentChapterResult {
|
|
110
|
+
const prevBook = books[bookIndex - 1];
|
|
111
|
+
if (!prevBook) return null;
|
|
112
|
+
|
|
113
|
+
const prevChapters: BibleChapter[] = prevBook.chapters ?? [];
|
|
114
|
+
for (let i = prevChapters.length - 1; i >= 0; i--) {
|
|
115
|
+
const ch = prevChapters[i]!;
|
|
116
|
+
if (!isIntroChapter(ch.id)) {
|
|
117
|
+
return { bookId: prevBook.id, chapterId: ch.id };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
package/src/index.ts
CHANGED