org-mode/lisp/org-latex-preview.el

3122 lines
143 KiB
EmacsLisp
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; org-latex-preview.el --- LaTeX previews for Org -*- lexical-binding: t; -*-
;; Copyright (C) 2022-2024 Free Software Foundation, Inc.
;; Authors: TEC <contact@tecosaur.net> and Karthik Chikmagalur
;; Keywords: tex, extensions, tools
;; This file is part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; LaTeX previews for Org
;;; Code:
(require 'org-macs)
(org-assert-version)
(require 'org-compat)
(require 'ox-latex)
(declare-function org-persist-read "org-persist")
(declare-function org-persist-register "org-persist")
(declare-function org-persist-unregister "org-persist")
(declare-function eldoc--invoke-strategy "eldoc")
(defvar org-src-mode-hook nil)
(defvar org-src--beg-marker nil)
(defvar org-latex-preview--dvisvgm3-minor-version
(or (and (executable-find "dvisvgm")
(with-temp-buffer
(call-process "dvisvgm" nil t nil "--version")
(let ((ver (version-to-list
(string-trim (buffer-string) "dvisvgm "))))
(and (= (car ver) 3) (cadr ver)))))
-1)
"The minor version of dvisvgm, if dvisvgm 3 is installed. Otherwise -1.")
(defgroup org-latex-preview nil
"Options for generation of LaTeX previews in Org mode."
:tag "Org LaTeX Preview"
:group 'org)
;;;###autoload
(defcustom org-latex-preview-appearance-options
'(:foreground auto :background "Transparent"
:scale 1.0 :zoom 1.0 :page-width 0.6
:matchers ("begin" "$1" "$" "$$" "\\(" "\\["))
"Options for creating images from LaTeX fragments.
This is a property list with the following properties:
:foreground The foreground color for images embedded in Emacs, e.g. \"Black\".
`default' means use the foreground of the default face.
`auto' means use the foreground from the text face.
:background The background color, or \"Transparent\".
`default' means use the background of the default face.
`auto' means use the background from the text face.
:scale A scaling factor for the size of the images, to get more pixels
:zoom when the image has associated font-relative height information,
the display size is scaled by this factor.
:page-width The width of the LaTeX document fragments are compiled in.
Either:
- A string giving a LaTeX dimension (e.g. \"12cm\").
- A floating point value between 0.0 and 1.0,
this sets the text width to this ratio of the page width.
- nil, in which case the default text width is unmodified.
:matchers A list indicating which matchers should be used to
find LaTeX fragments. Valid members of this list are:
\"begin\" find environments
\"$1\" find single characters surrounded by $.$
\"$\" find math expressions surrounded by $...$
\"$$\" find math expressions surrounded by $$....$$
\"\\(\" find math expressions surrounded by \\(...\\)
\"\\=\\[\" find math expressions surrounded by \\=\\[...\\]"
:group 'org-latex-preview
:package-version '(Org . "9.7")
:type 'plist)
(defcustom org-latex-preview-process-default
(if (executable-find "dvisvgm") 'dvisvgm 'dvipng)
"The default process to convert LaTeX fragments to image files.
All available processes and theirs documents can be found in
`org-latex-preview-process-alist', which see."
:group 'org-latex-preview
:package-version '(Org . "9.7")
:type 'symbol)
;;;###autoload
(defcustom org-latex-preview-process-alist
'((dvipng
:programs ("latex" "dvipng")
:description "dvi > png"
:message "you need to install the programs: latex and dvipng."
:image-input-type "dvi"
:image-output-type "png"
:latex-compiler ("%l -interaction nonstopmode -output-directory %o %f")
:latex-precompiler ("%l -output-directory %o -ini -jobname=%b \"&%L\" mylatexformat.ltx %f")
:image-converter ("dvipng --follow -D %D -T tight --depth --height -o %B-%%09d.png %f")
:transparent-image-converter
("dvipng --follow -D %D -T tight -bg Transparent --depth --height -o %B-%%09d.png %f"))
(dvisvgm
:programs ("latex" "dvisvgm")
:description "dvi > svg"
:message "you need to install the programs: latex and dvisvgm."
:image-input-type "dvi"
:image-output-type "svg"
:latex-compiler ("%l -interaction nonstopmode -output-directory %o %f")
:latex-precompiler ("%l -output-directory %o -ini -jobname=%b \"&%L\" mylatexformat.ltx %f")
;; The --optimise, --clipjoin, and --relative flags cause dvisvgm to
;; do some extra work to tidy up the SVG output, but barely add to
;; the overall dvisvgm runtime (<1% increace, from testing).
:image-converter determined-at-runtime)
(imagemagick
:programs ("pdflatex" "convert")
:description "pdf > png"
:message "you need to install the programs: latex and imagemagick."
:image-input-type "pdf"
:image-output-type "png"
:latex-compiler ("pdflatex -interaction nonstopmode -output-directory %o %f")
:latex-precompiler ("pdftex -output-directory %o -ini -jobname=%b \"&pdflatex\" mylatexformat.ltx %f")
:image-converter
("convert -density %D -trim -antialias %f -quality 100 %B-%%09d.png")))
"Definitions of external processes for LaTeX previewing.
Org mode can use some external commands to generate TeX snippet's images for
previewing or inserting into HTML files, e.g., \"dvipng\". This variable tells
`org-latex-preview-create-image' how to call them.
The value is an alist with the pattern (NAME . PROPERTIES). NAME is a symbol.
PROPERTIES accepts the following attributes:
:programs list of strings, required programs.
:description string, describe the process.
:message string, message it when required programs cannot be found.
:image-input-type string, input file type of image converter (e.g., \"dvi\").
:image-output-type string, output file type of image converter (e.g., \"png\").
:post-clean list of strings, files matched are to be cleaned up once
the image is generated. When nil, the files with \".dvi\",
\".xdv\", \".pdf\", \".tex\", \".aux\", \".log\", \".svg\",
\".png\", \".jpg\", \".jpeg\" or \".out\" extension will
be cleaned up.
:latex-header list of strings, the LaTeX header of the snippet file.
When nil, the fallback value is used instead, which is
controlled by `org-latex-preview-preamble',
`org-latex-default-packages-alist' and
`org-latex-packages-alist', which see.
:latex-compiler list of LaTeX commands, as strings. Each of them is given
to the shell. Place-holders \"%t\", \"%b\" and \"%o\" are
replaced with values defined below.
:image-converter list of image converter commands strings. Each of them is
given to the shell and supports any of the following
place-holders defined below.
If set, :transparent-image-converter is used instead of :image-converter to
convert an image when the background color is nil or \"Transparent\".
Place-holders used by `:image-converter', `:latex-precompiler',
and `:latex-compiler':
%f input file name
%b base name of input file
%o base directory of input file
%O absolute output file name
Place-holders only used by `:latex-precompiler' and `:latex-compiler':
%l LaTeX compiler command string
%L LaTeX compiler command name
Place-holders only used by `:image-converter':
%D dpi, which is used to adjust image size by some processing commands."
:group 'org-latex-preview
:package-version '(Org . "9.7")
:type '(alist :tag "LaTeX to image backends"
:value-type (plist)))
;; This is a bit hacky, but since we can't include reference to the
;; runtime value of `org-latex-preview--dvisvgm3-minor-version' in the
;; default value of `org-latex-preview-process-alist', we have to
;; resort to modifying the value at runtime like so.
;; Theoretically only the "load" condition is needed, but some people seemed
;; to have problems with this that are solved by adding "eval".
(cl-eval-when (load eval)
(when-let ((dvisvgm (alist-get 'dvisvgm org-latex-preview-process-alist)))
(when (eq (plist-get dvisvgm :image-converter) 'determined-at-runtime)
(plist-put dvisvgm :image-converter
(list
(concat "dvisvgm --page=1- --optimize --clipjoin --relative --no-fonts"
(if (>= org-latex-preview--dvisvgm3-minor-version 2)
" -v3 --message='processing page {?pageno}: output written to {?svgfile}'" "")
" --bbox=preview -o %B-%%9p.svg %f"))))))
(defcustom org-latex-preview-compiler-command-map
'(("pdflatex" . "latex")
("xelatex" . "xelatex -no-pdf")
("lualatex" . "dvilualatex"))
"An alist mapping from each of `org-latex-compilers' to command strings.
Each key is a LaTeX compiler name, for each compiler in
`org-latex-compilers', and the value the command that should be used
when producing a preview (optionally including flags).
This should only ever be changed in the event that PDF, not DVI output
is required."
:group 'org-latex-preview
:package-version '(Org . "9.7")
:type '(alist :tag "Compiler"
:value-type (string :type "command")))
(defcustom org-latex-preview-cache 'persist
"Persist produced LaTeX previews across Emacs sessions.
When non-nil, org-persist is used to cache the fragments and
data. Otherwise, a temporary directory is used for images and
the data is stored in `org-latex-preview--table' for the duration
of the Emacs session."
:group 'org-latex
:package-version '(Org . "9.7")
:type '(choice (const :tag "Use org-mode's persistent cache system" persist)
(const :tag "Use the system temporary directory" temp)
(string :tag "Path to cache directory")))
(defcustom org-latex-preview-persist-expiry 7
"A homologue of `org-persist-default-expiry' for preview data.
This is only relevant when `org-latex-preview-cache' is set to
persist."
:group 'org-latex
:package-version '(Org . "9.7")
:type '(choice (const :tag "Never" never)
(const :tag "Always" nil)
(number :tag "Keep N days")
(function :tag "Function")))
(defcustom org-latex-preview-numbered t
"Whether to calculate and apply correct equation numbering.
When nil, equation numbering is disabled and a diamond symbol is
shown in place of the equation number.
When non-nil, equation numbering is tracked across the document.
Alternatively, when set to the symbol \"preview\" numbering will
simply be left as the automatic LaTeX numbering generated when
previewing the batch of fragments. This may be mostly-correct,
or mostly-incorrect depending on the situation."
:group 'org-latex
:package-version '(Org . "9.7")
:type '(choice (const :tag "No" nil)
(const :tag "Preview " preview)
(const :tag "Yes" t)))
(defcustom org-latex-preview-process-active-indicator 'fringe
"The style of visual indicator for LaTeX currently being processed.
This sets the method used to indicated that a LaTeX fragment is
currently being processed for display.
There are three recognised value symbols:
- nil, do not indicate fragment processing.
- face, apply a special face to fragments that are being processed.
You can customize the face `org-latex-preview-processing-face' to
change how it appears.
- fringe, apply a fringe marker to lines where fragments are being
processed."
:group 'org-latex
:package-version '(Org . "9.7")
:type '(choice
(const :tag "No indicator" nil)
(const :tag "Fringe marker" fringe)
(const :tag "Processing face" face)))
(defcustom org-latex-preview-auto-ignored-environments '("figure")
"List of LaTeX environments that should not be automatically previewed."
:type '(repeat string)
:package-version '(Org . "9.7")
:group 'org-latex-preview)
(defcustom org-latex-preview-auto-ignored-commands nil
"List of movement commands that should not affect preview display.
When these commands are invoked, they will not cause previews to
be revealed when using `org-latex-preview-auto-mode'."
:type '(repeat symbol)
:package-version '(Org . "9.7")
:group 'org-latex-preview)
(defcustom org-latex-preview-process-finish-functions nil
"Abnormal hook run after preview generation.
Each function in this hook is called with three arguments:
- The exit-code of the preview generation process. More
specifically, this is the exit-code of the image-converter, the
final process in the chain of processes that generates a LaTeX
preview image.
- The process buffer.
- The processing-info plist, containing the state of the LaTeX
preview process. See `org-latex-preview--create-image-async'
for details.
This hook can be used for introspection of or additional
processing after the LaTeX preview process."
:group 'org-latex-preview
:type 'hook)
(defcustom org-latex-preview-overlay-update-functions nil
"Abnormal hook run after a preview-overlay is updated.
Each function in this hook is called with one argument, the
overlay that was updated."
:group 'org-latex-preview
:type 'hook)
(defcustom org-latex-preview-overlay-close-functions nil
"Abnormal hook run after placing a LaTeX preview image.
This hook typically runs when the cursor is moved out of a LaTeX
fragment or environment with `org-latex-preview-auto-mode'
active, causing the display of text contents to be replaced by
the corresponding preview image.
Functions in this hook are called with one argument, the overlay
that the cursor moved out of."
:group 'org-latex-preview
:type 'hook)
(defcustom org-latex-preview-overlay-open-functions nil
"Hook run after hiding a LaTeX preview image.
This hook typically runs when the cursor is moved into a LaTeX
fragment or environment with `org-latex-preview-auto-mode'
active, causing the display of the preview image to be replaced
by the corresponding LaTeX fragment text.
Functions in this hook are called with one argument, the overlay
that the cursor moved into."
:group 'org-latex-preview
:type 'hook)
(defface org-latex-preview-processing-face '((t :inherit shadow))
"Face applied to LaTeX fragments for which a preview is being generated.
See `org-latex-preview-process-active-indicator'."
:group 'org-faces)
(defconst org-latex-preview--image-log "*Org Preview Convert Output*"
"Buffer name for Preview image conversion output.")
(defconst org-latex-preview--latex-log "*Org Preview LaTeX Output*"
"Buffer name for Preview LaTeX output.")
(defconst org-latex-preview--precompile-log "*Org Preview Preamble Precompilation*"
"Buffer name for Preview LaTeX output.")
(defcustom org-latex-preview-preamble "\\documentclass{article}
\[DEFAULT-PACKAGES]
\[PACKAGES]
\\usepackage{xcolor}"
"The document header used for processing LaTeX fragments.
It is imperative that this header make sure that no page number
appears on the page. The package defined in the variables
`org-latex-default-packages-alist' and `org-latex-packages-alist'
will either replace the placeholder \"[PACKAGES]\" in this
header, or they will be appended."
:group 'org-latex-preview
:type 'string)
(defcustom org-latex-preview-process-precompiled t
"Use LaTeX header precompilation when previewing fragments.
This causes a slight delay the first time `org-latex-pdf-process'
is called in a buffer, but subsequent calls will be faster.
This requires the LaTeX package \"mylatexformat\" to be installed."
:group 'org-latex-preview
:package-version '(Org . "9.7")
:type 'boolean)
(defcustom org-latex-preview-auto-track-inserts t
"Whether `org-latex-preview-auto-mode' should apply to newly inserted fragments.
When `org-latex-preview-auto-mode' is on, existing LaTeX previews
will be automatically hidden/shown on cursor movement and
regenerated after edits. This option controls how newly inserted
and edited fragments are previewed.
The following values are supported:
- t: Generate previews for newly inserted fragments.
- nil: Do not generate previews for newly inserted fragments.
Note that existing previews are always updated after the cursor
moves out of them."
:group 'org-latex
:package-version '(Org . "9.7")
:type '(choice
(const :tag "Track inserts" t)
(const :tag "Don't track inserts" nil)))
(defconst org-latex-preview--tentative-math-re
"\\$\\|\\\\[([]\\|^[ \t]*\\\\begin{[A-Za-z0-9*]+}"
"Regexp whith will match all instances of LaTeX math.
Note that this will also produce false postives, and
`org-element-context' should be used to verify that matches are
indeed LaTeX fragments/environments.")
(defconst org-latex-preview--ignored-faces
'(org-indent whitespace-face)
"Faces that should not affect the color of preview overlays.")
(defconst org-latex-preview--svg-fg-standin "#000001"
"Hex color that is used as a stand-in for the current color.
The entire purpose of this is to be replaced by \"currentColor\"
in `org-latex-preview--svg-make-fg-currentColor', and so it
should be a color that is extremely likely not otherwise found in
the image.")
(defconst org-latex-preview--overlay-priority -80
"The priority used with preview overlays.")
(defun org-latex-preview--ensure-overlay (beg end)
"Build an overlay between BEG and END."
(let (ov)
(dolist (o (overlays-in beg end))
(when (eq (overlay-get o 'org-overlay-type)
'org-latex-overlay)
(if (or ov (not (and (= beg (overlay-start o))
(= end (overlay-end o)))))
(delete-overlay o)
(setq ov o)
;; Reset all potentially modified properties.
(overlay-put ov 'face nil)
(overlay-put ov 'help-echo nil) ;tooltip error display
(overlay-put ov 'before-string nil) ;error fringe marker
;; (overlay-put ov 'hidden-face nil) ;(re)store svg face
;; We do not set the display property of preview image
;; overlays to nil when ensuring that an overlay exists.
;; This causes flicker during regeneration as the the
;; underlying text is shown and then replaced with the new
;; image.
;;
;; We also do not reset the image spec stored in the
;; `preview-image' property, or the state of the preview
;; stored in the `view-text' property, as persisting the
;; state of an already existing overlay is required for live
;; previews.
(overlay-put ov 'preview-state nil) ;is fragment modified?
)))
(unless ov
(setq ov (make-overlay beg end nil 'front-advance))
(overlay-put ov 'org-overlay-type 'org-latex-overlay)
(overlay-put ov 'evaporate t)
(overlay-put ov 'priority org-latex-preview--overlay-priority)
(overlay-put ov 'modification-hooks
(list #'org-latex-preview-auto--mark-overlay-modified))
(overlay-put ov 'insert-in-front-hooks
(list #'org-latex-preview-auto--insert-front-handler))
(overlay-put ov 'insert-behind-hooks
(list #'org-latex-preview-auto--insert-behind-handler)))
ov))
(defun org-latex-preview--indicate-processing (ov &optional on)
"Modify OV to provide visual indication of LaTeX fragment preview generation.
When `org-latex-preview-process-active-indicator' is set to fringe, a
triangle in the left fringe will be shown or hidden depending on ON.
When `org-latex-preview-process-active-indicator' is set to face, the
overlay face is set to `org-latex-preview-processing-face'."
(pcase org-latex-preview-process-active-indicator
('fringe
(overlay-put
ov 'before-string
(and on (propertize "!" 'display
`(left-fringe right-triangle
fringe)))))
('face
(overlay-put ov 'face (and on 'org-latex-preview-processing-face)))))
(defun org-latex-preview-auto--mark-overlay-modified (ov after-p _beg _end &optional _l)
"When AFTER-P mark OV as modified and display nothing."
(when after-p
(unless (eq (overlay-get ov 'preview-state) 'modified)
(overlay-put ov 'preview-state 'modified)
(overlay-put ov 'face nil)
(overlay-put ov 'display nil))))
(defun org-latex-preview--update-overlay (ov path-info)
"Update the overlay OV to show the image specified by PATH-INFO."
(let* ((zoom (or (plist-get org-latex-preview-appearance-options :zoom) 1.0))
(height (plist-get (cdr path-info) :height))
(depth (plist-get (cdr path-info) :depth))
(errors (plist-get (cdr path-info) :errors))
(image-type (plist-get (cdr path-info) :image-type))
(image-display
(and (car path-info)
(list 'image
:type image-type
:file (car path-info)
:height (and height (cons (* height zoom) 'em))
:ascent (if (and depth height)
;; The baseline seems to tend to sit slightly
;; lower than it should be, and a very mild
;; bias seems to improve the visual result.
;; From testing with a collecting of LaTeX
;; maths fonts (cm, cmbright, arev, pxfonts,
;; notomath, nextxsf, eulervm) decreacing the
;; depth measurement by 0.02pt in the baseline
;; calculation seems to work well.
;; I have yet to come across any situation
;; where this results in a negative depth,
;; however we may as well ensure that never
;; occurs.
(round (* 100 (- 1 (/ (max 0.0 (- depth 0.02))
height))))
'center)))))
(overlay-put ov 'preview-image image-display)
(cond
((eq image-type 'svg)
(overlay-put
ov 'hidden-face
(or (and errors 'error)
(org-latex-preview--face-around
(overlay-start ov) (overlay-end ov)))))
(errors
(overlay-put
ov 'before-string
(propertize "!" 'display
`(left-fringe exclamation-mark error)))))
(when org-latex-preview-process-active-indicator
(org-latex-preview--indicate-processing ov))
;; This is a temporary measure until a more sophisticated
;; interface for errors is available in Org.
(when errors
(overlay-put
ov 'help-echo
(if (bound-and-true-p tooltip-mode)
errors
(concat (propertize "! " 'face '(bold error))
(substring (replace-regexp-in-string "[\n\r\t ]+" " " errors) 2)))))
(unless (overlay-get ov 'view-text) ;Live previewing this element, update in background
(when image-display (overlay-put ov 'display image-display))
(overlay-put ov 'face (overlay-get ov 'hidden-face)))
(run-hook-with-args 'org-latex-preview-overlay-update-functions ov)))
(defun org-latex-preview--face-around (start end)
"Return the relevant face symbol(s) around the region START to END.
A relevant face symbol before START is prefered, with END
examined if none could be found, and finally the default face
used as the final fallback.
Faces in `org-latex-preview--ignored-faces' are ignored."
(let ((face (or (and (> start (point-min))
(not (eq (char-before start) ?\n))
(get-text-property (1- start) 'face))
(and (> (point-max) end)
(not (eq (char-after end) ?\n))
(get-text-property end 'face))))
(normalising-face
(if (>= emacs-major-version 29) 'default '(:inherit default :extend t))))
(cond
((consp face)
(nconc (cl-set-difference face org-latex-preview--ignored-faces) (list normalising-face)))
((and face (not (memq face org-latex-preview--ignored-faces)))
(list face normalising-face))
(t normalising-face))))
;; Code for `org-latex-preview-auto-mode':
;;
;; The boundaries of latex preview image overlays are automatically
;; extended to track changes in the underlying text by the functions
;; `org-latex-preview-auto--insert-front-handler' and
;; `org-latex-preview-auto--insert-behind-handler'. These are placed in
;; the `insert-in-front-hooks' and `insert-behind-hooks' properties of
;; the iamge overlays. See (info "(elisp) Overlay Properties").
;; Additionally, when an overlay's text is modified,
;; `org-latex-preview-auto--mark-overlay-modified', placed in the overlay's
;; modification hook, notes this in the overlay's `preview-state'
;; property.
;;
;; This code examines the previous and current cursor
;; positions after each command. It uses the variables
;; `org-latex-preview-auto--from-overlay' and `org-latex-preview-auto--marker' to track
;; this.
;;
;; If the cursor has moved out of or into a latex preview overlay,
;; the overlay is changed to display or hide its image respectively.
;; The functions `org-latex-preview-auto--handle-pre-cursor' and
;; `org-latex-preview-auto--handle-post-cursor' do this. These are palced in
;; `pre-command-hook' and `post-command-hook' respectively.
;;
;; When the cursor positions pre- and post-command are inside an
;; overlay, it uses the overlay property `view-text' to check if the
;; source and destination overlays are distinct. If they are it shows
;; and hides images as appropriate.
;;
;; If the latex fragment text for an existing overlay is modified, a
;; new preview image will be generated automatically. The
;; modification state of the overlay is stored in the overlay property
;; `preview-state', and the function
;; `org-latex-preview-auto--close-previous-overlay' handles the recompilation.
;;
;; When the user option `org-latex-preview-auto-track-inserts' is
;; non-nil, previews are auto-generated for latex fragments as they
;; are inserted into the buffer. This work is handled by
;; `org-latex-preview-auto--detect-fragments-in-change', which is added to
;; `after-change-functions'. It does this by placing dummy overlays
;; that don't display images, but are marked as having been modified.
(defvar-local org-latex-preview-auto--from-overlay nil
"Whether the cursor if starting from within a preview overlay.")
(defvar-local org-latex-preview-auto--marker nil
"Marker to keep track of the previous cursor position.
This helps with tracking cursor movement into and out of preview overlays.")
(defvar-local org-latex-preview-auto--inhibit nil
"Delay the state machine that decides to auto-generate preview fragments.")
(defsubst org-latex-preview-auto--move-into (ov)
"Adjust column when moving into the overlay OV from below."
(when (> (marker-position org-latex-preview-auto--marker)
(line-end-position))
(goto-char (overlay-end ov))
(goto-char (max (line-beginning-position)
(overlay-start ov)))))
(defun org-latex-preview-auto--handle-pre-cursor ()
"Record the previous state of the cursor position.
This keeps track of the cursor relative to the positions of
Org latex preview overlays.
This is intended to be placed in `pre-command-hook'."
(if org-latex-preview-auto--inhibit
(setq org-latex-preview-auto--inhibit nil)
(setq org-latex-preview-auto--from-overlay
(eq (get-char-property (point) 'org-overlay-type)
'org-latex-overlay))
(set-marker org-latex-preview-auto--marker (point))))
(defun org-latex-preview-auto--handle-post-cursor ()
"Toggle or generate LaTeX previews based on cursor movement.
If the cursor is moving into a preview overlay, \"open\" it to
display the underlying latex fragment. If the cursor is moving
out of a preview overlay, show the image again or generate a new
one as appropriate.
This is intended to be placed in `post-command-hook'."
(let ((into-overlay-p (eq (get-char-property (point) 'org-overlay-type)
'org-latex-overlay)))
(cond
((and into-overlay-p org-latex-preview-auto--from-overlay)
(unless (get-char-property (point) 'view-text)
;; Jumped from overlay to overlay
(org-latex-preview-auto--close-previous-overlay)
(org-latex-preview-auto--open-this-overlay)))
((and into-overlay-p (not org-latex-preview-auto--from-overlay))
;; Moved into overlay
(org-latex-preview-auto--open-this-overlay))
(org-latex-preview-auto--from-overlay
;; Moved out of overlay
(org-latex-preview-auto--close-previous-overlay)))
(set-marker org-latex-preview-auto--marker (point))))
(defun org-latex-preview-auto--detect-fragments-in-change (beg end _)
"Examine the content between BEG and END, and preview LaTeX fragments found.
This is only active when either
`org-latex-preview-auto-track-inserts' or
`org-latex-preview-live' is enabled."
(when (or org-latex-preview-auto-track-inserts org-latex-preview-live)
(let ((initial-point (point))
fragments)
(save-excursion
;; Find every location in the changed region where a backslash
;; is succeeded by a parenthesis or square bracket, and check
;; for a LaTeX fragment.
(goto-char beg)
(unless (eobp)
(while (search-forward "\\" end t)
(and (memq (char-after) '(?\( ?\) ?\[ ?\]))
(push (org-latex-preview-auto--maybe-track-element-here
'latex-fragment initial-point)
fragments))))
;; Find every location in the changed region where a parenthesis
;; or square bracket is preceeded by a backslash, and check for
;; a LaTeX fragment.
(goto-char beg)
(unless (bobp)
(while (re-search-forward "[][()]" end t)
(and (eq (char-before (1- (point))) ?\\)
(push (org-latex-preview-auto--maybe-track-element-here
'latex-fragment initial-point)
fragments))))
;; Check for LaTeX environments on lines affected by the change.
;; Start by finding all affected lines with at least four
;; characters of content. Then we can check if the line starts
;; with "\beg" or "\end", and if so check for a LaTeX environment.
(goto-char beg)
(beginning-of-line)
(skip-chars-forward " \t")
(when (< (point) end)
(let ((line-start-positions
(and (> (point-max) (+ 4 (point)))
(list (point)))))
(while (and (< (point) end)
(search-forward "\n" end t))
(skip-chars-forward " \t")
(when (> (point-max) (+ 4 (point)))
(push (point) line-start-positions)))
(dolist (line-start line-start-positions)
(goto-char line-start)
(and (eq (char-after) ?\\)
(member (buffer-substring (point) (+ (point) 4))
'("\\beg" "\\end"))
(push (org-latex-preview-auto--maybe-track-element-here
'latex-environment initial-point)
fragments))))))
(when (setq fragments (delq nil fragments))
(when (and org-latex-preview-numbered
(cl-find 'latex-environment fragments
:key #'org-element-type :test #'eq))
(setq fragments
(append fragments
(org-latex-preview--get-numbered-environments
end nil))))
(org-latex-preview--place-from-elements
org-latex-preview-process-default
fragments)))))
(defun org-latex-preview-auto--maybe-track-element-here (type pos)
"Check for an org element of TYPE at `point' and ensure an overlay exists.
If POS lies within the element, nil is returned. Otherwise the
element is returned to be used to generate a preview.
If an org-latex-overlay is already present, nothing is done."
(and (not (eq (get-char-property (point) 'org-overlay-type)
'org-latex-overlay))
(when-let* ((element (org-element-context))
((eq (org-element-type element) type))
(elem-beg (or (org-element-property :post-affiliated element)
(org-element-property :begin element)))
(elem-end (- (org-element-property :end element)
(or (org-element-property :post-blank element) 0)
(if (eq (char-before (org-element-property :end element))
?\n)
1 0)))
((not (and (eq type 'latex-environment)
(save-excursion
(goto-char elem-beg)
(looking-at "\\\\begin{\\([^}]+\\)}"))
(member (match-string 1)
org-latex-preview-auto-ignored-environments))))
(ov (org-latex-preview--ensure-overlay elem-beg elem-end)))
(overlay-put ov 'preview-state 'modified)
(if (<= elem-beg pos elem-end)
(progn
(overlay-put ov 'view-text t)
;; Record a position safely inside the created overlay
(set-marker org-latex-preview-auto--marker
(min pos (1- elem-end)))
(setq org-latex-preview-auto--from-overlay t)
nil)
(setq org-latex-preview-auto--inhibit t)
element))))
(defun org-latex-preview-auto--open-this-overlay ()
"Open Org latex preview image overlays.
If there is a latex preview image overlay at point, hide the
image and display its text."
(dolist (ov (overlays-at (point)))
(when (and (eq (overlay-get ov 'org-overlay-type)
'org-latex-overlay)
(not (memq this-command
org-latex-preview-auto-ignored-commands)))
(overlay-put ov 'display nil)
(overlay-put ov 'view-text t)
(when-let ((f (overlay-get ov 'face)))
(overlay-put ov 'hidden-face f)
(overlay-put ov 'face nil))
(org-latex-preview-auto--move-into ov)
(setq org-latex-preview-auto--from-overlay nil)
(run-hook-with-args 'org-latex-preview-overlay-open-functions ov))))
(defun org-latex-preview-auto--close-previous-overlay ()
"Close Org latex preview image overlays.
If there is a latex preview image overlay at the previously
recorded cursor position, hide its text and display the
image. The preview image is regenerated if necessary."
(dolist (ov (overlays-at (marker-position org-latex-preview-auto--marker)))
(when (eq (overlay-get ov 'org-overlay-type) 'org-latex-overlay)
(overlay-put ov 'view-text nil)
(if (eq (overlay-get ov 'preview-state) 'modified)
;; It may seem odd to use an timer for this action, but by
;; introducing a brief window for Emacs to deal with input
;; events triggered during prior processing the perceptible
;; delay is reduced. Setting an 0.05s timer isn't
;; necesarily the optimal duration, but from a little
;; testing it appears to be fairly reasonable.
(run-at-time 0.01 nil #'org-latex-preview-auto--regenerate-overlay ov)
(when-let (f (overlay-get ov 'hidden-face))
(unless (eq f 'org-latex-preview-processing-face)
(overlay-put ov 'face f))
(overlay-put ov 'hidden-face nil))
(overlay-put ov 'display (overlay-get ov 'preview-image)))
(run-hook-with-args 'org-latex-preview-overlay-close-functions ov))))
(defun org-latex-preview-auto--regenerate-overlay (ov &optional inhibit-renumbering)
"Regenerate the LaTeX fragment under overlay OV.
When `org-latex-preview-numbered' is non-nil, and the overlay
being updated is for an environment, other numbered environments
will be updated should their numbering change.
Numbering checking and updating can be prevented by setting
INHIBIT-RENUMBERING to a non-nil value."
(with-current-buffer (overlay-buffer ov)
(let* ((start (overlay-start ov))
(end (overlay-end ov))
(fragment (save-excursion
(goto-char start)
(org-element-context)))
(others (and org-latex-preview-numbered
(not inhibit-renumbering)
(eq (org-element-type fragment) 'latex-environment)
(org-latex-preview--get-numbered-environments end nil))))
(if (memq (org-element-type fragment)
'(latex-fragment latex-environment))
(if (org-latex-preview--empty-fragment-p
(org-element-property :value fragment))
(progn (delete-overlay ov)
(org-latex-preview--ensure-overlay start end))
(org-latex-preview--place-from-elements
org-latex-preview-process-default
(cons fragment others)))
(delete-overlay ov)
(when others
(org-latex-preview--place-from-elements
org-latex-preview-process-default
others))))))
(defun org-latex-preview-auto--insert-front-handler
(ov after-p _beg end &optional _length)
"Extend Org LaTeX preview text boundaries when editing previews.
OV is the overlay displaying the preview. For the meaning of
AFTER-P, END and the other arguments, see the
`modification-hooks' property of overlays in the Elisp
manual: (elisp) Overlay Properties."
(when after-p
(unless undo-in-progress
(if (eq (overlay-get ov 'preview-state) 'active)
(move-overlay ov end (overlay-end ov))))))
(defun org-latex-preview-auto--insert-behind-handler
(ov after-p beg _end &optional _length)
"Extend Org LaTeX preview text boundaries when editing previews.
OV is the overlay displaying the preview. For the meaning of
AFTER-P, BEG and the other arguments, see the
`modification-hooks' property of overlays in the Elisp
manual: (elisp) Overlay Properties."
(when after-p
(unless undo-in-progress
(if (eq (overlay-get ov 'preview-state) 'active)
(move-overlay ov (overlay-end ov) beg)))))
;;;###autoload
(define-minor-mode org-latex-preview-auto-mode
"Minor mode to automatically preview LaTeX fragments.
When LaTeX preview auto mode is on, LaTeX fragments in Org
buffers are automatically previewed after being inserted, and
hidden when the cursor moves into them. This allows one to
seamlessly edit and preview LaTeX in Org buffers.
To enable auto-toggling of the preview images without
auto-generating them or vice-versa, customize the variable
`org-latex-preview-auto-track-inserts'.
To enable previews of LaTeX fragments while writing them,
customize the variable `org-latex-preview-live'."
:global nil
(if org-latex-preview-auto-mode
(progn
(setq org-latex-preview-auto--marker (make-marker))
(add-hook 'pre-command-hook #'org-latex-preview-auto--handle-pre-cursor nil 'local)
(org-latex-preview-auto--handle-pre-cursor) ; Invoke setup before the hook even fires.
(add-hook 'post-command-hook #'org-latex-preview-auto--handle-post-cursor nil 'local)
(add-hook 'after-change-functions #'org-latex-preview-auto--detect-fragments-in-change nil 'local)
(when org-latex-preview-live
(org-latex-preview-live--setup)))
(remove-hook 'pre-command-hook #'org-latex-preview-auto--handle-pre-cursor 'local)
(remove-hook 'post-command-hook #'org-latex-preview-auto--handle-post-cursor 'local)
(remove-hook 'after-change-functions #'org-latex-preview-auto--detect-fragments-in-change 'local)
(org-latex-preview-live--teardown)))
;; Code for "live" preview generation
;;
;; When `org-latex-preview-auto-mode' is turned on and
;; `org-latex-preview-live' is non-nil, previews are generated in the
;; background with each change to the LaTeX fragment being edited.
;; This continuously updated preview is shown to the right of the
;; LaTeX fragment, or under the LaTeX environment being edited.
;; Alternatively, it can be shown using Eldoc (see
;; `org-latex-preview-live-display-type').
;;
;; The code works as follows (simplified description):
;; - When the cursor enters a fragment and
;; `org-latex-preview-auto-mode' is active, the corresponding
;; overlay is "opened" up and the preview image is hidden. At this
;; time, the `after-string' property of this overlay is updated to
;; show the existing preview image.
;;
;; - A handler is added to `after-change-functions' to regenerate the
;; preview for the fragment when its contents change.
;;
;; - When the preview is regenerated, the `after-string' property of
;; the preview overlay is updated to show the new image. This
;; regeneration is modulated with a debounce
;; `org-latex-preview-live-debounce' and a dynamically updated
;; throttle `org-latex-preview-live-throttle'.
;;
;; - When the cursor exits the boundaries of the fragment, the
;; `after-string' property of the preview overlay is removed.
(defvar-local org-latex-preview-live--docstring " "
"String that holds the live LaTeX preview image as a text property.")
(defvar-local org-latex-preview-live--element-type nil)
(defvar-local org-latex-preview-live--generator nil)
(defcustom org-latex-preview-live '(block edit-special)
"Whether LaTeX previews should be generated during writing.
This affects the behaviour of `org-latex-preview-auto-mode'.
It is either a boolean value (preview everything or nothing), or
a list of context symbols within which live previews should be
shown.
The availible contexts are:
- inline, for inline LaTeX fragments
- block, for LaTeX environments
- edit-special, for org-edit-special buffers"
:group 'org-latex-preview-live
:type '(choice
(const :tag "Everywhere" t)
(const :tag "Never" nil)
(set :greedy t :tag "Contexts"
(const :tag "Inline LaTeX fragments" inline)
(const :tag "LaTeX environments" block)
(const :tag "When using org-edit-special" edit-special))))
(defcustom org-latex-preview-live-display-type 'buffer
"How to display live-updating previews of LaTeX snippets.
This option is meaningful when live previews are enabled via
`org-latex-preview-live'.
The currently supported options are the symbols
- buffer: Display live previews next to or under the LaTeX
fragment in the Org buffer.
- eldoc: Display live previews using Eldoc. Requires
`eldoc-mode' to be turned on in the Org buffer. Note that
Eldoc in turn offers various display functions, such as the
echo area, the dedicated `eldoc-doc-buffer' and more."
:group 'org-latex-preview
:type '(choice
(const :tag "Display next to fragment" buffer)
(const :tag "Display in Eldoc" eldoc)))
(defcustom org-latex-preview-live-debounce 1.0
"Idle time before regenerating LaTeX previews.
When `org-latex-preview-live' is non-nil and
`org-latex-preview-auto-mode' is active, live previews are
updated when there have been no changes to the LaTeX fragment or
environment for at least this much time."
:group 'org-latex-preview
:package-version '(Org . "9.7")
:type 'number)
(defvar org-latex-preview-live-throttle 1.0
"Throttle time for live LaTeX previews.
When `org-latex-preview-live' is non-nil and
`org-latex-preview-auto-mode' is active, live previews are
updated no more than once in this interval of time.
Its value is updated dynamically based on the average fragment
preview time.")
(defvar-local org-latex-preview-live--preview-times
(make-vector 3 1.0)
"Vector containing the last three preview run times in this buffer")
(defvar-local org-latex-preview-live--preview-times-index 0)
(defun org-latex-preview-live--update-times (latest-duration)
"Update preview times given the last preview took LATEST-DURATION seconds."
(aset org-latex-preview-live--preview-times
(% (cl-incf org-latex-preview-live--preview-times-index) 3)
latest-duration)
(setq-local org-latex-preview-live-throttle
(/ (apply #'+ (append org-latex-preview-live--preview-times nil))
3)))
(defun org-latex-preview-live--record-hook (exit-code _buf extended-info)
"A hook for `org-latex-preview-process-finish-functions' to track preview time.
Called with EXIT-CODE and EXTENDED-INFO from the async process."
(when (= exit-code 0)
(org-latex-preview-live--update-times
(- (float-time) (plist-get extended-info :start-time)))))
(defconst org-latex-preview-live-display-type 'buffer
"How to display live-updating previews of LaTeX snippets.
This option is meaningful when live previews are enabled, by
setting `org-latex-preview-auto-generate' to `live' and enabling
`org-latex-preview-auto-mode'.
The only currently supported option is the symbol buffer, to
display live previews next to or under the LaTeX fragment in the
Org buffer.")
(defun org-latex-preview-live--debounce (func duration)
"Return a debounced FUNC with DURATION applied."
(let ((debounce-timer))
(lambda (&rest args)
(if (timerp debounce-timer)
(timer-set-time debounce-timer (+ (float-time) duration))
(setq debounce-timer
(run-at-time
duration nil
(lambda ()
(cancel-timer debounce-timer)
(setq debounce-timer nil)
(apply func args))))))))
(defun org-latex-preview-live--throttle (func)
"Return a throttled FUNC.
Ensures that FUNC runs at the end of the throttle duration."
(let ((waiting))
(lambda (&rest args)
(unless waiting
(apply func args)
(setq waiting t)
(run-at-time
org-latex-preview-live-throttle nil
(lambda ()
(setq waiting nil)
(apply func args)))))))
(defun org-latex-preview-live--clearout (ov)
"Clear out the live LaTeX preview for the preview overlay OV."
(setq org-latex-preview-live--element-type nil)
(overlay-put ov 'after-string nil))
(defun org-latex-preview-live--regenerate (&rest _)
"Regenerate the LaTeX preview overlay that overlaps BEG and END.
This is meant to be run via the `after-change-functions' hook in
Org buffers when using live-updating LaTeX previews."
(pcase-let ((`(,type . ,ov)
(get-char-property-and-overlay (point) 'org-overlay-type)))
(when (and ov (eq type 'org-latex-overlay)
;; The following checks are redundant and can make
;; throttling inconsistent:
;; (<= (overlay-start ov) beg)
;; (>= (overlay-end ov) end)
(overlay-get ov 'preview-state))
(org-latex-preview-auto--regenerate-overlay ov t)
(unless (and (overlay-buffer ov) (overlay-get ov 'preview-image))
(org-latex-preview-live--clearout ov)))))
(defun org-latex-preview-live--update-props (image-spec &optional box-face)
"Update the live preview string with the IMAGE-SPEC display property.
BOX-FACE is the face to apply in addition."
(let ((l (length org-latex-preview-live--docstring)))
(put-text-property
(1- l) l 'display image-spec
org-latex-preview-live--docstring)
(when box-face
(put-text-property
(1- l) l 'face box-face
org-latex-preview-live--docstring))))
(defun org-latex-preview-live--ensure-overlay (&optional ov)
"Set up a live preview for the LaTeX fragment with overlay OV."
(when-let*
((ov (or ov
(let ((props (get-char-property-and-overlay (point) 'org-overlay-type)))
(and (eq (car props) 'org-latex-overlay)
(cdr props)))))
(image (overlay-get ov 'preview-image))
(end (overlay-end ov)))
(let ((latex-env-p
(progn
(unless org-latex-preview-live--element-type
(let* ((elm (org-element-context)))
;; Treat \[ ... \] as a latex-environment for the
;; purposes of live-previews.
(setq org-latex-preview-live--element-type
(or (and (string-prefix-p
"\\[" (org-element-property :value elm))
'latex-environment)
(org-element-type elm)))))
(eq org-latex-preview-live--element-type
'latex-environment))))
(when (or (eq org-latex-preview-live t)
(memq (if latex-env-p 'block 'inline) org-latex-preview-live))
(when (eq org-latex-preview-live-display-type 'buffer)
(unless (overlay-get ov 'after-string)
;; NOTE: The latex-env-specific string includes a zero
;; width space char. This is to force the box around the
;; image to render correctly.
(setq org-latex-preview-live--docstring
(concat (and latex-env-p "\n") " "))
(overlay-put ov 'view-text t)
(overlay-put ov 'after-string org-latex-preview-live--docstring)))
(org-latex-preview-live--update-props image '(:box t))))))
(defun org-latex-preview-live--update-overlay (ov)
"Update the live LaTeX preview for overlay OV."
(when (and (memq ov (overlays-at (point)))
(overlay-get ov 'view-text))
(if (or (overlay-get ov 'after-string)
(eq org-latex-preview-live-display-type
'eldoc))
(org-latex-preview-live--update-props
(overlay-get ov 'preview-image))
(org-latex-preview-live--ensure-overlay ov))))
;; Code for previews in org-src buffers
(defun org-latex-preview-live--src-buffer-setup ()
"Set up the org-src buffer for live LaTeX previews.
When `org-latex-preview-auto-mode' is turned on, and
`org-latex-preview-live' is either t or containes
\"edit-special\", live LaTeX previews are generated when editing
a LaTeX fragment or environment using `org-edit-special'.
If the source Org buffer is visible, these previews are displayed
over the original fragment. Otherwise previews are displayed in
the org-src buffer.
This is meant to be called via `org-src-mode-hook'."
(when (and (equal major-mode (org-src-get-lang-mode "latex"))
(buffer-local-value 'org-latex-preview-auto-mode
(marker-buffer org-src--beg-marker))
(let ((live (buffer-local-value 'org-latex-preview-live
(marker-buffer org-src--beg-marker))))
(or (eq live t) (memq 'edit-special live))))
(let* ((org-buf (marker-buffer org-src--beg-marker))
(src-buf (current-buffer))
(org-buf-visible-p (window-live-p (get-buffer-window org-buf)))
;; Do not use (org-element-property :begin element) to
;; find the bounds -- this is fragile under typos.
(beg (save-excursion (goto-char (point-min))
(skip-chars-forward "\n \t\r")
(point)))
(end (save-excursion (goto-char (point-max))
(skip-chars-backward "\n \t\r")
(point)))
preamble element skip-env-p numbering-offsets ov orig-ov)
(setq org-latex-preview-auto--marker (point-marker))
;; Copy the LaTeX preview overlay from the source Org buffer
;; into the org-src buffer and show a preview image over the
;; former:
(with-current-buffer org-buf
(setq element (org-element-context))
(when (eq (org-element-type element) 'latex-environment)
(setq skip-env-p
(and (save-excursion
(goto-char (or (org-element-property :post-affiliated element)
(org-element-property :begin element)))
(looking-at "\\\\begin{\\([^}]+\\)}"))
(member (match-string 1) org-latex-preview-auto-ignored-environments))))
(when (and (not skip-env-p)
(setq orig-ov
(let ((props (get-char-property-and-overlay
(point) 'org-overlay-type)))
(and (eq (car props) 'org-latex-overlay)
(cdr props)))))
(org-latex-preview-live--clearout orig-ov)
(setq ov (copy-overlay orig-ov)
preamble (or org-latex-preview--preamble-content
(setq org-latex-preview--preamble-content
(org-latex-preview--get-preamble))))
(overlay-put ov 'view-text t)
(move-overlay ov beg end src-buf))
(org-latex-preview-auto--close-previous-overlay))
(unless skip-env-p
(or ov (setq ov (org-latex-preview--ensure-overlay beg end)))
;; Adjust numbering if required
(when (and org-latex-preview-numbered
(eq (org-element-type element) 'latex-environment))
(with-current-buffer org-buf
(when-let ((numbering-table (org-latex-preview--environment-numbering-table)))
(setq numbering-offsets (list (gethash element numbering-table))))))
(when (buffer-local-value 'org-latex-preview-live org-buf)
(if org-buf-visible-p
;; Source Org buffer is visible: display live previews
;; over the fragment there
(progn
(setq-local org-latex-preview-live--generator
(thread-first
(lambda (&rest _)
(when (eq (current-buffer) src-buf)
(let* ((content
(string-trim (buffer-substring-no-properties
(point-min) (point-max)))))
(with-current-buffer org-buf
(org-latex-preview-place
org-latex-preview-process-default
(list (list (overlay-start orig-ov)
(overlay-end orig-ov)
content))
numbering-offsets)))))
(org-latex-preview-live--throttle)
(org-latex-preview-live--debounce
org-latex-preview-live-debounce)))
(add-hook 'after-change-functions org-latex-preview-live--generator 90 'local))
;; Source Org buffer not visible: display live previews in org-src buffer
;; Set up hooks for live preview updates in the org-src buffer
(let* ((element-type
(with-current-buffer org-buf
(or (and (string-prefix-p
"\\[" (org-element-property :value element))
'latex-environment)
(org-element-type element))))
(preview-clearout-func
(lambda (ov)
(org-latex-preview-live--clearout ov)
(setq org-latex-preview-live--element-type element-type))))
;; Set the element type ahead of time since we cannot call
;; org-element-context in the org-src buffer
(setq org-latex-preview-live--element-type element-type)
(add-hook 'org-latex-preview-overlay-close-functions
preview-clearout-func nil 'local))
(add-hook 'org-latex-preview-overlay-open-functions
#'org-latex-preview-live--ensure-overlay nil 'local)
(add-hook 'org-latex-preview-overlay-update-functions
#'org-latex-preview-live--update-overlay nil 'local)
(setq-local org-latex-preview-live--generator
(thread-first
(lambda (&rest _)
(when (eq (current-buffer) src-buf)
(org-latex-preview-place
org-latex-preview-process-default
(list (list (save-excursion (goto-char (point-min))
(skip-chars-forward "\n \t\r")
(point))
(save-excursion (goto-char (point-max))
(skip-chars-backward "\n \t\r")
(point))))
numbering-offsets preamble)))
(org-latex-preview-live--throttle)
(org-latex-preview-live--debounce
org-latex-preview-live-debounce)))
(add-hook 'after-change-functions org-latex-preview-live--generator 90 'local)
;; Show live preview if available
(org-latex-preview-live--ensure-overlay ov)))
;; Turn on auto-mode behavior in the org-src buffer
(add-hook 'pre-command-hook #'org-latex-preview-auto--handle-pre-cursor nil 'local)
(add-hook 'post-command-hook #'org-latex-preview-auto--handle-post-cursor nil 'local)))))
;; Eldoc support for live previews
(defun org-latex-preview-live--display-in-eldoc (callback)
"Eldoc documentation function for live LaTeX previews.
CALLBACK is supplied by Eldoc, see
`eldoc-documentation-functions'."
(when (and org-latex-preview-live--docstring
(get-char-property (point) 'org-overlay-type))
(funcall callback org-latex-preview-live--docstring)))
(defun org-latex-preview-live--update-eldoc (_ov)
"Force eldoc to update when a preview is updated."
(eldoc--invoke-strategy nil))
;; Live preview setup and teardown.
(defun org-latex-preview-live--setup ()
"Set up hooks for live LaTeX previews.
See `org-latex-preview-live' for details."
(setq org-latex-preview-live--docstring " ")
(setq-local org-latex-preview-live--generator
(thread-first #'org-latex-preview-live--regenerate
(org-latex-preview-live--throttle)
(org-latex-preview-live--debounce
org-latex-preview-live-debounce)))
(when (eq org-latex-preview-live-display-type 'eldoc)
(add-hook 'eldoc-documentation-functions #'org-latex-preview-live--display-in-eldoc nil t)
(add-hook 'org-latex-preview-overlay-update-functions #'org-latex-preview-live--update-eldoc nil 'local))
(add-hook 'org-src-mode-hook #'org-latex-preview-live--src-buffer-setup)
(add-hook 'org-latex-preview-overlay-close-functions #'org-latex-preview-live--clearout nil 'local)
(add-hook 'org-latex-preview-overlay-open-functions #'org-latex-preview-live--ensure-overlay nil 'local)
(add-hook 'after-change-functions org-latex-preview-live--generator 90 'local)
(add-hook 'org-latex-preview-process-finish-functions #'org-latex-preview-live--record-hook nil 'local)
(add-hook 'org-latex-preview-overlay-update-functions #'org-latex-preview-live--update-overlay nil 'local))
(defun org-latex-preview-live--teardown ()
"Remove hooks for live LaTeX previews.
See `org-latex-preview-live' for details."
(when-let* ((props (get-char-property-and-overlay (point) 'org-overlay-type))
((eq (car props) 'org-latex-overlay))
(ov (cdr props)))
(org-latex-preview-live--clearout ov))
(when (eq org-latex-preview-live-display-type 'eldoc)
(remove-hook 'eldoc-documentation-functions #'org-latex-preview-live--display-in-eldoc t)
(remove-hook 'org-latex-preview-overlay-update-functions #'org-latex-preview-live--update-eldoc 'local))
(remove-hook 'org-latex-preview-overlay-close-functions #'org-latex-preview-live--clearout 'local)
(remove-hook 'org-latex-preview-overlay-open-functions #'org-latex-preview-live--ensure-overlay 'local)
(remove-hook 'after-change-functions org-latex-preview-live--generator 'local)
(remove-hook 'org-latex-preview-overlay-update-functions #'org-latex-preview-live--update-overlay 'local)
(remove-hook 'org-latex-preview-process-finish-functions #'org-latex-preview-live--record-hook 'local)
(setq-local org-latex-preview-live--generator nil))
(defun org-latex-preview-clear-overlays (&optional beg end)
"Remove all overlays with LaTeX fragment images in current buffer.
When optional arguments BEG and END are non-nil, remove all
overlays between them instead. Return a non-nil value when some
overlays were removed, nil otherwise."
(let ((overlays
(cl-remove-if-not
(lambda (o) (eq (overlay-get o 'org-overlay-type) 'org-latex-overlay))
(overlays-in (or beg (point-min)) (or end (point-max))))))
(mapc #'delete-overlay overlays)
overlays))
(defun org-latex-preview--preview-region (beg end)
"Preview LaTeX fragments between BEG and END.
BEG and END are buffer positions."
(org-latex-preview-fragments
org-latex-preview-process-default
beg end))
;;;###autoload
(defun org-latex-preview (&optional mode)
"Generate or hide LaTeX fragment previews.
The particular behaviour depends on MODE (which recognises a
number of symbols and prefix arguments), or when MODE is nil the
context of the current buffer when called.
- point: Toggle preview of the LaTeX fragment at point
- region: Display previews of all fragments in the selected region
- section: Display previews of all fragments in the current section
- buffer: Display previews of all fragments in the buffer
- clear-region: Clear all previews in the current region
- clear-section: Clear all previews in the current section
- clear-buffer: Clear all previews in the buffer
When MODE is nil and the cursor is on a LaTeX fragment, toggle
previewing of the LaTeX fragment at point. If the there is no
preview, generate a preview and overlay it on the text. Remove
the preview image otherwise. If there is no fragment at point,
display previews for all fragments in the current section, or
active region should it exist.
Prefix arguments are handled as follows:
- `\\[universal-argument]' is equivalent to setting MODE to clear-region (if a region
is currently active) or clear-section
- `\\[universal-argument] \\[universal-argument]' is equivalent to setting MODE to buffer
- `\\[universal-argument] \\[universal-argument] \\[universal-argument]' is equivalent to setting MODE to clear-buffer
MODE can also be a org-element LaTeX environment or fragment, which
will be treated as \"point\"."
(interactive "P")
(when (display-graphic-p)
(when (integerp (car-safe mode)) ; Prefix argument
(setq mode
(pcase (car mode)
(64 'clear-buffer)
(16 'buffer)
(4 (if (use-region-p) 'clear-region 'clear-section))
(_ (and (use-region-p) 'region)))))
(unless mode ; Auto, i.e. element at point or section
(setq mode (if-let ((datum (org-element-context))
((memq (org-element-type datum) '(latex-environment latex-fragment))))
datum 'section)))
(pcase mode
('buffer
(org-latex-preview--preview-region (point-min) (point-max)))
('clear-buffer
(org-latex-preview-clear-overlays (point-min) (point-max))
(message "LaTeX previews removed from buffer"))
('section
(let ((beg (if (org-before-first-heading-p) (point-min)
(save-excursion
(org-with-limited-levels (org-back-to-heading t) (point)))))
(end (org-with-limited-levels (org-entry-end-position))))
(org-latex-preview--preview-region beg end)))
('clear-section
(org-latex-preview-clear-overlays
(if (org-before-first-heading-p) (point-min)
(save-excursion
(org-with-limited-levels (org-back-to-heading t) (point))))
(org-with-limited-levels (org-entry-end-position))))
('region
(org-latex-preview--preview-region (region-beginning) (region-end)))
('clear-region
(org-latex-preview-clear-overlays (region-beginning) (region-end)))
('point
(when-let ((datum (org-element-context))
((memq (org-element-type datum) '(latex-environment latex-fragment))))
(org-latex-preview--auto-aware-toggle datum)))
((guard (memq (org-element-type mode) '(latex-environment latex-fragment)))
(org-latex-preview--auto-aware-toggle mode))
(bad-value (error "Invalid `org-latex-preview' mode argument: %S" bad-value)))))
(defun org-latex-preview--auto-aware-toggle (datum)
"Toggle the preview of the LaTeX fragment/environment DATUM.
This is done with care to work nicely with `org-latex-preview-auto-mode',
should it be enabled."
(let* ((beg (org-element-property :begin datum))
(end (org-element-property :end datum))
(ov (cl-some
(lambda (o)
(and (eq (overlay-get o 'org-overlay-type)
'org-latex-overlay)
o))
(overlays-at beg))))
;; If using auto-mode, an overlay will already exist but
;; not be showing an image. We can detect this
;; situtation via the preview-state overlay property, and
;; in such cases the most reasonable action is to just
;; (re)generate the preview image.
(cond
;; When not using auto-mode.
((not org-latex-preview-auto-mode)
(if (org-latex-preview-clear-overlays beg end)
(message "LaTeX preview removed")
(org-latex-preview--place-from-elements
org-latex-preview-process-default (list datum))))
;; When using auto-mode, but no current preview.
((not ov)
(org-latex-preview--place-from-elements
org-latex-preview-process-default (list datum))
(message "Creating LaTeX preview"))
;; When on a just written/edited fragment that should be previewed.
((eq (overlay-get ov 'preview-state) 'modified)
(org-latex-preview-auto--regenerate-overlay ov)
(overlay-put ov 'view-text t))
;; When on an unmodified fragment that is currently showing an image,
;; clear the image.
((overlay-get ov 'display)
(org-latex-preview-clear-overlays beg end)
(message "LaTeX preview removed"))
;; Since we're on an unmodified fragment but not showing an image,
;; let's try to show the image if possible.
(ov
(overlay-put ov 'view-text t)
(unless (overlay-get ov 'after-string) ;Live preview being shown
(overlay-put ov 'face (overlay-get ov 'hidden-face))
(overlay-put ov 'display (overlay-get ov 'preview-image)))))))
(defun org-latex-preview-collect-fragments (&optional beg end)
"Collect all LaTeX maths fragments/environments between BEG and END."
(let (fragments)
(save-excursion
(goto-char (or beg (point-min)))
(while (re-search-forward org-latex-preview--tentative-math-re end t)
(let ((obj (org-element-context)))
(when (and (memq (org-element-type obj)
'(latex-fragment latex-environment))
;; Avoid duplicating nested latex environments
(not (and fragments
(= (org-element-property :begin obj)
(org-element-property :begin (car fragments))))))
(push obj fragments)))))
(nreverse fragments)))
(defun org-latex-preview-fragments (processing-type &optional beg end)
"Produce image overlays of LaTeX math fragments between BEG and END.
The LaTeX fragments are processed using PROCESSING-TYPE, a key of
`org-latex-preview-process-alist'.
If `point' is currently on an LaTeX overlay, then no overlays
will be generated. Since in practice `org-latex-preview-clear-overlays'
should have been called immediately prior to this function, this
situation should not occur in practice and mainly acts as
protection against placing doubled up overlays."
(when (fboundp 'clear-image-cache)
(clear-image-cache))
;; Optimize overlay creation: (info "(elisp) Managing Overlays").
(when (memq processing-type '(dvipng dvisvgm imagemagick))
(overlay-recenter (or end (point-max))))
(unless (eq (get-char-property (point) 'org-overlay-type)
'org-latex-overlay)
(let ((ws (window-start)))
(if (assq processing-type org-latex-preview-process-alist)
(org-latex-preview--place-from-elements
processing-type
(nconc (org-latex-preview-collect-fragments (max ws beg) end)
(when (< beg ws)
(org-latex-preview-collect-fragments beg (1- ws)))))
(error "Unknown conversion process %s for previewing LaTeX fragments"
processing-type)))))
(defun org-latex-preview--construct-entries
(elements &optional construct-numbering-p parse-tree)
"Constuct a well formatted list of entries and (optinally) numbering offsets.
This operates by processing ELEMENTS. When CONSTRUCT-NUMBERING-P is non-nil,
the number offsets will also be calculated, using PARSE-TREE if given."
(let ((numbering-table (and construct-numbering-p
(cl-find 'latex-environment elements
:key #'org-element-type :test #'eq)
(org-latex-preview--environment-numbering-table
parse-tree)))
entries numbering-offsets)
(dolist (element elements)
(let ((beg (or (org-element-property :post-affiliated element)
(org-element-property :begin element)))
(end (- (org-element-property :end element)
(or (org-element-property :post-blank element) 0)
(if (eq (char-before (org-element-property :end element))
?\n)
1 0)))
(content (org-element-property :value element)))
(push (list beg end content) entries)
(when numbering-table
(push (and (eq (org-element-type element) 'latex-environment)
(gethash element numbering-table))
numbering-offsets))))
(list (nreverse entries) (nreverse numbering-offsets))))
(defun org-latex-preview--empty-fragment-p (content)
"Test if the LaTeX string CONTENT is an empty LaTeX fragment (e.g. \\[\\])."
(let ((content-point 0)
(content-max (1- (length content)))
(only-blanks t))
(cond
((eq (aref content 0) ?$)
(if (eq (aref content 1) ?$)
(setq content-point 2 content-max (- content-max 2))
(setq content-point 1 content-max (- content-max 1))))
((eq (aref content 0) ?\\)
(setq content-point 2 content-max (- content-max 2))))
(while (and only-blanks (<= content-point content-max))
(if (memq (aref content content-point) '(?\s ?\t ?\n ?\r))
(cl-incf content-point)
(setq only-blanks nil)))
only-blanks))
(defun org-latex-preview--place-from-elements (processing-type elements)
"Preview LaTeX math fragments ELEMENTS using PROCESSING-TYPE."
(apply #'org-latex-preview-place processing-type
(org-latex-preview--construct-entries
elements org-latex-preview-numbered)))
;;;###autoload
(defun org-latex-preview-place (processing-type entries &optional numbering-offsets latex-preamble)
"Preview LaTeX math fragments ENTRIES using PROCESSING-TYPE.
Each entry of ENTRIES should be a list of 2-3 items, either
(BEG END), or
(BEG END VALUE)
Where BEG and END are the positions in the buffer, and the LaTeX previewed
is either the substring between BEG and END or (when provided) VALUE."
(unless latex-preamble
(setq latex-preamble
(or org-latex-preview--preamble-content
(setq org-latex-preview--preamble-content
(org-latex-preview--get-preamble)))))
(let* ((processing-info
(cdr (assq processing-type org-latex-preview-process-alist)))
(imagetype (or (plist-get processing-info :image-output-type) "png"))
(numbering-offsets (cons nil numbering-offsets))
fragment-info prev-fg prev-bg)
(save-excursion
(dolist (entry entries)
(pcase-let* ((`(,beg ,end ,provided-value) entry)
(value (or provided-value
(buffer-substring-no-properties beg end)))
(`(,fg ,bg) (org-latex-preview--colors-around beg end))
(number (car (setq numbering-offsets (cdr numbering-offsets))))
(hash (org-latex-preview--hash
processing-type latex-preamble value imagetype fg bg number))
(options (org-combine-plists
org-latex-preview-appearance-options
(list :foreground fg
:background bg
:number number
:continue-color
(and (equal prev-bg bg)
(equal prev-fg fg)))
(and (eq processing-type 'dvisvgm)
(list :foreground
org-latex-preview--svg-fg-standin)))))
(if-let ((path-info (org-latex-preview--get-cached hash)))
(org-latex-preview--update-overlay
(org-latex-preview--ensure-overlay beg end)
path-info)
(unless (org-latex-preview--empty-fragment-p value)
(push (list :string (org-latex-preview--tex-styled
processing-type value options)
:overlay (org-latex-preview--ensure-overlay beg end)
:key hash)
fragment-info)))
(setq prev-fg fg prev-bg bg))))
(when fragment-info
(org-latex-preview--create-image-async
processing-type
(nreverse fragment-info)
:latex-preamble latex-preamble
:appearance-options org-latex-preview-appearance-options
:place-preview-p t))))
(defun org-latex-preview--colors-around (start end &optional options)
"Find colors for LaTeX previews occuping the region START to END.
OPTIONS is a plist containing foreground/background specifications."
(let* ((face (org-latex-preview--face-around start end))
(options (or options org-latex-preview-appearance-options))
(fg (pcase (plist-get options :foreground)
('auto
(org-latex-preview--resolved-faces-attr face :foreground))
('default (face-attribute 'default :foreground nil))
(color color)))
(bg (pcase (plist-get options :background)
('auto
(org-latex-preview--resolved-faces-attr face :background))
('default (face-attribute 'default :background nil))
(color color))))
(list fg bg)))
(defun org-latex-preview--resolved-faces-attr (face attr)
"Find ATTR of the FACE text property.
This is surprisingly complicated given the various forms of output
\\=(get-text-property pos \\='face) can produce.
Faces in `org-latex-preview--ignored-faces' are ignored."
(when (consp face)
(setq face (cl-set-difference face org-latex-preview--ignored-faces))
(when (= (length face) 1)
(setq face (car face))))
(cond
((not face)
(face-attribute 'default attr))
((not (consp face)) ; Spec like org-level-1.
(face-attribute face attr nil 'default))
((keywordp (car face)) ; Spec like (:inherit org-block :extend t).
(or (plist-get face attr)
(face-attribute 'default attr)))
((consp (car face)) ; Spec like ((:inherit default :extend t) org-block).
(or (plist-get (car face) attr)
(face-attribute (cadr face) attr nil
(append (cddr face) '(default)))))
((symbolp (car face)) ; Spec like (org-level-1 default).
(face-attribute (car face) attr nil (append (cdr face) '(default))))))
(defun org-latex-preview--hash (processing-type preamble string imagetype fg bg &optional number)
"Return a SHA1 hash for referencing LaTeX fragments when previewing them.
PROCESSING-TYPE is the type of process used to create the
preview, see `org-latex-preview-process-default'.
PREAMBLE is the LaTeX preamble used in the generated LaTeX document.
STRING is the string to be hashed, typically the contents of a
LaTeX fragment.
IMAGETYPE is the type of image to be created, see
`org-latex-preview-process-alist'.
FG and BG are the foreground and background colors for the
image.
NUMBER is the equation number that should be used, if applicable."
(sha1 (prin1-to-string
(list processing-type
preamble
string
(if (equal imagetype "svg")
'svg
(list (plist-get
org-latex-preview-appearance-options :scale)
fg))
bg
number))))
(defconst org-latex-preview--numbered-environments-single
'("equation" "math" "displaymath" ; latex.ltx
"dmath" ; breqn.sty
"empheq") ; empheq.sty
"List of LaTeX environments that contain a single numbered equation.")
(defconst org-latex-preview--numbered-environments-multi
'("eqnarray" ; latex.ltx
"align" "alignat" "flalign" "gather" "multiline" ; amsmath.sty
"xalignat" "xxalignat" "subequations" ; amsmath.sty
"dseries" "dgroup" "darray") ; breqn.sty
"List of LaTeX environments that contain multiple numbered equations.")
(defconst org-latex-preview--numbered-environments-all
(append org-latex-preview--numbered-environments-single
org-latex-preview--numbered-environments-multi)
"List of LaTeX environments which produce numbered equations.")
(defun org-latex-preview--environment-numbering-table (&optional parse-tree)
"Creat a hash table from numbered equations to their initial index.
If the org-element cache is active or PARSE-TREE is provided, the
hash table will use `eq' equality, otherwise `equal' will be
used. When PARSE-TREE is provided, it is passed onto
`org-latex-preview--get-numbered-environments'."
(let ((table (make-hash-table
:test (if (or parse-tree (org-element--cache-active-p))
#'eq #'equal)))
(counter 1))
(save-match-data
(dolist (element (org-latex-preview--get-numbered-environments
nil nil parse-tree))
(puthash element counter table)
(cl-incf counter (org-latex-preview--count-numbered-equations element))))
table))
(defvar org-latex-preview--numbering-count-buffer nil)
(defun org-latex-preview--count-numbered-equations (element)
"Count the number of numbered equations within Org ELEMENT.
Note: this function changes the current match data."
(or (org-element-cache-get-key element :latex-numbered-equation-count)
(let ((count 0)
environment-name)
(with-current-buffer (org-element-property :buffer element)
(save-excursion
(goto-char (org-element-property :begin element))
(when (looking-at "\\\\begin{\\([^}]+\\)}")
(setq environment-name (match-string 1)))
(cond
((member environment-name org-latex-preview--numbered-environments-single)
(unless (or (search-forward "\\nonumber" (org-element-property :end element) t)
(search-forward "\\tag{" (org-element-property :end element) t))
(cl-incf count 1)))
((member environment-name org-latex-preview--numbered-environments-multi)
(cl-incf count (org-latex-preview--count-numbered-equations-multi
element environment-name))))))
(org-element-cache-store-key element :latex-numbered-equation-count count)
count)))
(defun org-latex-preview--count-numbered-equations-multi (element environment-name)
"Count the number of numbered equations within Org ELEMENT.
Element is assumed to be a LaTeX ENVIRONMENT-NAME environment.
Note: this function changes the current match data."
(let ((temp-buffer (or (and (buffer-live-p org-latex-preview--numbering-count-buffer)
org-latex-preview--numbering-count-buffer)
(setq org-latex-preview--numbering-count-buffer
(get-buffer-create " *Org LaTeX preview numbering calculation*"))))
(count-instances
(lambda (needle)
(let ((count 0))
(save-excursion
(while (search-forward needle nil t)
(cl-incf count))
count))))
(count 0))
(with-current-buffer temp-buffer
(erase-buffer)
(insert-buffer-substring
(org-element-property :buffer element)
(org-element-property :begin element) (org-element-property :end element))
(goto-char (point-min))
(org-skip-whitespace)
;; Remove the "outer" environment, so we can then
;; easily delete all inner environments.
(delete-region (point-min) (+ (point) 8 (length environment-name))) ; 8 = (length "\\begin{}")
(goto-char (point-max))
(when (search-backward (format "\\end{%s}" environment-name) nil t)
(delete-region (match-beginning 0) (match-end 0)))
;; Remove all "inner" environments. We do this since
;; they can contain false-positive indicators of numbered
;; equations.
(goto-char (point-min))
(while (re-search-forward "\\\\begin{\\([^}]+\\)}" nil t)
(let* ((start (match-beginning 0))
(max-end (point-max))
(env-name (match-string 1))
(begin-template (format "\\begin{%s}" env-name))
(end-template (format "\\end{%s}" env-name))
(env-depth 1))
(while (> env-depth 0)
(let ((next-begin (or (save-excursion (search-forward begin-template nil t)) max-end))
(next-end (or (save-excursion (search-forward end-template nil t)) max-end)))
(cond
((= next-begin next-end)
(setq env-depth 0))
((< next-end next-begin)
(cl-decf env-depth))
(t (cl-incf env-depth)))
(goto-char (min next-begin next-end))))
(delete-region start (match-end 0))))
;; Count all hard newlines, as they indicate the start of a new equation,
;; then decrement by the number of \nonumber and \tag-d instances.
(goto-char (point-min))
(cl-incf count (1+ (funcall count-instances "\\\\")))
(cl-decf count (funcall count-instances "\\nonumber"))
(cl-decf count (funcall count-instances "\\tag{"))
(erase-buffer))
count))
(defun org-latex-preview--get-numbered-environments (&optional beg end parse-tree)
"Find all numbered environments between BEG and END.
If PARSE-TREE is provided, it will be used insead of
`org-element-cache-map' or `org-element-parse-buffer'."
(cond
(parse-tree
(org-element-map
parse-tree
'(latex-environment)
(lambda (datum)
(and (<= (or beg (point-min)) (org-element-property :begin datum)
(org-element-property :end datum) (or end (point-max)))
(let* ((content (org-element-property :value datum))
(env (and (string-match "\\`\\\\begin{\\([^}]+\\)}" content)
(match-string 1 content))))
(and (member env org-latex-preview--numbered-environments-all)
datum))))))
((org-element--cache-active-p)
(org-element-cache-map
(lambda (datum)
(and (<= (or beg (point-min)) (org-element-property :begin datum)
(org-element-property :end datum) (or end (point-max)))
(let* ((content (org-element-property :value datum))
(env (and (string-match "\\`\\\\begin{\\([^}]+\\)}" content)
(match-string 1 content))))
(and (member env org-latex-preview--numbered-environments-all)
datum))))
:granularity 'element
:restrict-elements '(latex-environment)
:from-pos beg
:to-pos (or end (point-max-marker))))
(t
(org-element-map
(org-element-parse-buffer 'element)
'(latex-environment)
(lambda (datum)
(and (<= (or beg (point-min)) (org-element-property :begin datum)
(org-element-property :end datum) (or end (point-max)))
(let* ((content (org-element-property :value datum))
(env (and (string-match "\\`\\\\begin{\\([^}]+\\)}" content)
(match-string 1 content))))
(and (member env org-latex-preview--numbered-environments-all)
(save-excursion
(goto-char (org-element-property :begin datum))
(org-element-context))))))))))
(cl-defun org-latex-preview-cache-images
(parse-tree &optional export-info
&key preamble processing-type foreground
background page-width scale image-dir
&allow-other-keys)
"Create preview images for LaTeX fragments in PARSE-TREE synchronously.
Returns a hash table. Each key is a LaTeX fragment or
environment in PARSE-TREE, and correspdonding value is a list
containing image information. This list has the format
(path . image-info).
For example:
(\"/path/.../to/image.svg\"
:type svg
:height 1.4
:width 7.5
:depth 0.2
:errors nil)
The geometry information here is in em units.
EXPORT-INFO is a plist holding export options.
PREAMBLE is the LaTeX preamble to use for preview generation. If
nil, a preamble will be consructed from the contents of the
current buffer if it is in Org mode.
PROCESSING-TYPE is a symbol specifying a external command used to
generate the images. For supported image converters see
`org-latex-preview-process-default'.
The keyword arguments FOREGROUND, BACKGROUND, PAGE-WIDTH and
SCALE are as in `org-latex-preview-appearance-options' and will
default to its values, which see.
IMAGE-DIR, if non-nil, is a directory to contain the preview
images. It will be created if necessary. If IMAGE-DIR is nil,
image are cached as per `org-latex-preview-cache', which see."
(let* ((preamble
(or preamble
org-latex-preview--preamble-content
(setq org-latex-preview--preamble-content
(org-latex-preview--get-preamble))))
(appearance-options
(org-combine-plists
org-latex-preview-appearance-options
(nconc (and foreground (list :foreground foreground))
(and background (list :background background))
(and page-width (list :page-width page-width))
(and scale (list :scale scale)))))
(elements
(org-element-map parse-tree
'(latex-fragment latex-environment)
#'identity export-info))
(entries-and-numbering
(org-latex-preview--construct-entries
elements t parse-tree))
(processing-type (or processing-type
(plist-get export-info :with-latex)
org-latex-preview-process-default))
(processing-info
(cdr (assq processing-type org-latex-preview-process-alist)))
(imagetype (or (plist-get processing-info :image-output-type) "png"))
(numbering-offsets (cons nil (cadr entries-and-numbering)))
(element-preview-hash-table (make-hash-table :test #'eq :size (length elements)))
fragment-info prev-fg prev-bg)
;; Create fragment info for the preview process
(cl-loop
for entry in (car entries-and-numbering)
for element in elements
for (beg end provided-value) = entry
for value = (or provided-value (buffer-substring-no-properties beg end))
for (fg bg) = (org-latex-preview--colors-around beg end appearance-options)
for number = (car (setq numbering-offsets (cdr numbering-offsets)))
for hash = (org-latex-preview--hash processing-type preamble value imagetype fg bg number)
for options = (org-combine-plists
appearance-options
(list :foreground fg :background bg
:number number
:continue-color
(and (equal prev-bg bg)
(equal prev-fg fg))))
do
(puthash element hash element-preview-hash-table)
(unless (org-latex-preview--get-cached hash)
(push (list :string (org-latex-preview--tex-styled
processing-type value options)
:overlay (org-latex-preview--ensure-overlay beg end)
:key hash)
fragment-info))
(setq prev-fg fg prev-bg bg))
;; Generate fragment previews
(when fragment-info
(apply #'org-async-wait-for
(org-latex-preview--create-image-async
processing-type
(nreverse fragment-info)
:latex-preamble preamble
:appearance-options appearance-options)))
(when (and image-dir (not (file-directory-p image-dir)))
(make-directory image-dir 'parents)
(setq image-dir (expand-file-name image-dir)))
;; Fragments generated, update the hash table
(cl-loop for element in elements
for hash = (gethash element element-preview-hash-table)
for (source-file . image-info) = (org-latex-preview--get-cached hash)
if (not (and source-file (file-exists-p source-file)))
do (display-warning
'(org-latex-preview get-cache)
(format "No image generated for fragment:\n%s"
(org-element-property :value element)))
else do
(if image-dir
(let ((image-path (file-name-with-extension
(file-name-concat image-dir (substring hash 0 11))
(file-name-extension source-file))))
(copy-file source-file image-path 'replace)
(puthash element (cons image-path image-info) element-preview-hash-table))
(puthash element (cons source-file image-info) element-preview-hash-table))
finally return element-preview-hash-table)))
(cl-defun org-latex-preview--create-image-async (processing-type fragments-info &key latex-processor latex-preamble appearance-options place-preview-p)
"Preview PREVIEW-STRINGS asynchronously with method PROCESSING-TYPE.
FRAGMENTS-INFO is a list of plists, each of which provides
information on an individual fragment and should have the
following structure:
(:string fragment-string :overlay fragment-overlay :key fragment-hash)
where
- fragment-string is the literal content of the fragment
- fragment-overlay is the overlay placed for the fragment
- fragment-hash is a string that uniquely identifies the fragment
It is worth noting the FRAGMENTS-INFO plists will be modified
during processing to hold more information on the fragments.
When PLACE-PREVIEW-P is true, it will be set in the extended info
plist passed to filters, and is expected to result in the newly
generated fragment image being placed in the buffer.
LATEX-PROCESSOR is a member of `org-latex-compilers' which is guessed if unset.
When provided, LATEX-PREAMBLE overrides the default LaTeX preamble.
Returns a list of async tasks started."
(let* ((processing-type
(or processing-type org-latex-preview-process-default))
(latex-processor
(or latex-processor
(and (derived-mode-p 'org-mode)
(cdr (assoc "LATEX_COMPILER"
(org-collect-keywords
'("LATEX_COMPILER") '("LATEX_COMPILER")))))
org-latex-compiler))
(processing-info
(nconc (list :latex-processor latex-processor
:latex-header latex-preamble)
(alist-get processing-type org-latex-preview-process-alist)))
(programs (plist-get processing-info :programs))
(error-message (or (plist-get processing-info :message) "")))
;; xelatex produces .xdv (eXtended dvi) files, not .dvi, so as a special
;; case we check for xelatex + dvi and if so switch the file extension to xdv.
(when (and (equal latex-processor "xelatex")
(equal (plist-get processing-info :image-input-type) "dvi"))
(setq processing-info
(plist-put (copy-sequence processing-info) :image-input-type "xdv")))
(dolist (program programs)
(org-check-external-command program error-message))
(when org-latex-preview-process-active-indicator
(dolist (fragment fragments-info)
(org-latex-preview--indicate-processing
(plist-get fragment :overlay) 'on)))
;; At this point we will basically construct a tree of async calls:
;;
;; dvisvgm case:
;; └─ Compile tex file ⟶ stdout to `org-latex-preview--latex-preview-filter'
;; └─ (success or failure)
;; └─Extact images ⟶ stdout to `org-latex-preview--dvisvgm-filter'
;; ├─ (success)
;; │ ├─ Call `org-latex-preview--check-all-fragments-produced',
;; │ │ which can rerun the async tree if needed.
;; │ └─ Delete tempfiles (`org-latex-preview--cleanup-callback').
;; └─ (failure)
;; └─ Run `org-latex-preview--failure-callback' (remove overlays and emit msg).
;;
;; dvipng case:
;; ├─ Compile tex file ⟶ stdout to `org-latex-preview--latex-preview-filter'
;; └─ Extract images ("--follow" tex ouput) ⟶ stdout to `org-latex-preview--dvipng-filter'
;; ├─ (success)
;; │ ├─ Call `org-latex-preview--check-all-fragments-produced',
;; │ │ which can rerun the async tree if needed.
;; │ └─ Delete tempfiles (`org-latex-preview--cleanup-callback')
;; └─ (failure)
;; └─ Run `org-latex-preview--failure-callback' (remove overlays and emit msg).
;;
;; generic case:
;; └─ Compile tex file ⟶ stdout to `org-latex-preview--latex-preview-filter'
;; └─ (success or failure)
;; └─Extact images
;; ├─ (success)
;; │ ├─ Call `org-latex-preview--generic-callback'.
;; │ ├─ Delete tempfiles (`org-latex-preview--cleanup-callback')
;; │ └─ Call `org-latex-preview--check-all-fragments-produced',
;; │ which can rerun the async tree if needed.
;; └─ (failure)
;; └─ Run `org-latex-preview--failure-callback' (remove overlays and emit msg).
;;
;; With continuous, synchronous processing:
;;
;; ⟶ stdout to `org-latex-preview--latex-preview-filter'
;; ├─ read preview fontsize
;; ├─ capture compilation errors
;; └─ (conditionally) update overlays in buffer with png images and metadata
;;
;; ⟶ stdout to `org-latex-preview--dvisvgm-filter'
;; ├─ read preview image metadata
;; ├─ edit svgs and adjust colors
;; ├─ cache svgs with org-persist or in /tmp
;; └─ update overlays in buffer with svg images and metadata
;;
;; ⟶ stdout to `org-latex-preview--dvipng-filter'
;; ├─ read preview image metadata
;; ├─ cache pngs with org-persist or in /tmp
;; └─ update overlays in buffer with png images and metadata
;;
(let* ((extended-info
(append processing-info
(list :processor processing-type
:fragments fragments-info
:org-buffer (current-buffer)
:texfile (org-latex-preview--create-tex-file
processing-info fragments-info appearance-options)
:appearance-options appearance-options
:place-preview-p place-preview-p
:start-time (float-time))))
(tex-compile-async
(org-latex-preview--tex-compile-async extended-info))
(img-extract-async
(org-latex-preview--image-extract-async extended-info)))
(plist-put (cddr img-extract-async) :success
(list ; The order is important here.
#'org-latex-preview--check-all-fragments-produced
#'org-latex-preview--cleanup-callback))
(plist-put (cddr img-extract-async) :failure
(list
#'org-latex-preview--failure-callback
#'org-latex-preview--cleanup-callback))
(when org-latex-preview-process-finish-functions
;; Extra callbacks to run after image generation
(push #'org-latex-preview--run-finish-functions
(plist-get (cddr img-extract-async) :success))
(push #'org-latex-preview--run-finish-functions
(plist-get (cddr img-extract-async) :failure)))
(pcase processing-type
('dvipng
(plist-put (cddr img-extract-async) :filter
#'org-latex-preview--dvipng-filter))
('dvisvgm
(plist-put (cddr img-extract-async) :filter
#'org-latex-preview--dvisvgm-filter))
(_
(plist-put (cddr img-extract-async) :success
(list ; The order is important here.
#'org-latex-preview--generic-callback
#'org-latex-preview--cleanup-callback
#'org-latex-preview--check-all-fragments-produced))))
(if (and (eq processing-type 'dvipng)
(member "--follow" (cadr img-extract-async)))
(list (org-async-call tex-compile-async)
(org-async-call img-extract-async))
(plist-put (cddr tex-compile-async) :success img-extract-async)
(plist-put (cddr tex-compile-async) :failure img-extract-async)
(list (org-async-call tex-compile-async))))))
(defun org-latex-preview--run-finish-functions (&rest args)
"Run hooks after preview image generation, with arguments ARGS."
(apply #'run-hook-with-args
'org-latex-preview-process-finish-functions
args))
(defun org-latex-preview--failure-callback (exit-code _buf extended-info)
"Clear overlays corresponding to previews that failed to generate with EXIT-CODE.
EXTENDED-INFO contains the information needed to identify such
previews."
(message "Creating LaTeX preview images failed (exit code %d). Please see %s for details"
exit-code
(if (pcase (plist-get extended-info :processor)
('dvisvgm (eq exit-code 252)) ; Input file does not exist.
('dvipng ; Same check, just a bit more involved.
(and (eq exit-code 1)
(with-current-buffer
(save-excursion
(goto-char (point-min))
(search-forward ": No such file or directory" nil t))))))
(propertize org-latex-preview--latex-log 'face 'warning)
(propertize org-latex-preview--image-log 'face 'warning)))
(with-current-buffer (plist-get extended-info :org-buffer)
(cl-loop for fragment in (plist-get extended-info :fragments)
for path = (plist-get fragment :path)
when (not path)
for ov = (plist-get fragment :overlay)
when ov do
;; ;TODO: Other options here include:
;; ;Fringe marker
;; (overlay-put ov 'before-string
;; (propertize "!" 'display
;; `(left-fringe exclamation-mark
;; warning)))
;; ;Special face
;; (unless (overlay-get ov 'face)
;; (overlay-put ov 'face 'org-latex-preview-processing-face))
;;
;; ;Note: ov has buffer extended-info, no need to set current-buffer
(let ((start (overlay-start ov))
(end (overlay-end ov)))
(delete-overlay ov)
(when org-latex-preview-auto-mode
(org-latex-preview--ensure-overlay start end))))))
(defvar-local org-latex-preview--preamble-content nil
"Cache of the LaTeX preamble for snippet preview.")
(defun org-latex-preview--clear-preamble-cache ()
"Set `org-latex-preview--preamble-content' to nil."
(setq org-latex-preview--preamble-content nil))
(add-hook 'org-mode-hook #'org-latex-preview--clear-preamble-cache)
(defconst org-latex-preview--single-eqn-format
"\n\\makeatletter
\\renewcommand{\\theequation}{\\(\\diamond\\)\\ifnum\\value{equation}>1%
\\,+\\,\\@arabic{\\numexpr\\value{equation}-1\\relax}\\fi}
\\makeatother"
"A LaTeX preamble snippet that sets \"\"-based equation numbers.")
(defun org-latex-preview--get-preamble (&optional buf)
"Obtain the LaTeX preview for snippet preview in BUF."
(with-current-buffer (or buf (current-buffer))
(org-fold-core-ignore-modifications
(let ((org-inhibit-startup t)
(info (org-combine-plists
(org-export--get-export-attributes
(org-export-get-backend 'latex))
(org-export--get-buffer-attributes)
'(:time-stamp-file nil)))
org-export-use-babel
org-latex-precompile
;; (org-latex-conditional-features
;; (cl-remove-if
;; (lambda (feat)
;; (plist-get (alist-get (cdr feat)
;; org-latex-feature-implementations)
;; :not-preview))
;; org-latex-conditional-features))
)
(org-export-with-buffer-copy
:drop-narrowing t
(font-lock-mode -1)
(setq info
(org-export--annotate-info (org-export-get-backend 'latex) info))
(concat
(org-latex-make-preamble
(org-combine-plists
(org-export-get-environment
(org-export-get-backend 'latex))
'(:time-stamp-file nil))
org-latex-preview-preamble 'snippet)
(and (not org-latex-preview-numbered)
org-latex-preview--single-eqn-format)))))))
(defconst org-latex-preview--include-preview-string
"\n\\usepackage[active,tightpage,auctex,dvips]{preview}\n"
"A LaTeX preamble snippet that includes preview.sty for previews.
The options passed to preview.sty are:
- active: activate preview package
- auctex: Produce fake error messages at the start and end of
every preview environment. We use this to help with parsing
the tex compilation stdout using regexes.
- tightpage: (effectively) shrink page sizes to fit each
previewed math environment.
- dvips: Only required for xelatex runs. Add (as a side effect)
preview geometry data to the output XDV file.
This is an abridged summary. See the documentation of
preview.sty for more details.")
(defun org-latex-preview--create-tex-file (processing-info fragments appearance-options)
"Create a LaTeX file based on PROCESSING-INFO and FRAGMENTS.
More specifically, a preamble will be generated based on
PROCESSING-INFO. Then, if `org-latex-preview-process-precompiled' is
non-nil, a precompiled format file will be generated if needed
and used. Otherwise the preamble is used normally.
Within the body of the created LaTeX file, each of
FRAGMENTS will be placed in order, wrapped within a
\"preview\" environment.
The path of the created LaTeX file is returned."
(let* ((header
(concat
(plist-get processing-info :latex-header)
org-latex-preview--include-preview-string))
(textwidth
;; We can fetch width info from APPEARANCE-OPTIONS, but it's
;; possible that an old config using `org-format-latex-options'
;; won't have :page-width set, and so we need a default too.
(let ((w (or (plist-get appearance-options :page-width) 0.6)))
(cond
((stringp w)
(format "\n\\setlength{\\textwidth}{%s}\n" w))
((and (floatp w) (<= 0.0 w 1.0))
(format "\n\\setlength{\\textwidth}{%s\\paperwidth}\n" w)))))
(relative-file-p
(string-match-p "\\(?:\\\\input{\\|\\\\include{\\)[^/]" header))
(remote-file-p (file-remote-p default-directory))
(tex-temp-name
(expand-file-name
(concat (make-temp-name "org-tex-") ".tex")
(and remote-file-p temporary-file-directory)))
(write-region-inhibit-fsync t)
(coding-system-for-write buffer-file-coding-system)
(precompile-failed-msg))
(when (and relative-file-p remote-file-p)
(error "Org LaTeX Preview does not currently support \\input/\\include in remote files"))
(when org-latex-preview-process-precompiled
(pcase (plist-get processing-info :latex-processor)
("pdflatex"
(if-let ((format-file (org-latex-preview--precompile processing-info header
(not relative-file-p))))
(setq header (concat "%& " (file-name-sans-extension format-file)))
(setq precompile-failed-msg
"Precompile failed.")))
((or "xelatex" "lualatex")
(setq precompile-failed-msg
(concat
(plist-get processing-info :latex-processor)
" does not support precompilation."))))
(when precompile-failed-msg
(display-warning
'(org latex-preview disable-local-precompile)
(concat
precompile-failed-msg
" Disabling LaTeX preview precompile in this buffer.\n To re-enable, run `(setq-local org-latex-preview-process-precompiled t)' or reopen this buffer."))
(setq-local org-latex-preview-process-precompiled nil)))
(with-temp-file tex-temp-name
(insert header)
;; The \abovedisplayskip length must be set after \begin{document} because
;; it is usually set during the font size intialisation that occurs at
;; \begin{document}. We can either modify the \normalsize command to set
;; the \abovedisplayskip length, or just set it after \begin{document}.
(insert textwidth
"\n\\begin{document}\n\n"
"\\setlength\\abovedisplayskip{0pt}"
" % Remove padding before equation environments.\n\n")
(dolist (fragment-info fragments)
(insert
"\n\\begin{preview}\n"
(plist-get fragment-info :string)
"\n\\end{preview}\n"))
(insert "\n\\end{document}\n"))
tex-temp-name))
(defun org-latex-preview--tex-compile-async (extended-info)
"Create an `org-async-call' spec to compile the texfile in EXTENDED-INFO."
(let* ((tex-process-buffer
(with-current-buffer
(get-buffer-create org-latex-preview--latex-log)
(erase-buffer)
(current-buffer)))
(tex-compile-command-fmt
(pcase (plist-get extended-info :latex-compiler)
((and (pred stringp) cmd) cmd)
((and (pred consp) cmds)
(when (> (length cmds) 1)
(warn "Preview :latex-compiler must now be a single command. %S will be ignored."
(cdr cmds)))
(car cmds))))
(texfile (plist-get extended-info :texfile))
(org-tex-compiler
(cdr (assoc (plist-get extended-info :latex-processor)
org-latex-preview-compiler-command-map)))
(tex-command-spec
`((?o . ,(shell-quote-argument temporary-file-directory))
(?b . ,(shell-quote-argument (file-name-base texfile)))
(?f . ,(shell-quote-argument texfile))
(?l . ,org-tex-compiler)
(?L . ,(car (split-string org-tex-compiler)))))
(tex-formatted-command
(split-string-shell-command
(format-spec tex-compile-command-fmt tex-command-spec))))
(unless org-tex-compiler
(user-error "No `org-latex-preview-compiler-command-map' entry found for LaTeX processor %S, it should be a member of `org-latex-compilers' %S"
(plist-get extended-info :latex-processor)
org-latex-compilers))
(with-current-buffer tex-process-buffer
(erase-buffer)
(insert "RUNNING: "
(format-spec tex-compile-command-fmt tex-command-spec)
"\n")
(add-text-properties (point-min) (1- (point))
'(face ((:height 0.8) font-lock-comment-face header-line))))
(list 'org-async-task
tex-formatted-command
:buffer tex-process-buffer
:info extended-info
:filter #'org-latex-preview--latex-preview-filter
:failure
(lambda (exit-code _buf _info)
;; With how preview.sty works, an exit code of 1 is expectd.
(unless (eq exit-code 1)
(message "LaTeX compilation for preview failed (error code %d). Please see %s for details"
exit-code
(propertize org-latex-preview--latex-log
'face 'warning)))))))
(defun org-latex-preview--image-extract-async (extended-info)
"Create an `org-async-call' spec to extract images according to EXTENDED-INFO."
(let* ((img-process-buffer
(with-current-buffer
(get-buffer-create org-latex-preview--image-log)
(erase-buffer)
(current-buffer)))
(appearance-options (plist-get extended-info :appearance-options))
(img-extract-command
(pcase
(or (and (string= (plist-get appearance-options :background)
"Transparent")
(plist-get extended-info :transparent-image-converter))
(plist-get extended-info :image-converter))
((and (pred stringp) cmd) cmd)
((and (pred consp) cmds)
(when (> (length cmds) 1)
(warn "Preview converter must now be a single command. %S will be ignored."
(cdr cmds)))
(car cmds))))
(dpi (* 1.4 ; This factor makes it so generated PNGs are not blury
; at the displayed resulution.
(or (plist-get appearance-options :scale) 1.0)
(if (display-graphic-p)
(org-latex-preview--get-display-dpi)
140.0)))
(texfile (plist-get extended-info :texfile))
(texfile-base (file-name-base texfile))
(img-command-spec
`((?o . ,(shell-quote-argument temporary-file-directory))
(?b . ,(shell-quote-argument (file-name-base texfile)))
(?B . ,(shell-quote-argument
(expand-file-name texfile-base temporary-file-directory)))
(?D . ,(shell-quote-argument (format "%s" dpi)))
(?f . ,(shell-quote-argument
(expand-file-name
(concat texfile-base
"." (plist-get extended-info :image-input-type))
temporary-file-directory)))))
(img-formatted-command
(split-string-shell-command
(format-spec img-extract-command img-command-spec))))
(with-current-buffer org-latex-preview--image-log
(erase-buffer)
(insert "RUNNING: "
(format-spec img-extract-command img-command-spec)
"\n")
(add-text-properties (point-min) (1- (point))
'(face ((:height 0.8) font-lock-comment-face header-line))))
(list 'org-async-task
img-formatted-command
:buffer img-process-buffer
:info extended-info
:failure
(format "Creating LaTeX preview images failed (exit code %%d). Please see %s for details"
(propertize org-latex-preview--image-log 'face 'warning)))))
(defun org-latex-preview--cleanup-callback (_exit-code _stdout extended-info)
"Schedule cleanup with EXTENDED-INFO."
(run-with-idle-timer
1.0 nil
#'org-latex-preview--do-cleanup
extended-info))
(defun org-latex-preview--do-cleanup (extended-info)
"Delete files after image creation, in accord with EXTENDED-INFO."
(let* ((texfile (plist-get extended-info :texfile))
(outputs-no-ext (expand-file-name (file-name-base texfile)
temporary-file-directory))
(images
(mapcar
(lambda (fragment-info)
(plist-get fragment-info :path))
(plist-get extended-info :fragments)))
(clean-exts
(or (plist-get extended-info :post-clean)
'(".dvi" ".xdv" ".pdf" ".tex" ".aux" ".log"
".svg" ".png" ".jpg" ".jpeg" ".out"))))
(when (file-exists-p texfile) (delete-file texfile))
(dolist (img images)
(and img (delete-file img)))
(dolist (ext clean-exts)
(when (file-exists-p (concat outputs-no-ext ext))
(delete-file (concat outputs-no-ext ext))))))
(defun org-latex-preview--generic-callback (_exit-code _stdout extended-info)
"Place generated images, in accord with EXTENDED-INFO."
(let* ((texfile (plist-get extended-info :texfile))
(outputs-no-ext (expand-file-name (file-name-base texfile)
temporary-file-directory))
(images
(file-expand-wildcards
(concat outputs-no-ext "*." (plist-get extended-info :image-output-type))
'full)))
(save-excursion
(cl-loop
for fragment-info in (plist-get extended-info :fragments)
for image-file in images
for ov = (plist-get fragment-info :overlay)
for cached-img =
(org-latex-preview--cache-image
(plist-get fragment-info :key)
image-file
(org-latex-preview--display-info
extended-info fragment-info))
do
(plist-put fragment-info :path image-file)
(when (plist-get extended-info :place-preview-p)
(org-latex-preview--update-overlay ov cached-img))))))
(defun org-latex-preview--check-all-fragments-produced (_exit-code _stdout extended-info)
"Check each of the fragments in EXTENDED-INFO has a path.
Should this not be the case, the fragment immediately before the first
fragment without a path is marked as erronious, and the remaining
fragments are regenerated."
(let ((fragments (cons nil (copy-sequence (plist-get extended-info :fragments)))))
(while (cdr fragments)
(if (or (plist-get (cadr fragments) :path)
(plist-get (cadr fragments) :error))
(setq fragments (cdr fragments))
;; If output ends prematurely, this is most likely due to an issue with
;; the last "succesfully" produced fragment, and so we mark it as erronious
;; and attempt to re-generate the rest.
(when-let ((bad-fragment (car fragments)))
(plist-put bad-fragment :errors
(concat (when-let ((current-error (plist-get bad-fragment :errors)))
(concat current-error "\n\n"))
"Preview generation catastrophically failed after this fragment."))
(org-latex-preview--remove-cached
(plist-get bad-fragment :key))
(if (plist-get extended-info :place-preview-p)
(org-latex-preview--update-overlay
(plist-get bad-fragment :overlay)
(org-latex-preview--cache-image
(plist-get bad-fragment :key)
(plist-get bad-fragment :path)
(org-latex-preview--display-info
extended-info bad-fragment)))
(org-latex-preview--cache-image
(plist-get bad-fragment :key)
(plist-get bad-fragment :path)
(org-latex-preview--display-info
extended-info bad-fragment))))
;; Re-generate the remaining fragments.
(org-latex-preview--create-image-async
(plist-get extended-info :processor)
(cdr fragments)
:latex-preamble (plist-get extended-info :latex-header)
:place-preview-p (plist-get extended-info :place-preview-p))
(setq fragments nil)))))
(defun org-latex-preview--display-info (extended-info fragment-info)
"From FRAGMENT-INFO and EXTENDED-INFO obtain display-relevant information."
(let ((image-type (intern (plist-get extended-info :image-output-type)))
info)
(setq info (plist-put info :image-type image-type))
(dolist (key '(:width :height :depth))
(when-let ((val (plist-get fragment-info key)))
(plist-put info key val)))
(plist-put info :errors (plist-get fragment-info :errors))
info))
;; TODO: Figure out why this factor is needed.
(defconst org-latex-preview--shameful-magic-tex-scaling-factor
1.01659593
"Extra factor for aligning preview image baselines.
Sometimes a little sprinkling of pixie dust is needed to get
things just right. Even just 2.7% magic can suffice.
This is the ratio of image sizes as reported by preview.sty and
computed by dvisvgm. The latter is correct.")
(defconst org-latex-preview--tex-scale-divisor
(* 65781.76 org-latex-preview--shameful-magic-tex-scaling-factor)
"Base pt to point conversion for preview.sty output.
This is the product of three scaling quantities:
- Point to scaled point ratio: 1:65536
- Base point to scaled point ratio: 72:72.27
- The magic scaling factor
(see `org-latex-preview--shameful-magic-tex-scaling-factor').")
(defun org-latex-preview--latex-preview-filter (_proc _string extended-info)
"Examine the stdout from LaTeX compilation with preview.sty.
- The detected fontsize is directly entered into EXTENDED-INFO.
- The tightpage bounds information is captured and stored in EXTENDED-INFO.
- Fragment geometry and alignment info is computed using the
tightpage info and page geometry reported by preview.sty.
- Fragment errors are put into the :errors slot of the relevant
fragments in EXTENDED-INFO."
(unless (plist-get extended-info :fontsize)
(when (save-excursion
(re-search-forward "^Preview: Fontsize \\([0-9]+\\)pt$" nil t))
(plist-put extended-info :fontsize (string-to-number (match-string 1)))
;; Since at this point can infer that the preamble logging is complete,
;; we can also check for hyperref and warn if it seems to be used,
;; as it is currently known to cause issues.
(save-excursion
(goto-char (point-min))
(when (if (and org-latex-preview-process-precompiled
(re-search-forward "^PRELOADED FILES:" nil t))
(re-search-forward "^ *hyperref\\.sty" nil t)
(re-search-forward "^(.*hyperref/hyperref\\.sty" nil t))
(display-warning
'(org latex-preview hyperref)
"Hyperref seems to be loaded, this is known to cause issues with the reported size information"
:warning)))))
(let ((preview-start-re
"^! Preview: Snippet \\([0-9]+\\) started.\n<-><->\n *\nl\\.\\([0-9]+\\)[^\n]+\n")
(preview-end-re
"\\(?:^Preview: Tightpage.*$\\)?\n! Preview: Snippet [0-9]+ ended.(\\([0-9]+\\)\\+\\([0-9]+\\)x\\([0-9]+\\))")
(fragments (plist-get extended-info :fragments))
(tightpage-info (plist-get extended-info :tightpage))
(concurrentp (eq (plist-get extended-info :processor) 'dvipng))
preview-marks fragments-to-show)
(beginning-of-line)
(save-excursion
(while (re-search-forward preview-start-re nil t)
(push (list (match-beginning 0)
(match-end 0)
(string-to-number (match-string 1)) ; Preview index.
(1+ (string-to-number (match-string 2)))) ; Base line number.
preview-marks)))
(setq preview-marks (nreverse preview-marks))
(while preview-marks
(goto-char (caar preview-marks))
(unless tightpage-info
(save-excursion
(when (re-search-forward
"^Preview: Tightpage \\(-?[0-9]+\\) *\\(-?[0-9]+\\) *\\(-?[0-9]+\\) *\\(-?[0-9]+\\)"
(or (caadr preview-marks) (point-max)) t)
(setq tightpage-info
(mapcar #'string-to-number
;; left-margin bottom-margin
;; right-margin top-margin
(list (match-string 1) (match-string 2)
(match-string 3) (match-string 4))))
(plist-put extended-info :tightpage tightpage-info))))
;; Check for processed fragment
(if (re-search-forward preview-end-re (or (caadr preview-marks) (point-max)) t)
(let ((fragment-info (nth (1- (nth 2 (car preview-marks))) fragments))
(errors-substring
(save-match-data
(string-trim
(buffer-substring (cadar preview-marks)
(match-beginning 0));; In certain situations we can end up with non-error
;; logging informattion within the preview output.
;; To make sure this is not captured, we rely on the fact
;; that LaTeX error messages have a consistent format
;; and start with an exclamation mark "!". Thus, we
;; can safely strip everything prior to the first "!"
;; from the output.
"[^!]*")))
depth)
;; Gather geometry and alignment info
(if tightpage-info
(progn
(setq depth
(/ (- (string-to-number (match-string 2))
(nth 1 tightpage-info))
org-latex-preview--tex-scale-divisor
(or (plist-get fragment-info :fontsize) 10)))
(plist-put fragment-info :depth depth)
(plist-put fragment-info :height
(+ (or depth 0)
(/ (+ (string-to-number (match-string 1))
(nth 3 tightpage-info))
org-latex-preview--tex-scale-divisor
(or (plist-get fragment-info :fontsize) 10))))
(plist-put fragment-info :width
(/ (+ (string-to-number (match-string 3))
(nth 2 tightpage-info)
(- (nth 1 tightpage-info)))
org-latex-preview--tex-scale-divisor
(or (plist-get fragment-info :fontsize) 10))))
(cl-loop for (geom . match-index)
in '((:height . 1) (:depth . 2) (:width . 3))
do
(plist-put fragment-info geom
(/ (string-to-number (match-string match-index))
org-latex-preview--tex-scale-divisor
(or (plist-get fragment-info :fontsize) 10)))))
(plist-put fragment-info :errors
(and (not (string-blank-p errors-substring))
(replace-regexp-in-string
"^l\\.[0-9]+"
(lambda (linum)
(format "l.%d"
(- (string-to-number (substring linum 2))
(nth 3 (car preview-marks)))))
errors-substring)))
(when (and concurrentp (plist-get fragment-info :path))
;; path has been recorded by dvipng filter, can display image
(push fragment-info fragments-to-show)))
(goto-char (caar preview-marks)))
(setq preview-marks (cdr preview-marks)))
(when fragments-to-show
(org-latex-preview--place-images
extended-info (nreverse fragments-to-show)))))
(defun org-latex-preview--dvisvgm-filter (_proc _string extended-info)
"Look for newly created images in the dvisvgm stdout buffer.
Any matches found will be matched against the fragments recorded in
EXTENDED-INFO, and displayed in the buffer."
(let ((dvisvgm-processing-re "^processing page \\([0-9]+\\)[\n:]")
(fragments (plist-get extended-info :fragments))
page-marks fragments-to-show)
(beginning-of-line)
(save-excursion
(while (re-search-forward dvisvgm-processing-re nil t)
(push (cons (string-to-number (match-string 1))
(match-beginning 0))
page-marks)))
(setq page-marks (nreverse page-marks))
(while page-marks
(let ((start (cdar page-marks))
(end (or (cdadr page-marks) (point-max)))
(page (caar page-marks))
fragment-info)
(goto-char start)
(when (save-excursion
(re-search-forward "output written to \\(.*.svg\\)$" end t))
(setq fragment-info (nth (1- page) fragments))
(plist-put fragment-info :path (expand-file-name (match-string 1) temporary-file-directory))
(when (save-excursion
(re-search-forward "^ page is empty" end t))
(unless (plist-get fragment-info :error)
(plist-put fragment-info :error "Image file not produced."))
(plist-put fragment-info :path nil))
(push fragment-info fragments-to-show)
(goto-char end)))
(setq page-marks (cdr page-marks)))
(when fragments-to-show
(setq fragments-to-show (nreverse fragments-to-show))
(if (>= org-latex-preview--dvisvgm3-minor-version 1)
(mapc #'org-latex-preview--await-fragment-existance fragments-to-show)
(mapc #'org-latex-preview--svg-make-fg-currentColor fragments-to-show))
(org-latex-preview--place-images extended-info fragments-to-show))))
(defun org-latex-preview--await-fragment-existance (svg-fragment)
"Wait until SVG-FRAGMENT's file exists (up to 1s).
This is needed to accomidate for asyncronicity in file writing."
(let ((path (plist-get svg-fragment :path)))
(unless (or (not path) (file-exists-p path))
(catch 'svg-exists
(dotimes (_ 1000) ; Check for svg existance over 1s.
(when (file-exists-p path)
(throw 'svg-exists t))
(sleep-for 0.001))))))
(defun org-latex-preview--svg-make-fg-currentColor (svg-fragment)
"Replace the foreground color in SVG-FRAGMENT's file with \"currentColor\".
The foreground color is guessed to be the first specified <g>
fill color, which appears to be a reliable heuristic from a few
tests with the output of dvisvgm."
(let ((write-region-inhibit-fsync t)
;; dvisvgm produces UTF-8 encoded files, so we might as well
;; avoid calling `find-auto-coding'.
(coding-system-for-read 'utf-8)
(coding-system-for-write 'utf-8)
;; Prevent any file handlers (specifically
;; `image-file-handler') from being called.
(file-name-handler-alist nil)
(path (plist-get svg-fragment :path)))
(org-latex-preview--await-fragment-existance svg-fragment)
(when path
(catch 'svg-exists
(dotimes (_ 1000) ; Check for svg existance over 1s.
(when (file-exists-p path)
(throw 'svg-exists t))
(sleep-for 0.001)))
(with-temp-buffer
(insert-file-contents path)
(unless ; When the svg is incomplete, wait for it to be completed.
(string= (buffer-substring (max 1 (- (point-max) 6)) (point-max))
"</svg>")
(catch 'svg-complete
(dotimes (_ 1000) ; Check for complete svg over 1s.
(if (string= (buffer-substring (max 1 (- (point-max) 6)) (point-max))
"</svg>")
(throw 'svg-complete t)
(erase-buffer)
(sleep-for 0.001)
(insert-file-contents path)))
(erase-buffer)))
(goto-char (point-min))
(if (or (= (buffer-size) 0)
(re-search-forward "<svg[^>]*>\n<g[^>]*>\n</svg>" nil t))
;; We never want to show an empty SVG, instead it is better to delete
;; it and leave the LaTeX fragment without an image overlay.
;; This also works better with other parts of the system, such as
;; the display of errors.
(delete-file path)
(while (search-forward org-latex-preview--svg-fg-standin nil t)
(replace-match "currentColor" t t))
(write-region nil nil path nil 0))))))
(defun org-latex-preview--dvipng-filter (_proc _string extended-info)
"Look for newly created images in the dvipng stdout buffer.
Any matches found will be matched against the fragments recorded in
EXTENDED-INFO, and displayed in the buffer."
(let ((outputs-no-ext (expand-file-name
(file-name-base
(plist-get extended-info :texfile))
temporary-file-directory))
(fragments (plist-get extended-info :fragments))
fragments-to-show page-info-end)
(while (search-forward "]" nil t)
(setq page-info-end (point))
(save-excursion
(backward-list)
(if (re-search-forward "\\=\\[\\([0-9]+\\) " page-info-end t)
(let* ((page (string-to-number (match-string 1)))
(fragment-info (nth (1- page) fragments)))
(plist-put fragment-info :path
(format "%s-%09d.png" outputs-no-ext page))
(when (plist-get fragment-info :height)
;; geometry has been recorded by latex filter, can display image
(push fragment-info fragments-to-show))))))
(when fragments-to-show
(org-latex-preview--place-images
extended-info (nreverse fragments-to-show)))))
(defun org-latex-preview--place-images (extended-info &optional fragments)
"Cache and place images for FRAGMENTS, according to their data and EXTENDED-INFO.
Should FRAGMENTS not be explicitly provided, all of the fragments
listed in EXTENDED-INFO will be used.
If this is an export run, images will only be cached, not placed."
(let ((fragments (or fragments (plist-get extended-info :fragments))))
(if (plist-get extended-info :place-preview-p)
(when (buffer-live-p (plist-get extended-info :org-buffer))
(with-current-buffer (plist-get extended-info :org-buffer)
(save-excursion
(cl-loop
for fragment-info in fragments
for image-file = (plist-get fragment-info :path)
for ov = (plist-get fragment-info :overlay)
when (overlay-buffer ov)
do (org-latex-preview--update-overlay
ov
(org-latex-preview--cache-image
(plist-get fragment-info :key)
image-file
(org-latex-preview--display-info
extended-info fragment-info)))))))
(dolist (fragment-info fragments)
(org-latex-preview--cache-image
(plist-get fragment-info :key)
(plist-get fragment-info :path)
(org-latex-preview--display-info
extended-info fragment-info))))))
(defconst org-latex-preview--cache-name "LaTeX preview cached image data"
"The name used for Org LaTeX Preview objects in the org-persist cache.")
(defvar org-latex-preview--table nil
"Hash table to hold LaTeX preview image metadata.
This is only used for non-persist image caching, used when
`org-latex-preview-cache' is not set to persist.")
(defun org-latex-preview--cache-image (key path info)
"Save the image at PATH with associated INFO in the cache indexed by KEY.
Return (path . info).
The caching location is set by CACHE, which defaults to
`org-latex-preview-cache'. It should be the symbol \"persist\",
\"temp\", or an existing directory path as a string."
(if (not path)
(ignore
(display-warning
'(org latex-preview put-cache)
(format "Tried to cache %S without a path, skipping. This should not happen, please report it as a bug to the Org mailing list (M-x org-submit-bug-report)." key)
:warning))
(pcase org-latex-preview-cache
('persist
(let ((label-path-info
(or (org-persist-read org-latex-preview--cache-name
(list :key key)
nil nil :read-related t)
(org-persist-register `(,org-latex-preview--cache-name
(file ,path)
(elisp-data ,info))
(list :key key)
:expiry org-latex-preview-persist-expiry
:write-immediately t))))
(cons (cadr label-path-info) info)))
((and dir (or 'temp (pred stringp)))
(unless org-latex-preview--table
(setq org-latex-preview--table (make-hash-table :test 'equal :size 240)))
(when-let ((path)
(new-path (expand-file-name
(concat "org-tex-" key "." (file-name-extension path))
(if (eq dir 'temp) temporary-file-directory dir))))
(copy-file path new-path 'replace)
(puthash key (cons new-path info)
org-latex-preview--table)))
(bad (error "Invalid cache location: %S (must be persist, temp, or a string)" bad)))))
(defun org-latex-preview--get-cached (key)
"Retrieve the image path and info associated with KEY.
The result will be of the form (path . info).
Example result:
(\"/path/.../to/.../image.svg\"
:type svg
:height 1.4
:width 7.6
:depth 0.2
:errors nil)"
(cond
((eq org-latex-preview-cache 'persist)
(when-let ((label-path-info
(org-persist-read org-latex-preview--cache-name
(list :key key)
nil nil :read-related t)))
;; While /in theory/ this check isn't needed, sometimes the
;; org-persist cache can be modified outside the current Emacs
;; process. When this occurs the metadata of the fragment can
;; still exist in `org-persist--index', but the image file is
;; gone. This condition can be detected by checking if the
;; `cadr' is nil (indicating the image has gone AWOL).
(if (cadr label-path-info)
(cons (cadr label-path-info)
(caddr label-path-info))
(org-latex-preview--remove-cached key)
nil)))
(org-latex-preview--table
(gethash key org-latex-preview--table))))
(defun org-latex-preview--remove-cached (key)
"Remove the fragment cache associated with KEY."
(cond
((eq org-latex-preview-cache 'persist)
(org-persist-unregister org-latex-preview--cache-name
(list :key key)
:remove-related t))
(org-latex-preview--table
(remhash key org-latex-preview--table)
(dolist (ext '("svg" "png"))
(when-let ((image-file
(expand-file-name
(concat "org-tex-" key "." ext)
temporary-file-directory))
((file-exists-p image-file)))
(delete-file image-file))))))
(defun org-latex-preview-clear-cache (&optional beg end clear-entire-cache)
"Clear LaTeX preview cache for fragments between BEG and END.
Interactively, act on
- the region if it is active,
- the fragment at point if in a fragment,
- the whole buffer otherwise.
When CLEAR-ENTIRE-CACHE is non-nil (interactively set by \\[universal-argument]),
the *entire* preview cache will be cleared, and `org-persist-gc' run."
(interactive
(if current-prefix-arg
(list nil nil (y-or-n-p "This will clear the systemwide LaTeX preview cache, continue? "))
(let ((context (if (derived-mode-p 'org-mode)
(org-element-context)
(user-error "This command must be run in an org-mode buffer"))))
(cond
((use-region-p)
(list (region-beginning) (region-end)))
((memq (org-element-type context)
'(latex-fragment latex-environment))
(list (org-element-property :begin context)
(org-element-property :end context)))
(t (list nil nil))))))
;; Clear the precompile cache if clearing the whole buffer or everything.
(when (or clear-entire-cache (not (or beg end)))
(or org-latex-preview--preamble-content
(setq org-latex-preview--preamble-content
(org-latex-preview--get-preamble)))
(let ((full-preamble
(concat org-latex-preview--preamble-content
org-latex-preview--include-preview-string)))
(dolist (compiler org-latex-compilers)
(org-latex--remove-cached-preamble compiler full-preamble nil)
(org-latex--remove-cached-preamble compiler full-preamble t))))
(org-latex-preview-clear-overlays beg end)
(if clear-entire-cache
(let ((n 0))
(dolist (collection org-persist--index)
(when (equal (cadar (plist-get collection :container))
org-latex-preview--cache-name)
(org-latex-preview--remove-cached
(plist-get (plist-get collection :associated) :key))
(cl-incf n)))
(if (= n 0)
(message "The Org LaTeX preview cache was already empty.")
(org-persist-gc)
(message "Cleared all %d entries fom the Org LaTeX preview cache." n)))
(let ((imagetype
(or (plist-get (alist-get org-latex-preview-process-default
org-latex-preview-process-alist)
:image-output-type)
"png"))
(numbering-table
(and org-latex-preview-numbered
(org-latex-preview--environment-numbering-table))))
(dolist (element (org-latex-preview-collect-fragments beg end))
(pcase-let* ((begin (or (org-element-property :post-affiliated element)
(org-element-property :begin element)))
(end (- (org-element-property :end element)
(or (org-element-property :post-blank element) 0)
(if (eq (char-before (org-element-property :end element))
?\n)
1 0)))
(`(,fg ,bg) (org-latex-preview--colors-around begin end))
(value (org-element-property :value element))
(number (and numbering-table
(eq (org-element-type element)
'latex-environment)
(gethash element numbering-table))))
(org-latex-preview--remove-cached
(org-latex-preview--hash
org-latex-preview-process-default
org-latex-preview--preamble-content
value imagetype fg bg number))))
(message "Cleared LaTeX preview cache for %s."
(if (or beg end) "region" "buffer"))))
(when (or clear-entire-cache (not (or beg end)))
(org-latex-preview--clear-preamble-cache)))
(defun org-latex-preview--precompile (processing-info preamble &optional tempfile-p)
"Precompile/dump LaTeX PREAMBLE text.
The path to the format file (.fmt) is returned. If the format
file could not be found in the persist cache, it is generated
according to PROCESSING-INFO and stored.
If TEMPFILE-P is non-nil, then it is assumed the preamble does
not contain any relative references to other files.
This is intended to speed up Org's LaTeX preview generation
process."
(org-latex--precompile
(list :latex-compiler (plist-get processing-info :latex-processor)
:precompile-format-spec
(let ((org-tex-compiler
(cdr (assoc (plist-get processing-info :latex-processor)
org-latex-preview-compiler-command-map))))
`((?l . ,org-tex-compiler)
(?L . ,(car (split-string org-tex-compiler))))))
preamble
tempfile-p))
(defun org-latex-preview--tex-styled (processing-type value appearance-options)
"Apply LaTeX style commands to VALUE based on APPEARANCE-OPTIONS.
If PROCESSING-TYPE is dvipng, the colours are set with DVI
\"\\special\" commands instead of \"\\color\" and
\"\\pagecolor\".
VALUE is the math fragment text to be previewed.
APPEARANCE-OPTIONS is the plist in the form of
`org-latex-preview-appearance-options' with customized color
information for this run."
(let* ((fg (pcase (plist-get appearance-options :foreground)
('default (org-latex-preview--format-color (org-latex-preview--attr-color :foreground)))
((pred null) (org-latex-preview--format-color "Black"))
(color (org-latex-preview--format-color color))))
(bg (pcase (plist-get appearance-options :background)
('default (org-latex-preview--attr-color :background))
("Transparent" nil)
(bg (org-latex-preview--format-color bg))))
(num (or (plist-get appearance-options :number)
(and (not (eq org-latex-preview-numbered 'preview))
1))))
(concat (and (not (plist-get appearance-options :continue-color))
(if (eq processing-type 'dvipng)
(concat (and fg (format "\\special{color rgb %s}"
(subst-char-in-string
?, ?\s fg)))
(and bg (format "\\special{background rgb %s}"
(subst-char-in-string
?, ?\s bg))))
(concat
(and bg (format "\\pagecolor[rgb]{%s}" bg))
(and fg (format "\\color[rgb]{%s}" fg))
(and fg
(eq processing-type 'dvisvgm)
(>= org-latex-preview--dvisvgm3-minor-version 1)
"\\special{dvisvgm:currentcolor on}"))))
(and num (format "\\setcounter{equation}{%d}" (1- num)))
"%\n"
value)))
(defun org-latex-preview--get-display-dpi ()
"Get the DPI of the display.
The function assumes that the display has the same pixel width in
the horizontal and vertical directions."
(if (display-graphic-p)
(let ((mm-height (or (display-mm-height) 0))
(mm-per-inch 25.4))
(if (= 0 mm-height)
140 ; Fallback reasonable DPI
(round (/ (display-pixel-height) (/ mm-height mm-per-inch)))))
(error "Attempt to calculate the dpi of a non-graphic display")))
(defun org-latex-preview--attr-color (attr)
"Return a RGB color for the LaTeX color package."
(org-latex-preview--format-color (face-attribute 'default attr nil)))
(defvar org-latex-preview--format-color-cache nil
"Cache for `org-latex-preview--format-color'.
Because `org-latex-preview--format-color' is called multiple
times for every fragment, even though only few colors will be
used it can be worth storing the results to avoid re-computing.")
(defun org-latex-preview--format-color (color-name)
"Convert COLOR-NAME to a RGB color value."
(or (alist-get color-name org-latex-preview--format-color-cache nil nil #'equal)
(cdar (push (cons color-name
(apply #'format "%.3f,%.3f,%.3f"
(mapcar
(lambda (v) (/ v 65535.0))
(color-values color-name))))
org-latex-preview--format-color-cache))))
(provide 'org-latex-preview)
;;; org-latex-preview.el ends here