Skip to content

Allow users to interlinearize "verse 0" of books#123

Open
alex-rawlings-yyc wants to merge 3 commits into
mainfrom
verse-zero
Open

Allow users to interlinearize "verse 0" of books#123
alex-rawlings-yyc wants to merge 3 commits into
mainfrom
verse-zero

Conversation

@alex-rawlings-yyc

@alex-rawlings-yyc alex-rawlings-yyc commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

This change is Reviewable

Summary by CodeRabbit

Release Notes

  • New Features

    • Improved chapter superscription (“verse 0”) handling for Psalm-style titles and descriptive headings, with correct navigation and highlight behavior.
  • Bug Fixes

    • Ensured verse-0 selections are preserved when a matching verse-0 segment exists; otherwise chapter-level verse-0 selections now highlight the first numbered verse.
  • Documentation

    • Updated selection and verse numbering semantics to clarify how verse 0 behaves.
  • Tests

    • Expanded Jest coverage for verse-0 extraction, loader behavior, navigation context, and token/segment selection interactions.

@alex-rawlings-yyc alex-rawlings-yyc linked an issue Jun 22, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce724b58-b74a-433f-a7d2-d490ada099c5

📥 Commits

Reviewing files that changed from the base of the PR and between c67b43c and 18beaa3.

📒 Files selected for processing (13)
  • src/__tests__/components/InterlinearNavContext.test.tsx
  • src/__tests__/components/Interlinearizer.test.tsx
  • src/__tests__/components/InterlinearizerLoader.test.tsx
  • src/__tests__/components/SegmentView.test.tsx
  • src/__tests__/parsers/papi/bookTokenizer.test.ts
  • src/__tests__/parsers/papi/usjBookExtractor.test.ts
  • src/components/InterlinearNavContext.tsx
  • src/components/Interlinearizer.tsx
  • src/components/InterlinearizerLoader.tsx
  • src/components/SegmentListView.tsx
  • src/components/SegmentView.tsx
  • src/parsers/papi/usjBookExtractor.ts
  • src/types/interlinearizer.d.ts
✅ Files skipped from review due to trivial changes (3)
  • src/tests/parsers/papi/bookTokenizer.test.ts
  • src/types/interlinearizer.d.ts
  • src/components/Interlinearizer.tsx
🚧 Files skipped from review as they are similar to previous changes (9)
  • src/components/InterlinearizerLoader.tsx
  • src/tests/parsers/papi/usjBookExtractor.test.ts
  • src/components/SegmentListView.tsx
  • src/tests/components/SegmentView.test.tsx
  • src/components/SegmentView.tsx
  • src/tests/components/InterlinearizerLoader.test.tsx
  • src/tests/components/InterlinearNavContext.test.tsx
  • src/parsers/papi/usjBookExtractor.ts
  • src/components/InterlinearNavContext.tsx

📝 Walkthrough

Walkthrough

Adds end-to-end verse-0 (chapter superscription) support: usjBookExtractor synthesizes verse-0 RawVerse entries from d-marker paragraphs; InterlinearNavContext removes normalizeScrRef and keys references verbatim to track verse-0 distinct from verse-1; InterlinearizerLoader conditionally preserves or rewrites verseNum: 0 based on segment presence; Interlinearizer allows verse-0 navigation; SegmentView baseline click propagates token ref; SegmentListView routes active highlighting through focused-token segment identity.

Changes

Verse-0 Superscription Feature

