From d07ee7f7f2c591f170d55981c3acc0c402289749 Mon Sep 17 00:00:00 2001 From: Nicolas Goaziou Date: Thu, 9 Nov 2017 22:47:35 +0100 Subject: [PATCH] Implement :pre-blank property for items and footnotes definitions * lisp/org-element.el (org-element-footnote-definition-parser): (org-element-item-parser): Add `:pre-blank' property. (org-element-footnote-definition-interpreter): (org-element-item-interpreter): (org-element-interpret-data): * lisp/ox.el (org-export-data): Use new property. * testing/lisp/test-org-element.el (test-org-element/footnote-definition-parser): (test-org-element/item-parser): Add tests. --- lisp/org-element.el | 108 +++++++++++++++++++------------ lisp/ox.el | 23 +++---- testing/lisp/test-org-element.el | 49 ++++++++++++-- 3 files changed, 123 insertions(+), 57 deletions(-) diff --git a/lisp/org-element.el b/lisp/org-element.el index c21b2f971..3c8d7a737 100644 --- a/lisp/org-element.el +++ b/lisp/org-element.el @@ -812,7 +812,8 @@ their value. Return a list whose CAR is `footnote-definition' and CDR is a plist containing `:label', `:begin' `:end', `:contents-begin', -`:contents-end', `:post-blank' and `:post-affiliated' keywords. +`:contents-end', `:pre-blank',`:post-blank' and +`:post-affiliated' keywords. Assume point is at the beginning of the footnote definition." (save-excursion @@ -838,12 +839,16 @@ Assume point is at the beginning of the footnote definition." ((eq ?* (char-after (match-beginning 0))) (match-beginning 0)) (t (skip-chars-forward " \r\t\n" limit) (if (= limit (point)) limit (line-beginning-position)))))) + (pre-blank 0) (contents-begin (progn (search-forward "]") (skip-chars-forward " \r\t\n" end) (cond ((= (point) end) nil) ((= (line-beginning-position) post-affiliated) (point)) - (t (line-beginning-position))))) + (t + (setq pre-blank + (count-lines (line-beginning-position) begin)) + (line-beginning-position))))) (contents-end (progn (goto-char end) (skip-chars-backward " \r\t\n") @@ -855,6 +860,7 @@ Assume point is at the beginning of the footnote definition." :end end :contents-begin contents-begin :contents-end (and contents-begin contents-end) + :pre-blank pre-blank :post-blank (count-lines contents-end end) :post-affiliated post-affiliated) (cdr affiliated)))))) @@ -862,9 +868,18 @@ Assume point is at the beginning of the footnote definition." (defun org-element-footnote-definition-interpreter (footnote-definition contents) "Interpret FOOTNOTE-DEFINITION element as Org syntax. CONTENTS is the contents of the footnote-definition." - (concat (format "[fn:%s]" (org-element-property :label footnote-definition)) - " " - contents)) + (let ((pre-blank + (min (or (org-element-property :pre-blank footnote-definition) + ;; 0 is specific to paragraphs at the beginning of + ;; the footnote definition, so we use 1 as + ;; a fall-back value, which is more universal. + 1) + ;; Footnote ends after more than two consecutive empty + ;; lines: limit ourselves to 2 newline characters. + 2))) + (concat (format "[fn:%s]" (org-element-property :label footnote-definition)) + (if (= pre-blank 0) (concat " " (org-trim contents)) + (concat (make-string pre-blank ?\n) contents))))) ;;;; Headline @@ -1195,8 +1210,8 @@ STRUCT is the structure of the plain list. Return a list whose CAR is `item' and CDR is a plist containing `:bullet', `:begin', `:end', `:contents-begin', `:contents-end', -`:checkbox', `:counter', `:tag', `:structure', `:post-blank' and -`:post-affiliated' keywords. +`:checkbox', `:counter', `:tag', `:structure', `:pre-blank', +`:post-blank' and `:post-affiliated' keywords. When optional argument RAW-SECONDARY-P is non-nil, item's tag, if any, will not be parsed as a secondary string, but as a plain @@ -1223,20 +1238,25 @@ Assume point is at the beginning of the item." (string-to-number (match-string 0 c))))))) (end (progn (goto-char (nth 6 (assq (point) struct))) (if (bolp) (point) (line-beginning-position 2)))) + (pre-blank 0) (contents-begin - (progn (goto-char - ;; Ignore tags in un-ordered lists: they are just - ;; a part of item's body. - (if (and (match-beginning 4) - (save-match-data (string-match "[.)]" bullet))) - (match-beginning 4) - (match-end 0))) - (skip-chars-forward " \r\t\n" end) - (cond ((= (point) end) nil) - ;; If first line isn't empty, contents really - ;; start at the text after item's meta-data. - ((= (line-beginning-position) begin) (point)) - (t (line-beginning-position))))) + (progn + (goto-char + ;; Ignore tags in un-ordered lists: they are just + ;; a part of item's body. + (if (and (match-beginning 4) + (save-match-data (string-match "[.)]" bullet))) + (match-beginning 4) + (match-end 0))) + (skip-chars-forward " \r\t\n" end) + (cond ((= (point) end) nil) + ;; If first line isn't empty, contents really + ;; start at the text after item's meta-data. + ((= (line-beginning-position) begin) (point)) + (t + (setq pre-blank + (count-lines (line-beginning-position) begin)) + (line-beginning-position))))) (contents-end (and contents-begin (progn (goto-char end) (skip-chars-backward " \r\t\n") @@ -1251,6 +1271,7 @@ Assume point is at the beginning of the item." :checkbox checkbox :counter counter :structure struct + :pre-blank pre-blank :post-blank (count-lines (or contents-end begin) end) :post-affiliated begin)))) (org-element-put-property @@ -1275,26 +1296,30 @@ CONTENTS is the contents of the element." (counter (org-element-property :counter item)) (tag (let ((tag (org-element-property :tag item))) (and tag (org-element-interpret-data tag)))) - ;; Compute indentation. - (ind (make-string (length bullet) 32)) - (item-starts-with-par-p - (eq (org-element-type (car (org-element-contents item))) - 'paragraph))) + (pre-blank + (min (or (org-element-property :pre-blank item) + ;; 0 is specific to paragraphs at the beginning of + ;; the item, so we use 1 as a fall-back value, + ;; which is more universal. + 1) + ;; Lists ends after more than two consecutive empty + ;; lines: limit ourselves to 2 newline characters. + 2)) + (ind (make-string (length bullet) ?\s))) ;; Indent contents. - (concat - bullet - (and counter (format "[@%d] " counter)) - (pcase checkbox - (`on "[X] ") - (`off "[ ] ") - (`trans "[-] ") - (_ nil)) - (and tag (format "%s :: " tag)) - (when contents - (let ((contents (replace-regexp-in-string - "\\(^\\)[ \t]*\\S-" ind contents nil nil 1))) - (if item-starts-with-par-p (org-trim contents) - (concat "\n" contents))))))) + (concat bullet + (and counter (format "[@%d] " counter)) + (pcase checkbox + (`on "[X] ") + (`off "[ ] ") + (`trans "[-] ") + (_ nil)) + (and tag (format "%s :: " tag)) + (when contents + (let ((contents (replace-regexp-in-string + "\\(^\\)[ \t]*\\S-" ind contents nil nil 1))) + (if (= pre-blank 0) (org-trim contents) + (concat (make-string pre-blank ?\n) contents))))))) ;;;; Plain List @@ -4532,8 +4557,9 @@ to interpret. Return Org syntax as a string." (and (eq type 'paragraph) (memq (org-element-type parent) '(footnote-definition item)) - (eq data - (car (org-element-contents parent))))))) + (eq data (car (org-element-contents parent))) + (eq (org-element-property :pre-blank parent) + 0))))) "")))))) (if (memq type '(org-data plain-text nil)) results ;; Build white spaces. If no `:post-blank' property diff --git a/lisp/ox.el b/lisp/ox.el index 953a72327..afce3e2cc 100644 --- a/lisp/ox.el +++ b/lisp/ox.el @@ -2001,17 +2001,18 @@ Return a string." ;; normalized first. (org-element-normalize-contents data - ;; When normalizing contents of the - ;; first paragraph in an item or - ;; a footnote definition, ignore - ;; first line's indentation: there is - ;; none and it might be misleading. - (when (eq type 'paragraph) - (and - (eq (car (org-element-contents parent)) - data) - (memq (org-element-type parent) - '(footnote-definition item))))))) + ;; When normalizing first paragraph + ;; of an item or + ;; a footnote-definition, ignore + ;; first line's indentation. + (and + (eq type 'paragraph) + (memq (org-element-type parent) + '(footnote-definition item)) + (eq (car (org-element-contents parent)) + data) + (eq (org-element-property :pre-blank parent) + 0))))) ""))) (broken-link-handler (funcall transcoder data diff --git a/testing/lisp/test-org-element.el b/testing/lisp/test-org-element.el index 7c359aabc..b17e86b8b 100644 --- a/testing/lisp/test-org-element.el +++ b/testing/lisp/test-org-element.el @@ -969,7 +969,20 @@ Some other text (org-test-with-temp-text "[fn:1]\n\n" (let ((footnote (org-element-at-point))) (or (org-element-property :contents-begin footnote) - (org-element-property :contents-end footnote)))))) + (org-element-property :contents-end footnote))))) + ;; Parse `:pre-blank'. + (should + (= 0 + (org-test-with-temp-text "[fn:1] A" + (org-element-property :pre-blank (org-element-at-point))))) + (should + (= 1 + (org-test-with-temp-text "[fn:1]\nA" + (org-element-property :pre-blank (org-element-at-point))))) + (should + (= 2 + (org-test-with-temp-text "[fn:1]\n\nA" + (org-element-property :pre-blank (org-element-at-point)))))) ;;;; Footnotes Reference. @@ -1433,9 +1446,8 @@ DEADLINE: <2012-03-29 thu.>" - [-] item 1 - [X] item 1.1 - [ ] item 1.2" - (org-element-map - (org-element-parse-buffer) 'item - (lambda (item) (org-element-property :checkbox item)))))) + (org-element-map (org-element-parse-buffer) 'item + (lambda (item) (org-element-property :checkbox item)))))) ;; Item starting with special syntax. (should (equal '(("- item")) @@ -1447,6 +1459,19 @@ DEADLINE: <2012-03-29 thu.>" (org-test-with-temp-text "- item\n #+begin_src emacs-lisp\n(+ 1 1)\n #+end_src" (= (org-element-property :end (org-element-at-point)) (point-max)))) + ;; Parse `:pre-blank'. + (should + (= 0 + (org-test-with-temp-text "- A" + (org-element-property :pre-blank (org-element-at-point))))) + (should + (= 1 + (org-test-with-temp-text "-\n A" + (org-element-property :pre-blank (org-element-at-point))))) + (should + (= 2 + (org-test-with-temp-text "-\n\n A" + (org-element-property :pre-blank (org-element-at-point))))) ;; Last item in a list or sub-list has no `:post-blank' lines, since ;; those belong to the plain-list. (should @@ -2562,7 +2587,14 @@ Outside list" (ert-deftest test-org-element/footnote-definition-interpreter () "Test footnote definition interpreter." - (should (equal (org-test-parse-and-interpret "[fn:1] Test") "[fn:1] Test\n"))) + (should (equal (org-test-parse-and-interpret "[fn:1] Test") "[fn:1] Test\n")) + ;; Handle `:pre-blank' in definitions. + (should + (equal (org-test-parse-and-interpret "[fn:1]\nparagraph") + "[fn:1]\nparagraph\n")) + (should + (equal (org-test-parse-and-interpret "[fn:1]\n\nparagraph") + "[fn:1]\n\nparagraph\n"))) (ert-deftest test-org-element/headline-interpreter () "Test headline and section interpreters." @@ -2683,6 +2715,13 @@ Outside list" (should (equal (org-test-parse-and-interpret "-\n | a | b |") "- \n | a | b |\n")) + ;; Handle `:pre-blank' in items. + (should + (equal (org-test-parse-and-interpret "-\n paragraph") + "- \n paragraph\n")) + (should + (equal (org-test-parse-and-interpret "-\n\n paragraph") + "- \n\n paragraph\n")) ;; Special case: correctly handle "*" bullets. (should (org-test-parse-and-interpret " * item")) ;; Special case: correctly handle empty items.