config

Personal configuration.
git clone git://code.dwrz.net/config
Log | Files | Refs

org-macro.el (18083B)


      1 ;;; org-macro.el --- Macro Replacement Code for Org  -*- lexical-binding: t; -*-
      2 
      3 ;; Copyright (C) 2013-2024 Free Software Foundation, Inc.
      4 
      5 ;; Author: Nicolas Goaziou <n.goaziou@gmail.com>
      6 ;; Keywords: outlines, hypermedia, calendar, text
      7 
      8 ;; This file is part of GNU Emacs.
      9 
     10 ;; GNU Emacs is free software: you can redistribute it and/or modify
     11 ;; it under the terms of the GNU General Public License as published by
     12 ;; the Free Software Foundation, either version 3 of the License, or
     13 ;; (at your option) any later version.
     14 
     15 ;; GNU Emacs is distributed in the hope that it will be useful,
     16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18 ;; GNU General Public License for more details.
     19 
     20 ;; You should have received a copy of the GNU General Public License
     21 ;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
     22 
     23 ;;; Commentary:
     24 
     25 ;; Macros are expanded with `org-macro-replace-all', which relies
     26 ;; internally on `org-macro-expand'.
     27 
     28 ;; Default templates for expansion are stored in the buffer-local
     29 ;; variable `org-macro-templates'.  This variable is updated by
     30 ;; `org-macro-initialize-templates', which recursively calls
     31 ;; `org-macro--collect-macros' in order to read setup files.
     32 
     33 ;; Argument in macros are separated with commas.  Proper escaping rules
     34 ;; are implemented in `org-macro-escape-arguments' and arguments can
     35 ;; be extracted from a string with `org-macro-extract-arguments'.
     36 
     37 ;; Along with macros defined through #+MACRO: keyword, default
     38 ;; templates include the following hard-coded macros:
     39 ;;   {{{time(format-string)}}},
     40 ;;   {{{property(node-property)}}},
     41 ;;   {{{input-file}}},
     42 ;;   {{{modification-time(format-string)}}},
     43 ;;   {{{n(counter,action}}}.
     44 
     45 ;; Upon exporting, "ox.el" will also provide {{{author}}}, {{{date}}},
     46 ;; {{{email}}} and {{{title}}} macros.
     47 
     48 ;;; Code:
     49 
     50 (require 'org-macs)
     51 (org-assert-version)
     52 
     53 (require 'cl-lib)
     54 (require 'org-macs)
     55 (require 'org-compat)
     56 
     57 (declare-function org-collect-keywords "org" (keywords &optional unique directory))
     58 (declare-function org-element-at-point "org-element" (&optional pom cached-only))
     59 (declare-function org-element-context "org-element" (&optional element))
     60 (declare-function org-element-copy "org-element-ast" (datum))
     61 (declare-function org-element-macro-parser "org-element" ())
     62 (declare-function org-element-keyword-parser "org-element" (limit affiliated))
     63 (declare-function org-element-put-property "org-element-ast" (node property value))
     64 (declare-function org-element-parse-secondary-string "org-element" (string restriction &optional parent))
     65 (declare-function org-element-property "org-element-ast" (property node))
     66 (declare-function org-element-begin "org-element" (node))
     67 (declare-function org-element-end "org-element" (node))
     68 (declare-function org-element-restriction "org-element" (element))
     69 (declare-function org-element-type "org-element-ast" (node &optional anonymous))
     70 (declare-function org-element-type-p "org-element-ast" (node types))
     71 (declare-function org-entry-get "org" (pom property &optional inherit literal-nil))
     72 (declare-function org-file-contents "org" (file &optional noerror nocache))
     73 (declare-function org-in-commented-heading-p "org" (&optional no-inheritance element))
     74 (declare-function org-link-search "ol" (s &optional avoid-pos stealth))
     75 (declare-function org-mode "org" ())
     76 (declare-function vc-backend "vc-hooks" (f))
     77 (declare-function vc-call "vc-hooks" (fun file &rest args) t)
     78 (declare-function vc-exec-after "vc-dispatcher" (code &optional success))
     79 
     80 (defvar org-link-search-must-match-exact-headline)
     81 
     82 ;;; Variables
     83 
     84 (defvar-local org-macro-templates nil
     85   "Alist containing all macro templates in current buffer.
     86 Associations are in the shape of (NAME . TEMPLATE) where NAME
     87 stands for macro's name and template for its replacement value,
     88 both as strings.  This is an internal variable.  Do not set it
     89 directly, use instead:
     90 
     91   #+MACRO: name template")
     92 
     93 ;;; Functions
     94 
     95 (defun org-macro--makeargs (template)
     96   "Compute the formal arglist to use for TEMPLATE."
     97   (let ((max 0) (i 0))
     98     (while (string-match "\\$\\([0-9]+\\)" template i)
     99       (setq i (match-end 0))
    100       (setq max (max max (string-to-number (match-string 1 template)))))
    101     (let ((args '(&rest _)))
    102       (if (< max 1) args ;Avoid `&optional &rest', refused by Emacs-26!
    103         (while (> max 0)
    104           (push (intern (format "$%d" max)) args)
    105           (setq max (1- max)))
    106         (cons '&optional args)))))
    107 
    108 (defun org-macro--set-templates (templates)
    109   "Set template for the macro NAME.
    110 VALUE is the template of the macro.  The new value override the
    111 previous one, unless VALUE is nil.  Return the updated list."
    112   (let ((new-templates nil))
    113     (pcase-dolist (`(,name . ,value) templates)
    114       (let ((old-definition (assoc name new-templates)))
    115         ;; This code can be evaluated unconditionally, as a part of
    116         ;; loading Org mode.  We *must not* evaluate any code present
    117         ;; inside the Org buffer while loading.  Org buffers may come
    118         ;; from various sources, like received email messages from
    119         ;; potentially malicious senders.  Org mode might be used to
    120         ;; preview such messages and no code evaluation from inside the
    121         ;; received Org text should ever happen without user consent.
    122         (when (and (stringp value) (string-match-p "\\`(eval\\>" value))
    123           ;; Pre-process the evaluation form for faster macro expansion.
    124           (let* ((args (org-macro--makeargs value))
    125                  (body
    126                   (condition-case nil
    127                       ;; `value' is of the form "(eval ...)" but we
    128                       ;; don't want this to mean to pass the result to
    129                       ;; `eval' (which would cause double evaluation),
    130                       ;; so we strip the `eval' away with `cadr'.
    131 		      (cadr (read value))
    132 		    (error
    133                      (user-error "Invalid definition for macro %S" name)))))
    134 	    (setq value `(lambda ,args ,body))))
    135         (cond ((and value old-definition) (setcdr old-definition value))
    136 	      (old-definition)
    137 	      (t (push (cons name (or value "")) new-templates)))))
    138     new-templates))
    139 
    140 (defun org-macro--collect-macros ()
    141   "Collect macro definitions in current buffer and setup files.
    142 Return an alist containing all macro templates found."
    143   (let ((templates
    144          `(("author" . ,(org-macro--find-keyword-value "AUTHOR" t))
    145 	   ("email" . ,(org-macro--find-keyword-value "EMAIL"))
    146 	   ("title" . ,(org-macro--find-keyword-value "TITLE" t))
    147 	   ("date" . ,(org-macro--find-date)))))
    148     (pcase (org-collect-keywords '("MACRO"))
    149       (`(("MACRO" . ,values))
    150        (dolist (value values)
    151 	 (when (string-match "^\\(\\S-+\\)[ \t]*" value)
    152 	   (let ((name (match-string 1 value))
    153 		 (definition (substring value (match-end 0))))
    154              (push (cons name definition) templates))))))
    155     templates))
    156 
    157 (defun org-macro-initialize-templates (&optional default)
    158   "Collect macro templates defined in current buffer.
    159 
    160 DEFAULT is a list of globally available templates.
    161 
    162 Templates are stored in buffer-local variable `org-macro-templates'.
    163 
    164 In addition to buffer-defined macros, the function installs the
    165 following ones: \"n\", \"author\", \"email\", \"keyword\",
    166 \"time\", \"property\", and, if the buffer is associated to
    167 a file, \"input-file\" and \"modification-time\"."
    168   (require 'org-element)
    169   (org-macro--counter-initialize)	;for "n" macro
    170   (setq org-macro-templates
    171 	(nconc
    172 	 ;; Install user-defined macros.  Local macros have higher
    173          ;; precedence than global ones.
    174          (org-macro--set-templates (append default (org-macro--collect-macros)))
    175 	 ;; Install file-specific macros.
    176 	 (let ((visited-file (buffer-file-name (buffer-base-buffer))))
    177 	   (and visited-file
    178 		(file-exists-p visited-file)
    179 		(list
    180 		 `("input-file" . ,(file-name-nondirectory visited-file))
    181 		 `("modification-time" .
    182 		   ,(let ((modtime (file-attribute-modification-time
    183 			            (file-attributes visited-file))))
    184 		      (lambda (arg1 &optional arg2 &rest _)
    185 		        (format-time-string
    186                          arg1
    187                          (or (and (org-string-nw-p arg2)
    188                                   (org-macro--vc-modified-time visited-file))
    189                              modtime))))))))
    190 	 ;; Install generic macros.
    191 	 '(("keyword" . (lambda (arg1 &rest _)
    192                           (org-macro--find-keyword-value arg1 t)))
    193 	   ("n" . (lambda (&optional arg1 arg2 &rest _)
    194                     (org-macro--counter-increment arg1 arg2)))
    195            ("property" . (lambda (arg1 &optional arg2 &rest _)
    196                            (org-macro--get-property arg1 arg2)))
    197 	   ("time" . (lambda (arg1 &rest _)
    198                        (format-time-string arg1)))))))
    199 
    200 (defun org-macro-expand (macro templates)
    201   "Return expanded MACRO, as a string.
    202 MACRO is an object, obtained, for example, with
    203 `org-element-context'.  TEMPLATES is an alist of templates used
    204 for expansion.  See `org-macro-templates' for a buffer-local
    205 default value.  Return nil if no template was found."
    206   (let ((template
    207 	 ;; Macro names are case-insensitive.
    208 	 (cdr (assoc-string (org-element-property :key macro) templates t))))
    209     (when template
    210       (let* ((value
    211 	      (if (functionp template)
    212 	          (apply template (org-element-property :args macro))
    213 	        (replace-regexp-in-string
    214 	         "\\$[0-9]+"
    215 	         (lambda (m)
    216 		   (or (nth (1- (string-to-number (substring m 1)))
    217 			    (org-element-property :args macro))
    218 		       ;; No argument: remove place-holder.
    219 		       ""))
    220 		 template nil 'literal))))
    221         ;; Force return value to be a string.
    222         (format "%s" (or value ""))))))
    223 
    224 (defun org-macro-replace-all (templates &optional keywords)
    225   "Replace all macros in current buffer by their expansion.
    226 
    227 TEMPLATES is an alist of templates used for expansion.  See
    228 `org-macro-templates' for a buffer-local default value.
    229 
    230 Optional argument KEYWORDS, when non-nil is a list of keywords,
    231 as strings, where macro expansion is allowed.
    232 
    233 Return an error if a macro in the buffer cannot be associated to
    234 a definition in TEMPLATES."
    235   (org-with-wide-buffer
    236    (goto-char (point-min))
    237    (let ((properties-regexp (format "\\`EXPORT_%s\\+?\\'"
    238 				    (regexp-opt keywords)))
    239 	 record)
    240      (while (re-search-forward "{{{[-A-Za-z0-9_]" nil t)
    241        (unless (save-match-data (org-in-commented-heading-p))
    242 	 (let* ((datum (save-match-data (org-element-context)))
    243 		(type (org-element-type datum))
    244 		(macro
    245 		 (cond
    246 		  ((eq type 'macro) datum)
    247 		  ;; In parsed keywords and associated node
    248 		  ;; properties, force macro recognition.
    249 		  ((or (and (eq type 'keyword)
    250 			    (member (org-element-property :key datum) keywords))
    251 		       (and (eq type 'node-property)
    252 			    (string-match-p properties-regexp
    253 					    (org-element-property :key datum))))
    254 		   (save-excursion
    255 		     (goto-char (match-beginning 0))
    256 		     (org-element-macro-parser))))))
    257 	   (when macro
    258              ;; `:parent' property might change as we modify buffer.
    259              ;; We do not care about it when checking for circular
    260              ;; dependencies.  So, setting `:parent' to nil making sure
    261              ;; that actual macro element (if org-element-cache is
    262              ;; active) is unchanged.
    263              (setq macro (cl-copy-list macro))
    264              (org-element-put-property macro :parent nil)
    265 	     (let* ((key (org-element-property :key macro))
    266 		    (value (org-macro-expand macro templates))
    267 		    (begin (org-element-begin macro))
    268 		    (signature (list begin
    269 				     macro
    270 				     (org-element-property :args macro))))
    271 	       ;; Avoid circular dependencies by checking if the same
    272 	       ;; macro with the same arguments is expanded at the
    273 	       ;; same position twice.
    274 	       (cond ((member signature record)
    275 		      (error "Circular macro expansion: %s" key))
    276 		     (value
    277 		      (push signature record)
    278 		      (delete-region
    279 		       begin
    280 		       ;; Preserve white spaces after the macro.
    281 		       (progn (goto-char (org-element-end macro))
    282 			      (skip-chars-backward " \t")
    283 			      (point)))
    284 		      ;; Leave point before replacement in case of
    285 		      ;; recursive expansions.
    286 		      (save-excursion (insert value)))
    287 		     ;; Special "results" macro: if it is not defined,
    288 		     ;; simply leave it as-is.  It will be expanded in
    289 		     ;; a second phase.
    290 		     ((equal key "results"))
    291 		     (t
    292 		      (error "Undefined Org macro: %s; aborting"
    293 			     (org-element-property :key macro))))))))))))
    294 
    295 (defun org-macro-escape-arguments (&rest args)
    296   "Build macro's arguments string from ARGS.
    297 ARGS are strings.  Return value is a string with arguments
    298 properly escaped and separated with commas.  This is the opposite
    299 of `org-macro-extract-arguments'."
    300   (let ((s ""))
    301     (dolist (arg (reverse args) (substring s 1))
    302       (setq s
    303 	    (concat
    304 	     ","
    305 	     (replace-regexp-in-string
    306 	      "\\(\\\\*\\),"
    307 	      (lambda (m)
    308 		(concat (make-string (1+ (* 2 (length (match-string 1 m)))) ?\\)
    309 			","))
    310 	      ;; If a non-terminal argument ends on backslashes, make
    311 	      ;; sure to also escape them as they will be followed by
    312 	      ;; a comma.
    313 	      (concat arg (and (not (equal s ""))
    314 			       (string-match "\\\\+\\'" arg)
    315 			       (match-string 0 arg)))
    316 	      nil t)
    317 	     s)))))
    318 
    319 (defun org-macro-extract-arguments (s)
    320   "Extract macro arguments from string S.
    321 S is a string containing comma separated values properly escaped.
    322 Return a list of arguments, as strings.  This is the opposite of
    323 `org-macro-escape-arguments'."
    324   ;; Do not use `org-split-string' since empty strings are
    325   ;; meaningful here.
    326   (split-string
    327    (replace-regexp-in-string
    328     "\\(\\\\*\\),"
    329     (lambda (str)
    330       (let ((len (length (match-string 1 str))))
    331 	(concat (make-string (/ len 2) ?\\)
    332 		(if (zerop (mod len 2)) "\000" ","))))
    333     s nil t)
    334    "\000"))
    335 
    336 
    337 ;;; Helper functions and variables for internal macros
    338 
    339 (defun org-macro--get-property (property location)
    340   "Find PROPERTY's value at LOCATION.
    341 PROPERTY is a string.  LOCATION is a search string, as expected
    342 by `org-link-search', or the empty string."
    343   (org-with-wide-buffer
    344    (when (org-string-nw-p location)
    345      (condition-case _
    346 	 (let ((org-link-search-must-match-exact-headline t))
    347 	   (org-link-search location nil t))
    348        (error
    349 	(error "Macro property failed: cannot find location %s" location))))
    350    (org-entry-get nil property 'selective)))
    351 
    352 (defun org-macro--find-keyword-value (name &optional collect)
    353   "Find value for keyword NAME in current buffer.
    354 Return value associated to the keywords named after NAME, as
    355 a string, or nil.  When optional argument COLLECT is non-nil,
    356 concatenate values, separated with a space, from various keywords
    357 in the buffer."
    358   (org-with-point-at 1
    359     (let ((regexp (format "^[ \t]*#\\+%s:" (regexp-quote name)))
    360 	  (case-fold-search t)
    361 	  (result nil))
    362       (catch :exit
    363 	(while (re-search-forward regexp nil t)
    364 	  (let ((element (org-with-point-at (match-beginning 0) (org-element-keyword-parser (line-end-position) (list (match-beginning 0))))))
    365 	    (when (org-element-type-p element 'keyword)
    366 	      (let ((value (org-element-property :value element)))
    367 		(if (not collect) (throw :exit value)
    368 		  (setq result (concat result " " value)))))))
    369 	(and result (org-trim result))))))
    370 
    371 (defun org-macro--find-date ()
    372   "Find value for DATE in current buffer.
    373 Return value as a string."
    374   (let* ((value (org-macro--find-keyword-value "DATE"))
    375 	 (date (org-element-parse-secondary-string
    376 		value (org-element-restriction 'keyword))))
    377     (if (and (consp date)
    378 	     (not (cdr date))
    379 	     (org-element-type-p (car date) 'timestamp))
    380 	(format "(eval (if (org-string-nw-p $1) %s %S))"
    381 		(format "(org-format-timestamp '%S $1)"
    382 			(org-element-put-property
    383                          (org-element-copy (car date))
    384                          ;; Remove non-printable.
    385                          :buffer nil))
    386 		value)
    387       value)))
    388 
    389 (defun org-macro--vc-modified-time (file)
    390   (require 'vc) ; Not everything we need is autoloaded.
    391   (save-window-excursion
    392     (when (vc-backend file)
    393       (let ((buf (get-buffer-create " *org-vc*"))
    394 	    (case-fold-search t)
    395 	    date)
    396 	(unwind-protect
    397 	    (progn
    398 	      (vc-call print-log (list file) buf nil nil 1)
    399 	      (with-current-buffer buf
    400 		(vc-exec-after
    401 		 (lambda ()
    402 		   (goto-char (point-min))
    403 		   (when (re-search-forward "Date:?[ \t]*" nil t)
    404 		     (let ((time (parse-time-string
    405 				  (buffer-substring
    406 				   (point) (line-end-position)))))
    407 		       (when (cl-some #'identity time)
    408 			 (setq date (org-encode-time time))))))))
    409 	      (let ((proc (get-buffer-process buf)))
    410 		(while (and proc (accept-process-output proc .5 nil t)))))
    411 	  (kill-buffer buf))
    412 	date))))
    413 
    414 (defvar org-macro--counter-table nil
    415   "Hash table containing counter value per name.")
    416 
    417 (defun org-macro--counter-initialize ()
    418   "Initialize `org-macro--counter-table'."
    419   (setq org-macro--counter-table (make-hash-table :test #'equal)))
    420 
    421 (defun org-macro--counter-increment (name &optional action)
    422   "Increment counter NAME.
    423 NAME is a string identifying the counter.
    424 
    425 When non-nil, optional argument ACTION is a string.
    426 
    427 If the string is \"-\", keep the NAME counter at its current
    428 value, i.e. do not increment.
    429 
    430 If the string represents an integer, set the counter to this number.
    431 
    432 Any other non-empty string resets the counter to 1."
    433   (let ((name-trimmed (if (stringp name) (org-trim name) ""))
    434         (action-trimmed (when (org-string-nw-p action)
    435                           (org-trim action))))
    436     (puthash name-trimmed
    437              (cond ((not (org-string-nw-p action-trimmed))
    438                     (1+ (gethash name-trimmed org-macro--counter-table 0)))
    439                    ((string= "-" action-trimmed)
    440                     (gethash name-trimmed org-macro--counter-table 1))
    441                    ((string-match-p "\\`[0-9]+\\'" action-trimmed)
    442                     (string-to-number action-trimmed))
    443                    (t 1))
    444              org-macro--counter-table)))
    445 
    446 (provide 'org-macro)
    447 
    448 ;;; org-macro.el ends here