From ff094ce3dc0acb2849fa8bc154863f0fc608b580 Mon Sep 17 00:00:00 2001 From: Danny McClanahan Date: Sat, 6 Feb 2016 21:44:31 -0600 Subject: [PATCH] Add yaml metadata parsing Also allow multiple pandoc metadata, with tests. Fix #66. --- markdown-mode.el | 143 +++++++++++++++++++++++++++++++++++------ tests/markdown-test.el | 63 +++++++++++++++++- 2 files changed, 183 insertions(+), 23 deletions(-) diff --git a/markdown-mode.el b/markdown-mode.el index bad960d5..33a3839d 100644 --- a/markdown-mode.el +++ b/markdown-mode.el @@ -1083,6 +1083,11 @@ completion." :group 'markdown :type 'boolean) +(defcustom markdown-use-pandoc-style-yaml-metadata nil + "When non-nil, allow yaml metadata anywhere in the document." + :group 'markdown + :type 'boolean) + (defcustom markdown-live-preview-window-function 'markdown-live-preview-window-eww "Function to display preview of Markdown output within Emacs. Function must @@ -1374,7 +1379,7 @@ Group 3 matches the mathematical expression contained within.") (defconst markdown-regex-math-display "^\\(\\\\\\[\\)\\(\\(?:.\\|\n\\)*\\)?\\(\\\\\\]\\)$" "Regular expression for itex \[..\] display mode expressions. -Groups 1 and 3 matche the opening and closing delimiters. +Groups 1 and 3 match the opening and closing delimiters. Group 2 matches the mathematical expression contained within.") (defconst markdown-regex-multimarkdown-metadata @@ -1382,9 +1387,17 @@ Group 2 matches the mathematical expression contained within.") "Regular expression for matching MultiMarkdown metadata.") (defconst markdown-regex-pandoc-metadata - "^\\(%\\)\\([ \t]*\\)\\(.*\\)$" + "^\\(%\\)\\([ \t]*\\)\\(.*\\(?:\n[ \t]+.*\\)*\\)" "Regular expression for matching Pandoc metadata.") +(defconst markdown-regex-yaml-metadata-border + "\\(\\-\\{3\\}\\)$" + "Regular expression for matching yaml metadata.") + +(defconst markdown-regex-yaml-pandoc-metadata-end-border + "\\(\\.\\{3\\}\\|\\-\\{3\\}\\)$" + "Regular expression for matching yaml metadata end borders.") + ;;; Syntax ==================================================================== @@ -1496,6 +1509,17 @@ Function is called repeatedly until it returns nil. For details, see 'markdown-blockquote (match-data t))))) + (defun markdown-syntax-propertize-yaml-metadata (start end) + (save-excursion + (goto-char start) + (while (markdown-match-yaml-metadata end) + (put-text-property (match-beginning 1) (match-end 1) + 'markdown-metadata-key (match-data t)) + (put-text-property (match-beginning 2) (match-end 2) + 'markdown-metadata-markup (match-data t)) + (put-text-property (match-beginning 3) (match-end 3) + 'markdown-metadata-value (match-data t))))) + (defun markdown-syntax-propertize-headings-generic (symbol regex start end) "Match headings of type SYMBOL with REGEX from START to END." (save-excursion @@ -1546,10 +1570,14 @@ Function is called repeatedly until it returns nil. For details, see (remove-text-properties start end '(markdown-heading-4-atx)) (remove-text-properties start end '(markdown-heading-5-atx)) (remove-text-properties start end '(markdown-heading-6-atx)) + (remove-text-properties start end '(markdown-metadata-key)) + (remove-text-properties start end '(markdown-metadata-value)) + (remove-text-properties start end '(markdown-metadata-markup)) (markdown-syntax-propertize-gfm-code-blocks start end) (markdown-syntax-propertize-fenced-code-blocks start end) (markdown-syntax-propertize-pre-blocks start end) (markdown-syntax-propertize-blockquotes start end) + (markdown-syntax-propertize-yaml-metadata start end) (markdown-syntax-propertize-headings-generic 'markdown-heading-1-setext markdown-regex-header-1-setext start end) (markdown-syntax-propertize-headings-generic @@ -1836,6 +1864,12 @@ See `font-lock-syntactic-face-function' for details." (defvar markdown-mode-font-lock-keywords-basic (list + (cons 'markdown-match-yaml-metadata-border + '((1 markdown-markup-face) + (2 markdown-markup-face))) + (cons 'markdown-match-yaml-metadata '((1 markdown-metadata-key-face) + (2 markdown-markup-face) + (3 markdown-metadata-value-face))) (cons 'markdown-match-gfm-code-blocks '((1 markdown-markup-face) (2 markdown-language-keyword-face nil t) (3 markdown-pre-face) @@ -1923,8 +1957,7 @@ See `font-lock-syntactic-face-function' for details." (3 markdown-markup-face prepend))) (cons markdown-regex-uri 'markdown-link-face) (cons markdown-regex-email 'markdown-link-face) - (cons markdown-regex-line-break '(1 markdown-line-break-face prepend)) - ) + (cons markdown-regex-line-break '(1 markdown-line-break-face prepend))) "Syntax highlighting for Markdown files.") (defconst markdown-mode-font-lock-keywords-math @@ -2599,30 +2632,57 @@ analysis." (set-match-data (list beg (point))) t)))) -(defun markdown-match-generic-metadata (regexp last) +(defun markdown-get-match-boundaries (start-header end-header last &optional pos) + (save-excursion + (goto-char (if pos pos (point-min))) + (cl-loop + with cur-result = nil + while (and (< (point) last) + (re-search-forward (or start-header "\\`") last t) + (progn + (setq cur-result (match-data)) + (re-search-forward + (or end-header "\n\n\\|\n\\'\\|\\'") nil t))) + collect (list cur-result (match-data))))) + +(defun markdown-search-until-condition (condition &rest args) + (let (ret) + (while (and (not ret) (apply #'re-search-forward args)) + (setq ret (funcall condition))) + ret)) + +(defun markdown-match-generic-metadata + (regexp last &optional start-header end-header) "Match generic metadata specified by REGEXP from the point to LAST." - (let ((header-end (save-excursion - (goto-char (point-min)) - (if (re-search-forward "\n\n" (point-max) t) - (match-beginning 0) - (point-max))))) - (cond ((>= (point) header-end) - ;; Don't match anything outside of the header. + ;; if start-header is nil, we assume metadata can only occur at the very top + ;; of a file ("\\`"). if end-header is nil, we assume it is "\n\n" + (let* ((header-bounds + (markdown-get-match-boundaries start-header end-header last)) + (enclosing-header + (cl-first ; just take first if multiple (maybe impossible) + (cl-remove-if-not + (lambda (match-bounds) + (cl-destructuring-bind (start-match end-match) match-bounds + (and + (< (point) (cl-first end-match)) + (save-excursion + (re-search-forward regexp (cl-second end-match) t))))) + header-bounds))) + (header-begin + (when enclosing-header (cl-second (cl-first enclosing-header)))) + (header-end + (when enclosing-header (cl-first (cl-second enclosing-header))))) + (cond ((null enclosing-header) + ;; Don't match anything outside of a header. nil) - ((re-search-forward regexp (min last header-end) t) + ((markdown-search-until-condition + (lambda () (> (point) header-begin)) regexp (min last header-end) t) ;; If a metadata item is found, it may span several lines. (let ((key-beginning (match-beginning 1)) (key-end (match-end 1)) (markup-begin (match-beginning 2)) (markup-end (match-end 2)) (value-beginning (match-beginning 3))) - (while (and (not (looking-at regexp)) - (not (> (point) (min last header-end))) - (not (eobp))) - (forward-line)) - (unless (eobp) - (forward-line -1) - (end-of-line)) (set-match-data (list key-beginning (point) ; complete metadata key-beginning key-end ; key markup-begin markup-end ; markup @@ -2638,6 +2698,49 @@ analysis." "Match Pandoc metadata from the point to LAST." (markdown-match-generic-metadata markdown-regex-pandoc-metadata last)) +(defun markdown-match-yaml-metadata (last) + "Match yaml metadata from the point to LAST." + (markdown-match-generic-metadata + markdown-regex-multimarkdown-metadata last + (concat + (if markdown-use-pandoc-style-yaml-metadata "^" "\\`") + markdown-regex-yaml-metadata-border) + (concat + "^" + (if markdown-use-pandoc-style-yaml-metadata + markdown-regex-yaml-pandoc-metadata-end-border + markdown-regex-yaml-metadata-border)))) + +(defun markdown-match-yaml-metadata-border (last) + (let ((res + (cl-first + (markdown-get-match-boundaries + (concat + (if markdown-use-pandoc-style-yaml-metadata "^" "\\`") + markdown-regex-yaml-metadata-border) + (concat + "^" + (if markdown-use-pandoc-style-yaml-metadata + markdown-regex-yaml-pandoc-metadata-end-border + markdown-regex-yaml-metadata-border)) + last (point))))) + (when res + (cl-destructuring-bind (start-header end-header) res + (set-match-data + (list (cl-third start-header) (cl-fourth end-header) + (cl-third start-header) (cl-fourth start-header) + (cl-third end-header) (cl-fourth end-header))) + t)))) + +(defun markdown-match-yaml-metadata-key (last) + (markdown-match-propertized-text 'markdown-metadata-key last)) + +(defun markdown-match-yaml-metadata-markup (last) + (markdown-match-propertized-text 'markdown-metadata-markup last)) + +(defun markdown-match-yaml-metadata-value (last) + (markdown-match-propertized-text 'markdown-metadata-value last)) + ;;; Syntax Table ============================================================== diff --git a/tests/markdown-test.el b/tests/markdown-test.el index 62b1af99..61a1061b 100644 --- a/tests/markdown-test.el +++ b/tests/markdown-test.el @@ -2252,7 +2252,8 @@ for (var i = 0; i < 10; i++) { (ert-deftest test-markdown-font-lock/mmd-metadata () "Basic MultMarkdown metadata tests." - (markdown-test-string "Title: peg-multimarkdown User's Guide + (markdown-test-string + "Title: peg-multimarkdown User's Guide Author: Fletcher T. Penney Base Header Level: 2 " (markdown-test-range-has-face 1 5 markdown-metadata-key-face) @@ -2270,7 +2271,8 @@ Base Header Level: 2 " (ert-deftest test-markdown-font-lock/mmd-metadata-after-header () "Ensure that similar lines are not matched after the header." - (markdown-test-string "Title: peg-multimarkdown User's Guide + (markdown-test-string + "Title: peg-multimarkdown User's Guide Author: Fletcher T. Penney Base Header Level: 2 " @@ -2282,7 +2284,8 @@ Base Header Level: 2 " (ert-deftest test-markdown-font-lock/pandoc-metadata () "Basic Pandoc metadata tests." - (markdown-test-string "% title + (markdown-test-string + "% title two-line title % first author; second author @@ -2297,6 +2300,60 @@ body" (markdown-test-range-has-face 60 63 markdown-metadata-value-face) (markdown-test-range-has-face 64 69 nil))) +(ert-deftest test-markdown-font-lock/yaml-metadata () + "Basic yaml metadata tests." + (markdown-test-string + "--- +layout: post +date: 2015-08-13 11:35:25 EST +--- +" + ;; first section + (markdown-test-range-has-face 1 3 markdown-markup-face) + (markdown-test-range-has-face 5 10 markdown-metadata-key-face) + (markdown-test-range-has-face 11 11 markdown-markup-face) + (markdown-test-range-has-face 13 16 markdown-metadata-value-face) + (markdown-test-range-has-face 18 21 markdown-metadata-key-face) + (markdown-test-range-has-face 22 22 markdown-markup-face) + (markdown-test-range-has-face 24 46 markdown-metadata-value-face) + (markdown-test-range-has-face 48 50 markdown-markup-face))) + +(ert-deftest test-markdown-font-lock/pandoc-yaml-metadata () + "Basic yaml metadata tests, with pandoc syntax." + (let ((markdown-use-pandoc-style-yaml-metadata t)) + (markdown-test-string + "some text + +--- +layout: post +date: 2015-08-13 11:35:25 EST +... + +more text + +--- +layout: post +date: 2015-08-13 11:35:25 EST +---" + ;; first section + (markdown-test-range-has-face 12 14 markdown-markup-face) + (markdown-test-range-has-face 16 21 markdown-metadata-key-face) + (markdown-test-range-has-face 22 22 markdown-markup-face) + (markdown-test-range-has-face 24 27 markdown-metadata-value-face) + (markdown-test-range-has-face 29 32 markdown-metadata-key-face) + (markdown-test-range-has-face 33 33 markdown-markup-face) + (markdown-test-range-has-face 35 57 markdown-metadata-value-face) + (markdown-test-range-has-face 59 61 markdown-markup-face) + ;; second section + (markdown-test-range-has-face 75 77 markdown-markup-face) + (markdown-test-range-has-face 79 84 markdown-metadata-key-face) + (markdown-test-range-has-face 85 85 markdown-markup-face) + (markdown-test-range-has-face 87 90 markdown-metadata-value-face) + (markdown-test-range-has-face 92 95 markdown-metadata-key-face) + (markdown-test-range-has-face 96 96 markdown-markup-face) + (markdown-test-range-has-face 98 120 markdown-metadata-value-face) + (markdown-test-range-has-face 122 124 markdown-markup-face)))) + (ert-deftest test-markdown-font-lock/line-break () "Basic line break tests." (markdown-test-string " \nasdf \n"