A metadata-driven, single-page book viewer for Islamic texts. All configuration lives in CSV files — adding a book or a category never requires code changes. The UI supports Dhivehi, English, and Arabic.
data/
01-registry-bookTags.csv ← Tag definitions (tagCode, trilingual labels, aliases, colors)
03-registry-bookMeta.csv ← Book registry (bookCode, authorCode, titles in AR/DV/EN, secondary tags, version hash)
04-update-bookRegistry.ps1 ← Auto-generate titleEN, sync new books
05-registry-quranSurahs.csv ← 114 surah names in AR/DV/EN with ayah counts
06-registry-quranJuz.csv ← 30 juz cut points (startSurah, startAyah)
07-registry-quranColumns.csv ← Quran column registry (source, labels, defaults)
08-rebuild-searchIndex.mjs ← Node script: builds the search index (rerun after book changes — or double-click rebuild-index.bat)
02-registry-bookAuthors.csv ← Author definitions (authorCode, trilingual names, Hijri birth/death years)
search-index-manifest.json ← Generated search-index MANIFEST (meta only — bookIds, shard hashes; ~2 KB)
search-index/ ← Generated per-book search shards (one flat word→postings file per book)
content/ ← Per-book content files (incl. Quran base data)
*.csv ← One file per book
src/
books/
index.html ← Dashboard — book list, search, tag filter, table/card view
reader.html ← Book viewer — loaded via ?book=CODE
library-search.html ← Cross-book search page — shareable ?q=/?tags= URLs
info.html ← Book/author info page — shareable ?book=/?author= URLs
css/
common.css ← Shared: themes, fonts, topBar, sidebar, settings modal, tag colors
reader.css ← Reader page: focus mode, toolbar, pagination, content, responsive
reader-search.css ← Reader: search bar, results dropdown, advanced search
reader-table-view.css ← Reader: table view mode, top scrollbar, sentinels
reader-quran.css ← Reader: Quran navigation row, dropdowns, surah overlay
dashboard.css ← Dashboard: grid, cards, controls, table view
library-search.css ← Library search page: results, peek previews
js/
common.js ← Shared init: theme, fonts, i18n, sidebar, settings, keyboard
book-data.js ← Book metadata: registry + tag loaders, page bootstrap
pins-history.js ← Pins & history: localStorage, modals, sidebar wiring
dashboard.js ← Dashboard UI: card/table grid, search, tags, sort, keyboard
reader.js ← Book viewer core: render, loaders, goTo, toolbar, keyboard, deep links
reader-position.js ← Reader position: pagination strip, progress bar, URL sync, read-history
reader-search-ui.js ← In-book search: results, history, whole-word toggle, advanced search
table-scroll-sync.js ← Table view: top scrollbar, width sync, arrow/wheel scrolling
quran-data.js ← Quran: data, decoration, structure (lazy — QRN books only)
quran-ui.js ← Quran: nav, dropdowns, on-demand column loading (lazy; re-exports quran-data.js)
csv.js ← Tiny CSV parser (~1 KB), replaces PapaParse
search-utils.js ← Search engine: normalisation, compiled queries, norm cache, matching, history
library-search-engine.js ← Cross-book search: index loader (IndexedDB-cached) + query engine
library-search-page.js ← Library search page UI: chips, results, peek previews
i18n.js ← Translations (dv/en/ar)
export.js ← Export feature: formats menu, downloads, lazy-loaded writers
export-xlsx.js ← Inline XLSX writer (~2.5 KB), lazy-loaded on export
export-epub.js ← EPUB 3 e-book writer (~4 KB), lazy-loaded on export
export-zip.js ← Minimal ZIP writer (store), shared by the XLSX + EPUB writers
static/
font/ ← Custom merged font (Arabic + Thaana + Latin)
notes/ ← Markdown book notes & author bios for the info modal
dist/ ← Generated: minified js/css + pages (tools/dist-build.mjs) — committed, rebuilt before every commit (dist-build.bat)
dist/manifest.json ← Generated: the service worker's freshness ledger (tools/hmv-manifest.mjs) — committed; the build, font tool and registry script refresh it, or run the tiny command alone after data-only edits
dist-build-report.md ← Generated: per-build size ledger (dist-build.mjs) — committed for diffable history
sw.js ← Static service worker — offline + repeat-visit caching via dist/manifest.json; never edited
docs/ ← User guide, architecture, API reference
-
Add a row to
data/03-registry-bookMeta.csv— thetagscolumn holds secondary tags (comma‑separated codes from01-registry-bookTags.csv); the primary tag is the first segment of thebookCode;authorCodeis optional (one or more comma‑separated codes from02-registry-bookAuthors.csv):bookCode,authorCode,titleAR,titleDV,titleEN,tags FQH-usululFiqh,ibn-rajab,أصول الفقه,އުޞޫލުލް ފިޤްހު,Usul ul-Fiqh,HDT
-
Create the data file at
data/content/FQH-usululFiqh.csv. -
Open
src/books/index.html?book=FQH-usululFiqh— the book appears. The dashboard atsrc/books/index.htmlpicks it up automatically.
Add a row to data/01-registry-bookTags.csv — the label is trilingual, straight in the file:
tagCode,labelAR,labelDV,labelEN,aliasesAR,aliasesDV,aliasesEN
FQH,فقه,ފިގުހު,Fiqh,,,Books with a FQH- prefix (primary tag) or FQH in their tags column will show a "Fiqh" badge in English, فقه in Arabic, ފިގުހު in Dhivehi. No code needed — this one row is the single source of truth for the tag in all three languages. Colours are auto‑generated using golden‑ratio HSL — no limit on tags, always distinct, dark‑mode built in.
The aliases* columns are optional extra search words — names beyond the label that should still match the tag when searched (e.g. RDF carries Radheef,Lexicon in English). Comma-separated lists go in a quoted cell ("Radheef,Lexicon"). They never appear on badges — they only join the search matching.
Blank lines are fine as group separators — the parser drops them and they don't shift the auto‑generated colours (each tag's colour slot follows its position among tags). Never add # comment lines: the parser has no comment syntax, so a # line parses as a phantom tag and silently recolours everything after it. The tag row order is the palette and the display order — chips render in file order, not alphabetical — 04-update-bookRegistry.ps1 never rewrites this file.
A book code carries exactly ONE tag — the primary — as its first segment (HDT-muwattaMalik); any further tags live in the tags column of the registry. Prefixes and suffixes control book behaviour — badges, visibility, row order, and more. See Architecture → Naming conventions for the full list. A quick summary:
DRFT-→ draft badge (or intags) ·-HDN→ hidden ·-DSC→ reversed rows ·KNSH-→ body heading style- Run
data/04-update-bookRegistry.ps1to sync new books and generatetitleEN(it preserves thetagscolumn)
The first row of each book's CSV is always the header row. If column 0 is # or blank, it's treated as row numbers (hidden from content, shown as #N labels in card view). Otherwise column 0 is regular content. Header names label the column toggle buttons.
For a representative sample, see data/content/AQD-nawaqidulIslam.csv — it's small and covers the common column patterns (headAR, bodyAR, headDV, bodyDV, foot).
#,headAR,bodyAR,headDV,bodyDV,foot
1,[الشرك في عبادة الله],"بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ
اعْلَمْ أَنَّ مِنْ أَعْظَمِ نَوَاقِضِ الإِسْلَامِ عَشَرَةً…",[ﷲ އަށް އަޅުކަން ކުރުމުގައި ޝަރީކު ކުރުން],…
2,[من جعل بينه وبين الله وسائط],الثَّانِي: مَنْ جَعَلَ بَيْنَهُ وَبَيْنَ اللَّهِ وَسَائِطَ…,[މެދުވެރިން],…Note the first row's bodyAR cell spans two lines — quoted cells may contain newlines; the reader collapses consecutive blank lines inside them.
- The page reads
?book=CODEfrom the URL. book-data.jsloads03-registry-bookMeta.csv,01-registry-bookTags.csvand02-registry-bookAuthors.csvfor metadata, badges and author lines.src/js/reader.jsloadsdata/content/{bookCode}.csvviafetch+parseCSV.- Content renders with infinite scroll — rows load as you scroll.
No book selected? The dashboard shows all registered books as a card grid.
The site is fully static with zero runtime dependencies. Serve the codebase/ folder over HTTP (the data loads via fetch, which file:// blocks):
cd codebase
python -m http.server 8000
# or: npx serve .Then open http://localhost:8000/src/books/index.html.
The optional build — npm install then node tools/dist-build.mjs — emits the
minified dist/ for deployment; source and dist behave identically (see
"Build (dist/)" in docs/ARCHITECTURE.md).
This repo is the GitHub Pages site (hadithmv.github.io) — pushing to main deploys automatically. The app lives at hadithmv.github.io/codebase/src/books/.
Version ritual: the version string (e.g. v6.9.85 (Web)) lives in src/js/i18n.js under appVersion (shown in the sidebar footer and export headers). Bump it whenever you ship a release, and title the release commit Update to vX.Y.Z.
- Fixed topBar with return (red), focus (green), title, and hamburger (blue) buttons — only hamburger visible on dashboard
- Sticky reader chrome below with bottom-border cutoff separating chrome from content
- Columns stacked vertically with
dir="auto"for RTL/LTR detection - Heading hierarchy: columns starting with
head/kitab/bab(e.g.headAR,kitabDV) rendered as large/medium/small bold headings; in plain text copy, heads get a dash-rule underline, kitabs get aKitab:prefix, babs get an indentedBab:prefix matn/sharhcolumns: sharh (commentary) text rendered ~10% smaller than matn (main text); a· · ·dotted separator line inserted between matn→sharh within the same language- Blank line inserted between the last
*ARcolumn and first*DVcolumn ◆divider between all rows (including across scroll-chunk boundaries)ـــــــــــtatweel line before columns whose header starts withfoot- Infinite scroll — content loads automatically as you scroll
- All columns toggleable via a dropdown; columns whose header ends with
-HDNstart hidden - View toggle — switch between vertical card view and horizontal table view (RDF/dictionary books default to table on desktop, card on mobile); table header row sticks below the toolbar when scrolling
- Consecutive blank lines within cells collapsed to single line breaks (handles both
\nand\r\nendings) - Progress bar along the topBar's bottom edge — milestone toasts at 25/50/75%, Quran books toast each completed surah by name, and at 100% a green completion border pulses around the screen
- First (
<<) / Prev (<) / page select / Next (>) / Last (>>), all same height - Subtitle and tag badges on the same row (scrolls horizontally if too wide)
- Centered on mobile (bottom nav), right-aligned (top nav)
All rows inside the collapsible panel use uniform 10px spacing via flex column gap. Toolbar and pagination rows are wrapped in .horizontal-scroll-wrap containers with padded space for absolutely-positioned arrow buttons. Rows scroll horizontally (overflow-x: auto, hidden scrollbar); mouse wheel is redirected to horizontal scroll. All interactive elements use em-based padding and line-height: 2.2, sized via --panel-font-size CSS variable (tied to the reader font size control).
- Real-time "find and jump" — search highlights matches in a dropdown, clicking jumps to that row without filtering the dataset
- Query syntax: wildcards (
*,?), whole-word (.word), fuzzy (~word~), negation (-word), column-scoped (col:N:word), regex (/pattern/) - Tashkeel-insensitive: strips Arabic diacritics before matching, normalises Arabic letter variants (أ إ آ → ا, ى → ي) and Thaana thikijehi (Arabic‑derived letters → their base forms, e.g. ޘ → ސ). Thaana fili (vowel marks) are deliberately preserved — they distinguish words (ކަތި ≠ ކުތި), unlike Arabic diacritics
- Fast on big books: cells are normalised once at load (precomputed cache), queries are compiled once per keystroke, and the input is debounced (120 ms) — a full scan of a 50k-row book runs in a few milliseconds
- Results dropdown with highlighted snippets (~300 chars of context), highlighting maps back to original text with tashkeel intact
- Click or Enter to jump; ↑/↓ to navigate; Escape to close
- Red bold ✕ clear button resets to full content
- Advanced search modal for column/condition/value filters with AND/OR logic — same find-and-jump behavior
| Control | Description |
|---|---|
| 📌 Pin | Bookmark current book + page (Alt+P) |
| 📋 Copy | Copy current row as formatted plain text |
| 🔗 Share | Copy deep link to current book + row |
| ◉ Hide diacritics | Toggle Arabic tashkeel visibility |
| 📖 Table/Card | Toggle between vertical card and horizontal table view |
| ↺ Reset | Reset all reader settings to defaults |
| 📥 Export | Dropdown: TXT, MD, JSON, CSV, TSV, YAML, TOON, XML, Excel, EPUB, Word, PDF, PNG |
| Hide columns ▾ | Dropdown of per-column toggle buttons |
Overflow buttons are accessible via ◀▶ arrow buttons that appear at the row edges — ◀ scrolls toward the end, ▶ scrolls back toward the start. Both arrow clicks and mouse-wheel redirection animate smoothly via an ease-out-cubic curve. Arrows auto-hide at the scroll extremes.
- Fixed bar with bottom border separator, always visible
- Dashboard: shows "ހަދީޘްއެމްވީ" branding (centered) + ☰ hamburger
- Reader: ↩ Return (red) + ▾ Focus (green) + Book Title (center) + ☰ Menu (blue)
- All three buttons: 35×35px at every breakpoint, tinted backgrounds
- 📌 Pins — button in sort row; opens a modal listing bookmarked books as a table (▲▼ reorder arrows, page, ✕ to remove, confirmed "Clear all"; max 10); click an entry to open at the bookmarked page. A
📌 ޕިންpins filter chip appears in the tag row (click to show only pinned books). Pinned books show a📌 ޕިންbadge on their card. Also accessible from the sidebar. - 🕐 History — button in sort row; opens a modal listing recently read books as a table (book, page, relative time like "3m ago", ✕ to remove, confirmed "Clear all"; max 10; one entry per book, latest position). Also accessible from the sidebar.
- Continue reading — in the collapsible panel above the book list, shows the most recent book from history (title, saved position, relative time) in every view; click it to resume exactly where you left off. Search and tag filters do not hide it — the card is a resume shortcut, independent of the book grid. Focus mode collapses the panel with it.
- Search bar — real-time filter across titleDV, titleAR, titleEN, and bookCode
- 🔎 Search in books — button next to the search bar opens the library search page (
library-search.html), carrying the current search text as?q=and any selected tag chips as?tags=; there the query runs against a machine-generated word index (whole-word, AND across words); the tag chips scope the search; results group by book with match counts and deep-link to the first matching row with the term pre-highlighted (reader.html?book=X&row=N&q=TERM); each result has a ▾ preview showing the first matching rows as highlighted snippets (paged with "Show next N"), every snippet linking to its exact row. Also reachable from the sidebar on every page; the URL stays shareable (?q=…&tags=…) Tags:/ޓެގުތައް:label before tag chips,Books:/ފޮތްތައް:label before result count- Tag chips — click to filter by tag (multiple = OR — a book shows if it carries any selected tag), active chips show ✕ to remove, each chip shows book count. An
Allchip (highlighted when no tags are selected) clears the tag filter. The URL updates with?tags=A,Bso filtered views are shareable. A📌 ޕިންchip (red) precedes the category tags for pinned-books filtering. Tag badges in the book reader header link back to the dashboard pre‑filtered by that tag. - Sort dropdown — A→Z / Z←A (arrows follow reading direction). The whole sort row stays on one line — on narrow screens it scrolls horizontally via ◀▶ edge arrows or the mouse wheel (same as the reader toolbar)
- Table/Card view toggle — card grid or full-width table with clickable rows; the table scrolls sideways (hidden scrollbar) when its four columns don't fit the screen
- ↺ Reset button — clears all filters, search, and view mode (pins & history are preserved)
- Card grid flows right-to-left (
direction: rtl) - All controls work together: search + tags + sort combined
- Blue ☰ hamburger button (always visible, opens right-side drawer)
- Navigation: Book list, Search in books, Pins, History, GitHub, FAQ, Help, Contact
- Settings modal: Theme (Light / Dark / Sepia), Widescreen, Font size ±, Font family dropdown, Language selector; ↺ Reset all settings is a confirmed factory reset — settings, pins, and history (the dashboard and reader resets are view-only and preserve pins/history)
- Scroll to top
- App version and creator credit
Toggled from the green ▾ button in the topBar or the Alt+Z key (reader) / z key (dashboard). Collapses the entire chrome smoothly via CSS Grid transition (no max-height stutter). Padding and border also hidden. Only the topBar and reader content remain. Button shows ▴ (rotated) when active.
The browser URL silently updates as you scroll (?book=CODE&row=N). Copy the address bar anytime to share your exact position. Or use the 🔗 Share button to copy a deep link. Opening a shared URL lands on the same row.
A subtle pill at the bottom-center of the screen shows 10 / 1 (total rows / current row) plus a muted reading percentage (e.g. 27%). Appears while scrolling, fades after 2 seconds.
- TopBar and chrome compress: controls keep their 35px height, icon buttons square up to 35×35px, tighter padding
- Toolbar and pagination rows scroll horizontally (hidden scrollbar, ◀▶ arrow buttons, wheel redirect)
- Pagination nav right-aligned for scrollability
- All chrome buttons share same height and font size (0.85rem, 7px padding)
- Sidebar has
overscroll-behavior: containto prevent scroll bleed - Search bar prevented from going offscreen
Three themes selectable from the settings modal: Light, Dark, and Sepia (warm cream/beige). Persisted, no flash on load.
| Key | Context | Action |
|---|---|---|
← / → |
Reader | Next / previous row (RTL: content flows right→left) |
Home / End |
Reader | First / last row |
↑ / ↓ |
Search focused | Navigate search results |
Enter |
Search focused | Select search result |
/ or Ctrl+F |
Anywhere | Focus search bar |
Ctrl+Shift+F |
Anywhere | Open advanced search |
Alt+Z |
Reader | Toggle focus mode (green ▾/▴ button) |
Alt+T |
Reader | Toggle tashkeel (diacritics) |
Alt+V |
Reader | Toggle card / table view |
Alt+P |
Reader | Toggle bookmark (pin) |
Alt+S |
Reader | Share link |
Alt+E |
Reader | Open export dropdown |
Escape |
Dashboard | Clear search & blur |
p |
Dashboard | Open pins modal |
h |
Dashboard | Open history modal |
Ctrl+, |
Anywhere | Open settings |
Ctrl+B |
Anywhere | Back to book list |
Escape |
Anywhere | Close sidebar / modal / search results |
All text formats include book title, URL, Hadithmv, and version. TOON uses the expanded list form ([N]: root array). Excel uses a lazy-loaded inline writer (src/js/export-xlsx.js, ~2.5 KB). EPUB uses a lazy-loaded e-book writer (src/js/export-epub.js, ~4 KB) with embedded Hadithmv font. PNG captures only the currently visible row (2× resolution) with the Hadithmv font embedded — one row, not the whole book. While an export is preparing, the Export button shows a "Preparing…" label and is disabled — large exports (54k rows, EPUB + font) take seconds, and the busy state prevents duplicate downloads from double-clicks.
All UI strings in src/js/i18n.js with dv, en, and ar translations. Static HTML uses data-i18n attributes; dynamic text uses t(). Language select in the settings modal. Persisted to localStorage. Tooltips and error messages are English-only and never translated. Every button should have a tooltip; if it has a keyboard shortcut, the shortcut is noted in the tooltip with the key in title case (e.g. title="Toggle focus (Alt+Z)", Ctrl+B, Ctrl+Shift+F).
All errors show visible messages in English, with a
- Registry-load failure → error message with a ↺ Retry button (re-fetches without a page refresh)
- CSV parse warnings → logged to browser console (non-fatal)
Zero runtime dependencies. No CDN — everything ships from the repo (the only devDependencies — @swc/core, lightningcss, @minify-html/node — run the optional dist/ build):
src/js/csv.js— tiny CSV parser (~1 KB), handles quoted fields and multiline valuessrc/js/export-xlsx.js— inline XLSX writer (~2.5 KB), lazy-loaded only when exporting to Excelsrc/js/export-epub.js— inline EPUB 3 e-book writer (~4 KB), lazy-loaded only when exporting to EPUBsrc/js/export-zip.js— minimal store-only ZIP writer (~1.5 KB), shared by the XLSX and EPUB writers
| Doc | Audience |
|---|---|
| User Guide | Readers — how to browse, search, read, and use settings |
| Architecture | Developers & LLMs — system design, data flow, conventions |
| API Reference | Developers — module exports, function signatures, search syntax |
| Toolbox | Maintainers — the double-click menu: build, refresh, checks, preview |
| Testing Guide | Maintainers — verification batteries, known non-errors, traps |