diff options
-rw-r--r-- | README.rst | 12 | ||||
-rw-r--r-- | nov.el | 175 |
2 files changed, 145 insertions, 42 deletions
@@ -13,9 +13,11 @@ Features: - Basic navigation (jump to TOC, previous/next chapter) - Remembering and restoring the last read position - Jump to next chapter when scrolling beyond end +- Storing and following Org links to EPUB files - Renders EPUB2 (.ncx) and EPUB3 (<nav>) TOCs - Hyperlinks to internal and external targets - Supports textual and image documents +- Info-style History navigation - View source of document files - Metadata display - Image rescaling @@ -82,13 +84,13 @@ By default text is filled by the window width. You can customize (setq nov-text-width 80) -It's also possible to set it to a huge number to inhibit text filling, -this can be used in combination with ``visual-line-mode`` and packages -such as ``visual-fill-column`` to implement more flexible filling: +It's also possible to set it to ``t`` to inhibit text filling, this +can be used in combination with ``visual-line-mode`` and packages such +as ``visual-fill-column`` to implement more flexible filling: .. code:: elisp - (setq nov-text-width most-positive-fixnum) + (setq nov-text-width t) (setq visual-fill-column-center-text t) (add-hook 'nov-mode-hook 'visual-line-mode) (add-hook 'nov-mode-hook 'visual-fill-column-mode) @@ -109,7 +111,7 @@ Here's an advanced example of text justification with the `justify-kp .. code:: elisp (require 'justify-kp) - (setq nov-text-width most-positive-fixnum) + (setq nov-text-width t) (defun my-nov-window-configuration-change-hook () (my-nov-post-html-render-hook) @@ -1,10 +1,10 @@ ;;; nov.el --- Featureful EPUB reader mode -;; Copyright (C) 2017-2018 Vasilij Schneidermann <mail@vasilij.de> +;; Copyright (C) 2017-2019 Vasilij Schneidermann <mail@vasilij.de> ;; Author: Vasilij Schneidermann <mail@vasilij.de> ;; URL: https://github.com/wasamasa/nov.el -;; Version: 0.2.7 +;; Version: 0.2.9 ;; Package-Requires: ((dash "2.12.0") (esxml "0.3.3") (emacs "24.4")) ;; Keywords: hypermedia, multimedia, epub @@ -32,9 +32,11 @@ ;; - Basic navigation (jump to TOC, previous/next chapter) ;; - Remembering and restoring the last read position ;; - Jump to next chapter when scrolling beyond end +;; - Storing and following Org links to EPUB files ;; - Renders EPUB2 (.ncx) and EPUB3 (<nav>) TOCs ;; - Hyperlinks to internal and external targets ;; - Supports textual and image documents +;; - Info-style History navigation ;; - View source of document files ;; - Metadata display ;; - Image rescaling @@ -71,16 +73,19 @@ Otherwise the default face is used." (defcustom nov-text-width nil "Width filled text shall occupy. An integer is interpreted as the number of columns. If nil, use -the full window's width. Note that this variable only has an -effect in Emacs 25.1 or greater." +the full window's width. If t, disable filling completely. Note +that this variable only has an effect in Emacs 25.1 or greater." :type '(choice (integer :tag "Fixed width in characters") - (const :tag "Use the width of the window" nil)) + (const :tag "Use the width of the window" nil) + (const :tag "Disable filling" t)) :group 'nov) (defcustom nov-render-html-function 'nov-render-html "Function used to render HTML. It's called without arguments with a buffer containing HTML and -should change it to contain the rendered version of it.") +should change it to contain the rendered version of it." + :type 'function + :group 'nov) (defcustom nov-pre-html-render-hook nil "Hook run before `nov-render-html'." @@ -124,6 +129,14 @@ Each alist item consists of the identifier and full path.") (defvar-local nov-toc-id nil "TOC identifier of the EPUB buffer.") +(defvar-local nov-history nil + "Stack of documents user has visited. +Each element of the stack is a list (NODEINDEX BUFFERPOS).") + +(defvar-local nov-history-forward nil + "Stack of documents user has visited with `nov-history-back' command. +Each element of the stack is a list (NODEINDEX BUFFERPOS).") + (defun nov-make-path (directory file) "Create a path from DIRECTORY and FILE." (concat (file-name-as-directory directory) file)) @@ -371,12 +384,15 @@ Each alist item consists of the identifier and full path." (define-key map (kbd "g") 'nov-render-document) (define-key map (kbd "v") 'nov-view-source) (define-key map (kbd "V") 'nov-view-content-source) + (define-key map (kbd "a") 'nov-reopen-as-archive) (define-key map (kbd "m") 'nov-display-metadata) (define-key map (kbd "n") 'nov-next-document) (define-key map (kbd "]") 'nov-next-document) (define-key map (kbd "p") 'nov-previous-document) (define-key map (kbd "[") 'nov-previous-document) (define-key map (kbd "t") 'nov-goto-toc) + (define-key map (kbd "l") 'nov-history-back) + (define-key map (kbd "r") 'nov-history-forward) (define-key map (kbd "RET") 'nov-browse-url) (define-key map (kbd "c") 'nov-copy-url) (define-key map (kbd "<follow-link>") 'mouse-face) @@ -418,23 +434,32 @@ Each alist item consists of the identifier and full path." (setq url (url-generic-parse-url url)) (mapcar 'nov-urldecode (list (url-filename url) (url-target url)))) -(defun nov-insert-image (path) - "Insert an image for PATH at point. +(defun nov-insert-image (path alt) + "Insert an image for PATH at point, falling back to ALT. This function honors `shr-max-image-proportion' if possible." - ;; adapted from `shr-rescale-image' - (if (fboundp 'imagemagick-types) - (let ((edges (window-inside-pixel-edges - (get-buffer-window (current-buffer))))) - (insert-image - (create-image path 'imagemagick nil - :ascent 100 - :max-width (truncate (* shr-max-image-proportion - (- (nth 2 edges) - (nth 0 edges)))) - :max-height (truncate (* shr-max-image-proportion - (- (nth 3 edges) - (nth 1 edges))))))) - (insert-image (create-image path nil nil :ascent 100)))) + (cond + ((not (display-graphic-p)) + (insert alt)) + ;; TODO: add native resizing support once it's official + ((fboundp 'imagemagick-types) + ;; adapted from `shr-rescale-image' + (let ((edges (window-inside-pixel-edges + (get-buffer-window (current-buffer))))) + (insert-image + (create-image path 'imagemagick nil + :ascent 100 + :max-width (truncate (* shr-max-image-proportion + (- (nth 2 edges) + (nth 0 edges)))) + :max-height (truncate (* shr-max-image-proportion + (- (nth 3 edges) + (nth 1 edges)))))))) + (t + ;; `create-image' errors out for unsupported image types + (let ((image (ignore-errors (create-image path nil nil :ascent 100)))) + (if image + (insert-image image) + (insert alt)))))) (defvar nov-original-shr-tag-img-function (symbol-function 'shr-tag-img)) @@ -443,14 +468,15 @@ This function honors `shr-max-image-proportion' if possible." "Custom <img> rendering function for DOM. Uses `shr-tag-img' for external paths and `nov-insert-image' for internal ones." - (let ((url (or url (cdr (assq 'src (cadr dom)))))) + (let ((url (or url (cdr (assq 'src (cadr dom))))) + (alt (or (cdr (assq 'alt (cadr dom))) ""))) (if (nov-external-url-p url) ;; HACK: avoid hanging in an infinite loop when using ;; `cl-letf' to override `shr-tag-img' with a function that ;; might call `shr-tag-img' again (funcall nov-original-shr-tag-img-function dom url) (setq url (expand-file-name (nov-urldecode url))) - (nov-insert-image url)))) + (nov-insert-image url alt)))) (defun nov-render-title (dom) "Custom <title> rendering function for DOM. @@ -478,12 +504,15 @@ chapter title." (let (;; HACK: make buttons use our own commands (shr-map nov-mode-map) (shr-external-rendering-functions nov-shr-rendering-functions) - (shr-use-fonts nov-variable-pitch) - (shr-width nov-text-width)) + (shr-use-fonts nov-variable-pitch)) ;; HACK: `shr-external-rendering-functions' doesn't cover ;; every usage of `shr-tag-img' (cl-letf (((symbol-function 'shr-tag-img) 'nov-render-img)) - (shr-render-region (point-min) (point-max)))) + (if (eq nov-text-width t) + (cl-letf (((symbol-function 'shr-fill-line) 'ignore)) + (shr-render-region (point-min) (point-max))) + (let ((shr-width nov-text-width)) + (shr-render-region (point-min) (point-max)))))) (run-hooks 'nov-post-html-render-hook)) (defun nov-render-document () @@ -505,7 +534,7 @@ the HTML is rendered with `nov-render-html-function'." (cond (imagep - (nov-insert-image path)) + (nov-insert-image path "")) ((and (version< nov-epub-version "3.0") (eq id nov-toc-id)) (insert (nov-ncx-to-html path))) @@ -528,14 +557,21 @@ the HTML is rendered with `nov-render-html-function'." (when done (1- i)))) +(defun nov-goto-document (index) + "Go to the document denoted by INDEX." + (let ((history (cons (list nov-documents-index (point)) + nov-history))) + (setq nov-documents-index index) + (nov-render-document) + (setq nov-history history))) + (defun nov-goto-toc () "Go to the TOC index and render the TOC document." (interactive) (let ((index (nov-find-document (lambda (doc) (eq (car doc) nov-toc-id))))) (when (not index) (error "Couldn't locate TOC")) - (setq nov-documents-index index) - (nov-render-document))) + (nov-goto-document index))) (defun nov-view-source () "View the source of the current document in a new buffer." @@ -547,6 +583,12 @@ the HTML is rendered with `nov-render-html-function'." (interactive) (find-file nov-content-file)) +(defun nov-reopen-as-archive () + "Reopen the EPUB document using `archive-mode'." + (interactive) + (with-current-buffer (find-file-literally nov-file-name) + (archive-mode))) + (defun nov-display-metadata () "View the metadata of the EPUB document in a new buffer." (interactive) @@ -576,15 +618,13 @@ the HTML is rendered with `nov-render-html-function'." "Go to the next document and render it." (interactive) (when (< nov-documents-index (1- (length nov-documents))) - (setq nov-documents-index (1+ nov-documents-index)) - (nov-render-document))) + (nov-goto-document (1+ nov-documents-index)))) (defun nov-previous-document () "Go to the previous document and render it." (interactive) (when (> nov-documents-index 0) - (setq nov-documents-index (1- nov-documents-index)) - (nov-render-document))) + (nov-goto-document (1- nov-documents-index)))) (defun nov-scroll-up (arg) "Scroll with `scroll-up' or visit next chapter if at bottom." @@ -612,9 +652,8 @@ the HTML is rendered with `nov-render-html-function'." (lambda (doc) (equal path (file-truename (cdr doc))))))) (when (not index) (error "Couldn't locate document")) - (setq nov-documents-index index) (let ((shr-target-id target)) - (nov-render-document)) + (nov-goto-document index)) (when target (let ((pos (next-single-property-change (point-min) 'shr-target-id))) (when (not pos) @@ -673,6 +712,34 @@ Saving is only done if `nov-save-place-file' is set." (>= index 0) (< index (length documents)))) +(defun nov-history-back () + "Go back in the history to the last visited document." + (interactive) + (or nov-history + (user-error "This is the first document you looked at")) + (let ((history-forward + (cons (list nov-documents-index (point)) + nov-history-forward)) + (index (car (car nov-history))) + (opoint (cadr (car nov-history)))) + (setq nov-history (cdr nov-history)) + (nov-goto-document index) + (setq nov-history (cdr nov-history)) + (setq nov-history-forward history-forward) + (goto-char opoint))) + +(defun nov-history-forward () + "Go forward in the history of visited documents." + (interactive) + (or nov-history-forward + (user-error "This is the last document you looked at")) + (let ((history-forward (cdr nov-history-forward)) + (index (car (car nov-history-forward))) + (opoint (cadr (car nov-history-forward)))) + (nov-goto-document index) + (setq nov-history-forward history-forward) + (goto-char opoint))) + ;;;###autoload (define-derived-mode nov-mode special-mode "EPUB" "Major mode for reading EPUB documents" @@ -720,7 +787,7 @@ Saving is only done if `nov-save-place-file' is set." (nov-render-document)))) -;;; interop +;;; recentf interop (require 'recentf) (defun nov-add-to-recentf () @@ -728,6 +795,40 @@ Saving is only done if `nov-save-place-file' is set." (recentf-add-file nov-file-name))) (add-hook 'nov-mode-hook 'nov-add-to-recentf) +(add-hook 'nov-mode-hook 'hack-dir-local-variables-non-file-buffer) + + +;;; org interop + +(require 'org) + +(defun nov-org-link-follow (path) + (if (string-match "^\\(.*\\)::\\([0-9]+\\):\\([0-9]+\\)$" path) + (let ((file (match-string 1 path)) + (index (string-to-number (match-string 2 path))) + (point (string-to-number (match-string 3 path)))) + (find-file file) + (when (not (nov--index-valid-p nov-documents index)) + (error "Invalid documents index")) + (setq nov-documents-index index) + (nov-render-document) + (goto-char point)) + (error "Invalid nov.el link"))) + +(defun nov-org-link-store () + (when (not (and (eq major-mode 'nov-mode) nov-file-name)) + (error "Not in a nov.el buffer")) + (when (not (integerp nov-documents-index)) + (setq nov-documents-index 0)) + (org-store-link-props + :type "nov" + :link (format "nov:%s::%d:%d" nov-file-name nov-documents-index (point)) + :description (format "EPUB file at %s" nov-file-name))) + +(org-link-set-parameters + "nov" + :follow 'nov-org-link-follow + :store 'nov-org-link-store) (provide 'nov) ;;; nov.el ends here |