Browse Source

Implement a basic API around macros

* lisp/org.el (org-mode): Initialize macros templates.
(org-macro-templates): New variable.
(org-macro-expand, org-macro-replace-all,
org-macro-initialize-templates): New functions.
* testing/lisp/test-org.el: Add tests.
Nicolas Goaziou 12 years ago
parent
commit
4a2f3c2093
2 changed files with 155 additions and 2 deletions
  1. 115 2
      lisp/org.el
  2. 40 0
      testing/lisp/test-org.el

+ 115 - 2
lisp/org.el

@@ -5085,11 +5085,13 @@ The following commands are available:
 		'local)
   ;; Check for running clock before killing a buffer
   (org-add-hook 'kill-buffer-hook 'org-check-running-clock nil 'local)
+  ;; Initialize macros templates.
+  (org-macro-initialize-templates)
+  ;; Initialize radio targets.
+  (org-update-radio-target-regexp)
   ;; Indentation.
   (org-set-local 'indent-line-function 'org-indent-line)
   (org-set-local 'indent-region-function 'org-indent-region)
-  ;; Initialize radio targets.
-  (org-update-radio-target-regexp)
   ;; Filling and auto-filling.
   (org-setup-filling)
   ;; Comments.
@@ -20864,6 +20866,117 @@ hierarchy of headlines by UP levels before marking the subtree."
       (call-interactively 'org-mark-element)
     (org-mark-element)))
 
+
+;;; Macros
+
+;; Macros are expanded with `org-macro-replace-all', which relies
+;; internally on `org-macro-expand'.
+
+;; Templates for expansion are stored in the buffer-local variable
+;; `org-macro-templates'.  This variable is updated by
+;; `org-macro-initialize-templates'.
+
+
+(defvar org-macro-templates nil
+  "Alist containing all macro templates in current buffer.
+Associations are in the shape of (NAME . TEMPLATE) where NAME
+stands for macro's name and template for its replacement value,
+both as strings.  This is an internal variable.  Do not set it
+directly, use instead:
+
+  #+MACRO: name template")
+(make-variable-buffer-local 'org-macro-templates)
+
+(defun org-macro-expand (macro)
+  "Return expanded MACRO, as a string.
+MACRO is an object, obtained, for example, with
+`org-element-context'.  Return nil if no template was found."
+  (let ((template
+	 (cdr (assoc-string (org-element-property :key macro)
+			    org-macro-templates
+			    ;; Macro names are case-insensitive.
+			    t))))
+    (when template
+      (let ((value (replace-regexp-in-string
+                    "\\$[0-9]+"
+                    (lambda (arg)
+                      (or (nth (1- (string-to-number (substring arg 1)))
+                               (org-element-property :args macro))
+                          ;; No argument provided: remove
+                          ;; place-holder.
+                          ""))
+                    template)))
+        ;; VALUE starts with "(eval": it is a s-exp, `eval' it.
+        (when (string-match "\\`(eval\\>" value)
+          (setq value (eval (read value))))
+        ;; Return string.
+        (format "%s" (or value ""))))))
+
+(defun org-macro-replace-all ()
+  "Replace all macros in current buffer by their expansion."
+  (save-excursion
+    (goto-char (point-min))
+    (while (re-search-forward "{{{[-A-Za-z0-9_]" nil t)
+      (let ((object (org-element-context)))
+        (when (eq (org-element-type object) 'macro)
+          (let ((value (org-macro-expand object)))
+            (when value
+              (delete-region
+               (org-element-property :begin object)
+               ;; Preserve white spaces after the macro.
+               (progn (goto-char (org-element-property :end object))
+                      (skip-chars-backward " \t")
+                      (point)))
+              ;; Leave point before replacement in case of recursive
+              ;; expansions.
+              (save-excursion (insert value)))))))))
+
+(defun org-macro-initialize-templates ()
+  "Collect macro templates defined in current buffer.
+Templates are stored in buffer-local variable
+`org-macro-templates'.  In addition to buffer-defined macros, the
+function installs the following ones: \"property\", \"date\",
+\"time\". and, if appropriate, \"input-file\" and
+\"modification-time\"."
+  (org-with-wide-buffer
+   (goto-char (point-min))
+   (let ((case-fold-search t)
+	 (set-template
+	  (lambda (cell)
+	    ;; Add CELL to `org-macro-templates' if there's no
+	    ;; association matching its CAR already.  Otherwise,
+	    ;; replace old association with CELL.
+	    (let* ((value (cdr cell))
+		   (key (car cell))
+		   (old-template (assoc key org-macro-templates)))
+	      (if old-template (setcdr old-template value)
+		(push cell org-macro-templates))))))
+     ;; Install buffer-local macros.
+     (while (re-search-forward "^[ \t]*#\\+MACRO:" nil t)
+       (let ((element (org-element-at-point)))
+	 (when (eq (org-element-type element) 'keyword)
+	   (let ((value (org-element-property :value element)))
+	     (when (string-match "^\\(.*?\\)\\(?:\\s-+\\(.*\\)\\)?\\s-*$" value)
+	       (funcall set-template
+			(cons (match-string 1 value)
+			      (or (match-string 2 value) ""))))))))
+     ;; Install hard-coded macros.
+     (mapc (lambda (cell) (funcall set-template cell))
+	   (list
+	    (cons "property" "(eval (org-entry-get nil \"$1\" 'selective))")
+	    (cons "date" "(eval (format-time-string \"$1\"))")
+	    (cons "time" "(eval (format-time-string \"$1\"))")))
+     (let ((visited-file (buffer-file-name (buffer-base-buffer))))
+       (when (and visited-file (file-exists-p visited-file))
+	 (mapc (lambda (cell) (funcall set-template cell))
+	       (list
+		(cons "input-file" (file-name-nondirectory visited-file))
+		(cons "modification-time"
+		      (format "(eval (format-time-string \"$1\" '%s))"
+			      (prin1-to-string
+			       (nth 5 (file-attributes visited-file))))))))))))
+
+
 ;;; Indentation
 
 (defun org-indent-line ()

+ 40 - 0
testing/lisp/test-org.el

@@ -129,6 +129,46 @@ http://article.gmane.org/gmane.emacs.orgmode/21459/"
     (should (looking-at "\\* Test"))))
 
 
