浏览代码

org-export: Implement a generic smart quote module

* contrib/lisp/org-export.el (org-export-options-alist): Add an option
  item to toggle smart quotes during export.
(org-export-with-smart-quotes, org-export-smart-quotes-alist,
org-export-smart-quotes-regexps): New variables.
(org-export-activate-smart-quotes): New function.
(org-export-data): Remove residual text properties.
* testing/lisp/test-org-export.el: Add tests.
Nicolas Goaziou 12 年之前
父节点
当前提交
b2047a2565
共有 2 个文件被更改,包括 299 次插入6 次删除
  1. 212 6
      contrib/lisp/org-export.el
  2. 87 0
      testing/lisp/test-org-export.el

+ 212 - 6
contrib/lisp/org-export.el

@@ -124,6 +124,7 @@
     (:with-inlinetasks nil "inline" org-export-with-inlinetasks)
     (:with-plannings nil "p" org-export-with-planning)
     (:with-priority nil "pri" org-export-with-priority)
+    (:with-smart-quotes nil "'" org-export-with-smart-quotes)
     (:with-special-strings nil "-" org-export-with-special-strings)
     (:with-statistics-cookies nil "stat" org-export-with-statistics-cookies)
     (:with-sub-superscript nil "^" org-export-with-sub-superscripts)
@@ -471,6 +472,13 @@ This option can also be set with the #+SELECT_TAGS: keyword."
   :group 'org-export-general
   :type '(repeat (string :tag "Tag")))
 
