Browse Source

org-export: Internal changes to back-end definition

* contrib/lisp/org-export.el (org-export-registered-backends): New
  variable.
(org-export-define-backend, org-export-define-derived-backend): Use
new variable. Also redefine how sub-menus are defined.
(org-export-backend-filters, org-export-backend-menu,
org-export-backend-options, org-export-backend-translate-table): New
functions.
(org-export-get-environment, org-export--parse-option-keyword,
org-export--get-subtree-options, org-export--get-inbuffer-options,
org-export--get-global-options, org-export-install-filters,
org-export-with-backend): Access to data stored in new variable.
(org-export-dispatch-ui): Display sub-menus according to new
definition.
(org-export-dispatch-menu-entries): Removed variable.
* contrib/lisp/org-e-beamer.el: Use new sub-menu definition.
(org-e-beamer--format-section, org-e-beamer-item,
org-e-beamer-keyword): Use `org-export-with-backend' instead of
relying on removed variables.
* testing/lisp/test-org-export.el: Update tests.

This patch gets rid of "invisible" variables, that is variables
defvar'ed within a macro.
Nicolas Goaziou 5 years ago
parent
commit
3d56f56399
3 changed files with 241 additions and 255 deletions
  1. 11 14
      contrib/lisp/org-e-beamer.el
  2. 157 179
      contrib/lisp/org-export.el
  3. 73 62
      testing/lisp/test-org-export.el

+ 11 - 14
contrib/lisp/org-e-beamer.el

@@ -262,13 +262,14 @@ brackets.  Return overlay specification, as a string, or nil."
 
 (org-export-define-derived-backend e-beamer e-latex
   :export-block "BEAMER"
-  :sub-menu-entry
-  (?l (?B "As TEX buffer (Beamer)" org-e-beamer-export-as-latex)
-      (?b "As TEX file (Beamer)" org-e-beamer-export-to-latex)
-      (?P "As PDF file (Beamer)" org-e-beamer-export-to-pdf)
-      (?O "As PDF file and open (Beamer)"
-	  (lambda (s v b)
-	    (org-open-file (org-e-beamer-export-to-pdf s v b)))))
+  :menu-entry
+  (?l 1
+      ((?B "As TEX buffer (Beamer)" org-e-beamer-export-as-latex)
+       (?b "As TEX file (Beamer)" org-e-beamer-export-to-latex)
+       (?P "As PDF file (Beamer)" org-e-beamer-export-to-pdf)
+       (?O "As PDF file and open (Beamer)"
+	   (lambda (s v b)
+	     (org-open-file (org-e-beamer-export-to-pdf s v b))))))
   :options-alist
   ((:beamer-theme "BEAMER_THEME" nil org-e-beamer-theme)
    (:beamer-color-theme "BEAMER_COLOR_THEME" nil nil t)
@@ -393,9 +394,7 @@ CONTENTS holds the contents of the headline.  INFO is a plist
 used as a communication channel."
   ;; Use `e-latex' back-end output, inserting overlay specifications
   ;; if possible.
