aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst12
-rw-r--r--nov.el175
2 files changed, 145 insertions, 42 deletions
diff --git a/README.rst b/README.rst
index 982bd29..46d9800 100644
--- a/README.rst
+++ b/README.rst
@@ -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)
diff --git a/nov.el b/nov.el
index e8452b1..5736395 100644
--- a/nov.el
+++ b/nov.el
@@ -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