;;; org-ref-glossary.el --- glossary support in org-ref -*- lexical-binding: t; -*- ;; Copyright (C) 2016-2021 John Kitchin ;; Author: John Kitchin ;; Keywords: ;; This program 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. ;; This program 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 this program. If not, see . ;;; Commentary: ;; Provides Some acronym and glossary support for org-mode. Only export to LaTeX ;; is currently supported. The functionality is based on the LaTeX glossaries ;; package. See https://en.wikibooks.org/wiki/LaTeX/Glossary and ;; http://ctan.math.washington.edu/tex-archive/macros/latex/contrib/glossaries/glossaries-user.pdf ;; Put something like this in your org-file. ;; #+latex_header: \usepackage{glossaries} ;; #+latex_header: \makeglossaries ;; Put this where you want the glossaries to appear in your org-file. ;; [[printglossaries:]] ;; This is the preferred way to add glossary entries to an org-file. The table ;; must be named glossary. ;; ;; #+name: glossary ;; | label | name | description | ;; |-------+-------+---------------| ;; | tree | Tree | A woody plant | ;; | shrub | Shrub | A woody bush | ;; ;; This is the preferred way to add acronyms ;; #+name: acronyms ;; | key | abbreviation | full form | ;; |------+--------------+--------------------------------| ;; | mimo | | multiple-input multiple output | ;; | qos | QoS | quality-of-service | ;; | bb | BB | branch and bound | ;; ;; Then add `org-ref-glossary-before-parsing' and ;; `org-ref-acronyms-before-parsing' to the `org-export-before-parsing-hook'. ;; See `org-ref-acronym-types' and `org-ref-glossary-gls-commands' for the types defined. ;; Use `org-ref-insert-glossary-link' and `org-ref-insert-acronym-link' to ;; insert a link with completion. ;; * Known limitations ;; ;; This only works well in a single file (i.e. you cannot include the tables ;; from another file). I tried implementing that once, and it was too slow. ;; * DEPRECATED way of adding entries ;; This is still supported but using the tables is preferred. ;; Add new glossary entries to your org-file like this. Enclose strings ;; containing a comma in {}. Multiline entries are supported. ;; #+latex_header_extra: \newglossaryentry{computer}{name=computer,description={A machine, that computes}} ;; #+latex_header_extra: \newglossaryentry{tree}{name=tree,description=a big plant} ;; #+latex_header_extra: \newglossaryentry{naiive} ;; #+latex_header_extra: { ;; #+latex_header_extra: name=na\"{\i}ve, ;; #+latex_header_extra: description={is a French loanword (adjective, form of naïf) ;; #+latex_header_extra: indicating having or showing a lack of experience, ;; #+latex_header_extra: understanding or sophistication} ;; #+latex_header_extra: } ;; Here is an example acronym definition ;; #+latex_header_extra: \newacronym{lvm}{LVM}{Logical Volume Manager} (require 'org-element) (require 'org-ref-utils) (require 'ox) ;;; Code: (defgroup org-ref-glossary nil "Customization group for org-ref-glossary." :tag "Org Ref glossary" :group 'org) (defvar org-ref-glsentries '() "Variable to hold locations of glsentries load files.") (defun or-find-closing-curly-bracket (&optional limit) "Find closing bracket for the bracket at point and move point to it. Go up to LIMIT or `point-max'. This is a parsing function. I wrote this because using `forward-list' does not always work if there is an escaped \" for example. This seems pretty robust." (unless (looking-at "{") (error "Not at a curley bracket")) (let ((level 1)) (while (and (not (= 0 level)) (not (eobp)) (< (point) (or limit (point-max)))) (forward-char) (when (and (looking-at "{") (not (looking-back "\\\\" (- (point) 2)))) (cl-incf level)) (when (and (looking-at "}") (not (looking-back "\\\\" (- (point) 2)))) (cl-decf level))) (point))) ;;* Glossary (defun or-parse-glossary-entry (entry) "Parse a LaTeX glossary ENTRY definition to a p-list of key=value. Typically: (:name name :description description) but there could be other :key value pairs." (save-excursion (goto-char (point-min)) (let* (end-of-entry data (external (when (re-search-forward "\\loadglsentries\\(\\[.*\\]\\){\\(?1:.*\\)}" nil t) (match-string 1))) (glsentries (and external (or (cdr (assoc external org-ref-glsentries)) (progn (cl-pushnew (cons external (s-trim (shell-command-to-string (format "kpsewhich tex %s" external)))) org-ref-glsentries) (cdr (assoc external org-ref-glsentries)))))) key value p1 p2) (catch 'data ;; look inside first for latex-headers (goto-char (point-min)) (when (re-search-forward (format "\\newglossaryentry{%s}" entry) nil t) (re-search-forward "{") (save-excursion (backward-char) (or-find-closing-curly-bracket) (setq end-of-entry (point))) (while (re-search-forward "\\(\\w+?\\)=" end-of-entry t) (setq key (match-string 1)) ;; get value (goto-char (+ 1 (match-end 1))) (setq p1 (point)) (if (looking-at "{") ;; value is wrapped in {} (progn (or-find-closing-curly-bracket) (setq p2 (point) value (buffer-substring (+ 1 p1) p2))) ;; value is up to the next comma (re-search-forward "," end-of-entry 'mv) (setq value (buffer-substring p1 (- (point) 1)))) ;; remove #+latex_header_extra: (setq value (replace-regexp-in-string "#\\+latex_header_extra: " "" value)) (setq value (replace-regexp-in-string "\n +" " " value)) (setq data (append data (list :label entry) (list (intern (format ":%s" key))) (list value)))) (throw 'data data)) ;; check for a glossary table (let* ((entries (save-excursion (catch 'found (org-element-map (org-element-parse-buffer) 'table (lambda (el) (when (string= "glossary" (org-element-property :name el)) (goto-char (org-element-property :contents-begin el)) (throw 'found (nthcdr 2 (org-babel-read-table))))))))) (result (assoc entry entries))) (when result (throw 'data (list :label entry :name (cl-second result) :description (cl-third result))))) ;; then external (when (and glsentries (file-exists-p glsentries)) (with-current-buffer (find-file-noselect glsentries) (goto-char (point-min)) (when (re-search-forward (format "\\newglossaryentry{%s}" entry) nil t) (re-search-forward "{") (save-excursion (backward-char) (or-find-closing-curly-bracket) (setq end-of-entry (point))) (while (re-search-forward "\\(\\w+?\\)=" end-of-entry t) (setq key (match-string 1)) ;; get value (goto-char (+ 1 (match-end 1))) (setq p1 (point)) (if (looking-at "{") ;; value is wrapped in {} (progn (or-find-closing-curly-bracket) (setq p2 (point) value (buffer-substring (+ 1 p1) p2))) ;; value is up to the next comma (re-search-forward "," end-of-entry 'mv) (setq value (buffer-substring p1 (- (point) 1)))) (setq data (append data (list :label entry) (list (intern (format ":%s" key))) (list value)))) (throw 'data data)))))))) ;;;###autoload (defun org-ref-add-glossary-entry (label name description) "Insert a new glossary entry. LABEL is how you refer to it with links. NAME is the name of the entry to be defined. DESCRIPTION is the definition of the entry. Entry gets added after the last #+latex_header line. This is not a preferred way to add entries. It is preferred to manually add them to the glossary table." (interactive "sLabel: \nsName: \nsDescription: ") (save-excursion (goto-char (point-max)) ;; get to the last latex_header line (re-search-backward "#\\+latex_header" nil t) (forward-line) (when (not (looking-at "^$")) (beginning-of-line) (insert "\n") (forward-line -1)) (insert (format "#+latex_header_extra: \\newglossaryentry{%s}{name={%s},description={%s}}\n" label name description)))) (defun org-ref-glossary-face-fn (label) "Return a face for a glossary link." (save-match-data (cond ((or-parse-glossary-entry label) 'org-ref-glossary-face) (t 'font-lock-warning-face)))) ;;** Glossary links (defun or-follow-glossary (entry) "Goto beginning of the glossary ENTRY." (org-mark-ring-push) (cond ;; Try finding in the table ((progn (goto-char (point-min)) (and (re-search-forward "#\\+name: glossary" nil t) (re-search-forward entry nil t))) nil) ((progn (goto-char (point-min)) (re-search-forward (format "\\newglossaryentry{%s}" entry) nil t)) (goto-char (match-beginning 0))) (t (message "no entry found for %s" entry)))) (defvar org-ref-glossary-gls-commands '(("gls" "The term associated with the label") ("glspl" "The plural term") ("Gls" "The capitalized term") ("Glspl" "The plural capitalized term") ("glssymbol" "The symbol defined (only with latex definition)") ("Glssymbol" "The capitalized symbol defined (only with latex definition)") ("glsdesc" "The description associated with the label") ("Glsdesc" "The capitalized description associated with the label")) "An alist of (cmd description).") (dolist (command org-ref-glossary-gls-commands) (org-link-set-parameters (cl-first command) :follow #'or-follow-glossary :face 'org-ref-glossary-face-fn :help-echo 'or-glossary-tooltip :export (lambda (path _ format) (cond ((memq format '(latex beamer)) (format "\\%s{%s}" (cl-first command) path)) (t (format "%s" path)))))) (org-link-set-parameters "glslink" :follow #'or-follow-glossary :face 'org-ref-glossary-face-fn :help-echo 'or-glossary-tooltip :export (lambda (path desc format) (cond ((memq format '(latex beamer)) (format "\\glslink{%s}{%s}" path desc)) (t (format "%s" path))))) ;;** Tooltips on glossary entries (defface org-ref-glossary-face `((t (:inherit org-link :foreground "Mediumpurple3"))) "Face for glossary links.") (defun or-glossary-tooltip (_window _object position) "Return tooltip for the glossary entry. The entry is in WINDOW and OBJECT at POSITION. Used in fontification." (save-excursion (goto-char position) (let* ((label (org-element-property :path (org-element-context))) (data (or (or-parse-glossary-entry label) (or-parse-acronym-entry label))) (name (or (plist-get data :name) (plist-get data :abbrv))) (description (or (plist-get data :description) (plist-get data :full)))) (format "%s: %s" name (with-temp-buffer (insert (concat description ".")) (fill-paragraph) (buffer-string)))))) ;; ** printglossaries links ;; There is a printglossary command in LaTeX, but I am not supporting it for now. (org-link-set-parameters "printglossaries" :export (lambda (path _desc format) (cond ((memq format '(latex beamer)) "\\printglossaries") (t (format "%s" path))))) ;; ** exporting with a glossary table (defun org-ref-glossary-before-parsing (_backend) "Function to preprocess a glossary table on export. This assumes a table like #+name: glossary | label | name | description | |-------+-------+---------------| | tree | Tree | A woody plant | | shrub | Shrub | A woody bush | is in the org-buffer, and will add the relevant latex_header items if there is. The table is deleted in a copy of the buffer before the export, so you can put it where you want. The column names are arbitrary, but three columns are expected, and the hline is expected. This is intended to be run in `org-export-before-parsing-hook'." (let* (begin end (entries (save-excursion (catch 'found (org-element-map (org-element-parse-buffer) 'table (lambda (el) (when (and (org-element-property :name el) (stringp (org-element-property :name el)) (string= "glossary" (org-element-property :name el))) (setq begin (org-element-property :begin el) end (org-element-property :end el)) (goto-char (org-element-property :contents-begin el)) (throw 'found (nthcdr 2 (org-babel-read-table)))))))))) ;; Delete the table (when entries (setf (buffer-substring begin end) "") (goto-char (point-min)) (cl-loop for (label name description) in entries do (insert (format "#+latex_header_extra: \\newglossaryentry{%s}{name=%s,description={{%s}}}\n" label name description)))))) ;;* Acronyms ;;;###autoload (defun org-ref-add-acronym-entry (label abbrv full) "Add an acronym entry with LABEL. ABBRV is the abbreviated form. FULL is the expanded acronym. This is not the preferred way to add acronyms, you should add them manually to the acroynms table." (interactive "sLabel: \nsAcronym: \nsFull name: ") (save-excursion (re-search-backward "#\\+latex_header" nil t) (forward-line) (when (not (looking-at "^$")) (beginning-of-line) (insert "\n") (forward-line -1)) (insert (format "#+latex_header_extra: \\newacronym{%s}{%s}{%s}\n" label abbrv full)))) (defun or-parse-acronym-entry (label) "Parse an acronym entry LABEL to a plist. (:abbrv abbrv :full full :label label) The plist maps to \newacronym{