docs: add mustache templating implementation plan

This commit is contained in:
2026-06-21 07:37:29 -04:00
parent c7971eb297
commit 67e41ad110
@@ -0,0 +1,238 @@
# Mustache Template Rendering — Implementation Plan
**Goal:** Render the index page via Mustache (`spine-render`) using the mockup-A template, replacing inline HTML generation.
**Architecture:** Move `spine-mockup-a-agenda.mustache``templates/index.mustache`. Add `spine-index-model` function to build the view model `ht`. Update the index handler. Remove old `spine-render-index`.
**Tech Stack:** Emacs Lisp, `mustache`, `ht`.
---
## File structure
| File | Responsibility |
|------|---------------|
| `templates/index.mustache` | Index page Mustache template (moved from root) |
| `spine.el` | Add `spine-index-model`, update handler, remove `spine-render-index` |
---
### Task 1: Move template and add spine-index-model
**Files:**
- Create: `templates/index.mustache`
- Modify: `spine.el`
- Delete: `spine-mockup-a-agenda.mustache`
- [ ] **Step 1: Move the template**
```bash
mv spine-mockup-a-agenda.mustache templates/index.mustache
```
- [ ] **Step 2: Add `spine-index-model` function to `spine.el`**
Insert after `spine--h` (after the HTML escaping helper), before `spine-render-index`:
```elisp
(defun spine-index-model (books &optional selected-id)
"Build the view model ht for templates/index.mustache from BOOKS.
SELECTED-ID expands the matching book's detail section."
(let* ((status-labels '(("WANT" . "on deck")
("READING" . "reading")
("READ" . "read")
("ABANDONED" . "abandoned")))
(format-icons '(("hardcover" . "ti-book")
("ebook" . "ti-device-tablet")
("audiobook" . "ti-headphones")))
(order '("WANT" "READING" "READ" "ABANDONED"))
(grouped (make-hash-table :test 'equal))
(total (length books))
(reading-count 0))
;; Count reading books
(dolist (book books)
(when (equal "READING" (plist-get book :status))
(cl-incf reading-count)))
;; Group books by status
(dolist (book books)
(let ((status (or (plist-get book :status) "WANT")))
(push book (gethash status grouped))))
;; Build groups list for template
(let ((groups nil))
(dolist (status order)
(let ((group-books (nreverse (gethash status grouped))))
(when group-books
(let ((book-models nil))
(dolist (book group-books)
(let* ((id (plist-get book :id))
(selected (equal id selected-id))
(fmt (plist-get book :format))
(rating (plist-get book :rating))
(notes (plist-get book :notes))
(status (or (plist-get book :status) "WANT"))
(model
(ht
("format_icon"
(or (cdr (assoc fmt format-icons)) ""))
("status_class" (downcase status))
("status_label" (downcase status))
("title" (plist-get book :title))
("author" (or (plist-get book :author) ""))
("tags_inline"
(let ((tags (plist-get book :tags)))
(when tags
(mapconcat (lambda (t) (concat ":" t ":"))
tags " "))))
("rec_via" (plist-get book :rec_by))
("rating_display"
(when (and rating (> (length rating) 0))
(let ((n (string-to-number rating)))
(make-string n ?★))))
("selected" selected)
("detail"
(when selected
(ht
("meta"
(let ((added (plist-get book :added))
(fmt (plist-get book :format)))
(cond
((and added fmt)
(format "started %s · %s" added fmt))
(added (format "started %s" added))
(fmt (format "format: %s" fmt))
(t ""))))
("notes"
(when notes
(cl-loop for (date text) in notes
collect (ht ("date" (format "[%s]" date))
("text" text))))))))))))
(push model book-models)))
(push (ht ("label" (cdr (assoc status status-labels)))
("count" (length group-books))
("books" (nreverse book-models)))
groups)))))
(ht ("app_title" "spine")
("total_books" total)
("reading_count" reading-count)
("groups" (nreverse groups))))))
```
- [ ] **Step 3: Remove old `spine-render-index` function**
Delete the `spine-render-index` function entirely.
- [ ] **Step 4: Update the index handler**
Replace the handler's call from `spine-render-index` to `spine-render` + `spine-index-model`:
```elisp
(defservlet index text/html (path query request)
(let ((books (spine-books)))
(if books
(insert (spine-render "index.mustache"
(spine-index-model books (cadr (assoc "id" query)))))
(insert (spine-render-empty-state)))))
```
- [ ] **Step 5: Verify batch load**
```bash
emacs --quick --batch --eval "(setq spine-skip-server-start t)" --load spine.el
```
Expected: exits cleanly.
- [ ] **Step 6: Verify template renders**
```bash
emacs --quick --batch \
--eval "(setq spine-skip-server-start t spine-org-file \"$(pwd)/sample-books.org\")" \
--load spine.el \
--eval "(progn (message \"HTML length: %d\" (length (spine-render \"index.mustache\" (spine-index-model (spine-books) nil)))))"
```
Expected: non-zero length, no errors.
- [ ] **Step 7: Commit**
```bash
git add templates/index.mustache spine.el
git rm spine-mockup-a-agenda.mustache
git commit -m "feat: render index via Mustache template with spine-index-model"
```
---
### Task 2: Smoke test end-to-end
**Files:**
- No changes — verification only.
- [ ] **Step 1: Start server with sample data**
```bash
emacsclient --socket-name=spine --eval '(kill-emacs)' 2>/dev/null
pkill -f "emacs --quick" 2>/dev/null
sleep 1
emacs --quick --eval "(setq spine-org-file \"$(pwd)/sample-books.org\")" --load spine.el &
sleep 5
```
- [ ] **Step 2: Verify Pico + Tabler CDN links**
```bash
curl -s http://localhost:8080/index | grep -c "picocss"
curl -s http://localhost:8080/index | grep -c "tabler"
```
Expected: `1` and `1`.
- [ ] **Step 3: Verify groups and content**
```bash
curl -s http://localhost:8080/index | grep -oP 'on deck|reading|read'
```
Expected: `on deck`, `reading`, `read` (no `abandoned`).
- [ ] **Step 4: Verify Tabler icon glyphs**
```bash
curl -s http://localhost:8080/index | grep -oP 'ti-book|ti-headphones|ti-device-tablet'
```
- [ ] **Step 5: Verify expanded detail**
```bash
curl -s "http://localhost:8080/index?id=8c1e-uow" | grep -c "logline"
```
Expected: `3` (three reading notes).
- [ ] **Step 6: Kill server**
```bash
kill %1 2>/dev/null
```
---
### Task 3: ERT tests and final verification
**Files:**
- No changes — verification only.
- [ ] **Step 1: Run ERT tests**
```bash
emacs --quick --batch --directory "$(pwd)" --load test/spine-books-test.el -f ert-run-tests-batch-and-exit
```
Expected: all 6 tests pass.
- [ ] **Step 2: Git status**
```bash
git status --short
git log --oneline -3
```