+
+;;; Macros
+
+(ert-deftest test-org/macro-replace-all ()
+  "Test `org-macro-replace-all' specifications."
+  ;; Standard test.
+  (should
+   (equal
+    "#+MACRO: A B\n1 B 3"
+    (org-test-with-temp-text "#+MACRO: A B\n1 {{{A}}} 3"
+      (progn (org-macro-initialize-templates)
+	     (org-macro-replace-all)
+	     (buffer-string)))))
+  ;; Macro with arguments.
+  (should
+   (equal
+    "#+MACRO: macro $1 $2\nsome text"
+    (org-test-with-temp-text "#+MACRO: macro $1 $2\n{{{macro(some,text)}}}"
+      (progn (org-macro-initialize-templates)
+	     (org-macro-replace-all)
+	     (buffer-string)))))
+  ;; Macro with "eval".
+  (should
+   (equal
+    "#+MACRO: add (eval (+ $1 $2))\n3"
+    (org-test-with-temp-text "#+MACRO: add (eval (+ $1 $2))\n{{{add(1,2)}}}"
+      (progn (org-macro-initialize-templates)
+	     (org-macro-replace-all)
+	     (buffer-string)))))
+  ;; Nested macros.
+  (should
+   (equal
+    "#+MACRO: in inner\n#+MACRO: out {{{in}}} outer\ninner outer"
+    (org-test-with-temp-text
+	"#+MACRO: in inner\n#+MACRO: out {{{in}}} outer\n{{{out}}}"
+      (progn (org-macro-initialize-templates)
+	     (org-macro-replace-all)
+	     (buffer-string))))))
+
+
 
 ;;; Filling