diff options
| -rw-r--r-- | modules/calibredb-epub-config.el | 48 | ||||
| -rw-r--r-- | tests/test-calibredb-epub-config--bookmark-name.el | 87 |
2 files changed, 135 insertions, 0 deletions
diff --git a/modules/calibredb-epub-config.el b/modules/calibredb-epub-config.el index 4243e509a..9f09e3924 100644 --- a/modules/calibredb-epub-config.el +++ b/modules/calibredb-epub-config.el @@ -327,6 +327,54 @@ Try to use the Calibre book id from the parent folder name (for example, ("t" . nov-goto-toc) ("C-c C-b" . cj/nov-jump-to-calibredb))) +;; ------------------------- Nov bookmark naming ------------------------------- +;; In a nov buffer "m" is bound to `bookmark-set' (above). nov's +;; `nov-bookmark-make-record' names the record after `(buffer-name)' -- the EPUB +;; filename, extension and all. Rebuild it as "Author, Title" parsed from the +;; filename: under Calibre's "<Title> - <Author>.epub" naming the filename is +;; more complete than the EPUB's embedded metadata (which carries truncated +;; titles and author-sort "Last, First" forms). + +(defun cj/--nov-clean-title (s) + "Clean a title or author S parsed from an EPUB filename, or nil when blank. +Restores a colon where Calibre sanitized \":\" to \"_\" (\"Frege_ A Guide\" +-> \"Frege: A Guide\"), turns any leftover underscore into a space, and +collapses runs of whitespace." + (when (stringp s) + (let* ((colon (replace-regexp-in-string "_ " ": " s)) + (spaced (replace-regexp-in-string "_" " " colon)) + (out (string-trim (replace-regexp-in-string "[ \t]+" " " spaced)))) + (and (not (string-empty-p out)) out)))) + +(defun cj/--nov-bookmark-name-from-file (path) + "Return \"Author, Title\" derived from an EPUB PATH's filename, or nil. +Splits the filename (sans extension) on its last \" - \" into title and +author per Calibre's \"<Title> - <Author>\" convention, restoring colons and +reordering to \"Author, Title\". Falls back to the cleaned whole name when +there is no \" - \" separator." + (when (and (stringp path) (not (string-empty-p path))) + (let ((base (file-name-sans-extension (file-name-nondirectory path)))) + (if (string-match "\\`\\(.+\\) - \\(.+\\)\\'" base) + (let ((title (cj/--nov-clean-title (match-string 1 base))) + (author (cj/--nov-clean-title (match-string 2 base)))) + (cond ((and author title) (format "%s, %s" author title)) + (title title) + (author author) + (t nil))) + (cj/--nov-clean-title base))))) + +(defun cj/--nov-bookmark-rename-record (record) + "Replace RECORD's bookmark name with \"Author, Title\" from its EPUB filename. +Advice (:filter-return) on `nov-bookmark-make-record'. RECORD is +\(NAME . ALIST) carrying a `filename'; left unchanged when no name derives." + (let ((name (cj/--nov-bookmark-name-from-file + (alist-get 'filename (cdr record))))) + (if name (cons name (cdr record)) record))) + +(with-eval-after-load 'nov + (advice-add 'nov-bookmark-make-record :filter-return + #'cj/--nov-bookmark-rename-record)) + (defun cj/--nov-image-padding-cols (col-width img-px font-width-px) "Return left-padding columns to center an IMG-PX-wide image in COL-WIDTH cols. FONT-WIDTH-PX is the column width in pixels; clamped up to 1 so a zero or diff --git a/tests/test-calibredb-epub-config--bookmark-name.el b/tests/test-calibredb-epub-config--bookmark-name.el new file mode 100644 index 000000000..2e1d253e9 --- /dev/null +++ b/tests/test-calibredb-epub-config--bookmark-name.el @@ -0,0 +1,87 @@ +;;; test-calibredb-epub-config--bookmark-name.el --- Nov bookmark naming tests -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the clean "Author, Title" bookmark naming that replaces nov.el's +;; filename-based default. The name is parsed from the EPUB filename (Calibre's +;; "<Title> - <Author>.epub" convention), restoring colons that Calibre +;; sanitized to underscores and reordering to "Author, Title". + +;;; Code: + +(require 'ert) +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'calibredb-epub-config) + +;;; cj/--nov-clean-title + +(ert-deftest test-nov-clean-title-passthrough () + "Normal: a clean string is returned unchanged." + (should (equal (cj/--nov-clean-title "Agatha Christie") "Agatha Christie")) + (should (equal (cj/--nov-clean-title "The A.B.C. Murders") "The A.B.C. Murders"))) + +(ert-deftest test-nov-clean-title-restores-colon () + "Boundary: Calibre's \"_ \" colon substitution is restored to \": \"." + (should (equal (cj/--nov-clean-title "Frege_ A Guide for the Perplexed") + "Frege: A Guide for the Perplexed")) + (should (equal (cj/--nov-clean-title "The Fool's Progress_ An Honest Novel") + "The Fool's Progress: An Honest Novel"))) + +(ert-deftest test-nov-clean-title-stray-underscore-and-whitespace () + "Boundary: a non-colon underscore becomes a space; whitespace collapses." + (should (equal (cj/--nov-clean-title "a_b") "a b")) + (should (equal (cj/--nov-clean-title " x y ") "x y"))) + +(ert-deftest test-nov-clean-title-rejects-blank-and-nonstring () + "Error: nil, empty, all-whitespace, or non-string yields nil." + (should-not (cj/--nov-clean-title nil)) + (should-not (cj/--nov-clean-title "")) + (should-not (cj/--nov-clean-title " ")) + (should-not (cj/--nov-clean-title 42))) + +;;; cj/--nov-bookmark-name-from-file + +(ert-deftest test-nov-bookmark-name-real-examples () + "Normal: real Calibre filenames become \"Author, Title\" with colons restored." + (should (equal (cj/--nov-bookmark-name-from-file + "/books/Frege_ A Guide for the Perplexed - Edward Kanterian.epub") + "Edward Kanterian, Frege: A Guide for the Perplexed")) + (should (equal (cj/--nov-bookmark-name-from-file + "/books/The A.B.C. Murders - Agatha Christie.epub") + "Agatha Christie, The A.B.C. Murders")) + (should (equal (cj/--nov-bookmark-name-from-file + "/books/The Fool's Progress_ An Honest Novel - Edward Abbey.epub") + "Edward Abbey, The Fool's Progress: An Honest Novel"))) + +(ert-deftest test-nov-bookmark-name-splits-on-last-separator () + "Boundary: a title containing \" - \" splits on the LAST separator." + (should (equal (cj/--nov-bookmark-name-from-file "/b/Title - Part Two - Some Author.epub") + "Some Author, Title - Part Two"))) + +(ert-deftest test-nov-bookmark-name-no-separator () + "Boundary: a filename with no \" - \" falls back to the cleaned whole name." + (should (equal (cj/--nov-bookmark-name-from-file "/b/Untitled_ Draft.epub") + "Untitled: Draft"))) + +(ert-deftest test-nov-bookmark-name-nil-and-empty () + "Error: nil or empty path yields nil." + (should-not (cj/--nov-bookmark-name-from-file nil)) + (should-not (cj/--nov-bookmark-name-from-file ""))) + +;;; cj/--nov-bookmark-rename-record + +(ert-deftest test-nov-bookmark-rename-record-replaces-name () + "Normal: the record's name is rebuilt from its filename; the alist is kept." + (let* ((record (cons "The A.B.C. Murders - Agatha Christie.epub" + '((filename . "/b/The A.B.C. Murders - Agatha Christie.epub") + (index . 0)))) + (out (cj/--nov-bookmark-rename-record record))) + (should (equal (car out) "Agatha Christie, The A.B.C. Murders")) + (should (equal (cdr out) (cdr record))))) + +(ert-deftest test-nov-bookmark-rename-record-keeps-original-without-filename () + "Boundary: a record with no usable filename is returned unchanged." + (let ((record (cons "whatever" '((index . 0))))) + (should (equal (cj/--nov-bookmark-rename-record record) record)))) + +(provide 'test-calibredb-epub-config--bookmark-name) +;;; test-calibredb-epub-config--bookmark-name.el ends here |
