Skip to content

Commit dec5861

Browse files
authored
feat(drafting): store verse paragraph markers and book-level USFM fields (#264)
* feat(drafting): store verse paragraph markers and book-level USFM fields Closes #263. Unblocks fluent-web#314 (paragraph authoring in the RTE) and fluent-web#398 (Book Details panel). All additive: legacy rows and existing exports behave exactly as before. - translated_verses.markers (nullable jsonb): paragraph starts per verse. Offset 0 means the verse opens a paragraph; a mid-text offset splits the verse across paragraphs. Zod-validated: marker pattern (no USFM injection), strictly increasing offsets, offsets bounded by the content on insert. Upsert writes it, responses return it. - project_unit_bible_books.running_header / book_title: authored \h and \mt1 per unit and book, validated against marker injection, empty clears to null. - USFM export honors both: stored paragraph markers replace the hardcoded single \p per chapter (which remains the fallback for rows without markers), and \h/\mt prefer the authored fields with the display-name fallback. - GET /project-units/{id}/book-details and PATCH .../book-details/{bookId}, gated by authenticateUser + requirePermission + requireProjectUnitAccess. 21 new tests: the export generator (golden legacy output, opening markers, mid-verse splits, out-of-range offsets, book-field fallbacks) and the two validation schemas. Migration 0019 verified against a scratch Postgres. Claude-Session: https://claude.ai/code/session_01R6Xec2CgkML6mYyHmqiGK5 * fix(drafting): allowlist the paragraph markers a verse may open with The stored marker is emitted verbatim as `\<marker>` by the USFM export, so the previous `^[a-z][a-z0-9]{0,9}$` pattern let a client store `v` or `c` and put a bare `\v` / `\c` — a structural marker with no number — ahead of the real one. Replace the pattern with `z.enum` over the USFM 3.x body-text paragraph set, which also documents the accepted values in the OpenAPI spec for the editor. Introduction markers stay out: they precede \c 1 and cannot open inside a verse. Refs: #264 * fix(book-details): reject control and line-separator characters in book fields The guard covered `\`, LF and CR, but NUL, ESC, U+2028 and U+2029 passed straight into the `\h` / `\mt` line of the export, where a consumer treating the separators as line breaks sees a different file than the validator did. Reject the Cc, Zl and Zp ranges, spelled as literal ranges rather than `\p{...}` so the pattern published in the OpenAPI document carries the same meaning without the unicode flag. Also corrects the endpoint descriptions: the export emits `\mt`, not `\mt1`. Refs: #264 * test(usfm): assert a non-default marker on the untranslated-verse export The case stored `p`, which is exactly what the chapter falls back to, so it passed whether or not the exporter read `markers` at all. Use `q1` and assert no `\p` is emitted, so the test fails if the stored marker is ignored. Refs: #264 * style: prettier over the markers select and drizzle-generated migration meta Formatting only, and the cause of the red validate job: the markers select was inserted with off-by-two indentation, and drizzle-kit writes its meta JSON in its own style, which the repo's format check does not accept. Both meta files parse identically before and after. Claude-Session: https://claude.ai/code/session_01R6Xec2CgkML6mYyHmqiGK5 * refactor(book-details): own the project-unit auth check Review feedback: book-details imported requireProjectUnitAccess from usfm's middleware, coupling book metadata authoring to export generation because the two checks happen to look alike. The domain now carries its own. usfm and translated-verses each already do the same, so this follows the existing convention rather than inventing one; consolidating the three into shared infrastructure stays a separate, deliberate decision. A boundary test pins it: book-details imports nothing from a sibling feature domain, and its route uses its own middleware. Both fail against the code as reviewed. Claude-Session: https://claude.ai/code/session_01R6Xec2CgkML6mYyHmqiGK5 * feat(drafting): store the text of section headings (#269) Follow-up to #264, and the last storage gap behind fluent-web#397. A paragraph record is a marker plus an offset into the *verse's* text, so it cannot hold a section heading: a heading is a block of its own, with its own words, belonging to no verse. Exported today it renders as a bare "\s1" and the translator's words are gone. markers.headings carries them, in order, as blocks emitted before the verse: { "headings": [{ "marker": "s1", "text": "The Creation" }] } Markers are restricted to the heading subset of USFM_PARAGRAPH_MARKERS, so body text can never be stored as a heading and lost from the row, and the text is guarded against backslashes and line breaks exactly as the book fields are, since it is written straight into the USFM stream. paragraphs became optional, because a verse may carry only a heading, and the object now has to hold one or the other — null already means "no structure". No migration: the column is jsonb, so the shape lives in Zod. Confirmed with drizzle-kit generate ("No schema changes, nothing to migrate"). Claude-Session: https://claude.ai/code/session_01R6Xec2CgkML6mYyHmqiGK5 * style: prettier over the heading schema and its test Pre-merge validation caught the test file. The schema was unformatted too, and fixing only the reported file would have left the check red: prettier expands the USFM_HEADING_MARKERS array to one entry per line. Reflow only, no content change. Claude-Session: https://claude.ai/code/session_01R6Xec2CgkML6mYyHmqiGK5
1 parent 7aacbf3 commit dec5861

21 files changed

Lines changed: 4180 additions & 11 deletions

src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import '@/domains/bibles/bible-texts/bible-texts.route';
1818
import '@/domains/translated-verses/translated-verses.route';
1919
import '@/domains/verse-audio/verse-audio.route';
2020
import '@/domains/usfm/usfm.route';
21+
import '@/domains/book-details/book-details.route';
2122
import '@/domains/chapter-assignments/editor-state/user-chapter-assignment-editor-state.route';
2223
import '@/domains/projects/users/project-users.route';
2324
import '@/domains/organizations/users/org-users.route';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ALTER TABLE "project_unit_bible_books" ADD COLUMN "running_header" varchar;--> statement-breakpoint
2+
ALTER TABLE "project_unit_bible_books" ADD COLUMN "book_title" varchar;--> statement-breakpoint
3+
ALTER TABLE "translated_verses" ADD COLUMN "markers" jsonb;

0 commit comments

Comments
 (0)