Layer / File(s) Summary
ScriptureRef type contract and USJ extraction
src/types/interlinearizer.d.ts, src/parsers/papi/usjBookExtractor.ts
ScriptureRef.verse JSDoc extended to document verse-0 semantics. d marker removed from HEADING_PARA_MARKERS; TraversalState gains currentVerseIsSynthetic; closeCurrentVerse helper centralizes verse finalization; handleChapterNode opens a synthetic verse-0 accumulator; handleVerseNode and extractBookFromUsj use the shared helper.
USJ extractor and tokenizer tests
src/__tests__/parsers/papi/usjBookExtractor.test.ts, src/__tests__/parsers/papi/bookTokenizer.test.ts
Six new extractor tests cover d-marker synthesis, s1 exclusion, trailing title, missing chapter number, explicit :0 SID, and duplicate verse-0 error. One new tokenizer test verifies PSA 3:0 segment tokenization.
InterlinearNavContext: verbatim verse-0 keying
src/components/InterlinearNavContext.tsx
Removes exported normalizeScrRef; verseKey now keys verse-0 references verbatim instead of normalizing to verse-1. Introduces areScrRefsEqual for duplicate-delivery detection; sticky verse-0 and pendingInternalNavRef matching operate on un-normalized references.
InterlinearNavContext tests
src/__tests__/components/InterlinearNavContext.test.tsx
Test expectations updated: liveScrRef preserves verseNum: 0 unchanged; cross-chapter verse-0 is a real jump; verse-0 and verse-1 deliveries are distinct with no identity reuse; internal-nav marks keyed to verse-0 are not consumable by verse-1 references.
InterlinearizerLoader: activeScrRef normalization
src/components/InterlinearizerLoader.tsx, src/__tests__/components/InterlinearizerLoader.test.tsx
Loader memoizes activeScrRef: verse-0 is preserved when a matching superscription segment exists, otherwise rewritten to verse-1. Interlinearizer receives activeScrRef instead of raw scrRef. Tests updated/added covering normalization conditions and loading state.
Interlinearizer: verse-0 navigation support
src/components/Interlinearizer.tsx
focusToken JSDoc updated to document verse-0 (superscription) segment navigation. handleSegmentSelect JSDoc describes internal-nav marker handling for verse-0. scrRef prop JSDoc clarifies verse-0-when-superscription semantics.
SegmentView baseline click and SegmentListView active highlight
src/components/SegmentView.tsx, src/components/SegmentListView.tsx
handleBaselineClick calls onSelect(ref, firstWordTokenRef) instead of onSelect(ref) to propagate the token ref. SegmentListView derives activeSegmentId from displayFocusedTokenRef via tokenSegmentMap; isActive prefers token-derived segment id over verse-based fallback.
Interlinearizer and SegmentView tests
src/__tests__/components/Interlinearizer.test.tsx, src/__tests__/components/SegmentView.test.tsx
GEN_SUPERSCRIPTION_BOOK fixture added with verse-0 superscription segment. Three new Interlinearizer tests: verse-0 token selection triggers host navigate with verseNum: 0; ContinuousView verse-0 focus updates focusedTokenRef and navigates; verse-0 selection shifts isActive from verse-1 to verse-0. SegmentView baseline-click test updated to assert firstWordTokenRef in onSelect call.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

  • Follow global ref into verse 0 (superscription) #125: Addresses the same verse-0 navigation and internal-marker keying concerns by replacing blanket normalization with provenance-based tracking via dedicated verseKey values and loader-level segment-presence checks.

Possibly related PRs

  • sillsdev/interlinearizer-extension#30: Introduces usjBookExtractor.ts, the same file this PR extends to synthesize verse-0 RawVerse entries from d-marker paragraphs.
  • sillsdev/interlinearizer-extension#77: Overlaps in the same SegmentView baseline-click and Interlinearizer focus/active plumbing that this PR further modifies to pass firstWordTokenRef and document verse-0 navigation handling.

Suggested labels

up next

Suggested reviewers

  • imnasnainaec

Poem

🐇 Verse zero found its voice today,
The superscription claimed its way!
No longer mapped to verse one's key,
Each title now stands distinctly free.
With verbatim refs and segment glow,
The rabbit hops through verse-0! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: enabling users to interlinearize verse 0 (chapter superscriptions), which is the primary objective of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch verse-zero

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as outdated.