+(defcustom org-export-with-smart-quotes nil
+  "Non-nil means activate smart quotes during export.
+This option can also be set with the #+OPTIONS: line,
+e.g. \"':t\"."
+  :group 'org-export-general
+  :type 'boolean)
+
 (defcustom org-export-with-special-strings t
   "Non-nil means interpret \"\\-\", \"--\" and \"---\" for export.
 
@@ -1188,6 +1196,11 @@ structure of the values."
 ;;   - category :: option
 ;;   - type :: symbol (nil, t)
 ;;
+;; + `:with-smart-quotes' :: Non-nil means activate smart quotes in
+;;      plain text.
+;;   - category :: option
+;;   - type :: symbol (nil, t)
+;;
 ;; + `:with-special-strings' :: Non-nil means transcoding should
 ;;      interpret special strings in plain text.
 ;;   - category :: option
@@ -1883,13 +1896,15 @@ Return transcoded string."
 	      (cond
 	       ;; Ignored element/object.
 	       ((memq data (plist-get info :ignore-list)) nil)
-	       ;; Plain text.
+	       ;; Plain text.  All residual text properties from parse
+	       ;; tree (i.e. `:parent' property) are removed.
 	       ((eq type 'plain-text)
-		(org-export-filter-apply-functions
-		 (plist-get info :filter-plain-text)
-		 (let ((transcoder (org-export-transcoder data info)))
-		   (if transcoder (funcall transcoder data info) data))
-		 info))
+		(org-no-properties
+		 (org-export-filter-apply-functions
+		  (plist-get info :filter-plain-text)
+		  (let ((transcoder (org-export-transcoder data info)))
+		    (if transcoder (funcall transcoder data info) data))
+		  info)))
 	       ;; Uninterpreted element/object: change it back to Org
 	       ;; syntax and export again resulting raw string.
 	       ((not (org-export--interpret-p data info))
@@ -4188,6 +4203,197 @@ Return a list of src-block elements with a caption."
   (org-export-collect-elements 'src-block info))
 
 
+;;;; Smart Quotes
+
+(defconst org-export-smart-quotes-alist
+  '(("de"
+     (opening-double-quote :utf-8 "„" :html "„" :latex "\"`"
+			   :texinfo "@quotedblbase{}")
+     (closing-double-quote :utf-8 "“" :html "“" :latex "\"'"
+			   :texinfo "@quotedblleft{}")
+     (opening-single-quote :utf-8 "‚" :html "‚" :latex "\\glq{}"
+			   :texinfo "@quotesinglbase{}")
+     (closing-single-quote :utf-8 "‘" :html "‘" :latex "\\grq{}"
+			   :texinfo "@quoteleft{}")
+     (apostrophe :utf-8 "’" :html "’"))
+    ("en"
+     (opening-double-quote :utf-8 "“" :html "“" :latex "``" :texinfo "``")
+     (closing-double-quote :utf-8 "”" :html "”" :latex "''" :texinfo "''")
+     (opening-single-quote :utf-8 "‘" :html "‘" :latex "`" :texinfo "`")
+     (closing-single-quote :utf-8 "’" :html "’" :latex "'" :texinfo "'")
+     (apostrophe :utf-8 "’" :html "’"))
+    ("es"
+     (opening-double-quote :utf-8 "«" :html "«" :latex "\\guillemotleft{}"
+			   :texinfo "@guillemetleft{}")
+     (closing-double-quote :utf-8 "»" :html "»" :latex "\\guillemotright{}"
+			   :texinfo "@guillemetright{}")
+     (opening-single-quote :utf-8 "“" :html "“" :latex "``" :texinfo "``")
+     (closing-single-quote :utf-8 "”" :html "”" :latex "''" :texinfo "''")
+     (apostrophe :utf-8 "’" :html "’"))
+    ("fr"
+     (opening-double-quote :utf-8 "« " :html "« " :latex "\\og "
+			   :texinfo "@guillemetleft{}@tie{}")
+     (closing-double-quote :utf-8 " »" :html " »" :latex "\\fg{}"
+			   :texinfo "@tie{}@guillemetright{}")
+     (opening-single-quote :utf-8 "« " :html "« " :latex "\\og "
+			   :texinfo "@guillemetleft{}@tie{}")
+     (closing-single-quote :utf-8 " »" :html " »" :latex "\\fg{}"
+			   :texinfo "@tie{}@guillemetright{}")
+     (apostrophe :utf-8 "’" :html "’")))
+  "Smart quotes translations.
+
+Alist whose CAR is a language string and CDR is an alist with
+quote type as key and a plist associating various encodings to
+their translation as value.
+
+A quote type can be any symbol among `opening-double-quote',
+`closing-double-quote', `opening-single-quote',
+`closing-single-quote' and `apostrophe'.
+
+Valid encodings include `:utf-8', `:html', `:latex' and
+`:texinfo'.
+
+If no translation is found, the quote character is left as-is.")
+
+(defconst org-export-smart-quotes-regexps
+  (list
+   ;; Possible opening quote at beginning of string.
+   "\\`\\([\"']\\)\\(\\w\\|\\s.\\|\\s_\\)"
+   ;; Possible closing quote at beginning of string.
+   "\\`\\([\"']\\)\\(\\s-\\|\\s)\\|\\s.\\)"
+   ;; Possible apostrophe at beginning of string.
+   "\\`\\('\\)\\S-"
+   ;; Opening single and double quotes.
+   "\\(?:\\s-\\|\\s(\\)\\([\"']\\)\\(?:\\w\\|\\s.\\|\\s_\\)"
+   ;; Closing single and double quotes.
+   "\\(?:\\w\\|\\s.\\|\\s_\\)\\([\"']\\)\\(?:\\s-\\|\\s)\\|\\s.\\)"
+   ;; Apostrophe.
+   "\\S-\\('\\)\\S-"
+   ;; Possible opening quote at end of string.
+   "\\(?:\\s-\\|\\s(\\)\\([\"']\\)\\'"
+   ;; Possible closing quote at end of string.
+   "\\(?:\\w\\|\\s.\\|\\s_\\)\\([\"']\\)\\'"
+   ;; Possible apostrophe at end of string.
+   "\\S-\\('\\)\\'")
+  "List of regexps matching a quote or an apostrophe.
+In every regexp, quote or apostrophe matched is put in group 1.")
+
+(defun org-export-activate-smart-quotes (s encoding info &optional original)
+  "Replace regular quotes with \"smart\" quotes in string S.
+
+ENCODING is a symbol among `:html', `:latex' and `:utf-8'.  INFO
+is a plist used as a communication channel.
+
+The function has to retrieve information about string
+surroundings in parse tree.  It can only happen with an
+unmodified string.  Thus, if S has already been through another
+process, a non-nil ORIGINAL optional argument will provide that
+original string.
+
+Return the new string."
+  (if (equal s "") ""
+    (let ((quotes-alist (cdr (assoc (plist-get info :language)
+				    org-export-smart-quotes-alist))))
+      ;; 1. Replace quote character at the beginning of S.
+      (let* ((prev (org-export-get-previous-element (or original s) info))
+	     (pre-blank (and prev (org-element-property :post-blank prev))))
+	(cond
+	 ;; Apostrophe?
+	 ((and prev (zerop pre-blank)
+	       (string-match (nth 2 org-export-smart-quotes-regexps) s))
+	  (let ((smart-quote
+		 (plist-get (cdr (assq 'apostrophe quotes-alist)) encoding)))
+	    (when smart-quote
+	      (setq s (replace-match smart-quote nil t s 1)))))
+	 ;; Closing quote?
+	 ((and prev (zerop pre-blank)
+	       (string-match (nth 1 org-export-smart-quotes-regexps) s))
+	  (let ((smart-quote
+		 (plist-get (cdr (assq (if (equal (match-string 1 s) "'")
+					   'closing-single-quote
+					 'closing-double-quote)
+				       quotes-alist))
+			    encoding)))
+	    (when smart-quote
+	      (setq s (replace-match smart-quote nil t s 1)))))
+	 ;; Opening quote?
+	 ((and (or (not prev) (> pre-blank 0))
+	       (string-match (nth 0 org-export-smart-quotes-regexps) s))
+	  (let ((smart-quote
+		 (plist-get (cdr (assq (if (equal (match-string 1 s) "'")
+					   'opening-single-quote
+					 'opening-double-quote)
+				       quotes-alist))
+			    encoding)))
+	    (when smart-quote
+	      (setq s (replace-match smart-quote nil t s 1)))))))
+      ;; 2. Replace quotes in the middle of the string.
+      (setq s
+	    ;; Opening quotes.
+	    (replace-regexp-in-string
+	     (nth 3 org-export-smart-quotes-regexps)
+	     (lambda (text)
+	       (or (plist-get
+		    (cdr (assq (if (equal (match-string 1 text) "'")
+				   'opening-single-quote
+				 'opening-double-quote)
+			       quotes-alist))
+		    encoding)
+		   (match-string 1 text)))
+	     s nil t 1))
+      (setq s
+	    (replace-regexp-in-string
+	     ;; Closing quotes.
+	     (nth 4 org-export-smart-quotes-regexps)
+	     (lambda (text)
+	       (or (plist-get
+		    (cdr (assq (if (equal (match-string 1 text) "'")
+				   'closing-single-quote
+				 'closing-double-quote)
+			       quotes-alist))
+		    encoding)
+		   (match-string 1 text)))
+	     s nil t 1))
+      (setq s
+	    (replace-regexp-in-string
+	     ;; Apostrophes.
+	     (nth 5 org-export-smart-quotes-regexps)
+	     (lambda (text)
+	       (or (plist-get (cdr (assq 'apostrophe quotes-alist)) encoding)
+		   (match-string 1 text)))
+	     s nil t 1))
+      ;; 3. Replace quote character at the end of S.
+      (let ((next (org-export-get-next-element (or original s) info)))
+	(cond
+	 ;; Apostrophe?
+	 ((and next (string-match (nth 8 org-export-smart-quotes-regexps) s))
+	  (let ((smart-quote
+		 (plist-get (cdr (assq 'apostrophe quotes-alist)) encoding)))
+	    (when smart-quote (setq s (replace-match smart-quote nil t s 1)))))
+	 ;; Closing quote?
+	 ((and (not next)
+	       (string-match (nth 7 org-export-smart-quotes-regexps) s))
+	  (let ((smart-quote
+		 (plist-get (cdr (assq (if (equal (match-string 1 s) "'")
+					   'closing-single-quote
+					 'closing-double-quote)
+				       quotes-alist))
+			    encoding)))
+	    (when smart-quote (setq s (replace-match smart-quote nil t s 1)))))
+	 ;; Opening quote?
+	 ((and next (string-match (nth 6 org-export-smart-quotes-regexps) s))
+	  (let ((smart-quote
+		 (plist-get (cdr (assq (if (equal (match-string 1 s) "'")
+					   'opening-single-quote
+					 'opening-double-quote)
+				       quotes-alist))
+			    encoding)))
+	    (when smart-quote
+	      (setq s (replace-match smart-quote nil t s 1)))))))
+      ;; Return string with smart quotes.
+      s)))
+
+
 ;;;; Topology
 ;;
 ;; Here are various functions to retrieve information about the

+ 87 - 0
testing/lisp/test-org-export.el

@@ -1142,6 +1142,93 @@ Another text. (ref:text)
 		     '("(+ 2 2)\n(+ 3 3)\n" (2 . "one")))))))
 
 
+
+;;; Smart Quotes
+
+(ert-deftest test-org-export/activate-smart-quotes ()
+  "Test `org-export-activate-smart-quotes' specifications."
+  ;; Opening double quotes: standard test.
+  (should
+   (equal
+    '("some “paragraph")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "some \"paragraph"
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Opening quotes: at the beginning of a paragraph.
+  (should
+   (equal
+    '("“begin")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "\"begin"
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Opening quotes: after an object.
+  (should
+   (equal
+    '("“begin")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "=verb= \"begin"
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Closing quotes: standard test.
+  (should
+   (equal
+    '("some” paragraph")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "some\" paragraph"
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Closing quotes: at the end of a paragraph.
+  (should
+   (equal
+    '("end”")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "end\""
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Apostrophe: standard test.
+  (should
+   (equal
+    '("It shouldn’t fail")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "It shouldn't fail"
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Apostrophe: before an object.
+  (should
+   (equal
+    '("a’")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "a'=b="
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info)))))
+  ;; Apostrophe: after an object.
+  (should
+   (equal
+    '("’s")
+    (let ((org-export-default-language "en"))
+      (org-test-with-parsed-data "=code='s"
+	(org-element-map
+	 tree 'plain-text
+	 (lambda (s) (org-export-activate-smart-quotes s :html info))
+	 info))))))
+
+
 
 ;;; Tables