org-export: Do not treat unpaired ' and " as smart quotes

* lisp/ox.el (org-export--smart-quote-status): When quotes are not
balanced, treat " literally and ' as apostrophes.
* testing/lisp/test-ox.el (test-org-export/activate-smart-quotes): Fix
test with unbalanced " and add new tests for unbalanced quotes.

Reported-by: Juan Manuel Macías <maciaschain@posteo.net>
Link: https://list.orgmode.org/orgmode/875xxfqdpt.fsf@posteo.net/
This commit is contained in:
Ihor Radchenko 2024-03-23 14:34:06 +03:00
parent 8507efa848
commit 33503445e6
No known key found for this signature in database
GPG Key ID: 6470762A7DA11D8B
2 changed files with 85 additions and 2 deletions

View File

@ -5942,6 +5942,56 @@ INFO is the current export state, as a plist."
(when current-status (when current-status
(push (cons text (nreverse current-status)) full-status)))) (push (cons text (nreverse current-status)) full-status))))
info nil org-element-recursive-objects) info nil org-element-recursive-objects)
;; When quotes are not balanced, treat them as apostrophes.
(setq full-status (nreverse full-status))
(let (primary-openings secondary-openings)
(dolist (substatus full-status)
(let ((status (cdr substatus)))
(while status
(pcase (car status)
(`apostrophe nil)
(`primary-opening
(push status primary-openings))
(`secondary-opening
(push status secondary-openings))
(`secondary-closing
(if secondary-openings
;; Remove matched opening.
(pop secondary-openings)
;; No matching openings for a given closing. Replace
;; it with apostrophe.
(setcar status 'apostrophe)))
(`primary-closing
(when secondary-openings
;; Some secondary opening quotes are not closed
;; within "...". Replace them all with apostrophes.
(dolist (opening secondary-openings)
(setcar opening 'apostrophe))
(setq secondary-openings nil))
(if primary-openings
;; Remove matched opening.
(pop primary-openings)
;; No matching openings for a given closing.
(error "This should no happen"))))
(setq status (cdr status)))))
(when primary-openings
;; Trailing unclosed "
(unless (= 1 (length primary-openings))
(error "This should not happen"))
;; Mark for not replacing.
(setcar (car primary-openings) nil)
;; Mark all the secondary openings and closings after
;; trailing unclosed " as apostrophes.
(let ((after-unbalanced-primary nil))
(dolist (substatus full-status)
(let ((status (cdr substatus)))
(while status
(when (eq status (car primary-openings))
(setq after-unbalanced-primary t))
(when after-unbalanced-primary
(when (memq (car status) '(secondary-opening secondary-closing))
(setcar status 'apostrophe)))
(setq status (cdr status))))))))
(puthash (cons parent (org-element-secondary-p s)) full-status cache) (puthash (cons parent (org-element-secondary-p s)) full-status cache)
(cdr (assq s full-status)))))) (cdr (assq s full-status))))))

View File

@ -4134,9 +4134,9 @@ This test does not cover listings and custom environments."
;; Opening quotes: at the beginning of a paragraph. ;; Opening quotes: at the beginning of a paragraph.
(should (should
(equal (equal
'("&ldquo;begin") '("&ldquo;begin&rdquo;")
(let ((org-export-default-language "en")) (let ((org-export-default-language "en"))
(org-test-with-parsed-data "\"begin" (org-test-with-parsed-data "\"begin\""
(org-element-map tree 'plain-text (org-element-map tree 'plain-text
(lambda (s) (org-export-activate-smart-quotes s :html info)) (lambda (s) (org-export-activate-smart-quotes s :html info))
info))))) info)))))
@ -4267,6 +4267,39 @@ This test does not cover listings and custom environments."
(org-test-with-parsed-data "*\"foo\"*" (org-test-with-parsed-data "*\"foo\"*"
(org-element-map tree 'plain-text (org-element-map tree 'plain-text
(lambda (s) (org-export-activate-smart-quotes s :html info)) (lambda (s) (org-export-activate-smart-quotes s :html info))
info nil nil t)))))
;; Unmatched quotes.
(should
(equal '("\\guillemotleft{}my friends' party and the students' papers\\guillemotright{} \\guillemotleft{}``mothers''\\guillemotright{}")
(let ((org-export-default-language "es"))
(org-test-with-parsed-data
"\"my friends' party and the students' papers\" \"'mothers'\""
(org-element-map tree 'plain-text
(lambda (s) (org-export-activate-smart-quotes s :latex info))
info nil nil t)))))
(should
(equal '("\"'mothers'")
(let ((org-export-default-language "es"))
(org-test-with-parsed-data
"\"'mothers'"
(org-element-map tree 'plain-text
(lambda (s) (org-export-activate-smart-quotes s :latex info))
info nil nil t)))))
(should
(equal '("\"'mothers " "end'")
(let ((org-export-default-language "es"))
(org-test-with-parsed-data
"\"'mothers =verbatim= end'"
(org-element-map tree 'plain-text
(lambda (s) (org-export-activate-smart-quotes s :latex info))
info nil nil t)))))
(should
(equal '("\\guillemotleft{}να 'ρθώ το βράδυ\\guillemotright{}")
(let ((org-export-default-language "el"))
(org-test-with-parsed-data
"\"να 'ρθώ το βράδυ\""
(org-element-map tree 'plain-text
(lambda (s) (org-export-activate-smart-quotes s :latex info))
info nil nil t)))))) info nil nil t))))))