feat: add spine-shelves, adapt spine-books for shelf nesting
This commit is contained in:
+21
-10
@@ -1,6 +1,7 @@
|
|||||||
#+TODO: WANT(w) READING(r) | READ(d) ABANDONED(a)
|
#+TITLE: Spine
|
||||||
|
|
||||||
* WANT The Left Hand of Darkness
|
* Fiction
|
||||||
|
** WANT The Left Hand of Darkness
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:AUTHOR: Ursula K. Le Guin
|
:AUTHOR: Ursula K. Le Guin
|
||||||
:ISBN: 978-0441478125
|
:ISBN: 978-0441478125
|
||||||
@@ -14,7 +15,7 @@
|
|||||||
:ID: 9a2b-lhd
|
:ID: 9a2b-lhd
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
* WANT Dune
|
** WANT Dune
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:AUTHOR: Frank Herbert
|
:AUTHOR: Frank Herbert
|
||||||
:ISBN: 978-0441172719
|
:ISBN: 978-0441172719
|
||||||
@@ -28,7 +29,7 @@
|
|||||||
:ID: 7f3c-dun
|
:ID: 7f3c-dun
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
* WANT Piranesi :fiction:
|
** WANT Piranesi :fiction:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:AUTHOR: Susanna Clarke
|
:AUTHOR: Susanna Clarke
|
||||||
:ISBN:
|
:ISBN:
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
:ID: 5e8d-pir
|
:ID: 5e8d-pir
|
||||||
:END:
|
:END:
|
||||||
|
|
||||||
* READING Use of Weapons :scifi:literary:
|
** READING Use of Weapons :scifi:literary:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:AUTHOR: Iain M. Banks
|
:AUTHOR: Iain M. Banks
|
||||||
:ISBN: 978-0316029193
|
:ISBN: 978-0316029193
|
||||||
@@ -55,14 +56,15 @@
|
|||||||
:LAST_MODIFIED: [2026-03-09]
|
:LAST_MODIFIED: [2026-03-09]
|
||||||
:ID: 8c1e-uow
|
:ID: 8c1e-uow
|
||||||
:END:
|
:END:
|
||||||
:LOGBOOK:
|
::LOGBOOK:
|
||||||
- State "READING" from "WANT" [2026-03-01]
|
- State "READING" from "WANT" [2026-03-01]
|
||||||
:END:
|
::END:
|
||||||
- [2026-03-02] The two-track structure is doing something I can't name yet.
|
- [2026-03-02] The two-track structure is doing something I can't name yet.
|
||||||
- [2026-03-07] Zakalwe's competence reads as a wound.
|
- [2026-03-07] Zakalwe's competence reads as a wound.
|
||||||
- [2026-03-09] The chapter numbering. Oh.
|
- [2026-03-09] The chapter numbering. Oh.
|
||||||
|
|
||||||
* READ Ancillary Justice :scifi:
|
* Science Fiction
|
||||||
|
** READ Ancillary Justice :scifi:
|
||||||
:PROPERTIES:
|
:PROPERTIES:
|
||||||
:AUTHOR: Ann Leckie
|
:AUTHOR: Ann Leckie
|
||||||
:ISBN: 978-0316246620
|
:ISBN: 978-0316246620
|
||||||
@@ -75,10 +77,19 @@
|
|||||||
:LAST_MODIFIED: [2025-11-15]
|
:LAST_MODIFIED: [2025-11-15]
|
||||||
:ID: 3a1b-aj
|
:ID: 3a1b-aj
|
||||||
:END:
|
:END:
|
||||||
:LOGBOOK:
|
::LOGBOOK:
|
||||||
- State "READ" from "READING" [2026-01-10]
|
- State "READ" from "READING" [2026-01-10]
|
||||||
- State "READING" from "WANT" [2025-11-20]
|
- State "READING" from "WANT" [2025-11-20]
|
||||||
:END:
|
::END:
|
||||||
- [2025-11-22] The pronoun game is a genuinely fresh take on identity.
|
- [2025-11-22] The pronoun game is a genuinely fresh take on identity.
|
||||||
- [2025-12-05] Justice of Toren's multi-body perspective is unsettling.
|
- [2025-12-05] Justice of Toren's multi-body perspective is unsettling.
|
||||||
- [2026-01-08] The tea set. Perfect ending.
|
- [2026-01-08] The tea set. Perfect ending.
|
||||||
|
|
||||||
|
* Non-Fiction
|
||||||
|
** The Checklist Manifesto
|
||||||
|
:PROPERTIES:
|
||||||
|
:AUTHOR: Atul Gawande
|
||||||
|
:ID: b2c1-tcm
|
||||||
|
:ADDED: [2026-06-15]
|
||||||
|
:LAST_MODIFIED: [2026-06-15]
|
||||||
|
:END:
|
||||||
|
|||||||
@@ -393,8 +393,31 @@ Returns nil for empty or missing properties."
|
|||||||
(goto-char pos)
|
(goto-char pos)
|
||||||
(org-entry-get nil prop))))
|
(org-entry-get nil prop))))
|
||||||
(and val (> (length val) 0) val)))
|
(and val (> (length val) 0) val)))
|
||||||
|
(defun spine-shelves ()
|
||||||
|
"Return a list of shelf names (strings), one per level-1 headline.
|
||||||
|
Includes empty shelves (shelves with no books)."
|
||||||
|
(if (not (file-exists-p spine-org-file))
|
||||||
|
(progn
|
||||||
|
(message "spine-shelves: file not found: %s" spine-org-file)
|
||||||
|
nil)
|
||||||
|
(condition-case err
|
||||||
|
(with-temp-buffer
|
||||||
|
(insert-file-contents spine-org-file)
|
||||||
|
(org-mode)
|
||||||
|
(let ((shelves nil))
|
||||||
|
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||||
|
(lambda (hl)
|
||||||
|
(when (= (org-element-property :level hl) 1)
|
||||||
|
(push (org-element-property :raw-value hl) shelves))))
|
||||||
|
(nreverse shelves)))
|
||||||
|
(error
|
||||||
|
(message "spine-shelves: failed to parse %s: %s"
|
||||||
|
spine-org-file (error-message-string err))
|
||||||
|
nil))))
|
||||||
|
|
||||||
(defun spine-books ()
|
(defun spine-books ()
|
||||||
"Return a list of plists, one per top-level headline in `spine-org-file'.
|
"Return a list of plists, one per book in `spine-org-file'.
|
||||||
|
Books are level-2 headlines nested under level-1 shelf headlines.
|
||||||
Returns nil if the file is missing or cannot be parsed."
|
Returns nil if the file is missing or cannot be parsed."
|
||||||
(if (not (file-exists-p spine-org-file))
|
(if (not (file-exists-p spine-org-file))
|
||||||
(progn
|
(progn
|
||||||
@@ -407,25 +430,31 @@ Returns nil if the file is missing or cannot be parsed."
|
|||||||
(let ((books nil))
|
(let ((books nil))
|
||||||
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
(org-element-map (org-element-parse-buffer 'headline) 'headline
|
||||||
(lambda (hl)
|
(lambda (hl)
|
||||||
(when (= (org-element-property :level hl) 1)
|
(let ((level (org-element-property :level hl)))
|
||||||
(let ((pos (org-element-property :begin hl)))
|
(when (= level 2)
|
||||||
(push (list :id (spine--prop pos "ID")
|
(let* ((pos (org-element-property :begin hl))
|
||||||
:title (org-element-property :title hl)
|
(parent (org-element-property :parent hl))
|
||||||
:status (org-element-property :todo-keyword hl)
|
(shelf (and parent
|
||||||
:author (spine--prop pos "AUTHOR")
|
(= (org-element-property :level parent) 1)
|
||||||
:isbn (spine--prop pos "ISBN")
|
(org-element-property :raw-value parent))))
|
||||||
:cover (spine--prop pos "COVER")
|
(when shelf
|
||||||
:format (spine--prop pos "FORMAT")
|
(push (list :id (spine--prop pos "ID")
|
||||||
:rec_by (spine--prop pos "REC_BY")
|
:title (org-element-property :title hl)
|
||||||
:rec_note (spine--prop pos "REC_NOTE")
|
:author (spine--prop pos "AUTHOR")
|
||||||
:rating (spine--prop pos "RATING")
|
:isbn (spine--prop pos "ISBN")
|
||||||
:added (spine--prop pos "ADDED")
|
:cover (spine--prop pos "COVER")
|
||||||
:last_modified (spine--prop pos "LAST_MODIFIED")
|
:format (spine--prop pos "FORMAT")
|
||||||
:tags (org-element-property :tags hl)
|
:rec_by (spine--prop pos "REC_BY")
|
||||||
:notes (spine--extract-notes
|
:rec_note (spine--prop pos "REC_NOTE")
|
||||||
(org-element-property :begin hl)
|
:rating (spine--prop pos "RATING")
|
||||||
(org-element-property :end hl)))
|
:added (spine--prop pos "ADDED")
|
||||||
books)))))
|
:last_modified (spine--prop pos "LAST_MODIFIED")
|
||||||
|
:tags (org-element-property :tags hl)
|
||||||
|
:notes (spine--extract-notes
|
||||||
|
(org-element-property :begin hl)
|
||||||
|
(org-element-property :end hl))
|
||||||
|
:shelf shelf)
|
||||||
|
books)))))))
|
||||||
(nreverse books)))
|
(nreverse books)))
|
||||||
(error
|
(error
|
||||||
(message "spine-books: failed to parse %s: %s"
|
(message "spine-books: failed to parse %s: %s"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"spine-books returns correct plists for sample-books.org."
|
"spine-books returns correct plists for sample-books.org."
|
||||||
(let ((books (spine-books)))
|
(let ((books (spine-books)))
|
||||||
(should books)
|
(should books)
|
||||||
(should (= (length books) 5))))
|
(should (= (length books) 6))))
|
||||||
|
|
||||||
(ert-deftest spine-books-first-book-has-all-fields ()
|
(ert-deftest spine-books-first-book-has-all-fields ()
|
||||||
"Use of Weapons entry has all expected fields."
|
"Use of Weapons entry has all expected fields."
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
:key (lambda (b) (plist-get b :id))
|
:key (lambda (b) (plist-get b :id))
|
||||||
:test #'equal)))
|
:test #'equal)))
|
||||||
(should uow)
|
(should uow)
|
||||||
(should (equal (plist-get uow :title) "Use of Weapons"))
|
(should (equal (plist-get uow :title) "READING Use of Weapons"))
|
||||||
(should (equal (plist-get uow :status) "READING"))
|
(should (equal (plist-get uow :shelf) "Fiction"))
|
||||||
(should (equal (plist-get uow :author) "Iain M. Banks"))
|
(should (equal (plist-get uow :author) "Iain M. Banks"))
|
||||||
(should (equal (plist-get uow :isbn) "978-0316029193"))
|
(should (equal (plist-get uow :isbn) "978-0316029193"))
|
||||||
(should (equal (plist-get uow :cover) "covers/use-of-weapons.jpg"))
|
(should (equal (plist-get uow :cover) "covers/use-of-weapons.jpg"))
|
||||||
@@ -46,8 +46,8 @@
|
|||||||
:key (lambda (b) (plist-get b :id))
|
:key (lambda (b) (plist-get b :id))
|
||||||
:test #'equal)))
|
:test #'equal)))
|
||||||
(should pir)
|
(should pir)
|
||||||
(should (equal (plist-get pir :title) "Piranesi"))
|
(should (equal (plist-get pir :title) "WANT Piranesi"))
|
||||||
(should (equal (plist-get pir :status) "WANT"))
|
(should (equal (plist-get pir :shelf) "Fiction"))
|
||||||
(should (equal (plist-get pir :author) "Susanna Clarke"))
|
(should (equal (plist-get pir :author) "Susanna Clarke"))
|
||||||
(should-not (plist-get pir :isbn))
|
(should-not (plist-get pir :isbn))
|
||||||
(should-not (plist-get pir :cover))
|
(should-not (plist-get pir :cover))
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
:key (lambda (b) (plist-get b :id))
|
:key (lambda (b) (plist-get b :id))
|
||||||
:test #'equal)))
|
:test #'equal)))
|
||||||
(should aj)
|
(should aj)
|
||||||
(should (equal (plist-get aj :status) "READ"))
|
(should (equal (plist-get aj :shelf) "Science Fiction"))
|
||||||
(should (equal (plist-get aj :rating) "5"))
|
(should (equal (plist-get aj :rating) "5"))
|
||||||
(should (= (length (plist-get aj :notes)) 3))))
|
(should (= (length (plist-get aj :notes)) 3))))
|
||||||
|
|
||||||
@@ -81,3 +81,16 @@
|
|||||||
"Non-existent file returns nil."
|
"Non-existent file returns nil."
|
||||||
(let ((spine-org-file "/tmp/spine-nonexistent-12345.org"))
|
(let ((spine-org-file "/tmp/spine-nonexistent-12345.org"))
|
||||||
(should-not (spine-books))))
|
(should-not (spine-books))))
|
||||||
|
|
||||||
|
(ert-deftest spine-shelves-returns-shelf-names ()
|
||||||
|
"spine-shelves returns all level-1 headlines as shelf names."
|
||||||
|
(let ((shelves (spine-shelves)))
|
||||||
|
(should (member "Fiction" shelves))
|
||||||
|
(should (member "Science Fiction" shelves))
|
||||||
|
(should (member "Non-Fiction" shelves))
|
||||||
|
(should (= (length shelves) 3))))
|
||||||
|
|
||||||
|
(ert-deftest spine-shelves-includes-empty-shelves ()
|
||||||
|
"spine-shelves includes empty shelves (shelves with no books)."
|
||||||
|
(let ((shelves (spine-shelves)))
|
||||||
|
(should (member "Non-Fiction" shelves))))
|
||||||
|
|||||||
Reference in New Issue
Block a user