@alex-rawlings-yyc alex-rawlings-yyc self-assigned this Jun 22, 2026

@imnasnainaec imnasnainaec left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin (https://app.devin.ai/review/sillsdev/interlinearizer-extension/pull/123) caught a bug, but the code otherwise looks good to me. I'm still doing manual testing.

@imnasnainaec reviewed 14 files and all commit messages, and made 1 comment.
Reviewable status: :shipit: complete! all files reviewed, all discussions resolved (waiting on alex-rawlings-yyc).

@alex-rawlings-yyc

Copy link
Copy Markdown
Contributor Author

I believe I addressed that bug, but please let me know if I'm wrong about that.

@imnasnainaec

Copy link
Copy Markdown
Contributor

@alex-rawlings-yyc I think the extension's handling of verse 0 and the global ref selectors handling could be better connected. In one direction, clicking in or out of a verse 0 segment should update the global ref accordingly. In the other direction, if you're on verse 1 and click < in the global ref handler to go back a verse, it shows verse 0 but the extension doesn't match. (I recognize it might be a more significant refactor of how we avoid unnecessary jumps to actual get those synced up.)

@alex-rawlings-yyc

Copy link
Copy Markdown
Contributor Author

@imnasnainaec I told Claude to investigate and here's what it came up with. I don't think the refactor will be too much work. Let me know what you think:

Verse 0 ↔ global scroll-group reference sync

Design note on two related desync bugs between the interlinearizer's verse-0 (chapter
superscription) handling and Platform.Bible's global scroll-group reference. Captured for review
before implementation because the second direction may be a non-trivial refactor of the navigation
echo-suppression logic.

Background

A chapter's pre-verse-1 content (chiefly Psalm superscriptions) is modeled as a synthetic verse-0
segment
with SID "<book> <chapter>:0" (see
usjBookExtractor.ts). It is fully interlinearizable and its
tokens are focusable.

The global reference is the scroll-group scripture reference the extension reads/writes via the
PAPI-injected useWebViewScrollGroupScrRef hook
(InterlinearNavContext.tsx). Today verse 0 is treated
as effectively receive-only: the extension can display a verse-0 reference the host sends, but it
deliberately does not write verse 0 back, and it suppresses some incoming verse-0 references.

Relevant host (paranext-core) facts

Two facts from platform-bible-react's BookChapterControl (the toolbar verse selector) drive the
analysis:

  • The host can sit on verse 0. Its "Previous verse" (<) button is disabled only when
    scrRef.verseNum === 0 — i.e. verse 0 is a valid floor it can rest on, not an illegal value.
  • < from verse 1 emits a real verse 0. handlePreviousVerse submits
    verseNum: scrRef.verseNum > 1 ? scrRef.verseNum - 1 : 0. So clicking < on verse 1 of a chapter
    pushes a genuine verseNum: 0 for that same chapter to the scroll group.

This corrects an earlier assumption in our code comments that "the host scroll-group reference cannot
hold verse 0 (it echoes the chapter back)." The host can hold verse 0. The echo problem is
narrower: after a real verse navigation the host re-broadcasts the chapter as a separate
verseNum: 0 reference
. The trouble is that this spurious echo is byte-identical to the
intentional <-to-verse-0 the user can trigger.

The two bugs

Direction 1 — clicking in/out of a verse-0 segment does not update the global ref

Selecting a verse-0 segment (or focusing a token inside one) should move the global reference to that
verse 0. Today it does not: both write paths bail out for verse 0.

  • focusToken:
    if (seg.startRef.verse === 0 || isSameVerse(...)) return; — focuses locally, never calls
    navigate.
  • handleSegmentSelect:
    if (ref.verse !== 0 && !isSameVerse(ref, current)) navigate(...) — writes only for non-zero
    verses.

The verse-0 token stays focusable, but the selection never leaves the extension, so other panels in
the scroll group don't follow.

