branch: elpa/markdown-mode
commit 0525051da125cb835832a2b086e8a3797c75aef4
Author: systemfreund <[email protected]>
Commit: systemfreund <[email protected]>
Support fenced code blocks with 4+ backticks
Previously, GFM fenced code block regexes matched exactly three
backticks, causing blocks with four or more backticks to be
unrecognized. This meant headers inside such blocks were
incorrectly treated as document structure (outline headings,
imenu entries, font-lock targets).
Apply the same variable-length fence approach already used for
tilde fences: match 3+ backticks in the opening/closing regexes,
and dynamically require the closing fence to have at least as
many backticks as the opening fence.
Fixes: https://github.com/jrblevin/markdown-mode/issues/932
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
markdown-mode.el | 21 ++++++++++++------
tests/markdown-test.el | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 72 insertions(+), 7 deletions(-)
diff --git a/markdown-mode.el b/markdown-mode.el
index 21acc05b06..1116e37cc6 100644
--- a/markdown-mode.el
+++ b/markdown-mode.el
@@ -847,9 +847,9 @@ Groups 1 and 3 match the opening and closing tags.
Group 2 matches the key sequence.")
(defconst markdown-regex-gfm-code-block-open
-
"^[[:blank:]]*\\(?1:```\\)\\(?2:[[:blank:]]*{?[[:blank:]]*\\)\\(?3:[^`[:space:]]+?\\)?\\(?:[[:blank:]]+\\(?4:.+?\\)\\)?\\(?5:[[:blank:]]*}?[[:blank:]]*\\)$"
+
"^[[:blank:]]*\\(?1:`\\{3,\\}\\)\\(?2:[[:blank:]]*{?[[:blank:]]*\\)\\(?3:[^`[:space:]]+?\\)?\\(?:[[:blank:]]+\\(?4:.+?\\)\\)?\\(?5:[[:blank:]]*}?[[:blank:]]*\\)$"
"Regular expression matching opening of GFM code blocks.
-Group 1 matches the opening three backquotes and any following whitespace.
+Group 1 matches the opening three or more backquotes.
Group 2 matches the opening brace (optional) and surrounding whitespace.
Group 3 matches the language identifier (optional).
Group 4 matches the info string (optional).
@@ -857,9 +857,9 @@ Group 5 matches the closing brace (optional), whitespace,
and newline.
Groups need to agree with `markdown-regex-tilde-fence-begin'.")
(defconst markdown-regex-gfm-code-block-close
- "^[[:blank:]]*\\(?1:```\\)\\(?2:\\s *?\\)$"
+ "^[[:blank:]]*\\(?1:`\\{3,\\}\\)\\(?2:\\s *?\\)$"
"Regular expression matching closing of GFM code blocks.
-Group 1 matches the closing three backquotes.
+Group 1 matches the closing three or more backquotes.
Group 2 matches any whitespace and the final newline.")
(defconst markdown-regex-pre
@@ -1008,6 +1008,13 @@ Group 3 matches the mathematical expression contained
within.
Group 2 matches the opening slashes, and is used internally to
match the closing slashes.")
+(defsubst markdown-make-gfm-fence-regex (num-backticks &optional end-of-line)
+ "Return regexp matching a GFM code fence at least NUM-BACKTICKS long.
+END-OF-LINE is the regexp construct to indicate end of line; $ if
+missing."
+ (format "%s%d%s%s" "^[[:blank:]]*\\([`]\\{" num-backticks ",\\}\\)"
+ (or end-of-line "$")))
+
(defsubst markdown-make-tilde-fence-regex (num-tildes &optional end-of-line)
"Return regexp matching a tilde code fence at least NUM-TILDES long.
END-OF-LINE is the regexp construct to indicate end of line; $ if
@@ -1421,7 +1428,7 @@ giving the bounds of the current and parent list items."
(markdown-get-yaml-metadata-end-border markdown-yaml-metadata-end)
markdown-yaml-metadata-section)
((,markdown-regex-gfm-code-block-open markdown-gfm-block-begin)
- (,markdown-regex-gfm-code-block-close markdown-gfm-block-end)
+ (markdown-make-gfm-fence-regex markdown-gfm-block-end)
markdown-gfm-code))
"Mapping of regular expressions to \"fenced-block\" constructs.
These constructs are distinguished by having a distinctive start
@@ -1691,8 +1698,8 @@ MIDDLE-BEGIN is the start of the \"middle\" section of
the block."
(save-excursion
(goto-char begin)
(save-match-data
- (and (search-forward "```" nil t)
- (search-forward "```" (line-end-position) t)))))
+ (and (re-search-forward "`\\{3,\\}" nil t)
+ (re-search-forward "`\\{3,\\}" (line-end-position) t)))))
(defun markdown-syntax-propertize-fenced-block-constructs (start end)
"Propertize according to `markdown-fenced-block-pairs' from START to END.
diff --git a/tests/markdown-test.el b/tests/markdown-test.el
index 1a19f179fe..3778adb530 100644
--- a/tests/markdown-test.el
+++ b/tests/markdown-test.el
@@ -4125,6 +4125,64 @@ echo hey
(should (equal (markdown-code-block-at-pos 34) '(1 35)))
(should (equal (markdown-code-block-at-pos 35) nil))))
+(ert-deftest test-markdown-parsing/code-block-at-pos-gfm-fenced-4-backticks ()
+ "Ensure `markdown-code-block-at-pos' works with 4-backtick fenced blocks."
+ (markdown-test-string
+ "```` markdown
+# Not a heading
+```
+inner
+```
+````"
+ (should (markdown-code-block-at-pos 1))
+ ;; Inside the block, on the "# Not a heading" line
+ (should (markdown-code-block-at-pos 15))
+ ;; On the inner ``` line — should still be inside the outer block
+ (should (markdown-code-block-at-pos 33))
+ ;; After the closing ```` — should not be in a code block
+ (should-not (markdown-code-block-at-pos (point-max)))))
+
+(ert-deftest test-markdown-parsing/code-block-at-pos-gfm-fenced-5-backticks ()
+ "Ensure `markdown-code-block-at-pos' works with 5-backtick fenced blocks."
+ (markdown-test-string
+ "````` text
+# Not a heading
+`````"
+ (should (markdown-code-block-at-pos 1))
+ (should (markdown-code-block-at-pos 12))
+ (should-not (markdown-code-block-at-pos (point-max)))))
+
+(ert-deftest test-markdown-outline/heading-in-4-backtick-code-block ()
+ "Headers inside 4+ backtick code blocks should not be outline headings.
+See GitHub issue jrblevin/markdown-mode#932."
+ (markdown-test-string-gfm
+ "# Real heading
+
+```` markdown
+# Not a heading
+```
+inner block
+```
+````
+
+## Another real heading
+"
+ ;; The 4-backtick block should be detected as a code block
+ (should (markdown-code-block-at-pos 35))
+ ;; Navigate forward through headings — the fake heading should be skipped
+ (goto-char (point-min))
+ (markdown-next-visible-heading 1)
+ (should (looking-at "^## Another real heading"))))
+
+(ert-deftest test-markdown-font-lock/gfm-code-block-4-backticks ()
+ "Test font-lock for 4-backtick GFM code blocks."
+ (markdown-test-string-gfm
+ "```` markdown
+code line
+````"
+ ;; The code line should have markdown-pre-face
+ (markdown-test-range-has-face 15 23 'markdown-pre-face)))
+
(ert-deftest test-markdown-parsing/code-block-at-pos-yaml-metadata ()
"Ensure `markdown-code-block-at-pos' works in YAML metadata blocks."
(let ((markdown-use-pandoc-style-yaml-metadata t))