├── README.org ├── orgdiff-screenshot.png └── orgdiff.el /README.org: -------------------------------------------------------------------------------- 1 | #+title: OrgDiff 2 | #+author: tecosaur 3 | 4 | #+html:

5 | #+html:

6 | 7 | As we're all aware, one of the lovely things about working in plaintext is that 8 | you can run version control over your documents. =git diff= does a perfectly 9 | passable job at showing you how your Org files has changed, but what about when 10 | you want something richer? Well =latexdiff= is brilliant for LaTeX files, and you 11 | can export Org to LaTeX! However, it's a bit of a hassle, you need to 12 | 1. Check out an old revision of your repository 13 | 2. Export your Org file to LaTeX 14 | 3. Fast-forward to the newer version 15 | 4. Do /another/ Org export to LaTeX 16 | 5. Run =latexdiff= on the two =.tex= files 17 | 6. Compile to a PDF, and view it 18 | 19 | Gah! This is sounding like a bit of a pain. That's where *OrgDiff* comes in. It 20 | automates all of the above, and provides a few other quality of life 21 | improvements, namely: 22 | + Commit selection via the venerable Magit 23 | + Parallel exporting of the two Org files to TeX 24 | + Descriptive forms of the main =latexdiff= options, accessible via a Transient interface 25 | + Automatic TeX compiler-upgrading. I.e., if one document needs LuaLaTeX and the 26 | other needs XeLaTeX, then we need to use LuaLaTeX to compile the =latexdiff= result 27 | 28 | Along with that we provide some more basic options, like comparing any two Org 29 | files without git, just creating the =.tex= diff, or running ~ediff~ on the Org 30 | files. 31 | 32 | [[file:orgdiff-screenshot.png]] 33 | 34 | Do note that while this seems to work, it's quite early on in development, and 35 | it shows in the code (lack of documentation, etc.). It seems perfectly usable 36 | though 🙂. 37 | -------------------------------------------------------------------------------- /orgdiff-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tecosaur/orgdiff/e5e2d6e57a5e996ae0fdffc4b55522d9ee6fb476/orgdiff-screenshot.png -------------------------------------------------------------------------------- /orgdiff.el: -------------------------------------------------------------------------------- 1 | ;;; orgdiff.el --- Intelligently generate Org document diffs -*- lexical-binding: t; -*- 2 | ;; 3 | ;; Copyright (C) 2021 TEC 4 | ;; 5 | ;; Author: TEC 6 | ;; Maintainer: TEC 7 | ;; Created: June 24, 2021 8 | ;; Modified: June 24, 2021 9 | ;; Version: 0.0.1 10 | ;; Keywords: tools 11 | ;; Homepage: https://github.com/tec/orgdiff 12 | ;; Package-Requires: ((emacs "26.3") (transient "0.2.0") (magit "3.1.0")) 13 | ;; 14 | ;; This file is not part of GNU Emacs. 15 | ;; 16 | ;;; Commentary: 17 | ;; 18 | ;; Intelligently generate Org document diffs 19 | ;; 20 | ;;; Code: 21 | 22 | (eval-when-compile 23 | (require 'cl-lib) 24 | (require 'eieio)) 25 | 26 | (require 'ox) 27 | (require 'ox-latex) 28 | (require 'transient) 29 | (require 'magit) 30 | 31 | ;; Customisations 32 | 33 | (defgroup orgdiff nil 34 | "Generating Org document diffs." 35 | :group 'org 36 | :prefix "orgdiff-") 37 | 38 | (defcustom orgdiff-latexdiff-postprocess-hooks nil 39 | "Post-processing functions which are run on the latexdiff result." 40 | :type 'hook) 41 | 42 | (defcustom orgdiff-latexdiff-executable "latexdiff" 43 | "Path to the latexdiff executable to use on the generated tex files." 44 | :type 'string) 45 | 46 | (defcustom orgdiff-latex-compiler-priorities 47 | '("pdflatex" "xelatex" "lualatex") 48 | "A list of compiler priorities, ascending. 49 | If two documents require different compilers, the higher priority 50 | compiler will be used." 51 | :type '(repeat string)) 52 | 53 | (defcustom orgdiff-latex-compile-command 54 | (delq nil (list "latexmk" 55 | "-f" 56 | "-pdf" 57 | "-%compiler" 58 | (and (string-match-p "-shell-escape" (car org-latex-pdf-process)) 59 | "-shell-escape") 60 | "-interaction=nonstopmode" 61 | "%texfile")) 62 | "Compile command as a list in the form (\"CMD\" \"ARGS\"...). 63 | \"%compiler\" is replaced with the requested compiler, and 64 | \"%texfile\" replaced with the path to the .tex file." 65 | :type '(repeat string)) 66 | 67 | ;; Helper variables 68 | 69 | (defvar orgdiff--base-dir nil "The current directory for diffing files.") 70 | (defvar orgdiff--rev1 nil "The first revision.") 71 | 72 | (defvar orgdiff--export-processes nil) 73 | (defvar orgdiff--latexdiff-file-1 nil) 74 | (defvar orgdiff--latexdiff-file-2 nil) 75 | (defvar orgdiff--latexdiff-flatten nil) 76 | 77 | (defvar orgdiff--rev1dir nil) 78 | (defvar orgdiff--rev2dir nil) 79 | 80 | (defvar orgdiff--rev1file nil) 81 | (defvar orgdiff--rev2file nil) 82 | (defvar orgdiff--difffile nil) 83 | 84 | (defvar orgdiff--export-processes nil) 85 | 86 | ;; Transient setup 87 | 88 | (defclass orgdiff--transient-lisp-variable-formatted (transient-variable) 89 | ((reader :initform #'transient-lisp-variable--reader) 90 | (always-read :initform t) 91 | (set-value :initarg :set-value :initform #'set)) 92 | "Class used for Lisp variables, modified version of `transient-lisp-variable'.") 93 | 94 | (cl-defmethod transient-init-value ((obj orgdiff--transient-lisp-variable-formatted)) 95 | (oset obj value (symbol-value (oref obj variable)))) 96 | 97 | (cl-defmethod transient-infix-set ((obj orgdiff--transient-lisp-variable-formatted) value) 98 | (funcall (oref obj set-value) 99 | (oref obj variable) 100 | (oset obj value value))) 101 | 102 | (cl-defmethod transient-format-description ((obj orgdiff--transient-lisp-variable-formatted)) 103 | (or (oref obj description) 104 | (symbol-name (oref obj variable)))) 105 | 106 | (cl-defmethod transient-format-value ((obj orgdiff--transient-lisp-variable-formatted)) 107 | (let ((val (oref obj value))) 108 | (pcase val 109 | ('t (propertize "yes" 'face 'transient-argument)) 110 | ('nil (propertize "no" 'face 'transient-unreachable)) 111 | (_ (propertize (prin1-to-string val) 'face 'transient-value))))) 112 | 113 | (cl-defmethod transient-prompt ((obj orgdiff--transient-lisp-variable-formatted)) 114 | (format "Set %s: " (oref obj variable))) 115 | 116 | (defun orgdiff--transient-lisp-variable-formatted--reader (prompt initial-input _history) 117 | (read--expression prompt initial-input)) 118 | 119 | (eval-when-compile 120 | (defmacro orgdiff--define-infix (key name description type default 121 | &rest reader) 122 | "Define infix with KEY, NAME, DESCRIPTION, TYPE, DEFAULT and READER as arguments." 123 | (declare (indent 5) (doc-string 3)) 124 | (let ((infix-var (intern (format "orgdiff-%s" name)))) 125 | `(progn 126 | (defcustom ,infix-var ,default 127 | ,(concat description ".") 128 | :type ,type 129 | :group 'orgdiff) 130 | (transient-define-infix ,(intern (format "orgdiff--set-%s" name)) () 131 | ,(format "Set `%s' from a popup buffer." infix-var) 132 | :class 'orgdiff--transient-lisp-variable-formatted 133 | :variable ',infix-var 134 | :key ,key 135 | :description ,description 136 | :argument ,(format "--%s" name) 137 | :reader (lambda (&rest _) ,@reader)))))) 138 | 139 | (orgdiff--define-infix 140 | "-1" file-1 "First file" 141 | '(choice string boolean) nil 142 | (read-file-name "First file: " nil nil t nil 143 | (lambda (f) 144 | (or (equal (file-name-extension f) "org") 145 | (string-match-p "/$" f))))) 146 | 147 | (orgdiff--define-infix 148 | "-2" file-2 "Second file" 149 | '(choice string boolean) nil 150 | (read-file-name "Second file: " nil orgdiff-file-2 t nil 151 | (lambda (f) 152 | (or (and (equal (file-name-extension f) "org") 153 | (message f) 154 | (not (string= f orgdiff-file-1))) 155 | (string-match-p "/$" f))))) 156 | 157 | (orgdiff--define-infix 158 | "-r" git-revisions "Git revision" 159 | '(choice string boolean) nil 160 | (run-at-time 161 | nil nil 162 | (lambda () 163 | (transient--suspend-override) 164 | (magit-log-select 165 | (lambda (r1) 166 | (let ((r1tag (magit-rev-name r1))) 167 | (when r1tag (setq r1tag (replace-regexp-in-string "tags/" "" r1tag))) 168 | (setq orgdiff--rev1 (or r1tag r1))) 169 | (magit-log-select 170 | (lambda (r2) 171 | (let ((r2tag (magit-rev-name r2))) 172 | (when r2tag (setq r2tag (replace-regexp-in-string "tags/" "" r2tag))) 173 | (setq r2 (or r2tag r2))) 174 | (setq orgdiff-git-revisions (substring-no-properties (concat orgdiff--rev1 ".." r2))) 175 | (transient--resume-override) 176 | (orgdiff--transient)) 177 | (concat (propertize "Second revision (" 'face 'transient-heading) 178 | (propertize "newer" 'face 'transient-argument) 179 | (propertize "): " 'face 'transient-heading) 180 | "type %p to select commit at point, %q to use the current working state instead") 181 | (lambda () 182 | (setq orgdiff-git-revisions (substring-no-properties orgdiff--rev1)) 183 | (transient--resume-override) 184 | (orgdiff--transient)))) 185 | (concat (propertize "First revision (" 'face 'transient-heading) 186 | (propertize "older" 'face 'transient-argument) 187 | (propertize "): " 'face 'transient-heading) 188 | "type %p to select commit at point, %q to not compare any revisions") 189 | (lambda () 190 | (transient--resume-override) 191 | (orgdiff--transient))))) 192 | nil) 193 | 194 | ;;;###autoload 195 | (defun orgdiff () 196 | "Generate a diff of two Org files." 197 | (interactive) 198 | 199 | (setq orgdiff--base-dir 200 | (with-temp-buffer 201 | (if (= 0 (call-process "git" nil t nil "rev-parse" "--show-toplevel")) 202 | (string-trim (buffer-string)) 203 | default-directory))) 204 | 205 | (unless (and orgdiff-file-1 (string-prefix-p orgdiff--base-dir 206 | (expand-file-name orgdiff-file-1))) 207 | (setq orgdiff-file-1 208 | (if (and (stringp buffer-file-name) (string= ".org" (file-name-extension buffer-file-name))) 209 | buffer-file-name 210 | (car (directory-files orgdiff--base-dir t "\\.org$" nil 1))))) 211 | 212 | (when (and orgdiff-file-2 213 | (or (string= orgdiff-file-1 orgdiff-file-2) 214 | (not (string-prefix-p orgdiff--base-dir 215 | (expand-file-name orgdiff-file-2))))) 216 | (setq orgdiff-file-2 nil)) 217 | 218 | (if (= 0 (call-process "git" nil nil nil "rev-parse" "--git-dir")) ; are we in a git dir? 219 | (if (memq major-mode '(magit-status-mode magit-log-mode)) 220 | ;; If in a magit buffer, set git revisions from state 221 | (setq orgdiff-git-revisions 222 | (let* ((commits (magit-region-values '(commit branch) t)) 223 | (range (if commits 224 | (concat (car (last commits)) ".." (car commits)) 225 | (magit-branch-or-commit-at-point)))) 226 | (and range (substring-no-properties range)))) 227 | ;; Otherwise, just verify that the refs are valid 228 | (unless (or (null orgdiff-git-revisions) 229 | (cl-every #'magit-rev-verify-commit 230 | (split-string orgdiff-git-revisions "\\.\\."))) 231 | (setq orgdiff-git-revisions nil))) 232 | (setq orgdiff-git-revisions nil)) 233 | 234 | (orgdiff--transient)) 235 | 236 | (transient-define-prefix orgdiff--transient () 237 | ["Files" 238 | (orgdiff--set-file-1) 239 | (orgdiff--set-file-2)] 240 | ["Git" 241 | :if (lambda () (= 0 (call-process "git" nil nil nil "rev-parse" "--git-dir"))) 242 | (orgdiff--set-git-revisions)] 243 | ["Action" 244 | ("s" "swap files" orgdiff--swap-files) 245 | ("e" "ediff" orgdiff--ediff) 246 | ("l" "latexdiff" orgdiff--latexdiff)]) 247 | 248 | (defun orgdiff--swap-files (&optional no-transient) 249 | (interactive) 250 | (let ((old1 orgdiff-file-1)) 251 | (setq orgdiff-file-1 orgdiff-file-2 252 | orgdiff-file-2 old1)) 253 | (unless no-transient 254 | (orgdiff--transient))) 255 | 256 | ;;; simple diff 257 | 258 | (defun orgdiff--ediff () 259 | (interactive) 260 | (when (and orgdiff-file-2 (not orgdiff-file-1)) 261 | (orgdiff--swap-files t)) 262 | (orgdiff--extract-revisions) 263 | (ediff-files orgdiff--rev1file orgdiff--rev2file)) 264 | 265 | ;;; latexdiff 266 | 267 | (defun orgdiff--latexdiff () 268 | (interactive) 269 | (when orgdiff--export-processes 270 | (user-error "A latexdiff is currently in progress. Try `orgdiff--latexdiff-abort' if something seems wrong.")) 271 | (when (and orgdiff-file-2 (not orgdiff-file-1)) 272 | (orgdiff--swap-files t)) 273 | (setq orgdiff--latexdiff-file-1 274 | (and orgdiff-file-1 275 | (concat (file-name-sans-extension orgdiff-file-1) ".tex")) 276 | orgdiff--latexdiff-file-2 277 | (and orgdiff-file-2 278 | (concat (file-name-sans-extension orgdiff-file-2) ".tex")) 279 | orgdiff--latexdiff-flatten 280 | (and (or orgdiff-git-revisions 281 | (not (and orgdiff-file-1 orgdiff-file-2 282 | (string= (file-name-directory orgdiff-file-1) 283 | (file-name-directory orgdiff-file-2))))) 284 | t)) 285 | (orgdiff--latexdiff-transient)) 286 | 287 | (defface orgdiff--latexdiff-red 288 | '((t :foreground "#cd3d3e")) 289 | "Used to preview latexdiff's red.") 290 | 291 | (defface orgdiff--latexdiff-blue 292 | '((t :foreground "#4078f2")) 293 | "Used to preview latexdiff's red.") 294 | 295 | (defvar orgdiff--latexdiff-flags 296 | `(("--type" 297 | (UNDERLINE . 298 | ,(concat (propertize "discarded" 'face '((:strike-through t) orgdiff--latexdiff-red)) 299 | " " (propertize "added" 'face 'orgdiff--latexdiff-blue) 300 | " " (propertize "(default)" 'face 'shadow))) 301 | (CTRADITIONAL . 302 | ,(concat (propertize "[.."'face 'orgdiff--latexdiff-red) 303 | (propertize "1"'face '((:height 0.6) orgdiff--latexdiff-red)) 304 | (propertize "]"'face 'orgdiff--latexdiff-red) 305 | " " (propertize "added" 'face '((:height 1.15) variable-pitch orgdiff--latexdiff-blue)))) 306 | (TRADITIONAL . 307 | ,(concat "[.." (propertize "1" 'face '(:height 0.6)) "]" 308 | " " (propertize "added" 'face '((:height 1.15) variable-pitch)))) 309 | (CFONT . 310 | ,(concat (propertize "discarded" 'face '((:height 0.7) orgdiff--latexdiff-red)) 311 | " " (propertize "added" 'face '((:height 1.15) variable-pitch orgdiff--latexdiff-blue)))) 312 | (FONTSTRIKE . 313 | ,(concat (propertize "discarded" 'face '((:strike-through t) small)) 314 | " " (propertize "added" 'face '((:height 1.15) variable-pitch)))) 315 | (CHANGEBAR . ,(propertize "no markup, change marks in margins" 'face 'font-lock-doc-face)) 316 | (CCHANGEBAR . 317 | ,(concat (propertize "CHANGEBAR" 'face 'font-lock-function-name-face) 318 | (propertize " + " 'face 'font-lock-comment-face) 319 | (propertize "discarded" 'face 'orgdiff--latexdiff-red) 320 | " " (propertize "added" 'face 'orgdiff--latexdiff-blue))) 321 | (CFONTCHBAR . 322 | ,(concat (propertize "CHANGEBAR" 'face 'font-lock-function-name-face) 323 | (propertize " + " 'face 'font-lock-comment-face) 324 | (propertize "discarded" 'face '(small orgdiff--latexdiff-red)) 325 | " " (propertize "added" 'face '((:height 1.15) variable-pitch orgdiff--latexdiff-blue)))) 326 | (CULINECHBAR . 327 | ,(concat (propertize "CHANGEBAR" 'face 'font-lock-function-name-face) 328 | (propertize " + " 'face 'font-lock-comment-face) 329 | (propertize "discarded" 'face '((:strike-through t) orgdiff--latexdiff-red)) 330 | " " (propertize "added" 'face 'orgdiff--latexdiff-blue))) 331 | (INVISIBLE . "added") 332 | (BOLD . 333 | ,(propertize "added" 'face 'bold)) 334 | (PDFCOMMENT . 335 | ,(concat (propertize "discarded text in PDF comment, " 'face 'font-lock-doc-face) (propertize "added" 'face 'underline)))) 336 | ("--subtype" 337 | (SAFE . ,(propertize "No additional markup (default, reccomended)" 'face 'font-lock-doc-face)) 338 | (MARGIN . ,(propertize "Mark start and end of changed block with symbol in margin" 'face 'font-lock-doc-face)) 339 | (COLOR . 340 | ,(concat (propertize "deleted passages" 'face 'orgdiff--latexdiff-red) 341 | " " (propertize "added passages" 'face 'orgdiff--latexdiff-blue))) 342 | (ZLABEL . ,(propertize "Highlight changed pages, requires post-processing" 'face 'font-lock-doc-face)) 343 | (ONLYCHANGEDPAGE . ,(propertize "(Also) Highlights changed pages, no post-processing but dodgy floats" 'face 'font-lock-doc-face))) 344 | ("--floattype" 345 | (FLOATSAFE . ,(concat "Treat as main text, but skip begin/end commands" (propertize " (default)" 'face 'shadow))) 346 | (TRADITIONALSAFE . "Make deleted environments small and wrapped in brackets. Use with TRADITIONAL/CTRADITIONAL") 347 | (IDENTICAL . "Treat floats exactly the same as the main text")) 348 | ("--math-markup" 349 | (off . "Supress markup in math environments. Only show new version") 350 | (whole . "Any change causes the whole equation to be marked as changed") 351 | (coarse . ,(concat "Coarse granularity. Use when content and order being changed" (propertize " (default)" 'face 'shadow))) 352 | (fine . "Detect and mark up small changes. Suitable if minor changes (e.g. typo fixes) are expected")) 353 | ("--graphics-markup" 354 | (off . "No highlighting for figures") 355 | (new-only . ,(concat "Surround new/changed figures with a blue frame" (propertize " (default)" 'face 'shadow))) 356 | (both . "Surround new/changed figures with a blue frame, and shrink and cross out deleted figures"))) 357 | "LaTeXdiff flags and recognised values they may take.") 358 | 359 | (defun orgdiff--latexdiff-prompt-flag (flag) 360 | (let* ((max-key-width 361 | (thread-last (cdr (assoc flag orgdiff--latexdiff-flags)) 362 | (mapcar #'car) 363 | (mapcar #'symbol-name) 364 | (mapcar #'length) 365 | (seq-max))) 366 | (options 367 | (mapcar (lambda (opt) 368 | (concat (propertize (symbol-name (car opt)) 'face 'transient-value) 369 | (make-string (- max-key-width (length (symbol-name (car opt))) -2) ? ) 370 | (cdr opt))) 371 | (cdr (assoc flag orgdiff--latexdiff-flags)))) 372 | (selection 373 | (completing-read (concat flag ": ") 374 | options 375 | nil t))) 376 | (with-temp-buffer 377 | (insert selection) 378 | (goto-char (point-min)) 379 | (intern (buffer-substring-no-properties 380 | (point-min) 381 | (cdr (bounds-of-thing-at-point 'word))))))) 382 | 383 | (orgdiff--define-infix 384 | "-A" latexdiff-async "Export Org to TeX asyncronously" 385 | 'boolean nil 386 | (not orgdiff-latexdiff-async)) 387 | (orgdiff--define-infix 388 | "-F" latexdiff-flatten "Flatten includes" 389 | 'boolean nil 390 | (not orgdiff-latexdiff-flatten)) 391 | (orgdiff--define-infix 392 | "-S" latexdiff-allow-spaces "Allow spaces" 393 | 'boolean nil 394 | (not orgdiff-latexdiff-allow-spaces)) 395 | 396 | (orgdiff--define-infix 397 | "-d" latexdiff-skip-deleted "Omit all deleted content" 398 | 'boolean nil 399 | (not orgdiff-latexdiff-skip-deleted)) 400 | (orgdiff--define-infix 401 | "-t" latexdiff-type "Type" 402 | 'symbol 'UNDERLINE 403 | (orgdiff--latexdiff-prompt-flag "--type")) 404 | (orgdiff--define-infix 405 | "-s" latexdiff-subtype "Sub-type" 406 | 'symbol 'SAFE 407 | (orgdiff--latexdiff-prompt-flag "--subtype")) 408 | (orgdiff--define-infix 409 | "-f" latexdiff-floattype "Float type" 410 | 'symbol 'FLOATSAFE 411 | (orgdiff--latexdiff-prompt-flag "--floattype")) 412 | (orgdiff--define-infix 413 | "-m" latexdiff-math-markup "Math markup" 414 | 'symbol 'coarse 415 | (orgdiff--latexdiff-prompt-flag "--math-markup")) 416 | (orgdiff--define-infix 417 | "-g" latexdiff-graphics-markup "Graphics markup" 418 | 'symbol 'new-only 419 | (orgdiff--latexdiff-prompt-flag "--graphics-markup")) 420 | (orgdiff--define-infix 421 | "-c" latexdiff-citation-markup "Citation markup" 422 | 'boolean t 423 | (not orgdiff-latexdiff-citation-markup)) 424 | 425 | (transient-define-prefix orgdiff--latexdiff-transient () 426 | ["Processing" 427 | (orgdiff--set-latexdiff-async) 428 | (orgdiff--set-latexdiff-flatten) 429 | (orgdiff--set-latexdiff-allow-spaces) 430 | ;; Filter script 431 | ;; Preamble file 432 | ;; Cleanup output # custom 433 | ] 434 | ["Style" 435 | (orgdiff--set-latexdiff-skip-deleted) 436 | (orgdiff--set-latexdiff-type) 437 | (orgdiff--set-latexdiff-subtype) 438 | (orgdiff--set-latexdiff-floattype) 439 | (orgdiff--set-latexdiff-graphics-markup) 440 | (orgdiff--set-latexdiff-math-markup) 441 | (orgdiff--set-latexdiff-citation-markup) 442 | ] 443 | ["Action" 444 | ("l" "run latexdiff, compile, and open PDF" orgdiff--latexdiff--action-pdf-open) 445 | ("L" "run latexdiff only" orgdiff--latexdiff--action-latex) 446 | ;; ("P" "PDF" orgdiff--latexdiff--action-pdf) 447 | ;; ("p" "run latexdiff, and compile" orgdiff--latexdiff--action-latex) 448 | ]) 449 | 450 | (defvar orgdiff--latexdiff--completion-action nil) 451 | 452 | (defun orgdiff--latexdiff--action-latex () 453 | (interactive) 454 | (setq orgdiff--latexdiff--completion-action '(:format latex :action nil)) 455 | (orgdiff--latexdiff-execute-pt1)) 456 | 457 | (defun orgdiff--latexdiff--action-latex-open () 458 | (interactive) 459 | (setq orgdiff--latexdiff--completion-action '(:format latex :action open)) 460 | (orgdiff--latexdiff-execute-pt1)) 461 | 462 | (defun orgdiff--latexdiff--action-pdf () 463 | (interactive) 464 | (setq orgdiff--latexdiff--completion-action '(:format pdf :action nil)) 465 | (orgdiff--latexdiff-execute-pt1)) 466 | 467 | (defun orgdiff--latexdiff--action-pdf-open () 468 | (interactive) 469 | (setq orgdiff--latexdiff--completion-action '(:format pdf :action open)) 470 | (orgdiff--latexdiff-execute-pt1)) 471 | 472 | ;;; Actually building the diff 473 | 474 | (defun orgdiff--latexdiff-execute-pt1 () 475 | (orgdiff--extract-revisions) 476 | (orgdiff--latex-create-from-org) 477 | (unless (executable-find orgdiff-latexdiff-executable) 478 | (user-error "Could not locate the latexdiff executable!")) 479 | (orgdiff--latexdiff-wait-for-export-then #'orgdiff--latexdiff-execute-pt2)) 480 | 481 | (defun orgdiff--latexdiff-execute-pt2 () 482 | (unless 483 | (and (file-exists-p (concat (file-name-sans-extension orgdiff--rev1file) ".tex")) 484 | (file-exists-p (concat (file-name-sans-extension orgdiff--rev2file) ".tex"))) 485 | (user-error "Error! Org files were not sucessfully exported to LaTeX")) 486 | (orgdiff--latexdiff-expand) 487 | (orgdiff--latexdiff-do-diff) 488 | (pcase (plist-get orgdiff--latexdiff--completion-action :format) 489 | ('latex 490 | (let ((dest (concat (file-name-directory orgdiff-file-1) 491 | (file-name-nondirectory orgdiff--difffile)))) 492 | (rename-file orgdiff--difffile dest t) 493 | (when (eq 'open (plist-get orgdiff--latexdiff--completion-action :action)) 494 | (find-file-other-window dest)))) 495 | ('pdf 496 | (orgdiff--latexdiff-compile) 497 | (orgdiff--latexdiff-wait-for-export-then #'orgdiff--latexdiff-handle-pdf)))) 498 | 499 | (defun orgdiff--extract-revisions () 500 | (setq orgdiff--rev1dir nil 501 | orgdiff--rev2dir nil) 502 | (when orgdiff-git-revisions 503 | (message "%s%s" (propertize "Orgdiff" 'face 'bold) ": Checking out revisions...") 504 | (let ((revisions (split-string orgdiff-git-revisions "\\.\\.")) 505 | (default-directory (string-trim (shell-command-to-string "git rev-parse --show-toplevel")))) 506 | (setq orgdiff--rev1dir (make-temp-file "orgdiff-" t)) 507 | ;; I could use some sort of in-emacs piping, but I'd rather not 508 | (call-process "sh" nil nil nil "-c" (format "git archive --format=tar %s | tar -xf - -C %s" (car revisions) (shell-quote-argument orgdiff--rev1dir))) 509 | (when (cdr revisions) 510 | (setq orgdiff--rev2dir (make-temp-file "orgdiff-" t)) 511 | (call-process "sh" nil nil nil "-c" (format "git archive --format=tar %s | tar -xf - -C %s" (cadr revisions) (shell-quote-argument orgdiff--rev2dir)))))) 512 | (setq orgdiff--rev1file 513 | (if orgdiff--rev1dir 514 | (expand-file-name (file-relative-name orgdiff-file-1 orgdiff--base-dir) 515 | orgdiff--rev1dir) 516 | orgdiff-file-1) 517 | orgdiff--rev2file 518 | (if orgdiff--rev2dir 519 | (expand-file-name (file-relative-name (or orgdiff-file-2 orgdiff-file-1) orgdiff--base-dir) 520 | orgdiff--rev2dir) 521 | (or orgdiff-file-2 orgdiff-file-1)))) 522 | 523 | (defun orgdiff--latex-create-from-org () 524 | (dolist (file (list orgdiff--rev1file orgdiff--rev2file)) 525 | (with-temp-buffer 526 | (setq buffer-file-name file 527 | default-directory (file-name-directory file)) 528 | (insert-file-contents file) 529 | (unless orgdiff-latexdiff-async 530 | (message "%s%s%s%s" (propertize "Orgdiff" 'face 'bold) ": Exporting " file " to TeX")) 531 | (let ((inhibit-message t)) 532 | (org-mode) 533 | (org-latex-export-to-latex orgdiff-latexdiff-async) 534 | (when orgdiff-latexdiff-async 535 | (push (caddar org-export-stack-contents) orgdiff--export-processes)) 536 | (set-buffer-modified-p nil)))) 537 | (message "%s%s" (propertize "Orgdiff" 'face 'bold) ": Exporting Org files to TeX...")) 538 | 539 | (defun orgdiff--latexdiff-wait-for-export-then (then) 540 | (if orgdiff--export-processes 541 | (progn 542 | (setq orgdiff--export-processes 543 | (delq nil (mapcar 544 | (lambda (proc) 545 | (if (memq (process-status proc) '(exit signal failed)) nil proc)) 546 | orgdiff--export-processes))) 547 | (run-at-time 0.5 nil #'orgdiff--latexdiff-wait-for-export-then then)) 548 | (funcall then))) 549 | 550 | (defun orgdiff--latexdiff-abort () 551 | (interactive) 552 | (when orgdiff--export-processes 553 | (ignore-errors (mapcar #'kill-process orgdiff--export-processes)) 554 | (message "%s processes aborted" (length orgdiff--export-processes)) 555 | (setq orgdiff--export-processes nil) 556 | (org-export-stack-clear))) 557 | 558 | (defun orgdiff--latexdiff-expand () 559 | (when orgdiff--latexdiff-flatten 560 | (message "%s%s" (propertize "Orgdiff" 'face 'bold) ": Flattening tex files...") 561 | (call-process "latexpand" nil nil nil orgdiff--rev1file) 562 | (call-process "latexpand" nil nil nil orgdiff--rev2file))) 563 | 564 | (defun orgdiff--latexdiff-do-diff () 565 | (message "%s%s" (propertize "Orgdiff" 'face 'bold) ": latexdiff-ing tex files...") 566 | (with-temp-buffer 567 | (apply #'call-process orgdiff-latexdiff-executable nil '(t nil) nil 568 | (delq nil (list 569 | "-t" (symbol-name orgdiff-latexdiff-type) 570 | "-s" (symbol-name orgdiff-latexdiff-subtype) 571 | "-f" (symbol-name orgdiff-latexdiff-floattype) 572 | (and orgdiff-latexdiff-allow-spaces "--allow-spaces") 573 | (and orgdiff-latexdiff-skip-deleted "--no-del") 574 | (if orgdiff-latexdiff-citation-markup 575 | "--enable-citation-markup" "--disable-citation-markup") 576 | (format "--math-markup=%s" orgdiff-latexdiff-math-markup) 577 | (format "--graphics-markup=%s" orgdiff-latexdiff-graphics-markup) 578 | (expand-file-name (concat (file-name-sans-extension orgdiff--rev1file) ".tex")) 579 | (expand-file-name (concat (file-name-sans-extension orgdiff--rev2file) ".tex"))))) 580 | (setq orgdiff--difffile 581 | (concat 582 | (file-name-directory orgdiff--rev2file) 583 | (if (string= (file-name-nondirectory orgdiff--rev1file) (file-name-nondirectory orgdiff--rev2file)) 584 | (file-name-base orgdiff--rev1file) 585 | (concat (file-name-base orgdiff--rev1file) 586 | "-diff-" 587 | (file-name-base orgdiff--rev2file))) 588 | (when orgdiff-git-revisions 589 | (concat "-" 590 | (car (split-string orgdiff-git-revisions "\\.\\.")) 591 | ":" 592 | (or (cadr (split-string orgdiff-git-revisions "\\.\\.")) 593 | "current"))) 594 | ".tex")) 595 | (run-hooks 'orgdiff-latexdiff-postprocess-hooks) 596 | (let (before-save-hook after-save-hook write-file-functions) 597 | (setq buffer-file-name orgdiff--difffile) 598 | (save-buffer 0)))) 599 | 600 | (defun orgdiff--latexdiff-compile () 601 | (let* ((compilers (mapcar 602 | (lambda (file) 603 | (with-temp-buffer 604 | (insert-file-contents file) 605 | (when (search-forward 606 | (string-trim-right (format org-latex-compiler-file-string "") "\n") 607 | nil t) 608 | (buffer-substring-no-properties (point) (progn (end-of-line) (point)))))) 609 | (list (concat (file-name-sans-extension orgdiff--rev1file) ".tex") 610 | (concat (file-name-sans-extension orgdiff--rev2file) ".tex")))) 611 | (compiler (nth (max (cl-position (car compilers) orgdiff-latex-compiler-priorities :test #'equal) 612 | (cl-position (cadr compilers) orgdiff-latex-compiler-priorities :test #'equal)) 613 | orgdiff-latex-compiler-priorities)) 614 | (compile-command 615 | (mapcar 616 | (lambda (arg) 617 | (thread-last arg 618 | (replace-regexp-in-string "%compiler" compiler) 619 | (replace-regexp-in-string "%texfile" 620 | (expand-file-name orgdiff--difffile)))) 621 | orgdiff-latex-compile-command)) 622 | (default-directory (file-name-directory orgdiff--difffile))) 623 | (message "%s%s" (propertize "Orgdiff" 'face 'bold) ": compiling diff file...") 624 | (push 625 | (apply #'start-process "orgdiff--latexdiff-compile" "*orgdiff--latexdiff-compile*" compile-command) 626 | orgdiff--export-processes))) 627 | 628 | (defun orgdiff--latexdiff-handle-pdf () 629 | (message "%s%s" (propertize "Orgdiff" 'face 'bold) ": diff created.") 630 | ;; Cleanup 631 | (when org-latex-remove-logfiles 632 | (mapc #'delete-file 633 | (directory-files 634 | (file-name-directory orgdiff--difffile) 635 | t 636 | (concat (regexp-quote (file-name-base orgdiff--difffile)) 637 | "\\(?:\\.[0-9]+\\)?\\." 638 | (regexp-opt org-latex-logfiles-extensions)) 639 | t))) 640 | ;; Move the PDF to the right place 641 | (let ((pdf-file (concat (file-name-sans-extension orgdiff--difffile) ".pdf")) 642 | (dest (concat (file-name-directory orgdiff-file-1) 643 | (file-name-base orgdiff--difffile) 644 | ".pdf"))) 645 | (unless (file-exists-p pdf-file) 646 | (user-error "Error! Diff PDF was not produced")) 647 | (rename-file pdf-file dest t) 648 | (when (eq 'open (plist-get orgdiff--latexdiff--completion-action :action)) 649 | (find-file-other-window dest)))) 650 | 651 | (provide 'orgdiff) 652 | ;;; orgdiff.el ends here 653 | --------------------------------------------------------------------------------