├── README.org ├── images └── oxr-insert.png └── oxr.el /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: OXR (Org Experimental Cross-References) 2 | 3 | * Introduction 4 | 5 | This is an experiment to see what a minimal package to enhance cross-references in org-mode might look like, that might at some point contribute pieces to org. 6 | 7 | The general idea is to start with just using internal links, with =cleveref= as output target in LaTeX. 8 | 9 | * Features 10 | 11 | This package currently supports the following cross-reference target types: 12 | - sections 13 | - figures 14 | - tables 15 | - equations 16 | ... and the =oxr-insert-ref= command to insert cross-references to those targets. 17 | 18 | It also includes the following convenience commands to make it easy to create cross-reference targets that =oxr-insert-ref= can recognize. 19 | - =oxr-insert-section= 20 | - =oxr-insert-figure= 21 | - =oxr-insert-table= 22 | - =oxr-insert-equation= 23 | 24 | Finally, =oxr= can also access targets from included files (though this is currently limited and needs work). 25 | 26 | * Screenshot 27 | 28 | Figure [[fig-insert-screen]] is a simple screenshot showing the group-based insertion UI. 29 | 30 | #+caption: Group-based target insertion. 31 | #+name: fig-insert-screen 32 | [[./images/oxr-insert.png]] 33 | -------------------------------------------------------------------------------- /images/oxr-insert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bdarcus/oxr/46ccead3adc5e9b6ea62d27b24dcc7e2d8f33796/images/oxr-insert.png -------------------------------------------------------------------------------- /oxr.el: -------------------------------------------------------------------------------- 1 | ;;; oxr.el --- experimental cross-refernces for org -*- lexical-binding: t; -*- 2 | ;;; Commentary: 3 | ;;; 4 | ;;; An experimental package to enhance cross-reference support in org. 5 | ;;; 6 | ;;; Code: 7 | 8 | (require 'org-element) 9 | 10 | (declare-function 'org-element-property "ext:org") 11 | 12 | (defcustom oxr-group-ui t 13 | "Group cross-reference targets by type. 14 | 15 | In practice, if using Emacs 28, this allows you to turn off 16 | grouping, and add the target type to the annotation instead." 17 | :group 'oxr 18 | :type 'boolean) 19 | 20 | (defcustom oxr-insert-ref-function #'oxr-insert-ref-internal 21 | "Function to use for formatting ref links." 22 | :group 'oxr 23 | :type 'function) 24 | 25 | (defcustom oxr-create-table-function #'org-table-create 26 | "Function to create an org table." 27 | :group 'oxr 28 | :type 'function) 29 | 30 | (defface oxr-target 31 | '((t :foreground "Lightblue")) 32 | "Face for oxr targets." 33 | :group 'oxr) 34 | 35 | (defface oxr-annotation 36 | '((t :inherit font-lock-doc-face)) 37 | "Face for oxr annotations." 38 | :group 'oxr) 39 | 40 | (defvar oxr-types '((figure . "fig") 41 | (table . "tab") 42 | (equation . "eqn") 43 | (section . "sec"))) 44 | 45 | ;;; cache 46 | 47 | (defvar-local oxr-targets (list)) 48 | 49 | ;;; Cross-reference insert functions 50 | 51 | ;;;###autoload 52 | (defun oxr-insert-ref () 53 | "Insert cross-reference link in buffer." 54 | (interactive) 55 | (funcall oxr-insert-ref-function)) 56 | 57 | (defun oxr-insert-ref-internal () 58 | "Insert cross-reference in buffer as internal link." 59 | (let* ((target (oxr-select-targets)) 60 | (target-value (car target))) 61 | (org-insert-link 'internal target-value))) 62 | 63 | (defun oxr-insert-ref-typed () 64 | "Insert cross-reference in buffer as org-ref compatible typed link." 65 | (let ((type (completing-read "Cross-reference type: " oxr-types))) 66 | (let* ((target (oxr-select-targets)) 67 | (link-type (pcase type 68 | ("table" "tab") 69 | ("figure" "fig") 70 | ("latex-environment" "eqn") 71 | ("section" "sec"))) 72 | (typed-target (concat link-type ":" target))) 73 | (org-insert-link 'typed typed-target typed-target)))) 74 | 75 | (defun oxr-insert-annotate (target) 76 | "Annotate the cross-reference TARGET with type and caption." 77 | (let* ((caption (get-text-property 0 'oxr--caption target)) 78 | (target-type (get-text-property 0 'oxr--type target))) 79 | (propertize 80 | (concat (unless oxr-group-ui (truncate-string-to-width target-type 15 0 ?\s)) caption) 81 | 'face 'oxr-annotation))) 82 | 83 | (defun oxr--insert-group (target transform) 84 | "Function to group or TRANSFORM cross-reference candidates by TARGET type." 85 | (if transform target (get-text-property 0 'oxr--type target))) 86 | 87 | (defun oxr-select-targets () 88 | "Select cross-reference target." 89 | (let* ((targets (oxr--make-candidates)) 90 | (choice 91 | (if targets 92 | (completing-read 93 | "Cross-reference target: " 94 | (lambda (string predicate action) 95 | (if (eq action 'metadata) 96 | `(metadata 97 | (annotation-function . oxr-insert-annotate) 98 | (group-function . ,(when oxr-group-ui #'oxr--insert-group)) 99 | (category . cross-reference)) 100 | (complete-with-action action targets string predicate)))) 101 | (error "No cross-reference targets"))) 102 | ;; TDOO doesn't work; are the properties thrown out already? 103 | (choice-type (get-text-property 0 'oxr--type choice))) 104 | (cons (string-trim choice) choice-type))) 105 | 106 | (defun oxr--extract-string (thing) 107 | "Peel off 'car's from a nested list THING until the car is a string." 108 | ;; from https://orgmode.org/worg/exporters/beamer/beamer-dual-format.html 109 | (while (and thing (not (stringp thing))) 110 | (setq thing (car thing))) 111 | (substring-no-properties thing)) 112 | 113 | (defvar-local oxr--target-cache (make-hash-table :test 'equal)) 114 | 115 | (defun oxr--parse-includes (org-tree) 116 | "Extract include filepaths from ORG-TREE." 117 | (let ((results (list))) 118 | (org-element-map org-tree 'keyword 119 | (lambda (kwd) 120 | (when (string= "INCLUDE" 121 | (org-element-property :key kwd)) 122 | (push (org-element-property :value kwd) results)))) 123 | (when results results))) 124 | 125 | (defun oxr--parse-buffers () 126 | "Parse current buffer, as well as included files." 127 | (let* ((org-tree (org-element-parse-buffer)) 128 | (includes (mapcar 129 | (lambda (file) 130 | (oxr--parse-file file)) 131 | (oxr--parse-includes org-tree)))) 132 | (clrhash oxr--target-cache) 133 | (oxr--parse-targets org-tree) 134 | (when includes 135 | (dolist (otree includes) 136 | ;; FIX why doesn't this work? 137 | (oxr--parse-targets otree))))) 138 | 139 | (defun oxr--parse-file (file) 140 | "Parse FILE, return parse tree." 141 | (with-temp-buffer 142 | (insert-file-contents file) 143 | (set-buffer-modified-p nil) 144 | (org-with-wide-buffer 145 | (org-element-parse-buffer)))) 146 | 147 | (defun oxr--parse-targets (org-tree) 148 | "Parse cross-reference targets from ORG-TREE." 149 | (org-element-map org-tree '(table link headline latex-environment) 150 | (lambda (target) 151 | (let* ((el-type (org-element-type target)) 152 | (target-type (pcase el-type 153 | ('table "table") 154 | ('headline "section") 155 | ('latex-environment "equation") 156 | (_ "figure"))) 157 | (parent (car (cdr (org-element-property :parent target)))) 158 | (name (pcase el-type 159 | ('table (org-element-property :name target)) 160 | ('latex-environment (org-element-property :name target)) 161 | ('headline 162 | (concat "#" (org-element-property :CUSTOM_ID target))) 163 | (_ (plist-get parent :name)))) 164 | (caption (oxr--extract-string 165 | (or (org-element-property :caption target) 166 | (org-element-property :raw-value target) 167 | (plist-get parent :caption) 168 | "")))) 169 | (when (and name (not (string= name "#"))) 170 | (puthash name (cons target-type caption) oxr--target-cache)))))) 171 | 172 | (defun oxr--make-candidates () 173 | "Return completion candidates." 174 | (let ((results (list))) 175 | (oxr--parse-buffers) 176 | (maphash 177 | (lambda (key value) 178 | (push 179 | (oxr--make-candidate key (cdr value) (car value)) 180 | results)) 181 | oxr--target-cache) 182 | results)) 183 | 184 | (defun oxr--make-candidate (name caption type) 185 | "Make candidate string with NAME, CAPTION, TYPE." 186 | (propertize 187 | (truncate-string-to-width (concat " " name) 40 0 ?\s) 188 | 'face 'oxr-target 189 | ;; Attach metadata to target candidate string. 190 | 'oxr--caption caption 191 | 'oxr--type type)) 192 | 193 | ;;; Convenience functions for inserting new cross-reference targets (tables and figures) 194 | 195 | (defun oxr--metadata-prompt (name-prefix &optional properties) 196 | "Prompt user for name with NAME-PREFIX and caption. 197 | If PROPERTIES, return formatted." 198 | (let ((name (read-from-minibuffer "Name: ")) 199 | (caption (read-from-minibuffer "Caption: "))) 200 | (if properties 201 | (concat "#+caption: " caption "\n#+name: " name-prefix "-" name "\n") 202 | (cons (concat name-prefix "-" name) caption)))) 203 | 204 | (defun oxr--get-name-prefix (type) 205 | "Get the name prefix for TYPE from 'oxr-types'." 206 | (cdr (assoc type oxr-types))) 207 | 208 | ;;;###autoload 209 | (defun oxr-insert-table () 210 | "Insert a new table, with name and caption." 211 | (interactive) 212 | (insert (oxr--metadata-prompt (oxr--get-name-prefix 'table) t)) 213 | (funcall oxr-create-table-function)) 214 | 215 | ;;;###autoload 216 | (defun oxr-insert-equation () 217 | "Insert a new equation, with name and caption." 218 | (interactive) 219 | (let ((meta (oxr--metadata-prompt (oxr--get-name-prefix 'equation) t))) 220 | (insert meta) 221 | (insert "\\begin{equation}\n|\n") 222 | (insert "\\end{equation}\n") 223 | (search-backward "|") 224 | (delete-char 1) 225 | (when (fboundp 'evil-insert) 226 | (evil-insert 1)))) 227 | 228 | ;;;###autoload 229 | (defun oxr-insert-figure () 230 | "Insert a new figure, with name and caption." 231 | (interactive) 232 | ;; TODO this inserts absolute paths ATM, which is not ideal. 233 | (let ((image_file (read-file-name "Image file: " "~/Pictures/"))) 234 | (insert (oxr--metadata-prompt (oxr--get-name-prefix 'figure) t)) 235 | (org-insert-link 'file image_file))) 236 | 237 | ;;;###autoload 238 | (defun oxr-insert-section () 239 | "Insert a new section headline, with name." 240 | (interactive) 241 | (let* ((name (read-from-minibuffer "Section: ")) 242 | (id (oxr--make-headline-id name))) 243 | (insert (concat "* " name)) 244 | (org-entry-put nil "CUSTOM_ID" id))) 245 | 246 | (defun oxr--make-headline-id (str) 247 | "Return a custom id token from STR." 248 | (downcase 249 | (string-join 250 | (seq-take (split-string str "[:;,.\s]" 'omit-nulls) 5) "-"))) 251 | 252 | ;;;###autoload 253 | (defun oxr-insert-name () 254 | "Insert name for cross-referencing." 255 | (interactive) 256 | (let* ((object-type 257 | (completing-read "Object type: " oxr-types)) 258 | (name (read-from-minibuffer "Name: "))) 259 | (insert (concat (oxr--get-name-prefix object-type)) "-" name))) 260 | 261 | (provide 'oxr) 262 | ;;; oxr.el ends here 263 | --------------------------------------------------------------------------------