diff --git a/CHANGES.md b/CHANGES.md
index 76b5eba8..ed39ecd4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -43,6 +43,8 @@
- Add custom variables `markdown-xhtml-body-preamble` and
`markdown-xhtml-body-epilogue` for wrapping additional XHTML
tags around the output. ([GH-280][], [GH-281][])
+ - Add `markdown-unused-refs` command to list and clean up unused
+ references (available via `C-c C-c u`)
* Improvements:
diff --git a/README.md b/README.md
index 4ff11345..8a3bc1b9 100644
--- a/README.md
+++ b/README.md
@@ -355,6 +355,11 @@ can obtain a list of all keybindings by pressing C-c C-h.
end of the buffer. Similarly, selecting the line number will
jump to the corresponding line.
+ C-c C-c u will check for unused references. This will
+ also open a small buffer if any are found, similar to undefined
+ reference checking. The buffer for unused references will contain
+ `X` buttons that remove unused references when selected.
+
C-c C-c n renumbers any ordered lists in the buffer that are
out of sequence.
diff --git a/markdown-mode.el b/markdown-mode.el
index 790b5a24..5d527939 100644
--- a/markdown-mode.el
+++ b/markdown-mode.el
@@ -409,9 +409,9 @@ nil to disable this."
The car is used for subscript, the cdr is used for superscripts."
:group 'markdown
:type '(cons (choice (sexp :tag "Subscript form")
- (const :tag "No lowering" nil))
- (choice (sexp :tag "Superscript form")
- (const :tag "No raising" nil)))
+ (const :tag "No lowering" nil))
+ (choice (sexp :tag "Superscript form")
+ (const :tag "No raising" nil)))
:package-version '(markdown-mode . "2.4"))
(defcustom markdown-unordered-list-item-prefix " * "
@@ -2781,13 +2781,16 @@ intact additional processing."
(match-beginning 5) (match-end 5)))))))))
(defun markdown-get-defined-references ()
- "Return a list of all defined reference labels (not including square brackets)."
+ "Return all defined reference labels and their line numbers (not including square brackets)."
(save-excursion
(goto-char (point-min))
(let (refs)
(while (re-search-forward markdown-regex-reference-definition nil t)
(let ((target (match-string-no-properties 2)))
- (cl-pushnew target refs :test #'equal)))
+ (cl-pushnew
+ (cons (downcase target)
+ (markdown-line-number-at-pos (match-beginning 2)))
+ refs :test #'equal :key #'car)))
(reverse refs))))
(defun markdown-get-used-uris ()
@@ -3938,7 +3941,7 @@ This is an internal function called by
(let* ((ref (when ref (concat "[" ref "]")))
(defined-refs (append
(mapcar (lambda (ref) (concat "[" ref "]"))
- (markdown-get-defined-references))))
+ (mapcar #'car (markdown-get-defined-references)))))
(used-uris (markdown-get-used-uris))
(uri-or-ref (completing-read
"URL or [reference]: "
@@ -5288,6 +5291,7 @@ Assumes match data is available for `markdown-regex-italic'."
(propertize "e" 'face 'markdown-bold-face) "xport, "
"export & pre" (propertize "v" 'face 'markdown-bold-face) "iew, "
(propertize "c" 'face 'markdown-bold-face) "heck refs, "
+ (propertize "u" 'face 'markdown-bold-face) "nused refs, "
"C-h = more")))
(defvar markdown-mode-style-map
@@ -5332,6 +5336,7 @@ Assumes match data is available for `markdown-regex-italic'."
(define-key map (kbd "l") 'markdown-live-preview-mode)
(define-key map (kbd "w") 'markdown-kill-ring-save)
(define-key map (kbd "c") 'markdown-check-refs)
+ (define-key map (kbd "u") 'markdown-unused-refs)
(define-key map (kbd "n") 'markdown-cleanup-list-numbers)
(define-key map (kbd "]") 'markdown-complete-buffer)
(define-key map (kbd "^") 'markdown-table-sort-lines)
@@ -5588,6 +5593,7 @@ See also `markdown-mode-map'.")
:keys "C-c C-s w"]
"---"
["Check References" markdown-check-refs]
+ ["Find Unused References" markdown-unused-refs]
["Toggle URL Hiding" markdown-toggle-url-hiding
:style radio
:selected markdown-hide-urls]
@@ -5766,6 +5772,35 @@ the link, and line is the line number on which the link appears."
(cl-pushnew (list text start line) links :test #'equal))))
links))
+(defmacro markdown-for-all-refs (f)
+ `(let ((result))
+ (save-excursion
+ (goto-char (point-min))
+ (while
+ (re-search-forward markdown-regex-link-reference nil t)
+ (let* ((text (match-string-no-properties 3))
+ (reference (match-string-no-properties 6))
+ (target (downcase (if (string= reference "") text reference))))
+ (,f text target result))))
+ (reverse result)))
+
+(defmacro markdown-collect-always (_ target result)
+ `(cl-pushnew ,target ,result :test #'equal))
+
+(defmacro markdown-collect-undefined (text target result)
+ `(unless (markdown-reference-definition target)
+ (let ((entry (assoc ,target ,result)))
+ (if (not entry)
+ (cl-pushnew
+ (cons ,target (list (cons ,text (markdown-line-number-at-pos))))
+ ,result :test #'equal)
+ (setcdr entry
+ (append (cdr entry) (list (cons ,text (markdown-line-number-at-pos)))))))))
+
+(defun markdown-get-all-refs ()
+ "Return a list of all Markdown references."
+ (markdown-for-all-refs markdown-collect-always))
+
(defun markdown-get-undefined-refs ()
"Return a list of undefined Markdown references.
Result is an alist of pairs (reference . occurrences), where
@@ -5773,23 +5808,35 @@ occurrences is itself another alist of pairs (label . line-number).
For example, an alist corresponding to [Nice editor][Emacs] at line 12,
\[GNU Emacs][Emacs] at line 45 and [manual][elisp] at line 127 is
\((\"emacs\" (\"Nice editor\" . 12) (\"GNU Emacs\" . 45)) (\"elisp\" (\"manual\" . 127)))."
- (let ((missing))
- (save-excursion
- (goto-char (point-min))
- (while
- (re-search-forward markdown-regex-link-reference nil t)
- (let* ((text (match-string-no-properties 3))
- (reference (match-string-no-properties 6))
- (target (downcase (if (string= reference "") text reference))))
- (unless (markdown-reference-definition target)
- (let ((entry (assoc target missing)))
- (if (not entry)
- (cl-pushnew
- (cons target (list (cons text (markdown-line-number-at-pos))))
- missing :test #'equal)
- (setcdr entry
- (append (cdr entry) (list (cons text (markdown-line-number-at-pos))))))))))
- (reverse missing))))
+ (markdown-for-all-refs markdown-collect-undefined))
+
+(defun markdown-get-unused-refs ()
+ (cl-sort
+ (cl-set-difference
+ (markdown-get-defined-references) (markdown-get-all-refs)
+ :test (lambda (e1 e2) (equal (car e1) e2)))
+ #'< :key #'cdr))
+
+(defmacro defun-markdown-buffer (name docstring)
+ "Define a function to name and return a buffer.
+
+By convention, NAME must be a name of a string constant with
+%buffer% placeholder used to name the buffer, and will also be
+used as a name of the function defined.
+
+DOCSTRING will be used as the first part of the docstring."
+ `(defun ,name (&optional buffer-name)
+ ,(concat docstring "\n\nBUFFER-NAME is the name of the main buffer being visited.")
+ (or buffer-name (setq buffer-name (buffer-name)))
+ (let ((refbuf (get-buffer-create (markdown-replace-regexp-in-string
+ "%buffer%" buffer-name
+ ,name))))
+ (with-current-buffer refbuf
+ (when view-mode
+ (View-exit-and-edit))
+ (use-local-map button-buffer-map)
+ (erase-buffer))
+ refbuf)))
(defconst markdown-reference-check-buffer
"*Undefined references for %buffer%*"
@@ -5797,38 +5844,28 @@ For example, an alist corresponding to [Nice editor][Emacs] at line 12,
The string %buffer% will be replaced by the corresponding
`markdown-mode' buffer name.")
-(defun markdown-reference-check-buffer (&optional buffer-name)
- "Name and return buffer for reference checking.
-BUFFER-NAME is the name of the main buffer being visited."
- (or buffer-name (setq buffer-name (buffer-name)))
- (let ((refbuf (get-buffer-create (markdown-replace-regexp-in-string
- "%buffer%" buffer-name
- markdown-reference-check-buffer))))
- (with-current-buffer refbuf
- (when view-mode
- (View-exit-and-edit))
- (use-local-map button-buffer-map)
- (erase-buffer))
- refbuf))
+(defun-markdown-buffer
+ markdown-reference-check-buffer
+ "Name and return buffer for reference checking.")
+
+(defconst markdown-unused-references-buffer
+ "*Unused references for %buffer%*"
+ "Pattern for name of buffer for listing unused references.
+The string %buffer% will be replaced by the corresponding
+`markdown-mode' buffer name.")
+
+(defun-markdown-buffer
+ markdown-unused-references-buffer
+ "Name and return buffer for unused reference checking.")
(defconst markdown-reference-links-buffer
"*Reference links for %buffer%*"
"Pattern for name of buffer for listing references.
The string %buffer% will be replaced by the corresponding buffer name.")
-(defun markdown-reference-links-buffer (&optional buffer-name)
- "Name, setup, and return a buffer for listing links.
-BUFFER-NAME is the name of the main buffer being visited."
- (or buffer-name (setq buffer-name (buffer-name)))
- (let ((linkbuf (get-buffer-create (markdown-replace-regexp-in-string
- "%buffer%" buffer-name
- markdown-reference-links-buffer))))
- (with-current-buffer linkbuf
- (when view-mode
- (View-exit-and-edit))
- (use-local-map button-buffer-map)
- (erase-buffer))
- linkbuf))
+(defun-markdown-buffer
+ markdown-reference-links-buffer
+ "Name, setup, and return a buffer for listing links.")
;; Add an empty Markdown reference definition to buffer
;; specified in the 'target-buffer property. The reference name is
@@ -5848,18 +5885,31 @@ BUFFER-NAME is the name of the main buffer being visited."
(markdown-check-refs t))))
;; Jump to line in buffer specified by 'target-buffer property.
-;; Line number is button's 'line property.
+;; Line number is button's 'target-line property.
(define-button-type 'markdown-goto-line-button
'help-echo "mouse-1, RET: go to line"
'follow-link t
'face 'italic
'action (lambda (b)
- (message (button-get b 'buffer))
(switch-to-buffer-other-window (button-get b 'target-buffer))
;; use call-interactively to silence compiler
(let ((current-prefix-arg (button-get b 'target-line)))
(call-interactively 'goto-line))))
+;; Kill a line in buffer specified by 'target-buffer property.
+;; Line number is button's 'target-line property.
+(define-button-type 'markdown-kill-line-button
+ 'help-echo "mouse-1, RET: kill line"
+ 'follow-link t
+ 'face 'italic
+ 'action (lambda (b)
+ (switch-to-buffer-other-window (button-get b 'target-buffer))
+ ;; use call-interactively to silence compiler
+ (let ((current-prefix-arg (button-get b 'target-line)))
+ (call-interactively 'goto-line))
+ (kill-line 1)
+ (markdown-unused-refs t)))
+
;; Jumps to a particular link at location given by 'target-char
;; property in buffer given by 'target-buffer property.
(define-button-type 'markdown-location-button
@@ -5876,7 +5926,7 @@ BUFFER-NAME is the name of the main buffer being visited."
(defun markdown-insert-undefined-reference-button (reference oldbuf)
"Insert a button for creating REFERENCE in buffer OLDBUF.
REFERENCE should be a list of the form (reference . occurrences),
-as by `markdown-get-undefined-refs'."
+as returned by `markdown-get-undefined-refs'."
(let ((label (car reference)))
;; Create a reference button
(insert-button label
@@ -5896,6 +5946,26 @@ as by `markdown-get-undefined-refs'."
(insert ")")
(newline)))
+(defun markdown-insert-unused-reference-button (reference oldbuf)
+ "Insert a button for creating REFERENCE in buffer OLDBUF.
+REFERENCE must be a pair of (ref . line-number)."
+ (let ((label (car reference))
+ (line (cdr reference)))
+ ;; Create a reference button
+ (insert-button label
+ :type 'markdown-goto-line-button
+ 'face 'bold
+ 'target-buffer oldbuf
+ 'target-line line)
+ (insert (format " (%d) [" line))
+ (insert-button "X"
+ :type 'markdown-kill-line-button
+ 'face 'bold
+ 'target-buffer oldbuf
+ 'target-line line)
+ (insert "]")
+ (newline)))
+
(defun markdown-insert-link-button (link oldbuf)
"Insert a button for jumping to LINK in buffer OLDBUF.
LINK should be a list of the form (text char line) containing
@@ -5933,30 +6003,66 @@ the link text, location, and line number."
(t
(error "No links for reference %s" reference)))))
-(defun markdown-check-refs (&optional silent)
+(defmacro defun-markdown-ref-checker
+ (name docstring checker-function buffer-function none-message buffer-header insert-reference)
+ "Define a function NAME acting on result of CHECKER-FUNCTION.
+
+DOCSTRING is used as a docstring for the defined function.
+
+BUFFER-FUNCTION should name and return an auxiliary buffer to put
+results in.
+
+NONE-MESSAGE is used when CHECKER-FUNCTION returns no results.
+
+BUFFER-HEADER is put into the auxiliary buffer first, followed by
+calling INSERT-REFERENCE for each element in the list returned by
+CHECKER-FUNCTION."
+ `(defun ,name (&optional silent)
+ ,(concat
+ docstring
+ "\n\nIf SILENT is non-nil, do not message anything when no
+such references found.")
+ (interactive "P")
+ (when (not (memq major-mode '(markdown-mode gfm-mode)))
+ (user-error "Not available in current mode"))
+ (let ((oldbuf (current-buffer))
+ (refs (,checker-function))
+ (refbuf (,buffer-function)))
+ (if (null refs)
+ (progn
+ (when (not silent)
+ (message ,none-message))
+ (kill-buffer refbuf))
+ (with-current-buffer refbuf
+ (insert ,buffer-header)
+ (dolist (ref refs)
+ (,insert-reference ref oldbuf))
+ (view-buffer-other-window refbuf)
+ (goto-char (point-min))
+ (forward-line 2))))))
+
+(defun-markdown-ref-checker
+ markdown-check-refs
"Show all undefined Markdown references in current `markdown-mode' buffer.
-If SILENT is non-nil, do not message anything when no undefined
-references found.
+
Links which have empty reference definitions are considered to be
defined."
- (interactive "P")
- (when (not (memq major-mode '(markdown-mode gfm-mode)))
- (user-error "Not available in current mode"))
- (let ((oldbuf (current-buffer))
- (refs (markdown-get-undefined-refs))
- (refbuf (markdown-reference-check-buffer)))
- (if (null refs)
- (progn
- (when (not silent)
- (message "No undefined references found"))
- (kill-buffer refbuf))
- (with-current-buffer refbuf
- (insert "The following references are undefined:\n\n")
- (dolist (ref refs)
- (markdown-insert-undefined-reference-button ref oldbuf))
- (view-buffer-other-window refbuf)
- (goto-char (point-min))
- (forward-line 2)))))
+ markdown-get-undefined-refs
+ markdown-reference-check-buffer
+ "No undefined references found"
+ "The following references are undefined:\n\n"
+ markdown-insert-undefined-reference-button)
+
+
+(defun-markdown-ref-checker
+ markdown-unused-refs
+ "Show all unused Markdown references in current `markdown-mode' buffer."
+ markdown-get-unused-refs
+ markdown-unused-references-buffer
+ "No unused references found"
+ "The following references are unused:\n\n"
+ markdown-insert-unused-reference-button)
+
;;; Lists =====================================================================
diff --git a/tests/markdown-test.el b/tests/markdown-test.el
index f818b38c..78503370 100644
--- a/tests/markdown-test.el
+++ b/tests/markdown-test.el
@@ -3605,7 +3605,15 @@ only partially propertized."
"Test `markdown-get-defined-references'."
(markdown-test-file "syntax.text"
(should (equal (markdown-get-defined-references)
- '("src" "1" "2" "3" "4" "5" "6" "bq" "l"))))
+ '(("src" . 37)
+ ("1" . 55)
+ ("2" . 56)
+ ("3" . 57)
+ ("4" . 58)
+ ("5" . 59)
+ ("6" . 60)
+ ("bq" . 205)
+ ("l" . 206)))))
(markdown-test-file "outline.text"
(should (equal (markdown-get-defined-references) nil)))
(markdown-test-file "wiki-links.text"
@@ -3825,6 +3833,19 @@ puts 'hello, world'
;;; Reference Checking:
+(ert-deftest test-markdown-references/get-unused-refs ()
+ "Test `markdown-get-unused-refs'."
+ (markdown-test-file "refs.text"
+ (should (equal (markdown-get-unused-refs)
+ '(("logorrhea" . 8)
+ ("orphan" . 11))))))
+
+(ert-deftest test-markdown-references/get-undefined-refs ()
+ "Test `markdown-get-undefined-refs'."
+ (markdown-test-file "refs.text"
+ (should (equal (markdown-get-undefined-refs)
+ '(("problems" ("problems" . 3) ("controversy" . 5)))))))
+
(ert-deftest test-markdown-references/goto-line-button ()
"Create and test a goto line button."
(markdown-test-string "line 1\nline 2\n"
@@ -3857,6 +3878,25 @@ puts 'hello, world'
(should (equal (local-key-binding (kbd "TAB")) 'forward-button))
(should (equal (local-key-binding (kbd "")) 'backward-button))))))
+(ert-deftest test-markdown-references/undefined-refs-killing ()
+ "Test that buttons in unused references buffer delete lines when pushed."
+ (markdown-test-file "refs.text"
+ (let* ((target (buffer-name))
+ (check (markdown-replace-regexp-in-string
+ "%buffer%" target
+ markdown-unused-references-buffer))
+ (original-unused-refs (markdown-get-unused-refs)))
+ (markdown-unused-refs)
+ ;; Push X
+ (with-current-buffer (get-buffer check)
+ (forward-button 1)
+ (call-interactively 'push-button))
+ ;; The first orphan should now be gone with the rest of orphans
+ ;; moved up by one line
+ (should (equal (markdown-get-unused-refs)
+ (mapcar (lambda (o) (cons (car o) (1- (cdr o))))
+ (cdr original-unused-refs)))))))
+
;;; Lists:
(ert-deftest test-markdown-lists/nested-list-file ()
diff --git a/tests/refs.text b/tests/refs.text
new file mode 100644
index 00000000..34f3d077
--- /dev/null
+++ b/tests/refs.text
@@ -0,0 +1,11 @@
+Now this a pretty [long][graphomania] text. Written to conclude a
+period of intensive thinking, it spans many paragraphs, expressing the
+author's original views on numerous [problems][] of our society. Once
+published, it will draw international attention and spark
+[controversy][problems] for months to follow.
+
+[graphomania]: https://en.wikipedia.org/wiki/Graphomania
+[logorrhea]: https://en.wikipedia.org/wiki/Logorrhea_(psychology)
+
+
+[orphan]: http://some.link