mirror of
https://github.com/scotttomaszewski/obsidian-disciples-journal.git
synced 2026-07-22 12:20:30 +00:00
Compare commits
No commits in common. "master" and "0.13.2" have entirely different histories.
46 changed files with 179 additions and 4258 deletions
|
|
@ -52,10 +52,6 @@ Source is organized under `src/` by responsibility:
|
|||
display setting, then delegates to the renderer and handles errors.
|
||||
- **`BibleEventHandlers.ts`** — the single, plugin-owned hover/popup lifecycle
|
||||
(mouseover/mouseout, show/hide timing). Loaded as a child component.
|
||||
- **`VerseSelection.ts`** — pure value object: a verse set within one book →
|
||||
contiguous `BibleReference[]` runs + a display label (`Genesis 1:2-3, 5`).
|
||||
- **`VerseSelectionService.ts`** — plugin-owned (`addChild`) holder of the *single*
|
||||
active verse selection across panes + its owning controller; notifies subscribers.
|
||||
|
||||
### `services/` — content resolution and storage
|
||||
|
||||
|
|
@ -86,18 +82,6 @@ Source is organized under `src/` by responsibility:
|
|||
window has its own `Document`, so styles are applied per-doc).
|
||||
- **`BookSuggest.ts`**, **`OpenBibleModal.ts`** — the `Open Bible` modal and its
|
||||
book autocomplete.
|
||||
- **`VerseWrapper.ts`** — `wrapPassageVerses(passageEl)`: wraps each rendered verse in a
|
||||
`.dj-verse` span (idempotent) so verses are selectable/highlightable.
|
||||
- **`VerseSelectionController.ts`** — one per rendered passage: wraps verses, binds
|
||||
gestures (desktop tap/shift, mobile long-press-drag), reflects the service's selection
|
||||
(highlight) and owns the action bar.
|
||||
- **`VerseActionBar.ts`** — floating bar shown while verses are selected; renders the
|
||||
Copy / Insert (and optional Append) actions with the configurable `split`/`toggle`/
|
||||
`submenu` format chooser.
|
||||
- **`VerseActions.ts`** — turns a selection + format into the payload and performs Copy
|
||||
(clipboard), Insert (Editor API), or Append (`Vault.process`); extracts verse text for
|
||||
the blockquote format from the passage's own document.
|
||||
- **`InsertTargetModal.ts`** — `FuzzySuggestModal<TFile>` note picker for "Append to note…".
|
||||
|
||||
### `utils/` — small helpers
|
||||
|
||||
|
|
@ -106,11 +90,9 @@ Source is organized under `src/` by responsibility:
|
|||
- **`BiblePassage.ts`** — `{ reference, html }` pairing for resolved content.
|
||||
- **`BibleApiResponse`/`BiblePassage`** are what flow back out of the content
|
||||
service to the renderer.
|
||||
- **`VerseId.ts`** — `parseVerseId(id)`: ESV marker id (`v01001002-1`) → `{chapter, verse}`.
|
||||
- **`VerseFormatter.ts`** — pure builders for the three insert formats (inline reference,
|
||||
`bible` code block, blockquote-with-text + citation).
|
||||
- **`FrontmatterUtil.ts`** — apply user-configured custom frontmatter to Bible
|
||||
notes (`getCustomFrontmatterForReference`, `applyCustomFrontmatter`).
|
||||
- **`BibleCodeblockFormatter.ts`** — formatting helper for the `bible` code block.
|
||||
|
||||
### `settings/`
|
||||
|
||||
|
|
@ -133,20 +115,7 @@ note renders
|
|||
```
|
||||
|
||||
Full passages follow the same resolution path through `getBibleContent`, but render
|
||||
the whole passage inline (`processFullBiblePassage`) with navigation + heading. Non-
|
||||
contiguous references (`Genesis 1:2-3, 5`) resolve via `getBibleContentList`, which parses
|
||||
the list with `BibleReference.parseList` and concatenates each run's HTML.
|
||||
|
||||
## Verse selection
|
||||
|
||||
After a full passage renders, `processFullBiblePassage` wraps its verses
|
||||
(`wrapPassageVerses` → `.dj-verse` spans) and attaches a `VerseSelectionController`.
|
||||
Selecting verses (tap/shift on desktop, long-press-drag on mobile) updates a per-passage
|
||||
`VerseSelection` and pushes it to the plugin-owned `VerseSelectionService`, which holds the
|
||||
single active selection. The owning controller highlights its verses and shows a
|
||||
`VerseActionBar`; Copy / Insert / Append (and the editor `Insert … here` menu + commands)
|
||||
run through `VerseActions`, formatting via `VerseFormatter`. See
|
||||
[docs/gotchas.md](docs/gotchas.md) for the wrapping/gesture/bar-lifecycle details.
|
||||
the whole passage inline (`processFullBiblePassage`) with navigation + heading.
|
||||
|
||||
## Storage model
|
||||
|
||||
|
|
@ -157,15 +126,8 @@ canonical reference. Because the frontmatter is the cache, re-opening a chapter
|
|||
reuses `toBibleApiResponse` on the stored data instead of re-hitting the API. See
|
||||
[docs/esv-api.md](docs/esv-api.md).
|
||||
|
||||
## Tests
|
||||
|
||||
`test/` holds the test suites (Node's built-in `node:test` runner via `tsx`), one
|
||||
`*.test.ts` per unit — currently `BibleReference`, `BookNames`, `VerseFormatter`,
|
||||
`VerseId`, and `VerseSelection`. Run with `npm test` (or `devbox run test`); they
|
||||
also gate `npm run build` and `just release`. See [docs/testing.md](docs/testing.md).
|
||||
|
||||
## Build
|
||||
|
||||
esbuild bundles `main.ts` → `main.js` (CJS, `es2018`); `obsidian`, `electron`, and
|
||||
CodeMirror packages are externalized. `npm run build` type-checks and runs tests
|
||||
first. See [docs/build-and-release.md](docs/build-and-release.md).
|
||||
CodeMirror packages are externalized. `npm run build` type-checks first. See
|
||||
[docs/build-and-release.md](docs/build-and-release.md).
|
||||
|
|
|
|||
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -1,35 +1,4 @@
|
|||
# Disciples Journal Releases
|
||||
|
||||
New changes land under `## Unreleased`; at release time it is renamed by hand to the
|
||||
version tag (see [docs/build-and-release.md](docs/build-and-release.md)). Each `## `
|
||||
header text is **exactly** the release tag (no leading `v`).
|
||||
|
||||
## Unreleased
|
||||
|
||||
- The "Open Bible" command (and ribbon icon) now opens the chosen chapter in a new tab
|
||||
instead of replacing the note you were reading
|
||||
|
||||
## 1.14.0
|
||||
|
||||
- Adds verse selection: tap verses in a rendered passage to select them (shift-click or
|
||||
long-press-drag for a range), then copy or insert them as an inline reference, a `bible`
|
||||
code block, or a blockquote of the verse text. A floating action bar (docked at the
|
||||
bottom of the view, with configurable split / toggle / submenu format choosers) and a
|
||||
desktop right-click "Insert … here" both drive it; an optional "Append to note…" action
|
||||
can be enabled in settings. "Insert" targets the note you were last editing (generated
|
||||
Bible notes are skipped), not the passage you selected from. Adds a *Verse selection*
|
||||
settings section
|
||||
- Adds support for non-contiguous references like `Genesis 1:2-3, 5` and `Genesis 1:2, 4, 8`
|
||||
— in full `bible` blocks, inline `` `code` `` references, and Live Preview (parsed by the
|
||||
new `BibleReference.parseList`)
|
||||
- Fixes chapter-range references (e.g. `Genesis 1-2`): `BibleReference.parse` was
|
||||
storing the end chapter in the `endVerse` field, so the range was dropped — it
|
||||
round-tripped to `Genesis 1` and was fetched/stored as a single chapter. The end
|
||||
chapter is now set correctly
|
||||
- Adds an automated test harness (Node's built-in `node:test` runner via `tsx`) with
|
||||
a first suite covering `BibleReference` parsing, round-tripping, and helpers; tests
|
||||
gate `npm run build` and `just release`
|
||||
- Corrects an overly greedy book-name normalization logic bug
|
||||
# Changelog
|
||||
|
||||
## 0.13.2
|
||||
|
||||
|
|
|
|||
22
CLAUDE.md
22
CLAUDE.md
|
|
@ -10,14 +10,6 @@ passages inside notes (inline hover previews + full `bible` code blocks) and can
|
|||
download passages on demand from the ESV API. See `README.md` for the user-facing
|
||||
feature tour and `manifest.json` for plugin metadata.
|
||||
|
||||
## Design ethos
|
||||
|
||||
**Non-intrusive by default.** People use this plugin during devotion, prayer, and small
|
||||
group. Features must never distract, frustrate, or shake the user out of an intimate time
|
||||
with God. Default to quiet: nothing new appears, moves, or interrupts until the user
|
||||
deliberately asks for it. No text shifting, no surprise popups, no persistent chrome. When
|
||||
in doubt, do less.
|
||||
|
||||
## Where to look
|
||||
|
||||
- **[ARCHITECTURE.md](ARCHITECTURE.md)** — how the plugin is wired: services,
|
||||
|
|
@ -29,7 +21,6 @@ in doubt, do less.
|
|||
- [docs/esv-api.md](docs/esv-api.md) — ESV API integration, note storage, frontmatter
|
||||
- [docs/reference-formats.md](docs/reference-formats.md) — supported reference syntax + parsing
|
||||
- [docs/build-and-release.md](docs/build-and-release.md) — build, lint, and the `just release` flow
|
||||
- [docs/testing.md](docs/testing.md) — test harness, how to run tests, build/release gating
|
||||
- **[FOLLOWUPS.md](FOLLOWUPS.md)** — small deferred findings captured during other
|
||||
tasks. Add an entry here instead of silently dropping out-of-scope work.
|
||||
- **[ROADMAP.md](ROADMAP.md)** — larger planned / in-flight efforts.
|
||||
|
|
@ -43,9 +34,8 @@ in doubt, do less.
|
|||
entry shim that re-exports `src/core/DisciplesJournalPlugin.ts`.
|
||||
- This repo follows the **obsidian-plugin-development** skill (ESLint rules from
|
||||
`eslint-plugin-obsidianmd`). Invoke that skill when touching plugin APIs.
|
||||
- Before committing: `npm run build` (runs `tsc -noEmit` + `npm test` + esbuild) and
|
||||
`npx eslint .` must both pass with zero errors/warnings. Tests live in `test/`
|
||||
(`npm test` / `devbox run test`) — see [docs/testing.md](docs/testing.md).
|
||||
- Before committing: `npm run build` (runs `tsc -noEmit` + esbuild) and
|
||||
`npx eslint .` must both pass with zero errors/warnings.
|
||||
- `main.js` is a generated/bundled artifact — never edit it by hand.
|
||||
|
||||
## Working agreements
|
||||
|
|
@ -60,9 +50,5 @@ change the code, update the matching doc in the same change:
|
|||
- **Add a new reference format or change parsing** → update `docs/reference-formats.md`.
|
||||
- **Introduce a workaround, magic number, or non-obvious behavior** → give it a home:
|
||||
a precise inline comment if local, a `docs/gotchas.md` entry if cross-cutting.
|
||||
- **Ship a user-facing change** → add a bullet under `## Unreleased` in `CHANGELOG.md`.
|
||||
- **Hit a small in-scope tangent** (worth fixing, but it'd derail the current task) →
|
||||
add a numbered `## N.` section to `FOLLOWUPS.md`, and clear it before the next feature.
|
||||
- **Plan a new feature or larger effort** → add a numbered `## N.` section to
|
||||
`ROADMAP.md` (and a plan doc once work starts). `FOLLOWUPS.md` and `ROADMAP.md` serve
|
||||
different lifespans — don't fold one into the other.
|
||||
- **Defer something out of scope** → add a `FOLLOWUPS.md` entry; promote it to
|
||||
`ROADMAP.md` (and a plan doc) if it grows into a real effort.
|
||||
|
|
|
|||
64
FOLLOWUPS.md
64
FOLLOWUPS.md
|
|
@ -1,33 +1,55 @@
|
|||
# Follow-ups
|
||||
# Workspace Follow-Ups
|
||||
|
||||
In-scope tangents found while working — important to fix, but they'd derail the task
|
||||
at hand. Add a numbered `## N.` section below instead of chasing them now, and
|
||||
**clear these before starting a new feature.** New features and larger efforts go in
|
||||
[ROADMAP.md](ROADMAP.md), not here.
|
||||
Lightweight tracking for tasks identified during other work that weren't tackled in the original scope. These are intentionally deferred — captured here so they don't get lost.
|
||||
|
||||
Mark a finished item with a `**Status:** done` line rather than deleting it; completed
|
||||
items get pruned and the rest renumbered on a periodic cleanup pass. Each entry
|
||||
carries the repo's standard fields (Identified / What / Why / Context / Effort), where
|
||||
Effort is sized XS (<1 h) · S (1–4 h) · M (1 day) · L (multi-day).
|
||||
Add new entries at the top. Remove entries when done (commit message can reference them).
|
||||
|
||||
<!-- Template — copy for each item, numbering sequentially:
|
||||
## N. Short title
|
||||
**Status:** open
|
||||
- **Identified:** YYYY-MM-DD, the work it came up in.
|
||||
- **What:** brief description of the change.
|
||||
- **Why:** the motivation / what value it adds.
|
||||
- **Context:** file paths, gotchas, anything that saves the next person grep time.
|
||||
- **Effort:** XS | S | M | L
|
||||
-->
|
||||
## Entry format
|
||||
|
||||
## 1. Finish pop-out window styling
|
||||
Each entry should include:
|
||||
|
||||
**Status:** open
|
||||
- **Identified:** YYYY-MM-DD and the work it came up in
|
||||
- **What:** brief description of the change
|
||||
- **Why:** the motivation / what value it adds
|
||||
- **Context:** background, file paths, gotchas, anything that would save the next person 10 minutes of grepping
|
||||
- **Effort:** rough sizing — XS (<1 h), S (1–4 h), M (1 day), L (multi-day)
|
||||
|
||||
---
|
||||
|
||||
## Remove (or wire up) dead `BibleCodeblockFormatter`
|
||||
|
||||
- **Identified:** 2026-05-31, funky-logic sweep while documenting the repo.
|
||||
- **What:** `src/utils/BibleCodeblockFormatter.ts` has no callers and carries a
|
||||
`// TODO - this probably belongs somewhere else`. Decide: delete it, or use it.
|
||||
- **Why:** Dead code misleads the next agent into thinking it's part of the flow.
|
||||
- **Context:** Confirmed unused via `grep -rn BibleCodeblockFormatter src/`.
|
||||
- **Effort:** XS
|
||||
|
||||
## Remove vestigial `ESV` / `ESVVerse` globals
|
||||
|
||||
- **Identified:** 2026-05-31, funky-logic sweep.
|
||||
- **What:** `src/types.d.ts` declares `interface ESVVerse` and
|
||||
`declare const ESV: ESVVerse[]`. The `ESV` global appears unused at runtime —
|
||||
verify, then drop the declaration if so.
|
||||
- **Why:** A declared global with no backing implementation is a trap.
|
||||
- **Context:** Only the declaration shows up in `src/`; confirm no consumer before removing.
|
||||
- **Effort:** XS
|
||||
|
||||
## Add a test harness, starting with `BibleReference.parse`
|
||||
|
||||
- **Identified:** 2026-05-31, repo documentation pass.
|
||||
- **What:** No automated tests exist. Add a lightweight runner and cover
|
||||
`BibleReference.parse` first.
|
||||
- **Why:** `parse` is pure, pattern-heavy, and the riskiest logic to refactor blind.
|
||||
- **Context:** `src/core/BibleReference.ts`; format matrix in `docs/reference-formats.md`.
|
||||
- **Effort:** S
|
||||
|
||||
## Finish pop-out window styling
|
||||
|
||||
- **Identified:** 2026-05-31, funky-logic sweep.
|
||||
- **What:** Styles aren't fully ported into freshly created pop-out windows.
|
||||
- **Why:** Passages render unstyled in pop-outs; a known rough edge.
|
||||
- **Context:** `DisciplesJournalPlugin.updateBibleStyles` (`:219-246`) flags this in a
|
||||
- **Context:** `DisciplesJournalPlugin.updateBibleStyles` (`:131-158`) flags this in a
|
||||
comment; see [docs/gotchas.md](docs/gotchas.md). Tracked upstream in an Obsidian
|
||||
Discord thread linked in the code.
|
||||
- **Effort:** M
|
||||
|
|
|
|||
85
ROADMAP.md
85
ROADMAP.md
|
|
@ -1,85 +1,20 @@
|
|||
# Roadmap
|
||||
|
||||
New features and larger planned / in-flight efforts, tracked as numbered `## N.`
|
||||
sections. For small in-scope tangents to clear before the next feature see
|
||||
Larger planned or in-flight efforts. For small deferred findings see
|
||||
[FOLLOWUPS.md](FOLLOWUPS.md); for released history see [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
> Entries below are **observations** about likely directions, inferred from the
|
||||
> code — not committed plans. When work actually starts, promote the item to a real
|
||||
> plan/spec doc and add a `## Status` line here. Mark a shipped item
|
||||
> `**Status:** done`; prune and renumber on a periodic cleanup pass.
|
||||
> code — not committed plans. Promote one to a real plan/spec doc (and a
|
||||
> `## Status` section) when work actually starts.
|
||||
|
||||
Each item carries a **Lens** (User-facing / Technical health / Growth & ecosystem)
|
||||
and an **Effort** tag: XS (<1 h) · S (1–4 h) · M (1 day) · L (multi-day).
|
||||
## No efforts currently in flight.
|
||||
|
||||
## 1. Multiple Bible versions
|
||||
## Observed future directions
|
||||
|
||||
- **Lens:** User-facing · **Effort:** L
|
||||
- The storage path scheme is already
|
||||
- **Multiple Bible versions.** The storage path scheme is already
|
||||
`<bibleContentVaultPath>/<preferredBibleVersion>/...` and settings expose a
|
||||
"preferred version", but `BookNames`, `ESVApiService`, and rendering assume ESV
|
||||
end-to-end. The headline feature; needs a version-agnostic content-source seam.
|
||||
|
||||
## 2. Bundled offline public-domain version (KJV/WEB)
|
||||
|
||||
- **Lens:** User-facing · **Effort:** M
|
||||
- Today the plugin is inert without an ESV API token. Shipping a public-domain
|
||||
translation bundled in the plugin means it works on install with zero setup — a big
|
||||
onboarding win and a natural first consumer of the multi-version seam (#1).
|
||||
|
||||
## 3. Copy / export passage with attribution
|
||||
|
||||
- **Lens:** User-facing · **Effort:** S
|
||||
- A "copy passage" action that emits clean markdown/text and auto-appends the required
|
||||
ESV copyright line. Serves the journaling use case and respects the copyright
|
||||
obligation noted in `README.md`.
|
||||
|
||||
## 4. Touch / mobile interaction
|
||||
|
||||
- **Lens:** User-facing · **Effort:** M
|
||||
- `isDesktopOnly` is `false`, but the core interaction is hover-to-preview, which
|
||||
doesn't exist on touch. Define a tap/long-press affordance so the plugin is actually
|
||||
usable on mobile.
|
||||
|
||||
## 5. Reading plans / daily reading
|
||||
|
||||
- **Lens:** User-facing · **Effort:** M
|
||||
- A code block or command that surfaces a day's passage from a plan (e.g. M'Cheyne,
|
||||
chronological). Leans on existing passage rendering; turns the plugin from a
|
||||
reference tool into a daily-habit tool.
|
||||
|
||||
## 6. Expand the test harness
|
||||
|
||||
- **Lens:** Technical health · **Effort:** M
|
||||
- `BibleReference.parse` is covered; extend to `BookNames` normalization, `BibleFiles`
|
||||
path/filename logic, and the `BibleContentService` resolution funnel
|
||||
(cache → note → API). The highest-leverage safety net before the multi-version
|
||||
refactor (#1). See [docs/testing.md](docs/testing.md).
|
||||
|
||||
## 7. Multi-source content seam
|
||||
|
||||
- **Lens:** Technical health · **Effort:** L
|
||||
- Decouple "where content comes from" from ESV specifically (bible-api.com, local
|
||||
USFM/USX import, etc.). Pairs with multiple versions (#1) but is the *source* axis
|
||||
rather than the *version* axis; enables offline import workflows.
|
||||
|
||||
## 8. Rendering robustness: finish pop-out styling + large-passage perf
|
||||
|
||||
- **Lens:** Technical health · **Effort:** M
|
||||
- Promote the pop-out styling gap tracked in [FOLLOWUPS.md](FOLLOWUPS.md), and address
|
||||
rendering very long passages (lazy/virtualized render, cache eviction) so whole-book
|
||||
blocks don't jank.
|
||||
|
||||
## 9. Community plugin store submission
|
||||
|
||||
- **Lens:** Growth & ecosystem · **Effort:** S–M
|
||||
- The repo already follows `eslint-plugin-obsidianmd` and gates on lint/tests. Getting
|
||||
listed in Obsidian's community catalog is the single biggest distribution lever and
|
||||
mostly a compliance/review task.
|
||||
|
||||
## 10. Clickable cross-references & footnotes
|
||||
|
||||
- **Lens:** Growth & ecosystem · **Effort:** M
|
||||
- The ESV HTML already carries footnotes and cross-references (rendered today as static
|
||||
text). Making cross-refs clickable to open/preview the target passage turns rendered
|
||||
passages into a navigable study surface.
|
||||
end-to-end. Generalizing would mean a version-agnostic content-source seam.
|
||||
- **A test harness.** There are no automated tests. `BibleReference.parse` (pure,
|
||||
pattern-heavy, highest-risk logic) is the natural first target. See
|
||||
[FOLLOWUPS.md](FOLLOWUPS.md).
|
||||
|
|
|
|||
42
devbox.json
42
devbox.json
|
|
@ -1,31 +1,15 @@
|
|||
{
|
||||
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.17.2/.schema/devbox.schema.json",
|
||||
"packages": [
|
||||
"nodejs@22",
|
||||
"just@latest",
|
||||
"github-cli@latest",
|
||||
"jq@latest"
|
||||
],
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
"npm install --no-audit --no-fund"
|
||||
],
|
||||
"scripts": {
|
||||
"build": [
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"npm run dev"
|
||||
],
|
||||
"test": [
|
||||
"npm run test"
|
||||
],
|
||||
"release": [
|
||||
"just release $@"
|
||||
],
|
||||
"run": [
|
||||
"$@"
|
||||
]
|
||||
}
|
||||
}
|
||||
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
|
||||
"packages": [
|
||||
"github-cli@latest",
|
||||
"jq@latest"
|
||||
],
|
||||
"shell": {
|
||||
"init_hook": [
|
||||
"echo 'Welcome to devbox!' > /dev/null"
|
||||
],
|
||||
"scripts": {
|
||||
"release": "just release $@"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
137
devbox.lock
137
devbox.lock
|
|
@ -7,10 +7,6 @@
|
|||
"source": "devbox-search",
|
||||
"version": "2.23.0"
|
||||
},
|
||||
"github:NixOS/nixpkgs/nixpkgs-unstable": {
|
||||
"last_modified": "2026-05-29T05:01:12Z",
|
||||
"resolved": "github:NixOS/nixpkgs/e9a7635a57597d9754eccebdfc7045e6c8600e6b?lastModified=1780030872&narHash=sha256-u6WU%2Fyd%2Fo8iYQrHX3RAwO1hYa3LkoSL%2BWNQD0rJfJZQ%3D"
|
||||
},
|
||||
"jq@latest": {
|
||||
"last_modified": "2026-03-16T02:27:38Z",
|
||||
"resolved": "github:NixOS/nixpkgs/f8573b9c935cfaa162dd62cc9e75ae2db86f85df#jq",
|
||||
|
|
@ -126,139 +122,6 @@
|
|||
"store_path": "/nix/store/jyi1vkcbaihqbfbra4qd0107sj1jhdk4-jq-1.8.1-bin"
|
||||
}
|
||||
}
|
||||
},
|
||||
"just@latest": {
|
||||
"last_modified": "2026-05-21T08:15:18Z",
|
||||
"resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#just",
|
||||
"source": "devbox-search",
|
||||
"version": "1.51.0",
|
||||
"systems": {
|
||||
"aarch64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/18ml04rix1526kmcv5fcir6n7cb8lxyf-just-1.51.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/48iac05gjz4kjpx605hcgn95an8ary9q-just-1.51.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/d0n7b7xd2miyja6m2nlzc2szj0gqzgxb-just-1.51.0-doc"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/18ml04rix1526kmcv5fcir6n7cb8lxyf-just-1.51.0"
|
||||
},
|
||||
"aarch64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/6q8i59br2gzxnqax4dz3g1fmkwp2mfs2-just-1.51.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/8ygmlmpxmggc2wqbkz1yhf7fz5nwg084-just-1.51.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/ppj3c78k6x3bql77f3mgrjjrxb2jjc0q-just-1.51.0-doc"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/6q8i59br2gzxnqax4dz3g1fmkwp2mfs2-just-1.51.0"
|
||||
},
|
||||
"x86_64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/7ppiil9ycr565vqgbc18x4rpgbfgcnzy-just-1.51.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/144wqvh3s441y05xvzn8m16rhcqbki7d-just-1.51.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/x5g644hwbjdlij9w37gwgg1z1h4b6qqk-just-1.51.0-doc"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/7ppiil9ycr565vqgbc18x4rpgbfgcnzy-just-1.51.0"
|
||||
},
|
||||
"x86_64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/3j32zmchv9wd60qng1mm04yj07wf04i8-just-1.51.0",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "man",
|
||||
"path": "/nix/store/p04vig0h2hj7hg5vgd74n5nphlcy23l8-just-1.51.0-man",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"name": "doc",
|
||||
"path": "/nix/store/r800x8xppg70s00hw3h6x9cxpd6nh06y-just-1.51.0-doc"
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/3j32zmchv9wd60qng1mm04yj07wf04i8-just-1.51.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodejs@22": {
|
||||
"last_modified": "2026-05-21T08:15:18Z",
|
||||
"plugin_version": "0.0.2",
|
||||
"resolved": "github:NixOS/nixpkgs/4a29d733e8a7d5b824c3d8c958a946a9867b3eb2#nodejs_22",
|
||||
"source": "devbox-search",
|
||||
"version": "22.22.3",
|
||||
"systems": {
|
||||
"aarch64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/k1yy2d1h7pvpac7m14vmaw23j529fpza-nodejs-22.22.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/k1yy2d1h7pvpac7m14vmaw23j529fpza-nodejs-22.22.3"
|
||||
},
|
||||
"aarch64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/d8zana2n6i904c088i533ivg1j817x8v-nodejs-22.22.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/d8zana2n6i904c088i533ivg1j817x8v-nodejs-22.22.3"
|
||||
},
|
||||
"x86_64-darwin": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/9yvmgryhhwnswgaji30cpw4m9swhv1ia-nodejs-22.22.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/9yvmgryhhwnswgaji30cpw4m9swhv1ia-nodejs-22.22.3"
|
||||
},
|
||||
"x86_64-linux": {
|
||||
"outputs": [
|
||||
{
|
||||
"name": "out",
|
||||
"path": "/nix/store/ac9bklddx1klg92hj7r08xmpky1nwag2-nodejs-22.22.3",
|
||||
"default": true
|
||||
}
|
||||
],
|
||||
"store_path": "/nix/store/ac9bklddx1klg92hj7r08xmpky1nwag2-nodejs-22.22.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,8 @@
|
|||
| Script | What it does |
|
||||
| --- | --- |
|
||||
| `npm run dev` | esbuild in watch mode (inline sourcemaps, no minify). |
|
||||
| `npm run build` | `tsc -noEmit -skipLibCheck` (type-check) → `npm test` → production esbuild (minified). |
|
||||
| `npm run build-no-check` | Production esbuild only — skips type-checking **and tests**. |
|
||||
| `npm test` | Runs the test suite (`tsx --test test/*.test.ts`). See [testing.md](testing.md). |
|
||||
| `npm run build` | `tsc -noEmit -skipLibCheck` (type-check) then a production esbuild (minified). |
|
||||
| `npm run build-no-check` | Production esbuild only — skips type-checking. |
|
||||
| `npm run lint` | `eslint .` |
|
||||
| `npm run version` | `version-bump.mjs` (syncs `manifest.json` + `versions.json`). |
|
||||
|
||||
|
|
@ -31,24 +30,14 @@ The repo follows `eslint-plugin-obsidianmd` (see `eslint.config.mjs`) and the
|
|||
|
||||
## Releasing
|
||||
|
||||
Releases go through the `justfile` (`just release <version>`). Accumulate user-facing
|
||||
changes under `## Unreleased` in [../CHANGELOG.md](../CHANGELOG.md) as you work — the
|
||||
recipe promotes that section to the release version for you (no manual rename needed).
|
||||
The recipe:
|
||||
Releases go through the `justfile` (`just release <version>`), which:
|
||||
|
||||
1. Normalizes the version (strips a leading `v`) and rejects anything that isn't semver
|
||||
(e.g. `1.0.1` or `1.0.1-rc.1`).
|
||||
2. Refuses to run if `git status` is not clean, or if the tag already exists.
|
||||
3. Syncs the version into `manifest.json`, `package.json`, and `versions.json` (via
|
||||
`jq`) — `versions.json` maps the new version to the current `minAppVersion`.
|
||||
4. Promotes `## Unreleased` → `## <version>` in `CHANGELOG.md` and uses that section's
|
||||
body as the GitHub release notes (falls back to `Release <version>` if empty).
|
||||
5. Builds with `npm run build` (type-check + tests + production esbuild) — a failing
|
||||
type-check or test aborts the release after the file edits but before commit/push.
|
||||
6. Commits (`Prepares for release '<version>'`) the four metadata/changelog files and
|
||||
pushes the current branch (`git push -u origin HEAD`).
|
||||
7. Creates a GitHub release with `gh`, targeting the just-pushed branch and uploading
|
||||
`main.js`, `manifest.json`, and `styles.css` as assets.
|
||||
1. Refuses to run if `git status` is not clean.
|
||||
2. Sets `version` in both `manifest.json` and `package.json` (via `jq`).
|
||||
3. Builds with `npm run build-no-check`.
|
||||
4. Commits (`Prepares for release '<version>'`) and pushes.
|
||||
5. Creates a GitHub release with `gh`, uploading `main.js`, `manifest.json`, and
|
||||
`styles.css` as assets.
|
||||
|
||||
### gh token note
|
||||
|
||||
|
|
|
|||
|
|
@ -82,13 +82,3 @@ across existing notes.
|
|||
All failures funnel through `BibleApiResponse.error(message, ErrorType)`
|
||||
(`ApiAuthentication`, `BadApiResponse`, `RequestsForbidden`, …). User-visible cases
|
||||
surface an Obsidian `Notice`; internal ones are logged to the console.
|
||||
|
||||
## Attribution when reproducing verse text
|
||||
|
||||
The verse-selection **blockquote** format (`VerseFormatter.formatBlockquote`) reproduces
|
||||
ESV text into the user's note and appends a `— <ref> (ESV)` citation. The ESV API
|
||||
[copyright/permissions terms](https://api.esv.org/) require attribution when ESV text is
|
||||
quoted; the `(ESV)` suffix is the minimum, and larger reproductions carry a fuller
|
||||
copyright-notice obligation. If the quoting story expands (e.g. exporting many passages),
|
||||
revisit the notice requirement here. The other two formats (inline reference, `bible`
|
||||
block) store only a reference, not text, so they don't trigger this.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ frontmatter-as-cache model, `:`→`v` filename encoding) live in
|
|||
loads its document listeners/timer and, crucially, tears them down on plugin
|
||||
unload. A previous version leaked global `document` listeners; do **not** reintroduce
|
||||
per-render or per-reference global listeners — route hover behavior through this one
|
||||
handler. (`DisciplesJournalPlugin.ts:73-77`, `BibleEventHandlers.ts`.)
|
||||
handler. (`DisciplesJournalPlugin.ts:60-65`, `BibleEventHandlers.ts`.)
|
||||
|
||||
## Pop-out windows: styles don't fully carry over (known-incomplete)
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ Each Obsidian pop-out window is a separate `Document`, so `updateBibleStyles`
|
|||
iterates all leaves and applies styles per-document. A code comment flags that this
|
||||
**still doesn't fully work** — a freshly created pop-out doesn't get the style
|
||||
vars ported over. If you touch styling and a pop-out looks unstyled, this is why,
|
||||
not your change. (`DisciplesJournalPlugin.ts:219-246`.)
|
||||
not your change. (`DisciplesJournalPlugin.ts:131-158`.)
|
||||
|
||||
## The passage cache is keyed by object identity — it effectively never hits
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ up** with the inbound (freshly parsed) `ref`. Two equal-but-distinct
|
|||
`BibleReference` objects are different map keys, so cross-call lookups essentially
|
||||
always miss; the real cache is the saved note on disk. Harmless today, but if you
|
||||
ever rely on this in-memory cache, key it by `ref.toString()` instead.
|
||||
(`BibleContentService.ts:14,97-103`.)
|
||||
(`BibleContentService.ts:14,72-80`.)
|
||||
|
||||
## scrollToVerse watches the DOM instead of guessing a delay
|
||||
|
||||
|
|
@ -39,44 +39,3 @@ uses a `MutationObserver` to scroll the instant the `.verse-N` element appears,
|
|||
with a **5000 ms** fallback that disconnects the observer if it never does (wrong
|
||||
ref, or an edit mode that produces no verse elements). Don't replace this with a
|
||||
fixed `setTimeout`. (`BibleFiles.ts:118-145`.)
|
||||
|
||||
## Verse selection: wrapping, gestures, and bar lifecycle
|
||||
|
||||
Verse selection is layered onto rendered passages by `VerseSelectionController` (one per
|
||||
full `bible` passage, attached in `BibleReferenceRenderer.processFullBiblePassage`). A few
|
||||
non-obvious things:
|
||||
|
||||
- **Verses aren't pre-wrapped.** ESV HTML marks a verse only with a
|
||||
`<b class="verse-num|chapter-num" id="vBBCCCVVV-N">`; the verse's text then runs as loose
|
||||
inline nodes until the next marker, sometimes across `<p>` boundaries.
|
||||
`wrapPassageVerses` (`VerseWrapper.ts`) walks each block and wraps each verse's run in a
|
||||
`<span class="dj-verse" data-chapter data-verse>`. It's **idempotent** (bails if a
|
||||
`.dj-verse` already exists) and a verse split across paragraphs yields two spans sharing
|
||||
the same `data-verse` — both highlight together. `parseVerseId` ignores the book digits;
|
||||
the book comes from the passage's canonical reference.
|
||||
- **Mobile selection vs. scroll.** Touch uses a long-press threshold (`LONG_PRESS_MS`): a
|
||||
quick swipe scrolls normally; only a deliberate long-press engages drag-select (which
|
||||
then calls `preventDefault` on `touchmove`). A short tap toggles a single verse. Don't
|
||||
remove the threshold or you'll hijack scrolling.
|
||||
- **One selection, globally.** `VerseSelectionService` (plugin-owned, `addChild`) holds a
|
||||
single active selection + its owning controller. Only the owner renders highlight and the
|
||||
action bar, so selecting in one passage clears another.
|
||||
- **The action bar is per-`Document` and bottom-center docked.** It's appended to the
|
||||
passage's own `doc.body` (popout-safe). It's docked at the bottom-center of the viewport
|
||||
via CSS — **not** anchored to the passage — because a full chapter is taller than the
|
||||
viewport, so anchoring to the passage's `getBoundingClientRect().bottom` rendered the bar
|
||||
off-screen. Blockquote text is extracted from the owner's `sourceEl` (its own document) so
|
||||
popouts stay correct — never query the global `document`.
|
||||
- **Selection clears when its passage unloads.** `controller.onunload` calls
|
||||
`service.clearIfOwner(this)`, so a note re-render or pane close drops a stale selection
|
||||
and its bar.
|
||||
- **"Insert" doesn't target the active pane.** When you click a verse, the active pane
|
||||
*is* the passage's pane — usually a generated note under the Bible content folder, which
|
||||
is the last place you want the verse inserted. So the plugin tracks the last active
|
||||
**editable, non-generated** markdown note (`DisciplesJournalPlugin.handleActiveLeafChange`
|
||||
→ `lastEditableLeaf`, filtered by `isBibleContentFile`) and `resolveInsertTarget()`
|
||||
returns it. There's no public "leaves in MRU order" API — this `active-leaf-change`
|
||||
tracking is the stand-in. Insert does **not** steal focus; it drops the text in and shows
|
||||
a `Passage … inserted into note …` notice (important when the target pane is in the
|
||||
background). The right-click "Insert here" path is unaffected — it always uses the clicked
|
||||
editor.
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ with [../CLAUDE.md](../CLAUDE.md); for how the pieces fit together read
|
|||
syntax and how `BibleReference.parse` interprets it.
|
||||
- **[build-and-release.md](build-and-release.md)** — building, type-checking,
|
||||
linting, and the `just release` workflow.
|
||||
- **[testing.md](testing.md)** — the test harness: how to run tests, where they
|
||||
live, and how they gate the build/release flows.
|
||||
|
||||
## See also
|
||||
|
||||
|
|
|
|||
|
|
@ -44,19 +44,3 @@ provides:
|
|||
|
||||
These fields also drive note filenames — see
|
||||
[esv-api.md](esv-api.md) and `BibleFiles.pathForPassage`.
|
||||
|
||||
## Non-contiguous (comma) lists
|
||||
|
||||
Verse selection (see [gotchas.md](gotchas.md)) can produce non-contiguous references
|
||||
like `Genesis 1:2-3, 5` or `Genesis 1:31, 2:1`. These are parsed by
|
||||
`BibleReference.parseList`, **not** the single-range `parse`:
|
||||
|
||||
- The first comma-separated item is a full reference (any format above).
|
||||
- Later items may be a bare verse (`5`), a bare verse range (`5-7`) — both inheriting the
|
||||
previous item's book **and** chapter — or `chapter:verse[-end]`, inheriting only the book.
|
||||
- The result is a `BibleReference[]` (one per contiguous run); any invalid item makes the
|
||||
whole list `null`.
|
||||
|
||||
A rendered list resolves each run and concatenates the HTML
|
||||
(`BibleContentService.getBibleContentList`); the passage heading/link uses the **first**
|
||||
run.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,213 +0,0 @@
|
|||
# Verse Selection & Insert-into-Note — Design
|
||||
|
||||
## Context
|
||||
|
||||
Today the plugin renders Bible passages (full `bible` code blocks) but offers no way
|
||||
to *act* on the verses inside them. The user wants to select one or more verses from a
|
||||
rendered passage and pull them into a note — fluidly, the way they journal (inline
|
||||
reference mid-sentence), study (full code block), or share (blockquote with the actual
|
||||
text). This must work across multiple open notes/windows, on desktop **and** mobile,
|
||||
and must never feel intrusive: users are often in devotion, prayer, or small group, and
|
||||
the feature should stay completely invisible until they choose to act.
|
||||
|
||||
This is also the **foundation** for a future feature ("select verses → find all notes
|
||||
that reference them"), so verse selection is being designed as a clean, reusable
|
||||
primitive with a small action registry, not a one-off.
|
||||
|
||||
## Design ethos (new, to be recorded in CLAUDE.md)
|
||||
|
||||
A `## Design ethos` section will be added inline to `CLAUDE.md`:
|
||||
|
||||
> **Non-intrusive by default.** People use this plugin during devotion, prayer, and
|
||||
> small group. Features must never distract, frustrate, or shake the user out of an
|
||||
> intimate time with God. Default to quiet: nothing new appears, moves, or interrupts
|
||||
> until the user deliberately asks for it. No text shifting, no surprise popups, no
|
||||
> persistent chrome. When in doubt, do less.
|
||||
|
||||
Every decision below follows from this: selection adds zero visible chrome until a verse
|
||||
is tapped; the action bar exists only while a selection exists; insertion never happens
|
||||
automatically.
|
||||
|
||||
## Interaction model
|
||||
|
||||
**Selection (inside a rendered `bible` passage):**
|
||||
- **Desktop tap / click a verse** → toggles that verse (non-contiguous by default: tap
|
||||
v2 then v5 selects exactly 2 and 5). Selected verses get a subtle inline highlight
|
||||
that follows the text across line wraps (verses share lines, so the highlight is on an
|
||||
inline run, not a line).
|
||||
- **Desktop Shift+click a verse** → selects the contiguous range from the anchor (last
|
||||
toggled verse) through the clicked verse.
|
||||
- **Mobile short tap** → toggles a single verse.
|
||||
- **Mobile long-press (≈400 ms) then drag** → initiates a range selection and extends it
|
||||
as the finger moves. A press threshold guards against hijacking normal scrolling; a
|
||||
quick swipe scrolls as usual, only a deliberate long-press enters selection-drag.
|
||||
- Selection **persists** across panes/windows until explicitly cleared (the bar's ✕, the
|
||||
`Esc` key, or a "Clear selection" command). Inserting does **not** auto-clear, so the
|
||||
user can insert the same selection in several places. Selection clears if its passage
|
||||
element is destroyed/re-rendered.
|
||||
|
||||
**Acting on a selection — same actions surfaced in three places:**
|
||||
1. **Floating action bar** — appears (anchored near the selection; bottom-docked on
|
||||
narrow/mobile layouts) only while ≥1 verse is selected. Carries the reference label,
|
||||
a ✕, and the **Copy** and **Insert** actions (plus **Append to note…** *only when the
|
||||
user enables it* — see below). Same on desktop and mobile (the bar is the primary
|
||||
surface on mobile, which has no right-click). **How format is chosen on the bar is
|
||||
configurable** (`formatChooserStyle` setting) — all three presentations are implemented
|
||||
and the user picks one:
|
||||
- **`split`** (default) — split buttons `[Copy ▾] [Insert ▾]`; the body uses the
|
||||
default format, the ▾ chevron opens the other two.
|
||||
- **`toggle`** — a segmented format toggle `[ Ref | Block | Quote ]` plus plain action
|
||||
buttons `[Copy] [Insert]`; pick the format once, then an action.
|
||||
- **`submenu`** — plain action buttons, each of which opens a format submenu.
|
||||
2. **Desktop right-click on the passage** → context menu with the same Copy / Insert
|
||||
actions (plus Append to note… when enabled), each with a format submenu.
|
||||
3. **Desktop right-click inside any editor** (while a selection exists) → an
|
||||
"Insert `<ref>` here" entry that inserts at the click point — the precise, reverse-flow
|
||||
path that sidesteps the multi-window problem because the user placed their own cursor.
|
||||
|
||||
**Locations for insertion:**
|
||||
- **Insert at cursor** — drops into the last-focused markdown editor at its cursor (bar
|
||||
button + a hotkey-able command). Primary path.
|
||||
- **Right-click in editor → "Insert `<ref>` here"** — inserts at the exact click point.
|
||||
- **Append to note…** *(optional, off by default)* — a fuzzy note-picker (`FuzzySuggestModal`
|
||||
over markdown `TFile`s) → appends to the end of the chosen note. This overlaps with
|
||||
Insert-at-cursor, so it ships disabled and is enabled via `enableAppendToNote`; only then
|
||||
does it appear on the bar / context menu and as a command.
|
||||
|
||||
**Three output formats (chosen per-insert; a configurable default sets the button body):**
|
||||
- **Inline reference** — `` `Genesis 1:2-3, 5` `` (renders with the existing hover preview).
|
||||
- **`bible` code block** — fenced block of the same reference (renders as the full passage).
|
||||
- **Blockquote with text** — the actual verse text as a markdown blockquote ending in a
|
||||
`— Genesis 1:2-3, 5 (ESV)` citation; self-contained / portable to other apps.
|
||||
|
||||
## Non-contiguous references
|
||||
|
||||
`BibleReference` (`src/core/BibleReference.ts`) models only a single contiguous range and
|
||||
its `parse` rejects comma lists. Because non-contiguous is the default selection behavior,
|
||||
we need to represent and render `Genesis 1:2-3, 5`:
|
||||
|
||||
- **Selection state** is a set of `{chapter, verse}` within the passage. A new
|
||||
`VerseSelection` value object collapses the set into **contiguous runs**, each a normal
|
||||
`BibleReference`, plus a combined display label (`"Genesis 1:2-3, 5"`,
|
||||
cross-chapter-aware). This keeps `BibleReference` single-range — a multi-run selection is
|
||||
just `BibleReference[]`.
|
||||
- **Blockquote** needs no parser change: verse text is extracted from the already-rendered
|
||||
DOM verse-spans (see wrapping below); citation uses the combined label.
|
||||
- **Inline reference & code block** must round-trip render. Extend the parse/resolve path
|
||||
to accept a comma-separated verse list within a chapter → `BibleReference[]`, and have
|
||||
the content service resolve each run (ESV API `q=` accepts comma lists; runs can also be
|
||||
resolved/concatenated individually from cache). Documented in `docs/reference-formats.md`.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Verse wrapping (the enabling primitive).** ESV HTML marks verses only with
|
||||
`<b class="verse-num" id="v01001002-1">` (and `chapter-num` for the first verse); a verse's
|
||||
text runs as loose inline nodes until the next marker, sometimes across `<p>` boundaries.
|
||||
After `processFullBiblePassage` builds `passageEl`
|
||||
(`src/components/BibleReferenceRenderer.ts:134`), a new pass walks each paragraph's child
|
||||
nodes, opens a new `<span class="dj-verse" data-book data-chapter data-verse>` at each
|
||||
verse-num/chapter-num marker, and collects following siblings until the next marker. The id
|
||||
(`v BB CCC VVV -N`) is parsed to book/chapter/verse (ignore the `-N` suffix). A verse split
|
||||
across paragraphs yields two spans sharing the same `data-verse`; both highlight together.
|
||||
This same `data-*` tagging is what the future "find referencing notes" feature will key on.
|
||||
|
||||
**New modules (`src/`):**
|
||||
- `core/VerseSelectionService.ts` — plugin-owned (added via `addChild`), holds the current
|
||||
`VerseSelection`, emits change events, exposes clear(). Single source of truth across panes.
|
||||
- `core/VerseSelection.ts` — value object: verse set → contiguous `BibleReference[]` runs +
|
||||
combined label; pure, unit-tested.
|
||||
- `components/VerseSelectionController.ts` — attached per rendered passage: does the verse
|
||||
wrapping, binds pointer/click/keyboard gestures (desktop tap/shift, mobile long-press+drag
|
||||
with scroll guard), applies/removes highlight classes, reflects the service's selection.
|
||||
- `components/VerseActionBar.ts` — the floating bar; shown/hidden by the service's
|
||||
selection state; per-`Document` aware (popout windows each have their own doc, like
|
||||
`BibleStyles`). Renders its format chooser in whichever mode `formatChooserStyle`
|
||||
selects (`split` / `toggle` / `submenu`) — all three implemented.
|
||||
- `components/InsertTargetModal.ts` — `FuzzySuggestModal<TFile>` note picker for the
|
||||
optional "Append to note…" action (only used when `enableAppendToNote` is on).
|
||||
- `utils/VerseFormatter.ts` — turns a `VerseSelection` into each of the three output strings
|
||||
(inline ref, code block, blockquote+citation); pure, unit-tested.
|
||||
|
||||
**Touch points (existing):**
|
||||
- `components/BibleReferenceRenderer.ts` — invoke wrapping + attach a
|
||||
`VerseSelectionController` in `processFullBiblePassage`.
|
||||
- `core/DisciplesJournalPlugin.ts` (`onload`) — construct `VerseSelectionService`
|
||||
(`addChild`); `registerEvent(workspace.on('editor-menu', …))` for "Insert `<ref>` here";
|
||||
add commands: *Insert selected verses at cursor*, *Copy selected verses*,
|
||||
*Clear verse selection*; pass the service into the renderer.
|
||||
- `settings/DisciplesJournalSettings.ts` — new settings (below); **reorganize the settings
|
||||
tab into clearly headed sections** for navigation (e.g. *Display*, *Typography & colors*,
|
||||
*Bible content & storage*, *ESV API*, *Verse selection*) using section headings
|
||||
(`new Setting(containerEl).setHeading()`), grouping the existing settings under them.
|
||||
- `components/BibleStyles.ts` — styles for `.dj-verse`, `.dj-verse-selected`, the action bar.
|
||||
|
||||
## Settings
|
||||
New settings (grouped under a *Verse selection* heading in the reorganized tab):
|
||||
- `enableVerseSelection: boolean` (default `true`).
|
||||
- `defaultInsertFormat: 'inline' | 'codeblock' | 'blockquote'` (default `'inline'`); also
|
||||
remembers the last-used format per session for the split-button body.
|
||||
- `formatChooserStyle: 'split' | 'toggle' | 'submenu'` (default `'split'`) — which of the
|
||||
three (all-implemented) format-chooser presentations the action bar uses.
|
||||
- `enableAppendToNote: boolean` (default `false`) — surfaces the optional "Append to note…"
|
||||
action on the bar / context menu / commands.
|
||||
|
||||
The settings tab itself is reorganized into headed sections (see touch points) so the
|
||||
growing list stays navigable.
|
||||
|
||||
## Docs to update (per the repo's working agreements)
|
||||
- **CLAUDE.md** — add the `## Design ethos` section (above).
|
||||
- **ARCHITECTURE.md** — add the new modules to the module map + a short "verse selection"
|
||||
data-flow note.
|
||||
- **docs/reference-formats.md** — document comma-list (non-contiguous) references.
|
||||
- **docs/gotchas.md** — the verse-wrapping approach, mobile long-press-vs-scroll handling,
|
||||
per-Document action bar in popouts, selection lifecycle/clear-on-rerender.
|
||||
- **docs/esv-api.md** — ESV attribution requirement note for reproduced verse text
|
||||
(blockquote includes `(ESV)`; document the copyright-notice obligation).
|
||||
- **CHANGELOG.md** — a bullet under `## Unreleased`.
|
||||
|
||||
## Extensibility
|
||||
The action surfaces (bar, passage menu) are populated from a small in-memory list of
|
||||
"verse-selection actions" `{ id, label, run(selection), enabled? }`. Copy / Insert (and the
|
||||
optional Append-to-note) are the first registrations; the future "find notes referencing
|
||||
these verses" is added by registering one more action — no UI rework.
|
||||
|
||||
## Risks / things to watch
|
||||
- **Mobile long-press vs. scroll** — the highest-risk UX. Press threshold + cancel-on-move
|
||||
before threshold. Fallback if janky: mobile uses tap-toggle only and relies on the bar.
|
||||
- **Non-contiguous round-trip rendering** — the comma-list parser/resolve extension is the
|
||||
largest code change; blockquote works without it, so it can land slightly later than the
|
||||
text format.
|
||||
- **Live Preview** — enable selection on rendered passages in reading mode and the
|
||||
live-preview rendered widget; do not interfere with source editing.
|
||||
- **Popout windows** — wrap/bar/styles are per-`Document` (follow the `BibleStyles` pattern).
|
||||
- **ESV licensing** — honor attribution on reproduced text.
|
||||
|
||||
## Implementation phases
|
||||
1. **Wrapping + selection state**: `VerseSelection` (+ tests), verse-id parsing & DOM
|
||||
wrapping in the renderer, `VerseSelectionService`, highlight styles. Desktop tap-toggle
|
||||
+ shift-range working; selection persists.
|
||||
2. **Action surfaces**: `VerseActionBar` (all three format-chooser styles +
|
||||
`formatChooserStyle` setting), passage right-click menu, editor `editor-menu`
|
||||
"Insert here", and the three commands.
|
||||
3. **Formats + locations**: `VerseFormatter` (+ tests) for all three formats; Copy
|
||||
(clipboard) and Insert-at-cursor (last-focused editor). Optional `InsertTargetModal`
|
||||
append behind `enableAppendToNote`.
|
||||
4. **Non-contiguous round-trip**: extend parse/resolve for comma lists; reference-formats doc.
|
||||
5. **Mobile gestures**: long-press-to-initiate + drag-to-extend with scroll guard.
|
||||
6. **Settings (incl. tab reorganization into headed sections), docs, CHANGELOG, ethos
|
||||
section**; build + lint gating.
|
||||
|
||||
## Verification
|
||||
- `npm run build` (tsc + `npm test` + esbuild) and `npx eslint .` pass clean.
|
||||
- Unit tests: `VerseSelection` run-collapsing (contiguous, non-contiguous, cross-chapter),
|
||||
`VerseFormatter` output for all three formats, comma-list `BibleReference` parse round-trip.
|
||||
- Manual in the demo vault (`disciples-journal-demo`):
|
||||
- Open a note with a `bible` block (e.g. Genesis 1). Tap v2 and v5 → both highlight, bar
|
||||
shows "Genesis 1:2, 5". Shift+click → contiguous range. ✕/Esc clears.
|
||||
- Each action × each format → correct Copy (paste-check) and Insert-at-cursor output,
|
||||
including the non-contiguous case. With `enableAppendToNote` on, the optional
|
||||
"Append to note…" picker appends to the chosen note.
|
||||
- Right-click in a second note → "Insert Genesis 1:2, 5 here" inserts at cursor.
|
||||
- Inserted inline ref and code block render correctly (incl. non-contiguous).
|
||||
- Mobile emulation: long-press initiates selection, drag extends, normal swipe scrolls.
|
||||
- Popout window: selection + bar work in the popped-out doc.
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# Testing
|
||||
|
||||
## Running the tests
|
||||
|
||||
```
|
||||
npm test # or: devbox run test
|
||||
```
|
||||
|
||||
Tests also run automatically as part of `npm run build` and `just release` (see
|
||||
below) — a failing test blocks both.
|
||||
|
||||
## Layout & runner
|
||||
|
||||
- Tests live in `test/`, one `*.test.ts` file per unit under test
|
||||
(e.g. `test/BibleReference.test.ts`).
|
||||
- The runner is **Node's built-in test runner** (`node:test` + `node:assert/strict`),
|
||||
with **`tsx`** transpiling TypeScript on the fly — no separate config file.
|
||||
- The npm script is `tsx --test test/*.test.ts`. The single-`*` glob is expanded by
|
||||
the shell (so it works on Node 20 and 22 alike); keep test files flat in `test/`
|
||||
rather than nested.
|
||||
- `@types/node` is pinned to a v22 line so `node:test` typings are available to
|
||||
`tsc` and eslint (which type-check `**/*.ts`, including the tests).
|
||||
|
||||
## What's covered
|
||||
|
||||
- `BibleReference` — `parse` (the full format matrix from
|
||||
[reference-formats.md](reference-formats.md), book-name variants, normalization,
|
||||
null cases), `toString` round-trips, and the value-object helpers.
|
||||
- `BookNames` — canonical book-name normalization and per-book chapter counts.
|
||||
- `VerseFormatter` — the three insert formats (inline reference, `bible` code block,
|
||||
blockquote-with-text + citation).
|
||||
- `VerseId` — `parseVerseId` of ESV marker ids (`v01001002-1` → `{chapter, verse}`).
|
||||
- `VerseSelection` — collapsing a verse set into contiguous `BibleReference` runs and
|
||||
its display label.
|
||||
|
||||
Coverage is the pure logic (parsing, formatting, value objects); extend it as other
|
||||
pure logic becomes test-worthy.
|
||||
|
||||
## Gating
|
||||
|
||||
- `npm run build` runs `tsc` → `npm test` → esbuild, so tests must pass before a
|
||||
bundle is produced (and before you commit, per [../CLAUDE.md](../CLAUDE.md)).
|
||||
- `just release` runs `npm test` before bumping versions, so a failing test aborts
|
||||
the release before anything is mutated. See [build-and-release.md](build-and-release.md).
|
||||
|
|
@ -40,16 +40,6 @@ export default defineConfig([
|
|||
"obsidianmd/ui/sentence-case": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
// Tests legitimately use Node builtins (node:test / node:assert), and the
|
||||
// node:test runner's top-level test() calls intentionally return unawaited
|
||||
// promises. Both are flagged by the plugin-oriented recommended set.
|
||||
files: ["test/**/*.ts"],
|
||||
rules: {
|
||||
"import/no-nodejs-modules": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
// obsidianmd's recommended set applies several type-aware rules globally
|
||||
// (no file filter). They require TS parser services and crash on the JSON
|
||||
|
|
|
|||
64
justfile
64
justfile
|
|
@ -1,65 +1,21 @@
|
|||
set shell := ["bash", "-c"]
|
||||
|
||||
# List available recipes.
|
||||
default:
|
||||
@just --list
|
||||
|
||||
# Cut a release: bump the version, promote the changelog, build, commit, push,
|
||||
# and publish a GitHub release with the plugin assets attached.
|
||||
# Usage: devbox run release 1.0.1 (or: just release 1.0.1)
|
||||
release version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Obsidian versions/tags carry no leading "v".
|
||||
version="{{version}}"
|
||||
version="${version#v}"
|
||||
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
||||
echo "Refusing to release: '$version' is not a semver version (e.g. 1.0.1)."
|
||||
exit 1
|
||||
if [[ `git status --porcelain` ]]; then
|
||||
echo "Cannot release: 'git status' is not clean. Commit/push or stash changes first"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
echo "Cannot release: working tree is not clean. Commit or stash changes first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git rev-parse "$version" >/dev/null 2>&1; then
|
||||
echo "Cannot release: tag '$version' already exists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keep manifest.json, package.json, and versions.json in lockstep. The new
|
||||
# version maps to the current minAppVersion in versions.json.
|
||||
min_app_version="$(jq -r '.minAppVersion' manifest.json)"
|
||||
jq --arg v "$version" '.version = $v' manifest.json > manifest.tmp && mv manifest.tmp manifest.json
|
||||
jq --arg v "$version" '.version = $v' package.json > package.tmp && mv package.tmp package.json
|
||||
jq --arg v "$version" --arg m "$min_app_version" '.[$v] = $m' versions.json > versions.tmp && mv versions.tmp versions.json
|
||||
|
||||
# Promote the changelog's "Unreleased" section to this version, then use that
|
||||
# section's body as the GitHub release notes.
|
||||
if [[ -f CHANGELOG.md ]] && grep -q '^## Unreleased$' CHANGELOG.md; then
|
||||
sed -i "s/^## Unreleased$/## $version/" CHANGELOG.md
|
||||
fi
|
||||
notes="$(awk -v ver="## $version" '$0==ver{g=1;next} /^## /&&g{exit} g' CHANGELOG.md 2>/dev/null | sed '/^$/d')"
|
||||
[[ -z "$notes" ]] && notes="Release $version"
|
||||
|
||||
# Produce the release artifact (type-check + tests + bundle to main.js).
|
||||
npm run build
|
||||
|
||||
git add manifest.json package.json versions.json CHANGELOG.md
|
||||
git commit --allow-empty -m "Prepares for release '$version'"
|
||||
git push -u origin HEAD
|
||||
|
||||
jq '.version = "{{version}}"' manifest.json > tmp && mv tmp manifest.json
|
||||
jq '.version = "{{version}}"' package.json > tmp && mv tmp package.json
|
||||
npm run build-no-check
|
||||
git add .
|
||||
git commit --allow-empty -am "Prepares for release '{{version}}'"
|
||||
git push
|
||||
token="$(just _gh-token)"
|
||||
if [[ -n "$token" ]]; then export GH_TOKEN="$token"; fi
|
||||
|
||||
# Tag at the just-pushed commit and attach the assets Obsidian expects.
|
||||
gh release create "$version" \
|
||||
--title "$version" \
|
||||
--target "$(git rev-parse --abbrev-ref HEAD)" \
|
||||
--notes "$notes" \
|
||||
main.js manifest.json styles.css
|
||||
gh release create "{{version}}" --title "{{version}}" --notes "" main.js manifest.json styles.css
|
||||
|
||||
# Echo a GitHub token for `gh`. devbox bundles its own gh (nixpkgs) that can't
|
||||
# read the host keyring where `gh auth login` stored the token, so fall back to
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "disciples-journal",
|
||||
"name": "Disciples Journal",
|
||||
"version": "1.14.0",
|
||||
"version": "0.13.2",
|
||||
"minAppVersion": "1.6.6",
|
||||
"description": "Embed Bible references and passages in your notes and read the Bible.",
|
||||
"author": "Scott Tomaszewski (Xentis)",
|
||||
|
|
|
|||
511
package-lock.json
generated
511
package-lock.json
generated
|
|
@ -1,25 +1,24 @@
|
|||
{
|
||||
"name": "obsidian-disciples-journal",
|
||||
"version": "0.13.3",
|
||||
"version": "0.12.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-disciples-journal",
|
||||
"version": "0.13.3",
|
||||
"version": "0.12.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/node": "^16.11.6",
|
||||
"esbuild": "0.17.3",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "^2.4.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
}
|
||||
|
|
@ -59,22 +58,6 @@
|
|||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.3.tgz",
|
||||
|
|
@ -331,22 +314,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz",
|
||||
|
|
@ -363,22 +330,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz",
|
||||
|
|
@ -395,22 +346,6 @@
|
|||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz",
|
||||
|
|
@ -857,18 +792,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
||||
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node/node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "16.18.126",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz",
|
||||
"integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
|
|
@ -2500,20 +2426,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
|
|
@ -4366,417 +4278,6 @@
|
|||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
|
||||
"integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.28.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tsx/node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
{
|
||||
"name": "obsidian-disciples-journal",
|
||||
"version": "1.14.0",
|
||||
"version": "0.13.2",
|
||||
"description": "Embed Bible references and passages in your notes and read the Bible.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && npm test && node esbuild.config.mjs production",
|
||||
"test": "tsx --test test/*.test.ts",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"lint": "eslint .",
|
||||
"version": "node version-bump.mjs && git add manifest.json versions.json",
|
||||
"build-no-check": "node esbuild.config.mjs production"
|
||||
|
|
@ -25,13 +24,12 @@
|
|||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/node": "^22.10.0",
|
||||
"@types/node": "^16.11.6",
|
||||
"esbuild": "0.17.3",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"obsidian": "latest",
|
||||
"tslib": "^2.4.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function createInlineReferenceExtension(contentService: BibleContentServi
|
|||
const content = view.state.doc.sliceString(node.from, node.to);
|
||||
// Try to parse as a Bible reference
|
||||
try {
|
||||
const reference = BibleReference.parseList(content);
|
||||
const reference = BibleReference.parse(content);
|
||||
if (reference) {
|
||||
const decor = Decoration.mark({
|
||||
inclusive: true,
|
||||
|
|
@ -67,13 +67,13 @@ export function createInlineReferenceExtension(contentService: BibleContentServi
|
|||
new Notice("No text content found for Bible reference", 10000);
|
||||
return;
|
||||
}
|
||||
const reference = BibleReference.parseList(t.textContent);
|
||||
const reference = BibleReference.parse(t.textContent);
|
||||
if (!reference) {
|
||||
new Notice("Invalid Bible reference: " + t.textContent, 10000);
|
||||
return;
|
||||
}
|
||||
|
||||
void contentService.getBibleContentList(t.textContent).then(response => {
|
||||
void contentService.getBibleContent(reference).then(response => {
|
||||
if (response.isError()) {
|
||||
new Notice(response.errorMessage, 10000);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -6,8 +6,6 @@ import {BibleEventHandlers} from "src/core/BibleEventHandlers";
|
|||
import {BibleFiles} from "../services/BibleFiles";
|
||||
import {BibleReference} from "../core/BibleReference";
|
||||
import {BiblePassage} from "../utils/BiblePassage";
|
||||
import {VerseSelectionController} from "./VerseSelectionController";
|
||||
import {VerseSelectionService} from "../core/VerseSelectionService";
|
||||
|
||||
/**
|
||||
* Component for rendering Bible references in Obsidian
|
||||
|
|
@ -17,18 +15,15 @@ export class BibleReferenceRenderer {
|
|||
private bibleNavigation: BibleNavigation;
|
||||
private plugin: DisciplesJournalPlugin;
|
||||
private eventHandlers: BibleEventHandlers;
|
||||
private selectionService: VerseSelectionService;
|
||||
|
||||
constructor(
|
||||
bibleContentService: BibleContentService,
|
||||
bibleFiles: BibleFiles,
|
||||
plugin: DisciplesJournalPlugin,
|
||||
selectionService: VerseSelectionService
|
||||
plugin: DisciplesJournalPlugin
|
||||
) {
|
||||
this.bibleContentService = bibleContentService;
|
||||
this.plugin = plugin;
|
||||
this.bibleNavigation = new BibleNavigation(bibleFiles, plugin.app);
|
||||
this.selectionService = selectionService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -58,17 +53,17 @@ export class BibleReferenceRenderer {
|
|||
}
|
||||
|
||||
try {
|
||||
// Accept single refs and non-contiguous lists (e.g. "Genesis 1:2, 4, 8")
|
||||
if (!BibleReference.parseList(codeText)) {
|
||||
const reference = BibleReference.parse(codeText);
|
||||
if (!reference) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Create a Bible reference element
|
||||
const referenceEl = element.doc.createElement('span');
|
||||
referenceEl.classList.add('bible-reference');
|
||||
referenceEl.textContent = codeText;
|
||||
|
||||
const response = await this.bibleContentService.getBibleContentList(codeText);
|
||||
const response = await this.bibleContentService.getBibleContent(reference);
|
||||
if (response.isError()) {
|
||||
new Notice(response.errorMessage, 10000);
|
||||
continue;
|
||||
|
|
@ -93,9 +88,10 @@ export class BibleReferenceRenderer {
|
|||
* Process full Bible passage code blocks
|
||||
*/
|
||||
public async processFullBiblePassage(source: string, el: HTMLElement): Promise<void> {
|
||||
// Parse the reference (supports non-contiguous lists like "Genesis 1:2-3, 5")
|
||||
// Parse the reference
|
||||
const reference = source.trim();
|
||||
if (!BibleReference.parseList(reference) && !BibleReference.parse(reference)) {
|
||||
const parsedRef = BibleReference.parse(reference);
|
||||
if (!parsedRef) {
|
||||
const message = `Invalid bible reference: ${source}`;
|
||||
console.error(message);
|
||||
const errorContainer = el.createEl('div');
|
||||
|
|
@ -104,8 +100,8 @@ export class BibleReferenceRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Grab the content (resolves and concatenates each contiguous run)
|
||||
const response = await this.bibleContentService.getBibleContentList(reference);
|
||||
// Grab the content
|
||||
const response = await this.bibleContentService.getBibleContent(parsedRef);
|
||||
|
||||
if (response.isError()) {
|
||||
const errorContainer = el.createEl('div');
|
||||
|
|
@ -141,14 +137,6 @@ export class BibleReferenceRenderer {
|
|||
|
||||
containerEl.appendChild(passageEl);
|
||||
el.appendChild(containerEl);
|
||||
|
||||
if (this.plugin.settings.enableVerseSelection) {
|
||||
const controller = new VerseSelectionController(
|
||||
this.plugin, passageEl, canonicalRef.book, this.selectionService
|
||||
);
|
||||
this.selectionService.addChild(controller); // unloads with the plugin/service
|
||||
controller.load();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { App, FuzzySuggestModal, TFile } from "obsidian";
|
||||
|
||||
/** Fuzzy-pick a markdown note to append the selection to. */
|
||||
export class InsertTargetModal extends FuzzySuggestModal<TFile> {
|
||||
constructor(app: App, private onChoose: (file: TFile) => void | Promise<void>) {
|
||||
super(app);
|
||||
this.setPlaceholder("Append to note…");
|
||||
}
|
||||
|
||||
getItems(): TFile[] {
|
||||
return this.app.vault.getMarkdownFiles();
|
||||
}
|
||||
|
||||
getItemText(file: TFile): string {
|
||||
return file.path;
|
||||
}
|
||||
|
||||
onChooseItem(file: TFile): void {
|
||||
void this.onChoose(file);
|
||||
}
|
||||
}
|
||||
|
|
@ -68,7 +68,7 @@ export class OpenBibleModal extends SuggestModal<BibleTarget> {
|
|||
if (chapterCount === 1) {
|
||||
// Single-chapter book, open directly
|
||||
const loadingNotice = new Notice("Loading chapter...", 0);
|
||||
await this.bibleFiles.openChapterNote(new BibleReference(item.book, 1), true);
|
||||
await this.bibleFiles.openChapterNote(new BibleReference(item.book, 1));
|
||||
loadingNotice.hide();
|
||||
} else {
|
||||
// Reopen for chapter selection
|
||||
|
|
@ -77,7 +77,7 @@ export class OpenBibleModal extends SuggestModal<BibleTarget> {
|
|||
}
|
||||
} else {
|
||||
const loadingNotice = new Notice("Loading chapter...", 0);
|
||||
await this.bibleFiles.openChapterNote(new BibleReference(item.book, item.chapter), true);
|
||||
await this.bibleFiles.openChapterNote(new BibleReference(item.book, item.chapter));
|
||||
loadingNotice.hide();
|
||||
}
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ class OpenBibleChapterModal extends SuggestModal<BibleTarget> {
|
|||
|
||||
private async openChapter(item: BibleTarget): Promise<void> {
|
||||
const loadingNotice = new Notice("Loading chapter...", 0);
|
||||
await this.bibleFiles.openChapterNote(new BibleReference(item.book, item.chapter), true);
|
||||
await this.bibleFiles.openChapterNote(new BibleReference(item.book, item.chapter));
|
||||
loadingNotice.hide();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
import { Component, Menu, setIcon } from "obsidian";
|
||||
import { VerseSelection } from "../core/VerseSelection";
|
||||
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
|
||||
import { InsertFormat, runVerseAction, VerseActionKind } from "./VerseActions";
|
||||
|
||||
const FORMAT_LABEL: Record<InsertFormat, string> = {
|
||||
inline: "Ref",
|
||||
codeblock: "Block",
|
||||
blockquote: "Quote",
|
||||
};
|
||||
|
||||
const FORMATS: InsertFormat[] = ["inline", "codeblock", "blockquote"];
|
||||
|
||||
export class VerseActionBar extends Component {
|
||||
private root: HTMLElement | null = null;
|
||||
|
||||
constructor(
|
||||
private plugin: DisciplesJournalPlugin,
|
||||
private passageEl: HTMLElement,
|
||||
private onClose: () => void,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.root?.remove();
|
||||
this.root = null;
|
||||
}
|
||||
|
||||
render(selection: VerseSelection): void {
|
||||
const doc = this.passageEl.doc;
|
||||
this.root?.remove();
|
||||
const bar = doc.body.createDiv({ cls: "dj-verse-action-bar" });
|
||||
this.root = bar;
|
||||
|
||||
bar.createSpan({ cls: "dj-verse-action-label", text: selection.label() });
|
||||
|
||||
const actions: { kind: VerseActionKind; label: string }[] = [
|
||||
{ kind: "copy", label: "Copy" },
|
||||
{ kind: "insert", label: "Insert" },
|
||||
];
|
||||
if (this.plugin.settings.enableAppendToNote) {
|
||||
actions.push({ kind: "append", label: "Append to note…" });
|
||||
}
|
||||
|
||||
const style = this.plugin.settings.formatChooserStyle;
|
||||
const defFormat = this.plugin.settings.defaultInsertFormat;
|
||||
|
||||
if (style === "toggle") {
|
||||
let format: InsertFormat = defFormat;
|
||||
const toggle = bar.createDiv({ cls: "dj-format-toggle" });
|
||||
for (const f of FORMATS) {
|
||||
const btn = toggle.createEl("button", { text: FORMAT_LABEL[f] });
|
||||
btn.toggleClass("is-active", f === format);
|
||||
btn.onClickEvent(() => {
|
||||
format = f;
|
||||
toggle.findAll("button").forEach((b) => b.removeClass("is-active"));
|
||||
btn.addClass("is-active");
|
||||
});
|
||||
}
|
||||
for (const a of actions) {
|
||||
const btn = bar.createEl("button", { text: a.label });
|
||||
btn.onClickEvent(() => void runVerseAction(this.plugin, a.kind, selection, format, this.passageEl));
|
||||
}
|
||||
} else if (style === "submenu") {
|
||||
for (const a of actions) {
|
||||
const btn = bar.createEl("button", { text: a.label });
|
||||
btn.onClickEvent((e) => this.openFormatMenu(e, a.kind, a.label, selection));
|
||||
}
|
||||
} else {
|
||||
// split: body = default format, chevron = other formats
|
||||
for (const a of actions) {
|
||||
const group = bar.createDiv({ cls: "dj-split-button" });
|
||||
const main = group.createEl("button", { cls: "dj-split-main", text: a.label });
|
||||
main.onClickEvent(() => void runVerseAction(this.plugin, a.kind, selection, defFormat, this.passageEl));
|
||||
const chevron = group.createEl("button", {
|
||||
cls: "dj-split-chevron",
|
||||
attr: { "aria-label": `${a.label}: choose format` },
|
||||
});
|
||||
setIcon(chevron, "chevron-down");
|
||||
chevron.onClickEvent((e) => this.openFormatMenu(e, a.kind, a.label, selection));
|
||||
}
|
||||
}
|
||||
|
||||
const close = bar.createEl("button", {
|
||||
cls: "dj-verse-action-close",
|
||||
attr: { "aria-label": "Clear verse selection" },
|
||||
});
|
||||
setIcon(close, "x");
|
||||
close.onClickEvent(() => this.onClose());
|
||||
}
|
||||
|
||||
private openFormatMenu(e: MouseEvent, kind: VerseActionKind, label: string, selection: VerseSelection): void {
|
||||
const menu = new Menu();
|
||||
for (const f of FORMATS) {
|
||||
menu.addItem((item) =>
|
||||
item.setTitle(`${label}: ${FORMAT_LABEL[f]}`)
|
||||
.onClick(() => void runVerseAction(this.plugin, kind, selection, f, this.passageEl)));
|
||||
}
|
||||
menu.showAtMouseEvent(e);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import { Notice, TFile } from "obsidian";
|
||||
import { VerseSelection } from "../core/VerseSelection";
|
||||
import { formatBlockquote, formatCodeBlock, formatInlineReference } from "../utils/VerseFormatter";
|
||||
import { InsertTargetModal } from "./InsertTargetModal";
|
||||
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
|
||||
|
||||
export type InsertFormat = "inline" | "codeblock" | "blockquote";
|
||||
export type VerseActionKind = "copy" | "insert" | "append";
|
||||
|
||||
/**
|
||||
* Build the markdown payload for a selection in the chosen format. `sourceEl` is the
|
||||
* rendered passage the selection came from — only the blockquote format reads it (to
|
||||
* pull the visible verse text in its own document, which keeps popout windows correct).
|
||||
*/
|
||||
export function buildPayload(
|
||||
plugin: DisciplesJournalPlugin,
|
||||
selection: VerseSelection,
|
||||
format: InsertFormat,
|
||||
sourceEl: HTMLElement,
|
||||
): string {
|
||||
const label = selection.label();
|
||||
if (format === "inline") return formatInlineReference(label);
|
||||
if (format === "codeblock") return formatCodeBlock(label);
|
||||
const text = extractSelectedText(selection, sourceEl);
|
||||
return formatBlockquote(label, text, plugin.settings.preferredBibleVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull the visible text of the selected verses out of the rendered passage, reading
|
||||
* the `.dj-verse` spans the wrapper created (skipping verse-number markers/footnotes).
|
||||
*/
|
||||
function extractSelectedText(selection: VerseSelection, sourceEl: HTMLElement): string {
|
||||
const parts: string[] = [];
|
||||
for (const { chapter, verse } of selection.verseList()) {
|
||||
const spans = sourceEl.querySelectorAll<HTMLElement>(
|
||||
`.dj-verse[data-chapter="${chapter}"][data-verse="${verse}"]`,
|
||||
);
|
||||
let text = "";
|
||||
spans.forEach((span) => {
|
||||
const clone = span.cloneNode(true) as HTMLElement;
|
||||
clone.querySelectorAll(".verse-num, .chapter-num, .footnote, .footnotes").forEach((n) => n.remove());
|
||||
text += clone.textContent ?? "";
|
||||
});
|
||||
const trimmed = text.replace(/\s+/g, " ").trim();
|
||||
if (trimmed) parts.push(trimmed);
|
||||
}
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
export async function runVerseAction(
|
||||
plugin: DisciplesJournalPlugin,
|
||||
kind: VerseActionKind,
|
||||
selection: VerseSelection,
|
||||
format: InsertFormat,
|
||||
sourceEl: HTMLElement,
|
||||
): Promise<void> {
|
||||
const payload = buildPayload(plugin, selection, format, sourceEl);
|
||||
|
||||
if (kind === "copy") {
|
||||
await navigator.clipboard.writeText(payload);
|
||||
new Notice(`Copied ${selection.label()}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind === "insert") {
|
||||
const view = plugin.resolveInsertTarget();
|
||||
if (!view) {
|
||||
new Notice("No note to insert into — open a note (not a generated Bible note), or right-click where you want it.");
|
||||
return;
|
||||
}
|
||||
view.editor.replaceSelection(payload);
|
||||
new Notice(`Passage ${selection.label()} inserted into note ${view.file?.basename ?? "note"}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// append: pick a note and add to its end (background edit → Vault.process)
|
||||
new InsertTargetModal(plugin.app, async (file: TFile) => {
|
||||
await plugin.app.vault.process(file, (data) => {
|
||||
const sep = data.length === 0 || data.endsWith("\n") ? "" : "\n";
|
||||
return `${data}${sep}\n${payload}\n`;
|
||||
});
|
||||
new Notice(`Appended ${selection.label()} to ${file.basename}`);
|
||||
}).open();
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
import { Component } from "obsidian";
|
||||
import { VerseSelection } from "../core/VerseSelection";
|
||||
import { VerseRef } from "../utils/VerseId";
|
||||
import { VerseSelectionService, SelectionOwner } from "../core/VerseSelectionService";
|
||||
import { VerseActionBar } from "./VerseActionBar";
|
||||
import { wrapPassageVerses } from "./VerseWrapper";
|
||||
import DisciplesJournalPlugin from "../core/DisciplesJournalPlugin";
|
||||
|
||||
let nextId = 0;
|
||||
const LONG_PRESS_MS = 400;
|
||||
const MOVE_CANCEL_PX = 10;
|
||||
|
||||
export class VerseSelectionController extends Component implements SelectionOwner {
|
||||
readonly id = `dj-passage-${nextId++}`;
|
||||
private selection: VerseSelection;
|
||||
private anchor: VerseRef | null = null;
|
||||
private bar: VerseActionBar | null = null;
|
||||
|
||||
// touch state
|
||||
private pressTimer: number | null = null;
|
||||
private dragging = false;
|
||||
private touchStart: { x: number; y: number } | null = null;
|
||||
|
||||
constructor(
|
||||
private plugin: DisciplesJournalPlugin,
|
||||
public readonly sourceEl: HTMLElement,
|
||||
private book: string,
|
||||
private service: VerseSelectionService,
|
||||
) {
|
||||
super();
|
||||
this.selection = new VerseSelection(book);
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
wrapPassageVerses(this.sourceEl);
|
||||
this.sourceEl.addClass("dj-selectable");
|
||||
|
||||
this.registerDomEvent(this.sourceEl, "click", (e) => this.onClick(e));
|
||||
this.registerDomEvent(this.sourceEl, "touchstart", (e) => this.onTouchStart(e), { passive: true });
|
||||
this.registerDomEvent(this.sourceEl, "touchmove", (e) => this.onTouchMove(e), { passive: false });
|
||||
this.registerDomEvent(this.sourceEl, "touchend", () => this.onTouchEnd());
|
||||
|
||||
this.register(this.service.onChange(() => this.reflect()));
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
// If this passage owned the selection (e.g. note re-rendered), drop it.
|
||||
this.service.clearIfOwner(this);
|
||||
this.clearPressTimer();
|
||||
}
|
||||
|
||||
private verseAt(node: Element | null): { ref: VerseRef } | null {
|
||||
const el = node?.closest<HTMLElement>(".dj-verse") ?? null;
|
||||
if (!el || !this.sourceEl.contains(el)) return null;
|
||||
const chapter = Number(el.dataset.chapter);
|
||||
const verse = Number(el.dataset.verse);
|
||||
if (!chapter || !verse) return null;
|
||||
return { ref: { chapter, verse } };
|
||||
}
|
||||
|
||||
private onClick(e: MouseEvent): void {
|
||||
const hit = this.verseAt(e.target instanceof Element ? e.target : null);
|
||||
if (!hit) return;
|
||||
e.preventDefault();
|
||||
if (e.shiftKey && this.anchor) {
|
||||
this.selection.selectRange(this.anchor, hit.ref);
|
||||
} else {
|
||||
this.selection.toggle(hit.ref);
|
||||
this.anchor = hit.ref;
|
||||
}
|
||||
this.commit();
|
||||
}
|
||||
|
||||
private onTouchStart(e: TouchEvent): void {
|
||||
const hit = this.verseAt(e.target instanceof Element ? e.target : null);
|
||||
if (!hit) return;
|
||||
const t = e.touches[0];
|
||||
this.touchStart = { x: t.clientX, y: t.clientY };
|
||||
this.anchor = hit.ref;
|
||||
this.pressTimer = this.sourceEl.win.setTimeout(() => {
|
||||
// Long-press engaged: select the pressed verse for immediate feedback, then
|
||||
// subsequent moves extend the range (and stop the page scrolling).
|
||||
this.dragging = true;
|
||||
this.selection.add(hit.ref);
|
||||
this.commit();
|
||||
}, LONG_PRESS_MS);
|
||||
}
|
||||
|
||||
private onTouchMove(e: TouchEvent): void {
|
||||
if (!this.touchStart) return;
|
||||
const t = e.touches[0];
|
||||
if (!this.dragging) {
|
||||
// Moved before the long-press fired → treat as a scroll, cancel selection.
|
||||
const moved = Math.hypot(t.clientX - this.touchStart.x, t.clientY - this.touchStart.y);
|
||||
if (moved > MOVE_CANCEL_PX) this.clearPressTimer();
|
||||
return;
|
||||
}
|
||||
e.preventDefault(); // we own the gesture now; stop the page scrolling
|
||||
const under = this.sourceEl.doc.elementFromPoint(t.clientX, t.clientY);
|
||||
const hit = this.verseAt(under);
|
||||
if (hit && this.anchor) {
|
||||
this.selection.selectRange(this.anchor, hit.ref);
|
||||
this.commit();
|
||||
}
|
||||
}
|
||||
|
||||
private onTouchEnd(): void {
|
||||
const wasDragging = this.dragging;
|
||||
this.clearPressTimer();
|
||||
if (!wasDragging && this.anchor) {
|
||||
// short tap → toggle a single verse
|
||||
this.selection.toggle(this.anchor);
|
||||
this.commit();
|
||||
}
|
||||
this.dragging = false;
|
||||
this.touchStart = null;
|
||||
}
|
||||
|
||||
private clearPressTimer(): void {
|
||||
if (this.pressTimer !== null) {
|
||||
this.sourceEl.win.clearTimeout(this.pressTimer);
|
||||
this.pressTimer = null;
|
||||
}
|
||||
this.dragging = false;
|
||||
}
|
||||
|
||||
/** Push our selection into the shared service (it decides ownership). */
|
||||
private commit(): void {
|
||||
this.service.set(this.selection, this);
|
||||
}
|
||||
|
||||
/** Re-render highlight + bar from the service's current state. */
|
||||
private reflect(): void {
|
||||
const active = this.service.get();
|
||||
const owned = active?.owner.id === this.id ? active.selection : null;
|
||||
|
||||
this.sourceEl.querySelectorAll<HTMLElement>(".dj-verse").forEach((el) => {
|
||||
const ref = { chapter: Number(el.dataset.chapter), verse: Number(el.dataset.verse) };
|
||||
el.toggleClass("dj-verse-selected", !!owned && owned.has(ref));
|
||||
});
|
||||
|
||||
if (owned) {
|
||||
if (!this.bar) {
|
||||
this.bar = new VerseActionBar(this.plugin, this.sourceEl, () => this.clearSelection());
|
||||
this.addChild(this.bar);
|
||||
}
|
||||
this.bar.render(owned);
|
||||
} else {
|
||||
this.hideBar();
|
||||
}
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
this.selection.clear();
|
||||
this.anchor = null;
|
||||
this.service.clearIfOwner(this);
|
||||
}
|
||||
|
||||
private hideBar(): void {
|
||||
if (this.bar) {
|
||||
this.removeChild(this.bar);
|
||||
this.bar = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { parseVerseId } from "../utils/VerseId";
|
||||
|
||||
/**
|
||||
* Post-process a rendered ESV passage so each verse's inline text is wrapped in a
|
||||
* `<span class="dj-verse" data-chapter data-verse>`, making verses selectable and
|
||||
* highlightable. Idempotent: skips a passage that's already wrapped.
|
||||
*
|
||||
* ESV HTML marks verses only with `<b class="verse-num|chapter-num" id="vBBCCCVVV-N">`;
|
||||
* a verse's text runs as loose nodes until the next marker, sometimes across <p>
|
||||
* boundaries — so we wrap per block element and tag spans with the verse number.
|
||||
*/
|
||||
export function wrapPassageVerses(passageEl: HTMLElement): void {
|
||||
if (passageEl.querySelector(".dj-verse")) return;
|
||||
const doc = passageEl.doc;
|
||||
|
||||
const blocks = passageEl.querySelectorAll("p, li");
|
||||
blocks.forEach((block) => {
|
||||
let current: HTMLSpanElement | null = null;
|
||||
// Snapshot child nodes first; we re-parent them as we go.
|
||||
const nodes = Array.from(block.childNodes);
|
||||
for (const node of nodes) {
|
||||
const marker =
|
||||
node.instanceOf(HTMLElement) && (node.hasClass("verse-num") || node.hasClass("chapter-num"))
|
||||
? parseVerseId(node.id)
|
||||
: null;
|
||||
|
||||
if (marker) {
|
||||
current = doc.createElement("span");
|
||||
current.addClass("dj-verse");
|
||||
current.dataset.chapter = String(marker.chapter);
|
||||
current.dataset.verse = String(marker.verse);
|
||||
block.insertBefore(current, node);
|
||||
}
|
||||
|
||||
if (current) {
|
||||
current.appendChild(node); // moves node out of block into the span
|
||||
}
|
||||
// Nodes before the first marker (rare) stay where they are.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ export class BibleReference {
|
|||
const chapter = parseInt(chapterRangeMatch[1]);
|
||||
const endChapter = parseInt(chapterRangeMatch[2]);
|
||||
|
||||
return new BibleReference(standardizedBook, chapter, undefined, undefined, endChapter);
|
||||
return new BibleReference(standardizedBook, chapter, undefined, endChapter);
|
||||
}
|
||||
|
||||
// Verse range pattern (e.g., "1:2-10")
|
||||
|
|
@ -116,49 +116,6 @@ export class BibleReference {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a possibly non-contiguous list like "Genesis 1:2-3, 5" or "Genesis 1:31, 2:1".
|
||||
* The first item is a full reference; later comma-separated items may be a bare verse
|
||||
* ("5"), a bare verse range ("5-7") inheriting the prior book+chapter, or
|
||||
* "chapter:verse[-end]" inheriting the book. Returns null if any item is invalid.
|
||||
*/
|
||||
public static parseList(reference: string): BibleReference[] | null {
|
||||
const trimmed = reference.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const items = trimmed.split(",").map((s) => s.trim().replace(/[–—]/g, "-"));
|
||||
const out: BibleReference[] = [];
|
||||
|
||||
const first = BibleReference.parse(items[0]);
|
||||
if (!first) return null;
|
||||
out.push(first);
|
||||
|
||||
for (let i = 1; i < items.length; i++) {
|
||||
const prev = out[out.length - 1];
|
||||
const item = items[i];
|
||||
|
||||
// "chapter:verse" or "chapter:verse-end"
|
||||
const cv = /^(\d+):(\d+)(?:-(\d+))?$/.exec(item);
|
||||
if (cv) {
|
||||
out.push(new BibleReference(prev.book, parseInt(cv[1], 10), parseInt(cv[2], 10),
|
||||
cv[3] ? parseInt(cv[3], 10) : undefined));
|
||||
continue;
|
||||
}
|
||||
|
||||
// bare "verse" or "verse-end" (inherit prev chapter)
|
||||
const v = /^(\d+)(?:-(\d+))?$/.exec(item);
|
||||
if (v) {
|
||||
out.push(new BibleReference(prev.book, prev.chapter, parseInt(v[1], 10),
|
||||
v[2] ? parseInt(v[2], 10) : undefined));
|
||||
continue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted reference as a string
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {Plugin, MarkdownView, Notice, normalizePath, Editor, Menu, MenuItem, TFile, WorkspaceLeaf} from 'obsidian';
|
||||
import {Plugin, MarkdownView, Notice, normalizePath} from 'obsidian';
|
||||
import {ESVApiService} from '../services/ESVApiService';
|
||||
import {BibleContentService} from '../services/BibleContentService';
|
||||
import {BibleReferenceRenderer} from '../components/BibleReferenceRenderer';
|
||||
|
|
@ -15,8 +15,6 @@ import {BibleReference} from './BibleReference';
|
|||
import {BibleEventHandlers} from './BibleEventHandlers';
|
||||
import {applyCustomFrontmatter, getCustomFrontmatterForReference} from "../utils/FrontmatterUtil";
|
||||
import {OpenBibleModal} from "../components/OpenBibleModal";
|
||||
import {VerseSelectionService} from './VerseSelectionService';
|
||||
import {buildPayload, runVerseAction} from '../components/VerseActions';
|
||||
|
||||
/**
|
||||
* Disciples Journal Plugin for Obsidian
|
||||
|
|
@ -35,12 +33,6 @@ export default class DisciplesJournalPlugin extends Plugin {
|
|||
private bibleReferenceRenderer: BibleReferenceRenderer;
|
||||
private bibleEventHandlers: BibleEventHandlers;
|
||||
private bibleMarkupProcessor: BibleMarkupProcessor;
|
||||
private verseSelectionService: VerseSelectionService;
|
||||
|
||||
// The most recently active editable, non-generated markdown note — the target for
|
||||
// "Insert selected verses" from the action bar/command (the passage's own pane is
|
||||
// usually a generated Bible note, which we never want to insert into).
|
||||
private lastEditableLeaf: WorkspaceLeaf | null = null;
|
||||
|
||||
async onload() {
|
||||
// Initialize settings
|
||||
|
|
@ -50,10 +42,6 @@ export default class DisciplesJournalPlugin extends Plugin {
|
|||
this.esvApiService = new ESVApiService(this);
|
||||
this.bibleContentService = new BibleContentService(this, this.esvApiService);
|
||||
|
||||
// Holds the single active verse selection across panes; unloads with the plugin.
|
||||
this.verseSelectionService = new VerseSelectionService();
|
||||
this.addChild(this.verseSelectionService);
|
||||
|
||||
// Check if ESV API token is set and show a notice if it's not
|
||||
if (!this.settings.esvApiToken) {
|
||||
new Notice('Disciples Journal: ESV API token not set. Bible content may not load correctly. Visit the plugin settings to add your API token.', 10000);
|
||||
|
|
@ -66,8 +54,7 @@ export default class DisciplesJournalPlugin extends Plugin {
|
|||
this.bibleReferenceRenderer = new BibleReferenceRenderer(
|
||||
this.bibleContentService,
|
||||
this.bibleFiles,
|
||||
this,
|
||||
this.verseSelectionService
|
||||
this
|
||||
);
|
||||
|
||||
// Single, long-lived hover-preview event handler owned by the plugin.
|
||||
|
|
@ -106,45 +93,6 @@ export default class DisciplesJournalPlugin extends Plugin {
|
|||
callback: () => this.updateAllBibleNoteFrontmatter()
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'insert-selected-verses',
|
||||
name: 'Insert selected verses at cursor',
|
||||
callback: () => {
|
||||
const active = this.verseSelectionService.get();
|
||||
if (!active) { new Notice('No verses selected.'); return; }
|
||||
void runVerseAction(this, 'insert', active.selection, this.settings.defaultInsertFormat, active.owner.sourceEl);
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'copy-selected-verses',
|
||||
name: 'Copy selected verses',
|
||||
callback: () => {
|
||||
const active = this.verseSelectionService.get();
|
||||
if (!active) { new Notice('No verses selected.'); return; }
|
||||
void runVerseAction(this, 'copy', active.selection, this.settings.defaultInsertFormat, active.owner.sourceEl);
|
||||
}
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'clear-verse-selection',
|
||||
name: 'Clear verse selection',
|
||||
callback: () => this.verseSelectionService.clear()
|
||||
});
|
||||
|
||||
// Right-click in any editor while a selection exists → insert at the click point.
|
||||
this.registerEvent(this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor) => {
|
||||
const active = this.verseSelectionService.get();
|
||||
if (!active) return;
|
||||
const label = active.selection.label();
|
||||
menu.addItem((item: MenuItem) =>
|
||||
item.setTitle(`Insert ${label} here`)
|
||||
.setIcon('book-open')
|
||||
.onClick(() => editor.replaceSelection(
|
||||
buildPayload(this, active.selection, this.settings.defaultInsertFormat, active.owner.sourceEl)
|
||||
)));
|
||||
}));
|
||||
|
||||
// Ribbon icon
|
||||
this.addRibbonIcon('book-open', 'Open Bible', () => {
|
||||
new OpenBibleModal(this.app, this.bibleFiles).open();
|
||||
|
|
@ -175,42 +123,6 @@ export default class DisciplesJournalPlugin extends Plugin {
|
|||
handleActiveLeafChange() {
|
||||
// Refresh theme/styling when active leaf changes
|
||||
this.updateBibleStyles();
|
||||
|
||||
// Remember the last editable, non-generated note so "Insert" targets the note the
|
||||
// user was working in — not the (generated) passage note they clicked to select.
|
||||
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (view?.file && !this.isBibleContentFile(view.file)) {
|
||||
this.lastEditableLeaf = view.leaf;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when a file lives under the configured Bible content folder (a generated note). */
|
||||
public isBibleContentFile(file: TFile): boolean {
|
||||
return file.path.startsWith(normalizePath(this.settings.bibleContentVaultPath) + '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve where "Insert selected verses" should go: the most recently active editable,
|
||||
* non-generated markdown note (if still open), else the active note when it isn't a
|
||||
* generated Bible note. Returns null when there's no suitable target.
|
||||
*/
|
||||
public resolveInsertTarget(): MarkdownView | null {
|
||||
let leaf = this.lastEditableLeaf;
|
||||
if (leaf) {
|
||||
let stillOpen = false;
|
||||
this.app.workspace.iterateRootLeaves((l) => { if (l === leaf) stillOpen = true; });
|
||||
if (!stillOpen) leaf = null;
|
||||
}
|
||||
|
||||
if (leaf?.view instanceof MarkdownView) {
|
||||
return leaf.view;
|
||||
}
|
||||
|
||||
const active = this.app.workspace.getActiveViewOfType(MarkdownView);
|
||||
if (active?.file && !this.isBibleContentFile(active.file)) {
|
||||
return active;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,107 +0,0 @@
|
|||
import { BibleReference } from "./BibleReference";
|
||||
import { VerseRef } from "../utils/VerseId";
|
||||
|
||||
/**
|
||||
* A mutable, ordered, de-duplicated set of verses within one book. Produces
|
||||
* contiguous BibleReference runs and a human display label. Pure (no DOM / Obsidian).
|
||||
*/
|
||||
export class VerseSelection {
|
||||
readonly book: string;
|
||||
private verses: VerseRef[] = [];
|
||||
|
||||
constructor(book: string) {
|
||||
this.book = book;
|
||||
}
|
||||
|
||||
private indexOf(v: VerseRef): number {
|
||||
return this.verses.findIndex((x) => x.chapter === v.chapter && x.verse === v.verse);
|
||||
}
|
||||
|
||||
has(v: VerseRef): boolean {
|
||||
return this.indexOf(v) !== -1;
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.verses.length === 0;
|
||||
}
|
||||
|
||||
count(): number {
|
||||
return this.verses.length;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.verses = [];
|
||||
}
|
||||
|
||||
add(v: VerseRef): void {
|
||||
if (!this.has(v)) {
|
||||
this.verses.push({ chapter: v.chapter, verse: v.verse });
|
||||
this.sort();
|
||||
}
|
||||
}
|
||||
|
||||
toggle(v: VerseRef): void {
|
||||
const i = this.indexOf(v);
|
||||
if (i === -1) this.add(v);
|
||||
else this.verses.splice(i, 1);
|
||||
}
|
||||
|
||||
/** Add every verse from anchor..focus when they share a chapter; else just add focus. */
|
||||
selectRange(anchor: VerseRef, focus: VerseRef): void {
|
||||
if (anchor.chapter !== focus.chapter) {
|
||||
this.add(focus);
|
||||
return;
|
||||
}
|
||||
const lo = Math.min(anchor.verse, focus.verse);
|
||||
const hi = Math.max(anchor.verse, focus.verse);
|
||||
for (let v = lo; v <= hi; v++) this.add({ chapter: anchor.chapter, verse: v });
|
||||
}
|
||||
|
||||
/** Sorted copy of the selected verses. */
|
||||
verseList(): VerseRef[] {
|
||||
return this.verses.map((v) => ({ chapter: v.chapter, verse: v.verse }));
|
||||
}
|
||||
|
||||
private sort(): void {
|
||||
this.verses.sort((a, b) => a.chapter - b.chapter || a.verse - b.verse);
|
||||
}
|
||||
|
||||
/** Collapse consecutive verses within a chapter into BibleReference ranges. */
|
||||
runs(): BibleReference[] {
|
||||
const out: BibleReference[] = [];
|
||||
let start: VerseRef | null = null;
|
||||
let prev: VerseRef | null = null;
|
||||
|
||||
const flush = () => {
|
||||
if (!start || !prev) return;
|
||||
const endVerse = prev.verse === start.verse ? undefined : prev.verse;
|
||||
out.push(new BibleReference(this.book, start.chapter, start.verse, endVerse));
|
||||
};
|
||||
|
||||
for (const v of this.verses) {
|
||||
if (prev && v.chapter === prev.chapter && v.verse === prev.verse + 1) {
|
||||
prev = v;
|
||||
continue;
|
||||
}
|
||||
flush();
|
||||
start = v;
|
||||
prev = v;
|
||||
}
|
||||
flush();
|
||||
return out;
|
||||
}
|
||||
|
||||
/** YouVersion-style label, e.g. "Genesis 1:2-3, 5" or "Genesis 1:31, 2:1". */
|
||||
label(): string {
|
||||
const runs = this.runs();
|
||||
if (runs.length === 0) return this.book;
|
||||
let lastChapter: number | null = null;
|
||||
const parts: string[] = [];
|
||||
for (const r of runs) {
|
||||
const versePart = r.endVerse !== undefined ? `${r.verse}-${r.endVerse}` : `${r.verse}`;
|
||||
parts.push(r.chapter === lastChapter ? versePart : `${r.chapter}:${versePart}`);
|
||||
lastChapter = r.chapter;
|
||||
}
|
||||
return `${this.book} ${parts.join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { Component } from "obsidian";
|
||||
import { VerseSelection } from "./VerseSelection";
|
||||
|
||||
/** Anything that can own the current selection (a per-passage controller). */
|
||||
export interface SelectionOwner {
|
||||
readonly id: string;
|
||||
/** The rendered passage element — used to extract verse text in the right document. */
|
||||
readonly sourceEl: HTMLElement;
|
||||
}
|
||||
|
||||
interface ActiveSelection {
|
||||
selection: VerseSelection;
|
||||
owner: SelectionOwner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the single active verse selection across all panes/windows. Controllers
|
||||
* subscribe; only the owning controller renders highlight + action bar.
|
||||
*/
|
||||
export class VerseSelectionService extends Component {
|
||||
private active: ActiveSelection | null = null;
|
||||
private listeners = new Set<() => void>();
|
||||
|
||||
/** Subscribe to selection changes. Returns an unsubscribe function. */
|
||||
onChange(cb: () => void): () => void {
|
||||
this.listeners.add(cb);
|
||||
return () => this.listeners.delete(cb);
|
||||
}
|
||||
|
||||
get(): ActiveSelection | null {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
/** Replace the active selection (or clear with an empty one). */
|
||||
set(selection: VerseSelection, owner: SelectionOwner): void {
|
||||
this.active = selection.isEmpty() ? null : { selection, owner };
|
||||
this.emit();
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (!this.active) return;
|
||||
this.active = null;
|
||||
this.emit();
|
||||
}
|
||||
|
||||
/** Clear only if `owner` currently owns the selection (e.g. its passage re-rendered). */
|
||||
clearIfOwner(owner: SelectionOwner): void {
|
||||
if (this.active?.owner.id === owner.id) this.clear();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.listeners.clear();
|
||||
this.active = null;
|
||||
}
|
||||
|
||||
private emit(): void {
|
||||
for (const cb of this.listeners) cb();
|
||||
}
|
||||
}
|
||||
|
|
@ -69,30 +69,6 @@ export class BibleContentService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a possibly non-contiguous reference string ("Genesis 1:2-3, 5") by parsing
|
||||
* it into runs and resolving each. Returns one BiblePassage whose HTML is the runs'
|
||||
* HTML concatenated and whose reference is the first run (used for the heading/link).
|
||||
* Falls back to single-ref resolution when the string isn't a list.
|
||||
*/
|
||||
public async getBibleContentList(referenceText: string): Promise<BibleApiResponse> {
|
||||
const runs = BibleReference.parseList(referenceText);
|
||||
if (!runs || runs.length === 0) {
|
||||
const single = BibleReference.parse(referenceText);
|
||||
if (!single) return BibleApiResponse.error(`Invalid reference: ${referenceText}`, ErrorType.BadApiResponse);
|
||||
return this.getBibleContent(single);
|
||||
}
|
||||
if (runs.length === 1) return this.getBibleContent(runs[0]);
|
||||
|
||||
let html = "";
|
||||
for (const run of runs) {
|
||||
const res = await this.getBibleContent(run);
|
||||
if (res.isError()) return res;
|
||||
html += res.passage.html;
|
||||
}
|
||||
return BibleApiResponse.success(new BiblePassage(runs[0], html));
|
||||
}
|
||||
|
||||
private getCachedRef(ref: BibleReference): BiblePassage | undefined {
|
||||
if (this.passageCache.has(ref)) {
|
||||
return this.passageCache.get(ref);
|
||||
|
|
|
|||
|
|
@ -67,12 +67,8 @@ export class BibleFiles {
|
|||
* Open a chapter note for the given reference, creating it from the API first
|
||||
* if it isn't on disk yet. If the reference includes a verse, the note is
|
||||
* scrolled to that verse once it renders.
|
||||
*
|
||||
* Pass `openInNewTab` to open the chapter in a fresh tab instead of reusing
|
||||
* the active leaf (used by the "Open Bible" command so it never replaces the
|
||||
* note the user is reading).
|
||||
*/
|
||||
public async openChapterNote(reference: BibleReference, openInNewTab = false): Promise<void> {
|
||||
public async openChapterNote(reference: BibleReference): Promise<void> {
|
||||
try {
|
||||
const chapterPath = BibleFiles.pathForPassage(reference, this.plugin);
|
||||
if (!BibleFiles.fileExistsForPassage(reference, this.plugin)) {
|
||||
|
|
@ -93,7 +89,7 @@ export class BibleFiles {
|
|||
|
||||
const passageNoteFile = BibleFiles.getFileForPassage(reference, this.plugin);
|
||||
if (passageNoteFile instanceof TFile) {
|
||||
const leaf = this.plugin.app.workspace.getLeaf(openInNewTab ? 'tab' : false);
|
||||
const leaf = this.plugin.app.workspace.getLeaf(false);
|
||||
await leaf.openFile(passageNoteFile);
|
||||
|
||||
// If there's a specific verse, scroll to it once it has rendered
|
||||
|
|
|
|||
|
|
@ -13,12 +13,20 @@ export class BookNames {
|
|||
// Clean up the book name
|
||||
const cleanedName = bookName.trim();
|
||||
|
||||
// Every canonical name and abbreviation is registered as an exact key, so a
|
||||
// direct lookup resolves all legitimate inputs. We deliberately do NOT fall
|
||||
// back to prefix matching: that resolved any unknown word beginning with a
|
||||
// registered token (e.g. "Hobbiton" -> "Hosea" via "ho"). See FOLLOWUPS.md.
|
||||
const key = cleanedName.toLowerCase();
|
||||
return BookNames._instance.bookNameMap.get(key) ?? null;
|
||||
// First try a direct lookup
|
||||
if (BookNames._instance.bookNameMap.has(cleanedName.toLowerCase())) {
|
||||
return BookNames._instance.bookNameMap.get(cleanedName.toLowerCase()) as string;
|
||||
}
|
||||
|
||||
// Try to find a partial match
|
||||
for (const [key, value] of BookNames._instance.bookNameMap.entries()) {
|
||||
// Check if the key is a substring of the cleaned name
|
||||
if (cleanedName.toLowerCase().startsWith(key)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -22,10 +22,6 @@ export interface DisciplesJournalSettings {
|
|||
hideFootnotesInPreview: boolean;
|
||||
chapterNoteFrontmatter: string;
|
||||
passageNoteFrontmatter: string;
|
||||
enableVerseSelection: boolean;
|
||||
defaultInsertFormat: 'inline' | 'codeblock' | 'blockquote';
|
||||
formatChooserStyle: 'split' | 'toggle' | 'submenu';
|
||||
enableAppendToNote: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: DisciplesJournalSettings = {
|
||||
|
|
@ -45,11 +41,7 @@ export const DEFAULT_SETTINGS: DisciplesJournalSettings = {
|
|||
hideFootnotes: false,
|
||||
hideFootnotesInPreview: false,
|
||||
chapterNoteFrontmatter: '',
|
||||
passageNoteFrontmatter: '',
|
||||
enableVerseSelection: true,
|
||||
defaultInsertFormat: 'inline',
|
||||
formatChooserStyle: 'split',
|
||||
enableAppendToNote: false
|
||||
passageNoteFrontmatter: ''
|
||||
};
|
||||
|
||||
export class DisciplesJournalSettingsTab extends PluginSettingTab {
|
||||
|
|
@ -274,54 +266,6 @@ export class DisciplesJournalSettingsTab extends PluginSettingTab {
|
|||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl).setName('Verse selection').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable verse selection')
|
||||
.setDesc('Tap verses in a rendered passage to select them and copy/insert them into notes.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableVerseSelection)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableVerseSelection = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Default insert format')
|
||||
.setDesc('The format used by the main button before you pick another.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('inline', 'Inline reference')
|
||||
.addOption('codeblock', 'Bible code block')
|
||||
.addOption('blockquote', 'Blockquote with text')
|
||||
.setValue(this.plugin.settings.defaultInsertFormat)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.defaultInsertFormat = value as DisciplesJournalSettings['defaultInsertFormat'];
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Format chooser style')
|
||||
.setDesc('How the action bar lets you pick a format.')
|
||||
.addDropdown(dropdown => dropdown
|
||||
.addOption('split', 'Split buttons (default format + chevron)')
|
||||
.addOption('toggle', 'Format toggle + action buttons')
|
||||
.addOption('submenu', 'Action menus with format submenus')
|
||||
.setValue(this.plugin.settings.formatChooserStyle)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.formatChooserStyle = value as DisciplesJournalSettings['formatChooserStyle'];
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable "Append to note…"')
|
||||
.setDesc('Add an action that appends the selection to the end of a note you pick. Overlaps with Insert at cursor, so it is off by default.')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableAppendToNote)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableAppendToNote = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl).setName('ESV API').setHeading();
|
||||
|
||||
const apiInfoDiv = containerEl.createDiv({cls: 'disciples-journal-api-info'});
|
||||
|
|
|
|||
13
src/types.d.ts
vendored
13
src/types.d.ts
vendored
|
|
@ -2,3 +2,16 @@ declare module '*.json' {
|
|||
const value: unknown;
|
||||
export default value;
|
||||
}
|
||||
|
||||
// Define the structure of the Bible data for better type checking
|
||||
interface ESVVerse {
|
||||
pk: number;
|
||||
translation: string;
|
||||
book: number | string;
|
||||
chapter: number;
|
||||
verse: number;
|
||||
text: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
declare const ESV: ESVVerse[];
|
||||
|
|
|
|||
23
src/utils/BibleCodeblockFormatter.ts
Normal file
23
src/utils/BibleCodeblockFormatter.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import {BiblePassage} from "./BiblePassage";
|
||||
|
||||
/**
|
||||
* Utility for formatting Bible content consistently across the application
|
||||
*/
|
||||
// TODO - this probably belongs somewhere else
|
||||
export class BibleCodeblockFormatter {
|
||||
/**
|
||||
* Format chapter content as Markdown
|
||||
*/
|
||||
public static formatChapterContent(passage: BiblePassage): string {
|
||||
if (!passage) return "# Error: No passage content\n\nThe requested passage could not be loaded.";
|
||||
|
||||
let content = "";
|
||||
|
||||
// Add code block for rendering
|
||||
content += "```bible\n";
|
||||
content += passage.reference.toString();
|
||||
content += "\n```\n\n";
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
/** `` `Genesis 1:2-3, 5` `` — inline reference rendered by the plugin's hover preview. */
|
||||
export function formatInlineReference(label: string): string {
|
||||
return `\`${label}\``;
|
||||
}
|
||||
|
||||
/** A fenced ```bible block the plugin renders as the full passage. */
|
||||
export function formatCodeBlock(label: string): string {
|
||||
return "```bible\n" + label + "\n```";
|
||||
}
|
||||
|
||||
/**
|
||||
* A markdown blockquote of the actual verse text, ending in a "— <ref> (<version>)"
|
||||
* citation. Every line of `text` is prefixed so multi-line quotes stay inside the quote.
|
||||
*/
|
||||
export function formatBlockquote(label: string, text: string, version: string): string {
|
||||
const body = text
|
||||
.split("\n")
|
||||
.map((line) => `> ${line}`)
|
||||
.join("\n");
|
||||
return `${body}\n>\n> — ${label} (${version})`;
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
/** A chapter+verse coordinate within a single book. */
|
||||
export interface VerseRef {
|
||||
chapter: number;
|
||||
verse: number;
|
||||
}
|
||||
|
||||
const VERSE_ID = /^v\d{2}(\d{3})(\d{3})-\d+$/;
|
||||
|
||||
/**
|
||||
* Parse an ESV passage marker id (e.g. "v01001002-1" on a `verse-num`/`chapter-num`
|
||||
* `<b>`) into its chapter and verse. Returns null for ids that aren't verse markers.
|
||||
* Book digits are ignored — callers know the book from the passage reference.
|
||||
*/
|
||||
export function parseVerseId(id: string): VerseRef | null {
|
||||
const m = VERSE_ID.exec(id);
|
||||
if (!m) return null;
|
||||
return { chapter: parseInt(m[1], 10), verse: parseInt(m[2], 10) };
|
||||
}
|
||||
94
styles.css
94
styles.css
|
|
@ -527,97 +527,3 @@ button.clear-input-button {
|
|||
.disciples-journal-book-suggest-input {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
/* ===== Verse selection ===== */
|
||||
.dj-selectable .dj-verse {
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.dj-selectable .dj-verse:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.dj-verse-selected,
|
||||
.dj-selectable .dj-verse-selected:hover {
|
||||
background: var(--text-selection);
|
||||
box-shadow: inset 0 0 0 1px var(--text-accent);
|
||||
}
|
||||
|
||||
/* Floating action bar — bottom-center dock, so it stays in view regardless of how tall
|
||||
the passage is (a full chapter is taller than the viewport, so anchoring to the passage
|
||||
would push the bar off-screen). Appears only while verses are selected. */
|
||||
.dj-verse-action-bar {
|
||||
position: fixed;
|
||||
z-index: var(--layer-popover);
|
||||
left: 50%;
|
||||
bottom: var(--size-4-6);
|
||||
transform: translateX(-50%);
|
||||
max-width: calc(100% - var(--size-4-8));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: var(--size-4-2);
|
||||
padding: var(--size-4-2) var(--size-4-3);
|
||||
border-radius: var(--radius-m);
|
||||
background: var(--background-secondary);
|
||||
box-shadow: var(--shadow-l);
|
||||
}
|
||||
|
||||
.dj-verse-action-label {
|
||||
font-weight: var(--font-bold);
|
||||
margin-right: var(--size-4-1);
|
||||
}
|
||||
|
||||
.dj-verse-action-bar button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dj-split-button {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.dj-split-button .dj-split-main {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.dj-split-button .dj-split-chevron {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
padding-left: var(--size-4-1);
|
||||
padding-right: var(--size-4-1);
|
||||
}
|
||||
|
||||
.dj-format-toggle {
|
||||
display: inline-flex;
|
||||
gap: var(--size-2-1);
|
||||
margin-right: var(--size-4-1);
|
||||
}
|
||||
|
||||
.dj-format-toggle button.is-active {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.dj-verse-action-bar button:focus-visible {
|
||||
outline: 2px solid var(--interactive-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Full-width and enlarge touch targets on narrow / mobile layouts */
|
||||
@media (max-width: 600px) {
|
||||
.dj-verse-action-bar {
|
||||
left: var(--size-4-2);
|
||||
right: var(--size-4-2);
|
||||
bottom: var(--size-4-3);
|
||||
transform: none;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.dj-verse-action-bar button {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,213 +0,0 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { BibleReference } from "../src/core/BibleReference";
|
||||
|
||||
/**
|
||||
* Tests for BibleReference — the pure, pattern-heavy parsing/value-object logic.
|
||||
* Format matrix mirrors docs/reference-formats.md.
|
||||
*/
|
||||
|
||||
test("parse — format matrix", async (t) => {
|
||||
await t.test("whole chapter", () => {
|
||||
const ref = BibleReference.parse("Genesis 1");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.book, "Genesis");
|
||||
assert.equal(ref.chapter, 1);
|
||||
assert.equal(ref.verse, undefined);
|
||||
assert.equal(ref.endVerse, undefined);
|
||||
assert.equal(ref.endChapter, undefined);
|
||||
});
|
||||
|
||||
await t.test("chapter range", () => {
|
||||
const ref = BibleReference.parse("Genesis 1-2");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.chapter, 1);
|
||||
assert.equal(ref.verse, undefined);
|
||||
assert.equal(ref.endChapter, 2);
|
||||
assert.equal(ref.endVerse, undefined);
|
||||
});
|
||||
|
||||
await t.test("single verse", () => {
|
||||
const ref = BibleReference.parse("John 3:16");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.book, "John");
|
||||
assert.equal(ref.chapter, 3);
|
||||
assert.equal(ref.verse, 16);
|
||||
assert.equal(ref.endVerse, undefined);
|
||||
assert.equal(ref.endChapter, undefined);
|
||||
});
|
||||
|
||||
await t.test("verse range in one chapter", () => {
|
||||
const ref = BibleReference.parse("Genesis 1:1-10");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.chapter, 1);
|
||||
assert.equal(ref.verse, 1);
|
||||
assert.equal(ref.endVerse, 10);
|
||||
assert.equal(ref.endChapter, undefined);
|
||||
});
|
||||
|
||||
await t.test("cross-chapter range", () => {
|
||||
const ref = BibleReference.parse("Philippians 1:27-2:11");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.chapter, 1);
|
||||
assert.equal(ref.verse, 27);
|
||||
assert.equal(ref.endChapter, 2);
|
||||
assert.equal(ref.endVerse, 11);
|
||||
});
|
||||
});
|
||||
|
||||
test("parse — book name variants", async (t) => {
|
||||
await t.test("numbered book", () => {
|
||||
const ref = BibleReference.parse("1 Corinthians 13:4-7");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.book, "1 Corinthians");
|
||||
assert.equal(ref.chapter, 13);
|
||||
assert.equal(ref.verse, 4);
|
||||
assert.equal(ref.endVerse, 7);
|
||||
});
|
||||
|
||||
await t.test("multi-word book", () => {
|
||||
const ref = BibleReference.parse("Song of Solomon 2:1");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.book, "Song of Solomon");
|
||||
assert.equal(ref.chapter, 2);
|
||||
assert.equal(ref.verse, 1);
|
||||
});
|
||||
|
||||
await t.test("abbreviations normalize to canonical book", () => {
|
||||
assert.equal(BibleReference.parse("Gen 1:1")?.book, "Genesis");
|
||||
assert.equal(BibleReference.parse("Jn 3:16")?.book, "John");
|
||||
assert.equal(BibleReference.parse("1 Cor 13")?.book, "1 Corinthians");
|
||||
});
|
||||
|
||||
await t.test("case-insensitive book name", () => {
|
||||
const ref = BibleReference.parse("genesis 1:1");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.book, "Genesis");
|
||||
});
|
||||
});
|
||||
|
||||
test("parse — normalization", async (t) => {
|
||||
await t.test("en-dash is treated as a hyphen range", () => {
|
||||
// U+2013 EN DASH, as commonly pasted from other sources.
|
||||
const ref = BibleReference.parse("Genesis 1:1–10");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.chapter, 1);
|
||||
assert.equal(ref.verse, 1);
|
||||
assert.equal(ref.endVerse, 10);
|
||||
});
|
||||
|
||||
await t.test("surrounding whitespace is trimmed", () => {
|
||||
const ref = BibleReference.parse(" John 3:16 ");
|
||||
assert.ok(ref);
|
||||
assert.equal(ref.book, "John");
|
||||
assert.equal(ref.chapter, 3);
|
||||
assert.equal(ref.verse, 16);
|
||||
});
|
||||
});
|
||||
|
||||
test("parse — null cases", async (t) => {
|
||||
await t.test("empty string", () => assert.equal(BibleReference.parse(""), null));
|
||||
await t.test("whitespace only", () => assert.equal(BibleReference.parse(" "), null));
|
||||
await t.test("unknown book", () => assert.equal(BibleReference.parse("Xylophone 1:1"), null));
|
||||
// "Hobbiton" begins with the "ho" (Hosea) abbreviation; normalize must not
|
||||
// greedily resolve it. See test/BookNames.test.ts.
|
||||
await t.test("word starting with an abbreviation", () => assert.equal(BibleReference.parse("Hobbiton 1:1"), null));
|
||||
await t.test("book with no chapter", () => assert.equal(BibleReference.parse("John"), null));
|
||||
await t.test("trailing garbage", () => assert.equal(BibleReference.parse("John 3:16 foo"), null));
|
||||
});
|
||||
|
||||
test("round-trip — parse(s).toString() === s", () => {
|
||||
const canonical = [
|
||||
"Genesis 1",
|
||||
"Genesis 1-2",
|
||||
"John 3:16",
|
||||
"Genesis 1:1-10",
|
||||
"Philippians 1:27-2:11",
|
||||
"1 Corinthians 13:4-7",
|
||||
"Song of Solomon 2:1",
|
||||
];
|
||||
for (const s of canonical) {
|
||||
assert.equal(BibleReference.parse(s)?.toString(), s, `round-trip failed for "${s}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test("value-object helpers", async (t) => {
|
||||
await t.test("isChapterReference", () => {
|
||||
assert.equal(BibleReference.parse("Genesis 1")?.isChapterReference(), true);
|
||||
assert.equal(BibleReference.parse("Genesis 1-2")?.isChapterReference(), true);
|
||||
assert.equal(BibleReference.parse("John 3:16")?.isChapterReference(), false);
|
||||
});
|
||||
|
||||
await t.test("isRange", () => {
|
||||
assert.equal(BibleReference.parse("John 3:16")?.isRange(), false);
|
||||
assert.equal(BibleReference.parse("Genesis 1")?.isRange(), false);
|
||||
assert.equal(BibleReference.parse("Genesis 1:1-10")?.isRange(), true);
|
||||
assert.equal(BibleReference.parse("Genesis 1-2")?.isRange(), true);
|
||||
assert.equal(BibleReference.parse("Philippians 1:27-2:11")?.isRange(), true);
|
||||
});
|
||||
|
||||
await t.test("equals", () => {
|
||||
const a = BibleReference.parse("John 3:16");
|
||||
const b = BibleReference.parse("Jn 3:16");
|
||||
const c = BibleReference.parse("John 3:17");
|
||||
assert.ok(a && b && c);
|
||||
assert.equal(a.equals(b), true);
|
||||
assert.equal(a.equals(c), false);
|
||||
});
|
||||
|
||||
await t.test("clone is an independent copy", () => {
|
||||
const ref = BibleReference.parse("Genesis 1:1-10");
|
||||
assert.ok(ref);
|
||||
const copy = ref.clone();
|
||||
assert.equal(ref.equals(copy), true);
|
||||
copy.chapter = 99;
|
||||
assert.equal(ref.chapter, 1);
|
||||
});
|
||||
|
||||
await t.test("getChapterReference drops verse info", () => {
|
||||
const ref = BibleReference.parse("John 3:16");
|
||||
assert.ok(ref);
|
||||
const chapterRef = ref.getChapterReference();
|
||||
assert.equal(chapterRef.book, "John");
|
||||
assert.equal(chapterRef.chapter, 3);
|
||||
assert.equal(chapterRef.verse, undefined);
|
||||
assert.equal(chapterRef.isChapterReference(), true);
|
||||
});
|
||||
});
|
||||
|
||||
test("constructor throws on an unnormalizable book name", () => {
|
||||
assert.throws(() => new BibleReference("Xylophone", 1), /Illegal book name/);
|
||||
});
|
||||
|
||||
test("parseList — comma/non-contiguous lists", async (t) => {
|
||||
await t.test("single ref still parses as a one-element list", () => {
|
||||
const list = BibleReference.parseList("John 3:16");
|
||||
assert.ok(list);
|
||||
assert.equal(list.length, 1);
|
||||
assert.equal(list[0].toString(), "John 3:16");
|
||||
});
|
||||
|
||||
await t.test("bare verse inherits book + chapter", () => {
|
||||
const list = BibleReference.parseList("Genesis 1:2-3, 5");
|
||||
assert.ok(list);
|
||||
assert.deepEqual(list.map((r) => r.toString()), ["Genesis 1:2-3", "Genesis 1:5"]);
|
||||
});
|
||||
|
||||
await t.test("chapter:verse item inherits only the book", () => {
|
||||
const list = BibleReference.parseList("Genesis 1:31, 2:1");
|
||||
assert.ok(list);
|
||||
assert.deepEqual(list.map((r) => r.toString()), ["Genesis 1:31", "Genesis 2:1"]);
|
||||
});
|
||||
|
||||
await t.test("round-trips a VerseSelection label", () => {
|
||||
const list = BibleReference.parseList("Genesis 1:2-4, 7, 2:1");
|
||||
assert.ok(list);
|
||||
assert.deepEqual(list.map((r) => r.toString()), ["Genesis 1:2-4", "Genesis 1:7", "Genesis 2:1"]);
|
||||
});
|
||||
|
||||
await t.test("invalid item makes the whole list null", () => {
|
||||
assert.equal(BibleReference.parseList("Genesis 1:2, banana"), null);
|
||||
assert.equal(BibleReference.parseList(""), null);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { BookNames } from "../src/services/BookNames";
|
||||
import { BibleReference } from "../src/core/BibleReference";
|
||||
|
||||
/**
|
||||
* Tests for BookNames.normalize — book-name standardization.
|
||||
* Every canonical name and registered abbreviation must resolve; anything else
|
||||
* must return null (no greedy prefix matching). See docs/reference-formats.md.
|
||||
*/
|
||||
|
||||
test("normalize — canonical names", async (t) => {
|
||||
await t.test("exact canonical name", () => assert.equal(BookNames.normalize("Genesis"), "Genesis"));
|
||||
await t.test("case-insensitive", () => assert.equal(BookNames.normalize("genesis"), "Genesis"));
|
||||
await t.test("surrounding whitespace", () => assert.equal(BookNames.normalize(" John "), "John"));
|
||||
await t.test("multi-word", () => assert.equal(BookNames.normalize("Song of Solomon"), "Song of Solomon"));
|
||||
await t.test("numbered", () => assert.equal(BookNames.normalize("1 Corinthians"), "1 Corinthians"));
|
||||
});
|
||||
|
||||
test("normalize — registered abbreviations", async (t) => {
|
||||
await t.test("Gen", () => assert.equal(BookNames.normalize("Gen"), "Genesis"));
|
||||
await t.test("Jn", () => assert.equal(BookNames.normalize("Jn"), "John"));
|
||||
await t.test("Ho", () => assert.equal(BookNames.normalize("Ho"), "Hosea"));
|
||||
await t.test("1 Cor", () => assert.equal(BookNames.normalize("1 Cor"), "1 Corinthians"));
|
||||
await t.test("Song", () => assert.equal(BookNames.normalize("Song"), "Song of Solomon"));
|
||||
});
|
||||
|
||||
test("normalize — rejects unknown names without greedy prefix matching", async (t) => {
|
||||
await t.test("empty", () => assert.equal(BookNames.normalize(""), null));
|
||||
await t.test("whitespace only", () => assert.equal(BookNames.normalize(" "), null));
|
||||
// Regression: "Hobbiton" starts with the "ho" (Hosea) abbreviation but is not a
|
||||
// book; greedy startsWith matching used to resolve it to "Hosea". See FOLLOWUPS.md.
|
||||
await t.test("word starting with an abbreviation", () => assert.equal(BookNames.normalize("Hobbiton"), null));
|
||||
await t.test("canonical name with extra suffix", () => assert.equal(BookNames.normalize("Genesistein"), null));
|
||||
await t.test("unrelated word", () => assert.equal(BookNames.normalize("Xylophone"), null));
|
||||
});
|
||||
|
||||
test("parse rejects a reference whose book is a greedy prefix match", () => {
|
||||
// "Hobbiton 1:1" must not resolve to a Hosea reference.
|
||||
assert.equal(BibleReference.parse("Hobbiton 1:1"), null);
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { formatInlineReference, formatCodeBlock, formatBlockquote } from "../src/utils/VerseFormatter";
|
||||
|
||||
test("VerseFormatter", async (t) => {
|
||||
await t.test("inline reference is backtick-wrapped", () => {
|
||||
assert.equal(formatInlineReference("Genesis 1:2-3, 5"), "`Genesis 1:2-3, 5`");
|
||||
});
|
||||
|
||||
await t.test("code block is a fenced bible block", () => {
|
||||
assert.equal(formatCodeBlock("Genesis 1:2-3"), "```bible\nGenesis 1:2-3\n```");
|
||||
});
|
||||
|
||||
await t.test("blockquote quotes the text and cites the reference + version", () => {
|
||||
const out = formatBlockquote("John 3:16", "For God so loved the world...", "ESV");
|
||||
assert.equal(out, "> For God so loved the world...\n>\n> — John 3:16 (ESV)");
|
||||
});
|
||||
|
||||
await t.test("blockquote prefixes every wrapped line with '> '", () => {
|
||||
const out = formatBlockquote("Genesis 1:1", "line one\nline two", "ESV");
|
||||
assert.equal(out, "> line one\n> line two\n>\n> — Genesis 1:1 (ESV)");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { parseVerseId } from "../src/utils/VerseId";
|
||||
|
||||
test("parseVerseId", async (t) => {
|
||||
await t.test("verse-num id", () => {
|
||||
assert.deepEqual(parseVerseId("v01001002-1"), { chapter: 1, verse: 2 });
|
||||
});
|
||||
await t.test("chapter-num id (first verse)", () => {
|
||||
assert.deepEqual(parseVerseId("v01001001-1"), { chapter: 1, verse: 1 });
|
||||
});
|
||||
await t.test("multi-digit chapter and verse", () => {
|
||||
assert.deepEqual(parseVerseId("v43011035-2"), { chapter: 11, verse: 35 });
|
||||
});
|
||||
await t.test("non-matching ids return null", () => {
|
||||
assert.equal(parseVerseId("f1-1"), null);
|
||||
assert.equal(parseVerseId(""), null);
|
||||
assert.equal(parseVerseId("v123-1"), null);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { VerseSelection } from "../src/core/VerseSelection";
|
||||
|
||||
function sel(book: string, ...vs: [number, number][]): VerseSelection {
|
||||
const s = new VerseSelection(book);
|
||||
for (const [c, v] of vs) s.toggle({ chapter: c, verse: v });
|
||||
return s;
|
||||
}
|
||||
|
||||
test("VerseSelection", async (t) => {
|
||||
await t.test("toggle adds then removes", () => {
|
||||
const s = sel("Genesis");
|
||||
s.toggle({ chapter: 1, verse: 2 });
|
||||
assert.equal(s.has({ chapter: 1, verse: 2 }), true);
|
||||
s.toggle({ chapter: 1, verse: 2 });
|
||||
assert.equal(s.has({ chapter: 1, verse: 2 }), false);
|
||||
assert.equal(s.isEmpty(), true);
|
||||
});
|
||||
|
||||
await t.test("contiguous run collapses to a range label", () => {
|
||||
const s = sel("Genesis", [1, 2], [1, 3], [1, 4]);
|
||||
assert.equal(s.label(), "Genesis 1:2-4");
|
||||
const runs = s.runs();
|
||||
assert.equal(runs.length, 1);
|
||||
assert.equal(runs[0].toString(), "Genesis 1:2-4");
|
||||
});
|
||||
|
||||
await t.test("non-contiguous verses join with commas (out-of-order input)", () => {
|
||||
const s = sel("Genesis", [1, 5], [1, 2], [1, 3]);
|
||||
assert.equal(s.label(), "Genesis 1:2-3, 5");
|
||||
assert.deepEqual(s.runs().map((r) => r.toString()), ["Genesis 1:2-3", "Genesis 1:5"]);
|
||||
});
|
||||
|
||||
await t.test("single verse", () => {
|
||||
assert.equal(sel("John", [3, 16]).label(), "John 3:16");
|
||||
});
|
||||
|
||||
await t.test("multiple chapters repeat the chapter, no cross-chapter merge", () => {
|
||||
const s = sel("Genesis", [1, 31], [2, 1]);
|
||||
assert.equal(s.label(), "Genesis 1:31, 2:1");
|
||||
assert.equal(s.runs().length, 2);
|
||||
});
|
||||
|
||||
await t.test("selectRange fills verses within a chapter", () => {
|
||||
const s = new VerseSelection("Genesis");
|
||||
s.selectRange({ chapter: 1, verse: 2 }, { chapter: 1, verse: 5 });
|
||||
assert.equal(s.label(), "Genesis 1:2-5");
|
||||
});
|
||||
|
||||
await t.test("verseList is sorted", () => {
|
||||
const s = sel("Genesis", [1, 5], [1, 2]);
|
||||
assert.deepEqual(s.verseList(), [
|
||||
{ chapter: 1, verse: 2 },
|
||||
{ chapter: 1, verse: 5 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue