Skip to content

feat(core): auto-transform Bible HTML in getPassage - #216

Open
cameronapak wants to merge 23 commits into
mainfrom
transform-bible-html
Open

feat(core): auto-transform Bible HTML in getPassage#216
cameronapak wants to merge 23 commits into
mainfrom
transform-bible-html

Conversation

@cameronapak

@cameronapak cameronapak commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Auto-transforms Bible HTML inside getPassage so consumers never need to call transformBibleHtml manually. Uses native DOMParser in the browser and dynamic import('jsdom') on the server. Added data-yv-transformed idempotency marker so double-transforms are a no-op.

Runtime DOM choice: optional peer is jsdom (not linkedom). No workerd-specific path or CI smoke — server consumers that need auto-transform install jsdom, or pass transform: false to skip.

Verse.Html retains its transformBibleHtml call as defense-in-depth — the idempotency marker makes it a no-op for HTML that already went through getPassage.

Node floor: jsdom@28.1.0 (devDep / peer ^24 || ^28) declares ^20.19.0 || ^22.12.0 || >=24.0.0, which covers the repo engines.node >=22.13 floor (Decision 3).

Context: Why transformBibleHtml Exists — And Where It May Not Be Needed

Test plan

  • Verify getPassage with format: 'html' returns transformed content (data-yv-transformed present)
  • Verify getPassage with format: 'text' returns raw content (no transformation)
  • Verify double-transform produces identical output (idempotency)
  • Verify footnotes are extracted into data-verse-footnote attributes
  • Verify Verse.Html still sanitizes raw HTML passed directly (XSS protection)

Greptile Summary

The PR makes getPassage automatically sanitize and structurally transform HTML while retaining explicit opt-out and runtime-specific DOM behavior.

  • Adds browser-native and optional server-side jsdom adapters.
  • Makes transformation idempotent across nested markers and multi-root fragments.
  • Forwards the transformation option through usePassage.
  • Updates UI integration, styling, tests, package metadata, and build configuration.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains, and the previously reported marker, dependency-error, type-version, sibling-root, and hook-forwarding issues are fixed or invalid at the current head.

Important Files Changed

Filename Overview
packages/core/src/bible-html-transformer.ts Adds sanitized, multi-root-aware idempotency handling and marks every transformed top-level element.
packages/core/src/bible.ts Automatically transforms HTML passages using browser DOMParser or optional server-side jsdom, with an explicit opt-out.
packages/hooks/src/usePassage.ts Exposes, forwards, and correctly keys fetching on the passage transformation option.
packages/core/src/bible-html-transformer-server.ts Replaces the server transformer’s linkedom adapter with jsdom.
packages/core/package.json Aligns jsdom runtime and type versions, declares it as an optional peer, and prevents browser resolution.
packages/ui/src/components/verse.tsx Retains client-side transformation as an idempotent rendering safety layer.

Sequence Diagram

sequenceDiagram
  participant Consumer
  participant Hook as usePassage
  participant Core as BibleClient.getPassage
  participant API as YouVersion API
  participant DOM as DOMParser or jsdom
  Consumer->>Hook: Request passage with transform option
  Hook->>Core: Forward request and transform option
  Core->>API: Fetch passage
  API-->>Core: Passage content
  alt HTML and transform enabled
    Core->>DOM: Parse content
    DOM-->>Core: Document
    Core->>Core: Sanitize and structurally transform
  end
  Core-->>Hook: Passage
  Hook-->>Consumer: Passage state
Loading

Reviews (18): Last reviewed commit: "Merge branch 'main' into transform-bible..." | Re-trigger Greptile

Context used (3)

getPassage now automatically sanitizes and transforms HTML content
before returning — verse wrapping, footnote extraction, nbsp, and
table fixes all happen at the root. Uses native DOMParser in browser,
dynamic import('linkedom') on server. Added data-yv-transformed
idempotency marker so double-transforms are a no-op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Apr 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1948fac

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-core Minor
@youversion/platform-react-hooks Minor
@youversion/platform-react-ui Minor
vite-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cameronapak cameronapak changed the title feat(core): auto-transform Bible HTML in getPassage Auto-transform the Bible HTML from getPassage so the consumer doesn't have extra steps Apr 20, 2026
Comment thread packages/core/src/bible-html-transformer.ts Outdated
Comment thread packages/core/src/bible.ts Outdated
cameronapak and others added 2 commits April 20, 2026 14:10
Run XSS sanitization before idempotency check so data-yv-transformed
cannot bypass sanitizeBibleHtmlDocument. Add clear error message when
linkedom is missing on server instead of opaque module-not-found error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread packages/core/src/bible-html-transformer.test.ts Outdated