-  (let ((latex-headline
-	 (funcall (cdr (assq 'headline org-e-latex-translate-alist))
-		  headline contents info))
+  (let ((latex-headline (org-export-with-backend 'e-latex headline contents info))
 	(mode-specs (org-element-property :beamer-act headline)))
     (if (and mode-specs
 	     (string-match "\\`\\\\\\(.*?\\)\\(?:\\*\\|\\[.*\\]\\)?{"
@@ -645,8 +644,7 @@ contextual information."
   (let ((action (let ((first-element (car (org-element-contents item))))
 		  (and (eq (org-element-type first-element) 'paragraph)
 		       (org-e-beamer--element-has-overlay-p first-element))))
-	(output (funcall (cdr (assq 'item org-e-latex-translate-alist))
-			 item contents info)))
+	(output (org-export-with-backend 'e-latex item contents info)))
     (if (not action) output
       ;; If the item starts with a paragraph and that paragraph starts
       ;; with an export snippet specifying an overlay, insert it after
@@ -677,8 +675,7 @@ channel."
 	 (when (wholenump depth) (format "\\setcounter{tocdepth}{%s}\n" depth))
 	 "\\tableofcontents" options "\n"
 	 "\\end{frame}")))
-     (t (funcall (cdr (assq 'keyword org-e-latex-translate-alist))
-		 keyword contents info)))))
+     (t (org-export-with-backend 'e-latex keyword contents info)))))
 
 
 ;;;; Link

+ 157 - 179
contrib/lisp/org-export.el

@@ -48,11 +48,10 @@
 ;; buffer as a string.
 ;;
 ;; An export back-end is defined with `org-export-define-backend',
-;; which sets one mandatory variable: his translation table.  Its name
-;; is always `org-BACKEND-translate-alist' where BACKEND stands for
-;; the name chosen for the back-end.  Its value is an alist whose keys
-;; are elements and objects types and values translator functions.
-;; See function's docstring for more information about translators.
+;; which defines one mandatory information: his translation table.
+;; Its value is an alist whose keys are elements and objects types and
+;; values translator functions.  See function's docstring for more
+;; information about translators.
 ;;
 ;; Optionally, `org-export-define-backend' can also support specific
 ;; buffer keywords, OPTION keyword's items and filters.  Also refer to
@@ -252,6 +251,17 @@ whose extension is either \"png\", \"jpeg\", \"jpg\", \"gif\",
 See `org-export-inline-image-p' for more information about
 rules.")
 
+(defconst org-export-registered-backends nil
+  "List of backends currently available in the exporter.
+
+A backend is stored as a list where CAR is its name, as a symbol,
+and CDR is a plist with the following properties:
+`:filters-alist', `:menu-entry', `:options-alist' and
+`:translate-alist'.
+
+This variable is set with `org-export-define-backend' and
+`org-export-define-derived-backend' functions.")
+
 
 
 ;;; User-configurable Variables
@@ -702,7 +712,7 @@ to standard mode."
 
 
 
-;;; Defining New Back-ends
+;;; Defining Back-ends
 ;;
 ;; `org-export-define-backend' is the standard way to define an export
 ;; back-end.  It allows to specify translators, filters, buffer
@@ -710,6 +720,12 @@ to standard mode."
 ;; with another back-end, `org-export-define-derived-backend' may be
 ;; used instead.
 ;;
+;; Internally, a back-end is stored as a list, of which CAR is the
+;; name of the back-end, as a symbol, and CDR a plist.  Accessors to
+;; properties of a given back-end are: `org-export-backend-filters',
+;; `org-export-backend-menu', `org-export-backend-options' and
+;; `org-export-backend-translate-table'.
+;;
 ;; Eventually `org-export-barf-if-invalid-backend' returns an error
 ;; when a given back-end hasn't been registered yet.
 
@@ -774,12 +790,20 @@ keywords are understood:
     Menu entry for the export dispatcher.  It should be a list
     like:
 
-      \(KEY DESCRIPTION ACTION-OR-MENU)
+      \(KEY DESCRIPTION-OR-ORDINAL ACTION-OR-MENU)
 
     where :
 
       KEY is a free character selecting the back-end.
-      DESCRIPTION is a string naming the back-end.
+
+      DESCRIPTION-OR-ORDINAL is either a string or a number.
+
+      If it is a string, is will be used to name the back-end in
+      its menu entry.  If it is a number, the following menu will
+      be displayed as a sub-menu of the back-end with the same
+      KEY.  Also, the number will be used to determine in which
+      order such sub-menus will appear (lowest first).
+
       ACTION-OR-MENU is either a function or an alist.
 
       If it is an action, it will be called with three arguments:
@@ -828,21 +852,14 @@ keywords are understood:
         (:options-alist (setq options (pop body)))
         (t (pop body))))
     `(progn
-       ;; Define translators.
-       (defvar ,(intern (format "org-%s-translate-alist" backend)) ',translators
-	 "Alist between element or object types and translators.")
-       ;; Define options.
-       ,(when options
-	  `(defconst ,(intern (format "org-%s-options-alist" backend)) ',options
-	     ,(format "Alist between %s export properties and ways to set them.
-See `org-export-options-alist' for more information on the
-structure of the values."
-		      backend)))
-       ;; Define filters.
-       ,(when filters
-	  `(defconst ,(intern (format "org-%s-filters-alist" backend)) ',filters
-	     "Alist between filters keywords and back-end specific filters.
-See `org-export-filters-alist' for more information."))
+       ;; Register back-end.
+       (add-to-list
+	'org-export-registered-backends
+	',(cons backend
+		(append (list :translate-alist translators)
+			(and filters (list :filters-alist filters))
+			(and options (list :options-alist options))
+			(and menu-entry (list :menu-entry menu-entry)))))
        ;; Tell parser to not parse EXPORT-BLOCK blocks.
        ,(when export-block
 	  `(mapc
@@ -850,10 +867,6 @@ See `org-export-filters-alist' for more information."))
 	      (add-to-list 'org-element-block-name-alist
 			   `(,name . org-element-export-block-parser)))
 	    ',export-block))
-       ;; Add an entry for back-end in `org-export-dispatch'.
-       ,(when menu-entry
-	  `(unless (assq (car ',menu-entry) org-export-dispatch-menu-entries)
-	     (add-to-list 'org-export-dispatch-menu-entries ',menu-entry)))
        ;; Splice in the body, if any.
        ,@body)))
 
@@ -893,28 +906,6 @@ keywords are understood:
     `org-export-options-alist' for more information about
     structure of the values.
 
-  :sub-menu-entry
-
-    Append entries to an existing menu in the export dispatcher.
-    The associated value should be a list whose CAR is the
-    character selecting the menu to expand and CDR a list of
-    entries following the pattern:
-
-      \(KEY DESCRIPTION ACTION)
-
-    where KEY is a free character triggering the action,
-    DESCRIPTION is a string defining the action, and ACTION is
-    a function that will be called with three arguments:
-    SUBTREEP, VISIBLE-ONLY and BODY-ONLY.  See `org-export-as'
-    for further explanations.
-
-    Valid values include:
-
-      \(?l (?P \"As PDF file (Beamer)\" org-e-beamer-export-to-pdf)
-          \(?O \"As PDF file and open (Beamer)\"
-              \(lambda (s v b)
-                \(org-open-file (org-e-beamer-export-to-pdf s v b)))))
-
   :translate-alist
 
     Alist of element and object types and transcoders that will
@@ -934,7 +925,7 @@ The back-end could then be called with, for example:
   \(org-export-to-buffer 'my-latex \"*Test my-latex*\")"
   (declare (debug (&define name sexp [&rest [keywordp sexp]] def-body))
 	   (indent 2))
-  (let (export-block filters menu-entry options sub-menu-entry translate)
+  (let (export-block filters menu-entry options sub-menu-entry translators)
     (while (keywordp (car body))
       (case (pop body)
 	(:export-block (let ((names (pop body)))
@@ -945,9 +936,21 @@ The back-end could then be called with, for example:
 	(:menu-entry (setq menu-entry (pop body)))
         (:options-alist (setq options (pop body)))
 	(:sub-menu-entry (setq sub-menu-entry (pop body)))
-        (:translate-alist (setq translate (pop body)))
+        (:translate-alist (setq translators (pop body)))
         (t (pop body))))
     `(progn
+       ;; Register back-end.
+       (add-to-list
+	'org-export-registered-backends
+	',(cons child
+		(append
+		 (let ((p-table (org-export-backend-translate-table parent)))
+		   (list :translate-alist (append translators p-table)))
+		 (let ((p-filters (org-export-backend-filters parent)))
+		   (list :filters-alist (append filters p-filters)))
+		 (let ((p-options (org-export-backend-options parent)))
+		   (list :options-alist (append options p-options)))
+		 (and menu-entry (list :menu-entry menu-entry)))))
        ;; Tell parser to not parse EXPORT-BLOCK blocks.
        ,(when export-block
 	  `(mapc
@@ -955,49 +958,32 @@ The back-end could then be called with, for example:
 	      (add-to-list 'org-element-block-name-alist
 			   `(,name . org-element-export-block-parser)))
 	    ',export-block))
-       ;; Define filters.
-       ,(let ((parent-filters (intern (format "org-%s-filters-alist" parent))))
-	  (when (or (boundp parent-filters) filters)
-	    `(defconst ,(intern (format "org-%s-filters-alist" child))
-	       ',(append filters
-			 (and (boundp parent-filters)
-			      (copy-sequence (symbol-value parent-filters))))
-	       "Alist between filters keywords and back-end specific filters.
-See `org-export-filters-alist' for more information.")))
-       ;; Define options.
-       ,(let ((parent-options (intern (format "org-%s-options-alist" parent))))
-	  (when (or (boundp parent-options) options)
-	    `(defconst ,(intern (format "org-%s-options-alist" child))
-	       ',(append options
-			 (and (boundp parent-options)
-			      (copy-sequence (symbol-value parent-options))))
-	       ,(format "Alist between %s export properties and ways to set them.
-See `org-export-options-alist' for more information on the
-structure of the values."
-			child))))
-       ;; Define translators.
-       (defvar ,(intern (format "org-%s-translate-alist" child))
-	 ',(append translate
-		   (copy-sequence
-		    (symbol-value
-		     (intern (format "org-%s-translate-alist" parent)))))
-	 "Alist between element or object types and translators.")
-       ;; Add an entry for back-end in `org-export-dispatch'.
-       ,(when menu-entry
-	  `(unless (assq (car ',menu-entry) org-export-dispatch-menu-entries)
-	     (add-to-list 'org-export-dispatch-menu-entries ',menu-entry)))
-       ,(when sub-menu-entry
-	  (let ((menu (nth 2 (assq (car sub-menu-entry)
-				   org-export-dispatch-menu-entries))))
-	    (when menu `(nconc ',menu
-			       ',(org-remove-if (lambda (e) (member e menu))
-						(cdr sub-menu-entry))))))
        ;; Splice in the body, if any.
        ,@body)))
 
+(defun org-export-backend-filters (backend)
+  "Return filters for BACKEND."
+  (plist-get (cdr (assq backend org-export-registered-backends))
+	     :filters-alist))
+
+(defun org-export-backend-menu (backend)
+  "Return menu entry for BACKEND."
+  (plist-get (cdr (assq backend org-export-registered-backends))
+	     :menu-entry))
+
+(defun org-export-backend-options (backend)
+  "Return export options for BACKEND."
+  (plist-get (cdr (assq backend org-export-registered-backends))
+	     :options-alist))
+
+(defun org-export-backend-translate-table (backend)
+  "Return translate table for BACKEND."
+  (plist-get (cdr (assq backend org-export-registered-backends))
+	     :translate-alist))
+
 (defun org-export-barf-if-invalid-backend (backend)
   "Signal an error if BACKEND isn't defined."
-  (unless (boundp (intern (format "org-%s-translate-alist" backend)))
+  (unless (org-export-backend-translate-table backend)
     (error "Unknown \"%s\" back-end: Aborting export" backend)))
 
 
@@ -1327,8 +1313,7 @@ inferior to file-local settings."
     :back-end
     backend
     :translate-alist
-    (let ((trans-alist (intern (format "org-%s-translate-alist" backend))))
-      (when (boundp trans-alist) (symbol-value trans-alist)))
+    (org-export-backend-translate-table backend)
     :footnote-definition-alist
     ;; Footnotes definitions must be collected in the original
     ;; buffer, as there's no insurance that they will still be in
@@ -1366,12 +1351,8 @@ inferior to file-local settings."
   "Parse an OPTIONS line and return values as a plist.
 Optional argument BACKEND is a symbol specifying which back-end
 specific items to read, if any."
-  (let* ((all
-	  (append org-export-options-alist
-		  (and backend
-		       (let ((var (intern
-				   (format "org-%s-options-alist" backend))))
-			 (and (boundp var) (eval var))))))
+  (let* ((all (append org-export-options-alist
+		      (and backend (org-export-backend-options backend))))
 	 ;; Build an alist between #+OPTION: item and property-name.
 	 (alist (delq nil
 		      (mapcar (lambda (e)
@@ -1441,10 +1422,7 @@ for export.  Return options as a plist."
 			   value))))))))
 	;; Also look for both general keywords and back-end specific
 	;; options if BACKEND is provided.
-	(append (and backend
-		     (let ((var (intern
-				 (format "org-%s-options-alist" backend))))
-		       (and (boundp var) (symbol-value var))))
+	(append (and backend (org-export-backend-options backend))
 		org-export-options-alist)))
      ;; Return value.
      plist)))
@@ -1494,13 +1472,9 @@ Assume buffer is in Org mode.  Narrowing, if any, is ignored."
 	       (setq plist (org-combine-plists plist prop)))))))
      ;; 2. Standard options, as in `org-export-options-alist'.
      (let* ((all (append org-export-options-alist
-			 ;; Also look for back-end specific options
-			 ;; if BACKEND is defined.
-			 (and backend
-			      (let ((var
-				     (intern
-				      (format "org-%s-options-alist" backend))))
-				(and (boundp var) (eval var))))))
+			 ;; Also look for back-end specific options if
+			 ;; BACKEND is defined.
+			 (and backend (org-export-backend-options backend))))
 	    ;; Build ALIST between keyword name and property name.
 	    (alist
 	     (delq nil (mapcar
@@ -1569,10 +1543,7 @@ Optional argument BACKEND, if non-nil, is a symbol specifying
 which back-end specific export options should also be read in the
 process."
   (let ((all (append org-export-options-alist
-		     (and backend
-			  (let ((var (intern
-				      (format "org-%s-options-alist" backend))))
-			    (and (boundp var) (symbol-value var))))))
+		     (and backend (org-export-backend-options backend))))
 	;; Output value.
 	plist)
     (mapc
@@ -2064,9 +2035,9 @@ Any element in `:ignore-list' will be skipped when using
 ;;
 ;; From the developer side, filters sets can be installed in the
 ;; process with the help of `org-export-define-backend', which
-;; internally sets `org-BACKEND-filters-alist' variable.  Each
-;; association has a key among the following symbols and a function or
-;; a list of functions as value.
+;; internally stores filters as an alist.  Each association has a key
+;; among the following symbols and a function or a list of functions
+;; as value.
 ;;
 ;; - `:filter-parse-tree' applies directly on the complete parsed
 ;;   tree.  It's the only filters set that doesn't apply to a string.
@@ -2513,19 +2484,16 @@ Return the updated communication channel."
 	    (setq plist (plist-put plist (car p) (eval (cdr p)))))
 	  org-export-filters-alist)
     ;; Prepend back-end specific filters to that list.
-    (let ((back-end-filters (intern (format "org-%s-filters-alist"
-					    (plist-get info :back-end)))))
-      (when (boundp back-end-filters)
-	(mapc (lambda (p)
-		;; Single values get consed, lists are prepended.
-		(let ((key (car p)) (value (cdr p)))
-		  (when value
-		    (setq plist
-			  (plist-put
-			   plist key
-			   (if (atom value) (cons value (plist-get plist key))
-			     (append value (plist-get plist key))))))))
-	      (eval back-end-filters))))
+    (mapc (lambda (p)
+	    ;; Single values get consed, lists are prepended.
+	    (let ((key (car p)) (value (cdr p)))
+	      (when value
+		(setq plist
+		      (plist-put
+		       plist key
+		       (if (atom value) (cons value (plist-get plist key))
+			 (append value (plist-get plist key))))))))
+	  (org-export-backend-filters (plist-get info :back-end)))
     ;; Return new communication channel.
     (org-combine-plists info plist)))
 
@@ -3007,13 +2975,10 @@ Caption lines are separated by a white space."
   (let ((type (org-element-type data)))
     (if (or (memq type '(nil org-data)))
 	(error "No foreign transcoder available")
-      (let ((transcoder (cdr (assq type
-				   (symbol-value
-				    (intern (format "org-%s-translate-alist"
-						    back-end)))))))
-	(if (not (functionp transcoder))
-	    (error "No foreign transcoder available")
-	  (apply transcoder data args))))))
+      (let ((transcoder
+	     (cdr (assq type (org-export-backend-translate-table back-end)))))
+	(if (functionp transcoder) (apply transcoder data args)
+	  (error "No foreign transcoder available"))))))
 
 
 ;;;; For Export Snippets
@@ -4745,12 +4710,6 @@ to `:default' encoding. If it fails, return S."
 ;; for its interface, which, in turn, delegates response to key
 ;; pressed to `org-export-dispatch-action'.
 
-(defvar org-export-dispatch-menu-entries nil
-  "List of menu entries available for `org-export-dispatch'.
-This variable shouldn't be set directly.  Set-up :menu-entry
-keyword in either `org-export-define-backend' or
-`org-export-define-derived-backend' instead.")
-
 ;;;###autoload
 (defun org-export-dispatch ()
   "Export dispatcher for Org mode.
@@ -4815,22 +4774,35 @@ back to standard interface."
 	    (if (or (eq access-key t) (eq access-key first-key))
 		(org-add-props key nil 'face 'org-warning)
 	      (org-no-properties key))))
-	 ;; Make sure order of menu doesn't depend on the order in
-	 ;; which back-ends are loaded.
-	 (backends (sort (copy-sequence org-export-dispatch-menu-entries)
-			 (lambda (a b) (< (car a) (car b)))))
+	 ;; Prepare menu entries by extracting them from
+	 ;; `org-export-registered-backends', and sorting them by
+	 ;; access key and by ordinal, if any.
+	 (backends (sort
+		    (sort
+		     (delq nil
+			   (mapcar (lambda (b)
+				     (org-export-backend-menu (car b)))
+				   org-export-registered-backends))
+		     (lambda (a b)
+		       (let ((key-a (nth 1 a))
+			     (key-b (nth 1 b)))
+			 (cond ((and (numberp key-a) (numberp key-b))
+				(< key-a key-b))
+			       ((numberp key-b) t)))))
+		    (lambda (a b) (< (car a) (car b)))))
 	 ;; Compute a list of allowed keys based on the first key
 	 ;; pressed, if any.  Some keys (?1, ?2, ?3, ?4 and ?q) are
 	 ;; always available.
 	 (allowed-keys
-	  (nconc (list ?1 ?2 ?3 ?4)
-		 (mapcar 'car
-			 (if (not first-key) backends
-			   (nth 2 (assq first-key backends))))
-		 (cond ((eq first-key ?P) (list ?f ?p ?x ?a))
-		       ((not first-key) (list ?P)))
-		 (when expertp (list ??))
-		 (list ?q)))
+	  (org-uniquify
+	   (nconc (list ?1 ?2 ?3 ?4)
+		  (mapcar 'car
+			  (if (not first-key) backends
+			    (nth 2 (assq first-key backends))))
+		  (cond ((eq first-key ?P) (list ?f ?p ?x ?a))
+			((not first-key) (list ?P)))
+		  (when expertp (list ??))
+		  (list ?q))))
 	 ;; Build the help menu for standard UI.
 	 (help
 	  (unless expertp
@@ -4838,7 +4810,7 @@ back to standard interface."
 	     ;; Options are hard-coded.
 	     (format "Options
     [%s] Body only:    %s       [%s] Visible only:     %s
-    [%s] Export scope: %s   [%s] Force publishing: %s\n\n"
+    [%s] Export scope: %s   [%s] Force publishing: %s\n"
 		     (funcall fontify-key "1" t)
 		     (if (memq 'body options) "On " "Off")
 		     (funcall fontify-key "2" t)
@@ -4847,31 +4819,37 @@ back to standard interface."
 		     (if (memq 'subtree options) "Subtree" "Buffer ")
 		     (funcall fontify-key "4" t)
 		     (if (memq 'force options) "On " "Off"))
-	     ;; Display registered back-end entries.
-	     (mapconcat
-	      (lambda (entry)
-		(let ((top-key (car entry)))
-		  (concat
-		   (format "[%s] %s\n"
-			   (funcall fontify-key (char-to-string top-key))
-			   (nth 1 entry))
-		   (let ((sub-menu (nth 2 entry)))
-		     (unless (functionp sub-menu)
-		       ;; Split sub-menu into two columns.
-		       (let ((index -1))
-			 (concat
-			  (mapconcat
-			   (lambda (sub-entry)
-			     (incf index)
-			     (format (if (zerop (mod index 2)) "    [%s] %-24s"
-				       "[%s] %s\n")
-				     (funcall fontify-key
-					      (char-to-string (car sub-entry))
-					      top-key)
-				     (nth 1 sub-entry)))
-			   sub-menu "")
-			  (when (zerop (mod index 2)) "\n"))))))))
-	      backends "\n")
+	     ;; Display registered back-end entries.  When a key
+	     ;; appears for the second time, do not create another
+	     ;; entry, but append its sub-menu to existing menu.
+	     (let (last-key)
+	       (mapconcat
+		(lambda (entry)
+		  (let ((top-key (car entry)))
+		    (concat
+		     (unless (eq top-key last-key)
+		       (setq last-key top-key)
+		       (format "\n[%s] %s\n"
+			       (funcall fontify-key (char-to-string top-key))
+			       (nth 1 entry)))
+		     (let ((sub-menu (nth 2 entry)))
+		       (unless (functionp sub-menu)
+			 ;; Split sub-menu into two columns.
+			 (let ((index -1))
+			   (concat
+			    (mapconcat
+			     (lambda (sub-entry)
+			       (incf index)
+			       (format
+				(if (zerop (mod index 2)) "    [%s] %-24s"
+				  "[%s] %s\n")
+				(funcall fontify-key
+					 (char-to-string (car sub-entry))
+					 top-key)
+				(nth 1 sub-entry)))
+			     sub-menu "")
+			    (when (zerop (mod index 2)) "\n"))))))))
+		backends ""))
 	     ;; Publishing menu is hard-coded.
 	     (format "\n[%s] Publish
     [%s] Current file            [%s] Current project

+ 73 - 62
testing/lisp/test-org-export.el

@@ -23,18 +23,22 @@ BACKEND is the name of the back-end.  BODY is the body to
 execute.  The defined back-end simply returns parsed data as Org
 syntax."
   (declare (debug (form body)) (indent 1))
-  `(let ((,(intern (format "org-%s-translate-alist" backend))
-	  ',(let (transcode-table)
-	      (dolist (type (append org-element-all-elements
-				    org-element-all-objects)
-			    transcode-table)
-		(push
-		 (cons type
-		       (lambda (obj contents info)
-			 (funcall
-			  (intern (format "org-element-%s-interpreter" type))
-			  obj contents)))
-		 transcode-table)))))
+  `(let ((org-export-registered-backends
+	  ',(list
+	     (list backend
+		   :translate-alist
+		   (let (transcode-table)
+		     (dolist (type (append org-element-all-elements
+					   org-element-all-objects)
+				   transcode-table)
+		       (push
+			(cons type
+			      (lambda (obj contents info)
+				(funcall
+				 (intern (format "org-element-%s-interpreter"
+						 type))
+				 obj contents)))
+			transcode-table)))))))
      (progn ,@body)))
 
 (defmacro org-test-with-parsed-data (data &rest body)
@@ -353,13 +357,6 @@ text
       (forward-line)
       (org-cycle)
       (should (equal (org-export-as 'test nil 'visible) "* Head1\n"))
-      ;; Body only.
-      (flet ((org-test-template (body info) (format "BEGIN\n%sEND" body)))
-	(push '(template . org-test-template) org-test-translate-alist)
-	(should (equal (org-export-as 'test nil nil 'body-only)
-		       "* Head1\n** Head2\ntext\n*** Head3\n"))
-	(should (equal (org-export-as 'test)
-		       "BEGIN\n* Head1\n** Head2\ntext\n*** Head3\nEND")))
       ;; Region.
       (goto-char (point-min))
       (forward-line 3)
@@ -382,7 +379,17 @@ text
 #+END_SRC"
 	    (org-test-with-backend test
 	      (forward-line 1)
-	      (org-export-as 'test 'subtree))))))
+	      (org-export-as 'test 'subtree)))))
+  ;; Body only.
+  (org-test-with-temp-text "Text"
+    (org-test-with-backend test
+      (plist-put
+       (cdr (assq 'test org-export-registered-backends))
+       :translate-alist
+       (cons (cons 'template (lambda (body info) (format "BEGIN\n%sEND" body)))
+	     (org-export-backend-translate-table 'test)))
+      (should (equal (org-export-as 'test nil nil 'body-only) "Text\n"))
+      (should (equal (org-export-as 'test) "BEGIN\nText\nEND")))))
 
 (ert-deftest test-org-export/expand-include ()
   "Test file inclusion in an Org buffer."
@@ -571,16 +578,18 @@ body\n")))
   "Test export snippets transcoding."
   (org-test-with-temp-text "@@test:A@@@@t:B@@"
     (org-test-with-backend test
-      (let ((org-test-translate-alist
-	     (cons (cons 'export-snippet
-			 (lambda (snippet contents info)
-			   (when (eq (org-export-snippet-backend snippet) 'test)
-			     (org-element-property :value snippet))))
-		   org-test-translate-alist)))
-	(let ((org-export-snippet-translation-alist nil))
-	  (should (equal (org-export-as 'test) "A\n")))
-	(let ((org-export-snippet-translation-alist '(("t" . "test"))))
-	  (should (equal (org-export-as 'test) "AB\n")))))))
+      (plist-put
+       (cdr (assq 'test org-export-registered-backends))
+       :translate-alist
+       (cons (cons 'export-snippet
+		   (lambda (snippet contents info)
+		     (when (eq (org-export-snippet-backend snippet) 'test)
+		       (org-element-property :value snippet))))
+	     (org-export-backend-translate-table 'test)))
+      (let ((org-export-snippet-translation-alist nil))
+	(should (equal (org-export-as 'test) "A\n")))
+      (let ((org-export-snippet-translation-alist '(("t" . "test"))))
+	(should (equal (org-export-as 'test) "AB\n"))))))
 
 
 
@@ -595,29 +604,29 @@ body\n")))
      (equal
       '((1 . "A\n") (2 . "B") (3 . "C") (4 . "D"))
       (org-test-with-parsed-data
-	  "Text[fn:1] [1] [fn:label:C] [fn::D]\n\n[fn:1] A\n\n[1] B"
-	(org-element-map
-	 tree 'footnote-reference
-	 (lambda (ref)
-	   (let ((def (org-export-get-footnote-definition ref info)))
-	     (cons (org-export-get-footnote-number ref info)
-		   (if (eq (org-element-property :type ref) 'inline) (car def)
-		     (car (org-element-contents
-			   (car (org-element-contents def))))))))
-	 info))))
+       "Text[fn:1] [1] [fn:label:C] [fn::D]\n\n[fn:1] A\n\n[1] B"
+       (org-element-map
+	tree 'footnote-reference
+	(lambda (ref)
+	  (let ((def (org-export-get-footnote-definition ref info)))
+	    (cons (org-export-get-footnote-number ref info)
+		  (if (eq (org-element-property :type ref) 'inline) (car def)
+		    (car (org-element-contents
+			  (car (org-element-contents def))))))))
+	info))))
     ;; 2. Test nested footnotes order.
     (org-test-with-parsed-data
-	"Text[fn:1:A[fn:2]] [fn:3].\n\n[fn:2] B [fn:3] [fn::D].\n\n[fn:3] C."
-      (should
-       (equal
-	'((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
-	(org-element-map
-	 tree 'footnote-reference
-	 (lambda (ref)
-	   (when (org-export-footnote-first-reference-p ref info)
-	     (cons (org-export-get-footnote-number ref info)
-		   (org-element-property :label ref))))
-	 info))))
+     "Text[fn:1:A[fn:2]] [fn:3].\n\n[fn:2] B [fn:3] [fn::D].\n\n[fn:3] C."
+     (should
+      (equal
+       '((1 . "fn:1") (2 . "fn:2") (3 . "fn:3") (4))
+       (org-element-map
+	tree 'footnote-reference
+	(lambda (ref)
+	  (when (org-export-footnote-first-reference-p ref info)
+	    (cons (org-export-get-footnote-number ref info)
+		  (org-element-property :label ref))))
+	info))))
     ;; 3. Test nested footnote in invisible definitions.
     (org-test-with-temp-text "Text[1]\n\n[1] B [2]\n\n[2] C."
       ;; Hide definitions.
@@ -636,22 +645,24 @@ body\n")))
 \[fn:2] B [fn:3] [fn::D].
 
 \[fn:3] C."
-      (should (= (length (org-export-collect-footnote-definitions tree info))
-		 4)))
+			       (should (= (length (org-export-collect-footnote-definitions tree info))
+					  4)))
     ;; 5. Test export of footnotes defined outside parsing scope.
     (org-test-with-temp-text "[fn:1] Out of scope
 * Title
 Paragraph[fn:1]"
       (org-test-with-backend test
-	(let ((org-test-translate-alist
-	       (cons (cons 'footnote-reference
-			   (lambda (fn contents info)
-			     (org-element-interpret-data
-			      (org-export-get-footnote-definition fn info))))
-		     org-test-translate-alist)))
-	  (forward-line)
-	  (should (equal "ParagraphOut of scope\n"
-			 (org-export-as 'test 'subtree))))))))
+	(plist-put
+	 (cdr (assq 'test org-export-registered-backends))
+	 :translate-alist
+	 (cons (cons 'footnote-reference
+		     (lambda (fn contents info)
+		       (org-element-interpret-data
+			(org-export-get-footnote-definition fn info))))
+	       (org-export-backend-translate-table 'test)))
+	(forward-line)
+	(should (equal "ParagraphOut of scope\n"
+		       (org-export-as 'test 'subtree)))))))