Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/adjacent-chapter-next-intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@youversion/platform-core': patch
---

`getAdjacentChapter` now goes from the last chapter of a book to the next book's intro when it has one, instead of skipping to chapter 1. Next and previous are now inverses, and `BibleReader`'s next-chapter button reaches book intros the way the Swift reader does.
30 changes: 27 additions & 3 deletions packages/core/src/getAdjacentChapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,16 @@ describe('getAdjacentChapter', () => {
});
});

it('next chapter cross-book, skipping intro', () => {
it('next chapter cross-book → intro of next book', () => {
expect(getAdjacentChapter(books, 'GEN', '50', 'next')).toEqual({
bookId: 'EXO',
chapterId: 'INTRO',
});
});

it('next chapter cross-book when next book has no intro → chapter 1', () => {
expect(getAdjacentChapter(books, 'EXO', '40', 'next')).toEqual({
bookId: 'LEV',
chapterId: '1',
});
});
Expand Down Expand Up @@ -134,14 +141,31 @@ describe('getAdjacentChapter', () => {
});
});

it('cross-book next skips intro in chapters array of next book', () => {
it('cross-book next → intro of next book when intro is also in its chapters array', () => {
const booksWithIntroInArray = [
makeBook('GEN', 50),
makeBook('EXO', 40, { hasIntro: true, introInChapters: true }),
];
expect(getAdjacentChapter(booksWithIntroInArray, 'GEN', '50', 'next')).toEqual({
bookId: 'EXO',
chapterId: '1',
chapterId: 'INTRO',
});
});

it('next and previous are inverses across every chapter and intro', () => {
// Reading order: each book's intro (when present), then its numbered chapters.
const positions = books.flatMap((b) => [
...(b.intro ? [{ bookId: b.id, chapterId: b.intro.id }] : []),
...(b.chapters ?? []).map((c) => ({ bookId: b.id, chapterId: c.id })),
]);

positions.forEach((position, i) => {
expect(getAdjacentChapter(books, position.bookId, position.chapterId, 'next')).toEqual(
positions[i + 1] ?? null,
);
expect(getAdjacentChapter(books, position.bookId, position.chapterId, 'previous')).toEqual(
positions[i - 1] ?? null,
);
});
});

Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/getAdjacentChapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ function isIntroChapter(chapterId: string): boolean {
/**
* Computes the next or previous chapter across book boundaries.
*
* Reading order is each book's intro (when `book.intro` is present), then its numbered
* chapters. `next` from the last chapter of a book returns the next book's intro when it has
* one, otherwise its first numbered chapter. `previous` from chapter 1 returns the book's
* intro when it has one, otherwise the last chapter of the previous book. `next` and
* `previous` are inverses of each other.
*
* @param books - Ordered array of Bible books with populated chapters
* @param currentBookId - USFM book ID (e.g., "JHN")
* @param currentChapterId - Chapter ID string (e.g., "1", "INTRO")
Expand Down Expand Up @@ -61,9 +67,13 @@ function getNext(
}
}

// Cross-book: first non-intro chapter of next book
// Cross-book: next book's intro when it has one (mirrors previous from chapter 1, and
// matches Swift's goToNextChapter), else its first non-intro chapter
const nextBook = books[bookIndex + 1];
if (!nextBook) return null;
if (nextBook.intro) {
return { bookId: nextBook.id, chapterId: nextBook.intro.id };
}

const nextChapters: BibleChapter[] = nextBook.chapters ?? [];
const firstCanonical = nextChapters.find((c) => !isIntroChapter(c.id));
Expand Down
Loading