@davidfedor davidfedor left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Big picture: I love the idea of being helpful, without requiring the developer to have to make another call. My comments and questions are around whether this is the best way to do that. (Maybe it is! I'm not sure yet.)
I notice this would be blurring the lines between Core being merely an API helper-layer, but now it would be doing some of the prep-work of the UI (visualization layer). So at the least having that be optional seems wise.
I'm wondering if that parameter should default to do the transformation, or not... or whether we need to force the dev to make a choice (to attempt to force them to make an informed choice).

Comment thread packages/core/src/bible.ts Outdated
@davidfedor

Copy link
Copy Markdown
Member

(FYI I've asked for thoughts from Bryson H; not sure if he's got cycles to contribute or not)

cameronapak and others added 2 commits April 24, 2026 11:23
Add `transform` param to `getPassage` (default: true) so consumers can
receive untransformed HTML without needing linkedom on the server.
CSS now handles verse label spacing for raw HTML via ::after pseudo-element.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cameronapak
cameronapak requested a review from davidfedor April 24, 2026 16:51
@arinthros

Copy link
Copy Markdown

@davidfedor @cameronapak this looks good to me, I just don't see an "approve" button in my UI. Approved by me!

@cameronapak

Copy link
Copy Markdown
Collaborator Author

Some added context on this ticket is: Why transformBibleHtml Exists — And Where It May Not Be Needed

If we can get this merged in, then I can write better docs for helping people use our HTML and styles without them having to manually transformBibleHtml on their end

Comment thread packages/core/package.json
@cameronapak

Copy link
Copy Markdown
Collaborator Author

Adding some clarity. David sent me a DM on Teams the other day saying this:

Cam, at your leisure, can you rebase Auto-transform the Bible HTML from getPassage so the consumer doesn't have extra steps by cameronapa… (or merge, whichever) and have greptile re-review it? I finally had time to look at it again and consider things and I think it's a step in the right direction.
(From a strategic point of view I'd like to make it easier for agents to create pages using YVP, and supporting non-React web paths is a component of that.)

cameronapak and others added 6 commits July 28, 2026 09:11
jsdom fails at runtime on Cloudflare Workers after a clean bundle.
linkedom covers the transform DOM surface, runs in workerd, and keeps
the optional peer small. Consolidate adapters and add a workerd CI smoke
so the next peer swap cannot silently break edge.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
Aligns runtime jsdom with @types/jsdom@^28 so Greptile stops flagging
the major skew. jsdom is test-only here (vitest + tbody browser stand-in).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
Regenerate pnpm-lock.yaml from main rather than hand-merging.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
Point the workerd CI smoke at resolveHtmlAdapters + transformBibleHtml
so a Node-only import or broken dynamic linkedom load fails the gate.
Surface the original import error alongside the install guidance.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
@cameronapak cameronapak changed the title Auto-transform the Bible HTML from getPassage so the consumer doesn't have extra steps feat(core): auto-transform Bible HTML in getPassage Aug 4, 2026
Cam requested dropping the workerd CI gate; linkedom remains the
runtime peer and jsdom stays test-only for vitest/browser reparse.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
Comment thread packages/core/src/bible-html-transformer.ts Outdated
cameronapak and others added 4 commits August 4, 2026 09:21
Revert the linkedom runtime choice per Cam — no workerd target.
Keep native DOMParser in browser and dynamic jsdom on server.
Drop bible-html-adapters and realign docs/changeset.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
…r only on root

Stub jsdom via package.json browser field so Vite/Rollup examples do not
bundle cssstyle. Only short-circuit idempotency when data-yv-transformed is
on the transform root; tighten the onclick sanitizer expect.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
UI tsup was bundling platform-core's jsdom import into the React package
(~11MB) and breaking vite-react. Mark jsdom external and stub it via the
browser field; mirror external in core's tsup.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cameron Pak <cameronandrewpak@gmail.com>
Signed-off-by: Cameron Pak <cameronandrewpak@gmail.com>
Comment thread packages/core/src/bible-html-transformer.ts Outdated
The idempotency guard checked only the first top-level element. A fragment
that mixed transformed and raw roots skipped verse wrapping and footnote
extraction for the raw siblings.

Check every top-level element instead, and mark every top-level element on
transform so the marker means "this whole fragment came from here".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread packages/core/src/bible.ts
cameronapak and others added 2 commits August 4, 2026 13:32
BibleClient.getPassage has a transform escape hatch for skipping HTML
transformation, but usePassage dropped it. Callers who want raw HTML, or
who run without the optional jsdom peer in a non-DOM environment, had no
way to ask for it through the hook.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Jeff Hampton <jhampton@gmail.com>
@jhampton

jhampton commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

@davidfedor - the Idempotency guarantee you mentioned was already implemented and Greptile shows 5/5 - is there another place I'm not seeing?

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.

4 participants