Commit dec5861
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_01R6Xec2CgkML6mYyHmqiGK51 parent 7aacbf3 commit dec5861
21 files changed
Lines changed: 4180 additions & 11 deletions
File tree
- src
- db
- migrations
- meta
- domains
- book-details
- translated-verses
- usfm
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
0 commit comments