@youversion/platform-core 1.16.0 → 1.18.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @youversion/platform-core@1.16.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/core
2
+ > @youversion/platform-core@1.18.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
  CLI Building entry: src/index.ts
@@ -8,11 +8,11 @@
8
8
  CLI Target: es2022
9
9
  CJS Build start
10
10
  ESM Build start
11
- ESM dist/index.js 50.50 KB
12
- ESM ⚡️ Build success in 51ms
13
- CJS dist/index.cjs 53.58 KB
14
- CJS ⚡️ Build success in 51ms
11
+ ESM dist/index.js 53.00 KB
12
+ ESM ⚡️ Build success in 41ms
13
+ CJS dist/index.cjs 56.12 KB
14
+ CJS ⚡️ Build success in 41ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 1959ms
17
- DTS dist/index.d.cts 33.42 KB
18
- DTS dist/index.d.ts 33.42 KB
16
+ DTS ⚡️ Build success in 1707ms
17
+ DTS dist/index.d.cts 34.06 KB
18
+ DTS dist/index.d.ts 34.06 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @youversion/platform-core
2
2
 
3
+ ## 1.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b8c6e1b: In our BibleCard component, we've added an error UI to make it more clear when an error has occurred fetching the Bible verse.
8
+
9
+ ## 1.17.1
10
+
11
+ ### Patch Changes
12
+
13
+ - a7100fd: We've added support for footnotes in Bible book introduction chapters. This is a rare occurance, but an example can be found in Joshua's introduction chapter when using the TPT Bible Version
14
+
15
+ ## 1.17.0
16
+
17
+ ### Minor Changes
18
+
19
+ - c3d673e: added error ui for faild verses
20
+
21
+ ### Patch Changes
22
+
23
+ - a5f91bf: Add cross-book chapter navigation to Bible Reader toolbar with prev/next buttons, intro chapter support, and accessible aria-labels
24
+
3
25
  ## 1.16.0
4
26
 
5
27
  ### 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
- 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 };
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
- 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 };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youversion/platform-core",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -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
@@ -14,3 +14,4 @@ export * from './YouVersionAPI';
14
14
  export * from './YouVersionPlatformConfiguration';
15
15
  export * from './types';
16
16
  export * from './utils/constants';
17
+ export { getAdjacentChapter } from './getAdjacentChapter';