This is the tractable half: the host accepts a verse-0 write and will display it. The write was
suppressed only to dodge the echo collision described in Direction 2.

Direction 2 — host < to verse 0 does not move the extension to the superscription

From verse 1, clicking < in the global ref selector lands the host on verse 0, but the extension
stays on verse 1 instead of moving to the superscription.

The blocker is the verse-0 stickiness in
InterlinearNavContext.tsx:238-248:

incoming verseNum:0 + same book/chapter as currently shown  →  held sticky (ignored)

Two value-identical deliveries both match this rule:

Delivery Should be Today
Host re-broadcasting the chapter as verseNum:0 after a real verse nav (spurious echo) ignored ignored ✅
User clicking < from v1 to land on v0 (intentional) honored ignored ❌

They cannot be told apart by value. The only distinguishing signal is provenance/timing: the
spurious echo is an unsolicited follow-up to a navigation the extension itself just caused; the
intentional < is a fresh external action with no pending internal navigation.

Why Direction 2 is the hard one

The current verse-0 stickiness is a blunt value-match that predates / sidesteps the extension's
existing provenance machinery. That machinery already solves exactly this "is this host delivery my
own echo or a real external move?" problem for ordinary verses:

  • pendingInternalNavRef + consumeInternalNav
    (InterlinearNavContext.tsx:257-289) record a
    TTL-stamped marker per internal navigation and consume it when the matching host echo returns.
  • navigate(ref, 'internal') records the marker; an unmarked (or expired) delivery is treated as a
    genuine external move.

Verse 0 simply never got routed onto this path — it uses the value shortcut instead.

Proposed direction

Unify verse 0 onto the existing internal-nav arbitration rather than inventing new state:

  1. Replace the blanket verse-0 stickiness with the provenance check used elsewhere. A
    verseNum:0-same-chapter delivery is suppressed only when it correlates with a recently emitted
    internal nav to that chapter
    (the post-nav echo). Absent a fresh marker, treat it as a real
    external move to verse 0 — which flows to the loader, where
    InterlinearizerLoader.tsx:245-249 already
    resolves verse 0 to the superscription segment when one exists (and to verse 1 otherwise).

  2. Re-enable the verse-0 write in focusToken / handleSegmentSelect (drop the verse === 0
    guard), and have navigate(ref, 'internal') record the marker for the verse-0 key so the resulting
    host echo is consumed normally — closing Direction 1 without reintroducing the chapter bounce.

This is a refactor of which path verse 0 takes, not new machinery: verse 0 joins the same
marker-based echo arbitration every other verse already uses.

Open risk to decide before implementing

Because the <-from-v1 echo and the post-verse-nav chapter echo are byte-identical, even a
marker-based scheme has a narrow race: if a user clicks < to verse 0 immediately after an internal
nav to that same chapter (marker still fresh), the intentional verse-0 move would be eaten as an echo.
The window is small, but it is inherent to the host emitting indistinguishable references, so it
should be decided deliberately rather than discovered later. Options:

  • Accept the race (rare; the user can re-click <).
  • Shorten the verse-0 marker TTL specifically.
  • Track echo provenance more precisely (e.g. only suppress the first same-chapter verse-0 within a
    tighter window of an internal nav).

Files

@imnasnainaec

Copy link
Copy Markdown
Contributor

@alex-rawlings-yyc I'd say the Direction 1 recommendations look good for this pr. The subtlety and subjectivity of Direction 2 inclines me to just copy those details over to a new issue to handle independently.

@imnasnainaec

Copy link
Copy Markdown
Contributor

@alex-rawlings-yyc ...but if you have a clear preference for implementing Direction 2 now, you're welcome to go for it now.

@alex-rawlings-yyc

Copy link
Copy Markdown
Contributor Author

@imnasnainaec sounds good. I think making Dir. 2 a separate issue is a good call. I'll write up the issue and get Dir 1 finished.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include verse 0 in parsed data

2 participants