Browse Source

ox: Make footnotes file specific when including Org files

* lisp/ox.el (org-export-expand-include-keyword,
  org-export--prepare-file-contents): Make footnotes file specific
  when including Org files.

* doc/org.texi (Include files): Add documentation.

* testing/lisp/test-ox.el (test-org-export/expand-include): Add tests.

http://permalink.gmane.org/gmane.emacs.orgmode/83606
Nicolas Goaziou 11 years ago
parent
commit
b8781c4c85
3 changed files with 76 additions and 8 deletions
  1. 5 3
      doc/org.texi
  2. 29 4
      lisp/ox.el
  3. 42 1
      testing/lisp/test-ox.el

+ 5 - 3
doc/org.texi

@@ -9953,9 +9953,11 @@ include your @file{.emacs} file, you could use:
 @noindent
 The optional second and third parameter are the markup (e.g., @samp{quote},
 @samp{example}, or @samp{src}), and, if the markup is @samp{src}, the
-language for formatting the contents.  The markup is optional; if it is not
-given, the text will be assumed to be in Org mode format and will be
-processed normally.
+language for formatting the contents.
+
+If no markup is given, the text will be assumed to be in Org mode format and
+will be processed normally.  However, footnote labels (@pxref{Footnotes}) in
+the file will be made local to that file.
 
 Contents of the included file will belong to the same structure (headline,
 item) containing the @code{INCLUDE} keyword.  In particular, headlines within

+ 29 - 4
lisp/ox.el

@@ -3279,7 +3279,9 @@ with their line restriction, when appropriate.  It is used to
 avoid infinite recursion.  Optional argument DIR is the current
 working directory.  It is used to properly resolve relative
 paths."
-  (let ((case-fold-search t))
+  (let ((case-fold-search t)
+	(file-prefix (make-hash-table :test #'equal))
+	(current-prefix 0))
     (goto-char (point-min))
     (while (re-search-forward "^[ \t]*#\\+INCLUDE:" nil t)
       (let ((element (save-match-data (org-element-at-point))))
@@ -3349,13 +3351,16 @@ paths."
 		 (with-temp-buffer
 		   (let ((org-inhibit-startup t)) (org-mode))
 		   (insert
-		    (org-export--prepare-file-contents file lines ind minlevel))
+		    (org-export--prepare-file-contents
+		     file lines ind minlevel
+		     (or (gethash file file-prefix)
+			 (puthash file (incf current-prefix) file-prefix))))
 		   (org-export-expand-include-keyword
 		    (cons (list file lines) included)
 		    (file-name-directory file))
 		   (buffer-string)))))))))))))
 
-(defun org-export--prepare-file-contents (file &optional lines ind minlevel)
+(defun org-export--prepare-file-contents (file &optional lines ind minlevel id)
   "Prepare the contents of FILE for inclusion and return them as a string.
 
 When optional argument LINES is a string specifying a range of
@@ -3369,7 +3374,12 @@ headline encountered.
 
 Optional argument MINLEVEL, when non-nil, is an integer
 specifying the level that any top-level headline in the included
-file should have."
+file should have.
+
+Optional argument ID is an integer that will be inserted before
+each footnote definition and reference if FILE is an Org file.
+This is useful to avoid conflicts when more than one Org file
+with footnotes is included in a document."
   (with-temp-buffer
     (insert-file-contents file)
     (when lines
@@ -3428,6 +3438,21 @@ file should have."
 	       (org-map-entries
 		(lambda () (if (< offset 0) (delete-char (abs offset))
 			(insert (make-string offset ?*)))))))))))
+    ;; Append ID to all footnote references and definitions, so they
+    ;; become file specific and cannot collide with footnotes in other
+    ;; included files.
+    (goto-char (point-min))
+    (while (re-search-forward org-footnote-re nil t)
+      (let ((reference (org-element-context)))
+	(when (memq (org-element-type reference)
+		    '(footnote-reference footnote-definition))
+	  (goto-char (org-element-property :begin reference))
+	  (forward-char)
+	  (let ((label (org-element-property :label reference)))
+	    (cond ((not label))
+		  ((org-string-match-p "\\`[0-9]+\\'" label)
+		   (insert (format "fn:%d-" id)))
+		  (t (forward-char 3) (insert (format "%d-" id))))))))
     (org-element-normalize-string (buffer-string))))
 
 (defun org-export-execute-babel-code ()

+ 42 - 1
testing/lisp/test-ox.el

@@ -850,7 +850,48 @@ body\n")))
        org-test-dir)
     (org-export-expand-include-keyword)
     (should (equal (buffer-string)
-		   "#+BEGIN_SRC emacs-lisp\n(+ 2 1)\n#+END_SRC\n"))))
+		   "#+BEGIN_SRC emacs-lisp\n(+ 2 1)\n#+END_SRC\n")))
+  ;; Footnotes labels are local to each included file.
+  (should
+   (= 6
+      (length
+       (delete-dups
+	(let ((contents "
+Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
+\[fn:1] Footnote 1
+\[fn:test] Footnote \"test\""))
+	  (org-test-with-temp-text-in-file contents
+	    (let ((file1 (buffer-file-name)))
+	      (org-test-with-temp-text-in-file contents
+		(let ((file2 (buffer-file-name)))
+		  (org-test-with-temp-text
+		      (format "#+INCLUDE: \"%s\"\n#+INCLUDE: \"%s\""
+			      file1 file2)
+		    (org-export-expand-include-keyword)
+		    (let (unique-labels)
+		      (org-element-map (org-element-parse-buffer)
+			  'footnote-reference
+			(lambda (ref)
+			  (org-element-property :label ref))))))))))))))
+  ;; Footnotes labels are not local to each include keyword.
+  (should
+   (= 3
+      (length
+       (delete-dups
+	(let ((contents "
+Footnotes[fn:1], [fn:test] and [fn:inline:anonymous footnote].
+\[fn:1] Footnote 1
+\[fn:test] Footnote \"test\""))
+	  (org-test-with-temp-text-in-file contents
+	    (let ((file (buffer-file-name)))
+	      (org-test-with-temp-text
+		  (format "#+INCLUDE: \"%s\"\n#+INCLUDE: \"%s\"" file file)
+		(org-export-expand-include-keyword)
+		(let (unique-labels)
+		  (org-element-map (org-element-parse-buffer)
+		      'footnote-reference
+		    (lambda (ref)
+		      (org-element-property :label ref)))))))))))))
 
 (ert-deftest test-org-export/expand-macro ()
   "Test macro expansion in an Org buffer."