Skip to content

Commit

Permalink
Add yaml metadata parsing
Browse files Browse the repository at this point in the history
- Also allow multiple pandoc metadata, with tests. Fix jrblevin#66.
- Add defvar for markdown-search-until-condition
- Update with comments from @syohex
  • Loading branch information
Danny McClanahan committed Feb 10, 2016
1 parent 846a2d0 commit ac0f734
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 20 deletions.
147 changes: 127 additions & 20 deletions markdown-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,12 @@ completion."
:group 'markdown
:type 'boolean)

(defcustom markdown-use-pandoc-style-yaml-metadata nil
"When non-nil, allow yaml metadata anywhere in the document, and allow
ellipses to close a metadata region."
: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
Expand Down Expand Up @@ -1374,17 +1380,25 @@ 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
"^\\([[:alpha:]][[:alpha:] _-]*?\\)\\(:[ \t]*\\)\\(.*\\)$"
"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 ====================================================================

Expand Down Expand Up @@ -1496,6 +1510,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
Expand Down Expand Up @@ -1546,10 +1571,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
Expand Down Expand Up @@ -1836,6 +1865,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)
Expand Down Expand Up @@ -1923,8 +1958,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
Expand Down Expand Up @@ -2599,30 +2633,60 @@ 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 (or pos (point-min)))
(cl-loop
with cur-result = nil
and st-hdr = (or start-header "\\`")
and end-hdr = (or end-header "\n\n\\|\n\\'\\|\\'")
while (and (< (point) last)
(re-search-forward st-hdr last t)
(progn
(setq cur-result (match-data))
(re-search-forward end-hdr nil t)))
collect (list cur-result (match-data)))))

(defvar markdown-conditional-search-function #'re-search-forward
"Conditional search function used in `markdown-search-until-condition'. Made
into a variable to allow for dynamic let-binding.")
(defun markdown-search-until-condition (condition &rest args)
(let (ret)
(while (and (not ret) (apply markdown-conditional-search-function 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-find-if ; just take first if multiple
(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
Expand All @@ -2638,6 +2702,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 ==============================================================

Expand Down
54 changes: 54 additions & 0 deletions tests/markdown-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,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"
Expand Down

0 comments on commit ac0f734

Please sign in to comment.