;;; iadrian-iadrian-org-auto-tangle.el --- Automatically and Asynchronously tangles org files on save -*- lexical-binding: t; -*- ;; Original Author: Yilkal Argaw ;; Original License: https://github.com/yilkalargaw/org-auto-tangle/blob/master/License.org ;; Maintainer: Ionut Adrian Ciolan ;; Version: 0.6.1 ;; Keywords: outlines ;; Package-Requires: ((emacs "24.1") (async "1.9.3")) ;; This file is not part of GNU Emacs ;;; Commentary: ;; It is common to want to tangle org files everytime you save your changes, ;; especially for tangled init files. So this program allows you to ;; do so using #+auto_tangle option in an org file. It also accomplishes ;; this feat asynchronously so it does not let you Emacs session hang. ;;; Usage: ;; - Add #+auto_tangle: t to your tangled org file ;; - Make changes to the emacs file and save your changes (require 'async) (require 'cl-lib) (require 'org) (require 'ox) ; org-export--parse-option-keyword (defcustom iadrian-org-auto-tangle-default nil "Default behavior of iadrian-org-auto-tangle. If nil (default), auto-tangle will only happen on buffers with the `#+auto_tangle: t' keyword. If t, auto-tangle will happen on all Org buffers unless `#+auto_tangle: nil' is set." :group 'iadrian-org-auto-tangle :type 'boolean) (defcustom iadrian-org-auto-tangle-babel-safelist '() "List of full path of files for which code blocks need to be evaluated. By default, code blocks are not evaluated during the auto-tangle to avoid possible code execution from unstrusted source. To enable code blocks evaluation for a specific file, add its full path to this list." :group 'iadrian-org-auto-tangle :type '(repeat (file :tag "Full file path"))) (defun iadrian-org-auto-tangle-find-value (buffer) "Return the value of the `auto_tangle' keyword in BUFFER." (with-current-buffer buffer (cdr (assoc "AUTO_TANGLE" (org-collect-keywords '("AUTO_TANGLE")))))) ;; This is modeled after `org-export-filters-alist', since it is ;; passed to `org-export--parse-option-keyword'. (defconst iadrian-org-auto-tangle-options-alist '((:with-vars nil "vars" iadrian-org-auto-tangle-with-vars)) "Alist between auto-tangle properties and ways to set them. The key of the alist is the property name, and the value is a list like (KEYWORD OPTION DEFAULT BEHAVIOR) where: KEYWORD is a string representing a buffer keyword, or nil. Each property defined this way can also be set, during subtree export, through a headline property named after the keyword with the \"EXPORT_\" prefix (i.e. DATE keyword and EXPORT_DATE property). OPTION is a string that could be found in an #+OPTIONS: line. DEFAULT is the default value for the property. BEHAVIOR determines how Org should handle multiple keywords for the same property. It is a symbol among: nil Keep old value and discard the new one. t Replace old value with the new one. `space' Concatenate the values, separating them with a space. `newline' Concatenate the values, separating them with a newline. `split' Split values at white spaces, and cons them to the previous list. `parse' Parse value as a list of strings and Org objects, which can then be transcoded with, e.g., `org-export-data'. It implies `space' behavior. Values set through KEYWORD and OPTION have precedence over DEFAULT.") (defgroup iadrian-org-auto-tangle nil "Automatic tangling of `org-mode' documents." :tag "Org Auto Tangle" :group 'org-babel) (defcustom iadrian-org-auto-tangle-with-vars nil "Non-nil means pass VARS variables to the async tangling process. This option can also be set with the AUTO_TANGLE keyword, e.g. \"vars:calendar-latitude\". The `org-src-preserve-indentation', `org-babel-pre-tangle-hook', and `org-babel-post-tangle-hook' variables are automatically preserved and do not need to be listed here." :group 'iadrian-org-auto-tangle :type '(repeat (symbol :tag "Variable name"))) (defun iadrian-org-auto-tangle--get-inbuffer-options () "Return current buffer auto-tangle options, as a plist. Assume buffer is in Org mode. Narrowing, if any, is ignored." (let (plist) ;; Read options in the current buffer and return value. (dolist (entry (org-collect-keywords '("AUTO_TANGLE")) plist) (pcase entry (`("AUTO_TANGLE" . ,values) (setq plist (apply #'org-combine-plists plist (mapcar (lambda (v) (let ((org-export-options-alist))) (iadrian-org-auto-tangle--parse-auto-tangle-keyword v)) values)))))))) (defun iadrian-org-auto-tangle--parse-auto-tangle-keyword (auto-tangle) "Parse an AUTO-TANGLE line and return values as a plist." (let ((org-export-options-alist iadrian-org-auto-tangle-options-alist)) (org-export--parse-option-keyword auto-tangle))) (defun iadrian-org-auto-tangle-async (file) "Invoke `org-babel-tangle-file' asynchronously on FILE." (message "Tangling %s..." (buffer-file-name)) (async-start (let* ((buf-vars (plist-get (iadrian-org-auto-tangle--get-inbuffer-options) :with-vars)) (with-vars (if buf-vars (mapcar #'intern (org-uniquify (org-split-string (symbol-name buf-vars) ":"))) iadrian-org-auto-tangle-with-vars)) (preserved (mapcar (lambda (v) (cons v (symbol-value v))) (append '(org-src-preserve-indentation org-babel-pre-tangle-hook org-babel-post-tangle-hook) with-vars))) (evaluate (not (member file iadrian-org-auto-tangle-babel-safelist)))) (lambda () (require 'org) (let ((start-time (current-time)) (non-essential t) (org-confirm-babel-evaluate evaluate)) (cl-progv (mapcar #'car preserved) (mapcar #'cdr preserved) (org-babel-tangle-file file)) (format "%.2f" (float-time (time-since start-time)))))) (let ((message-string (format "Tangling %S completed after" file))) (lambda (tangle-time) (message "%s %s seconds" message-string tangle-time))))) (defun iadrian-org-auto-tangle-tangle-if-needed () "Call iadrian-org-auto-tangle-async if needed. Tangle will happen depending on the value of `iadrian-org-auto-tangle-default' and on the presence and value of the `#+auto_tangle' keyword in the current buffer. If present, `#+auto_tangle' always overrides `iadrian-org-auto-tangle-default'." (let ((auto-tangle-kw (iadrian-org-auto-tangle-find-value (current-buffer)))) (when (and (derived-mode-p 'org-mode) (if auto-tangle-kw (not (member "nil" auto-tangle-kw)) iadrian-org-auto-tangle-default)) (iadrian-org-auto-tangle-async (buffer-file-name))))) ;;;###autoload (define-minor-mode iadrian-org-auto-tangle-mode "Automatically tangle org-mode files with the option #+auto_tangle: t." :lighter " org-a-t" (if iadrian-org-auto-tangle-mode (add-hook 'after-save-hook #'iadrian-org-auto-tangle-tangle-if-needed nil 'local) (remove-hook 'after-save-hook #'iadrian-org-auto-tangle-tangle-if-needed 'local))) (provide 'iadrian-org-auto-tangle) ;;; iadrian-org-auto-tangle.el ends here