239 lines
7.6 KiB
Markdown
239 lines
7.6 KiB
Markdown
# 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
|
|
```
|