Markdown-mode для Emacs — замечательная вещь.
Длинные посты™ для блога я всегда пишу в Emacs (при помощи
Mozex + emacsclient(1) или просто копируя финишный
Markdown-документ в браузер).
Я заметил, что иногда бывает такой тупой облом: пишешь-пишешь, делаешь
ссылки в виде [label][id] и забываешь в конце прописать [id]:, в
результате чего в соответствующем месте уже парсеного документа
получается [вот такая][жопа] Я обычно ещё долго обнаруживаю
неопределённые ссылки через некоторое время после отправки поста.
Можно конечно во время написания делать C-c C-c m и искать в тексте
квадратные скобочки, но это имхо некошерно. В хорошем Длинном посте™
может быть несколько десятков ссылок и легко запариться, проверяя все.
Поэтому я нашкодил такую плюшку-расширение для markdown-mode: с её
помощью можно грабить корованы вывести список всех
неопределённых ссылок в тексте в отдельный буфер с такими удобствами:
ссылка выводится как кнопка, при нажатии на которую в исходный
Markdown-буфер добавляется пустое определение ссылки. Также
кнопочками выводится список строк, в которых применяется ссылка (если
вдруг (а такое бывает) во время написания Длинного поста™ забывается
контекст, в котором использовалась ссылка). Имена ссылок приводятся к
нижнему регистру, так как эталонные реализации Markdown не
различают регистр у имён ссылок.
Скриншот, а то непонятно:
Вот код:
;;; markdown-goodies.el -- additional features for Emacs markdown-mode
;;
;; Author: Dmitry Dzhus <mail@sphinx.net.ru>
;;
;; This software is in public domain. You may use and redistribute it
;; any way you like. Enjoy!
;;
;; This package adds an additional `markdown-check-refs` function
;; (also available using `C-c C-c c` keybinding) to markdown-mode.
;; Call it interactively to see a list of all [undefined][references]
;; in your text in a separate buffer. Hitting a reference name in a
;; new buffer creates an empty reference definition in the end of your
;; document, hitting line number (listed in parentheses after
;; reference name) moves you to the line reference occurs in the text.
;;
;; Install this by writing `(load "/path/to/markdown-goodies.el")`
;; somewhere in your Emacs initialization file.
(require 'markdown-mode)
(defconst markdown-refcheck-buffer "*Undefined references for %BUFFER%*"
"Name of buffer which will contain a list of undefined
references in markdown-mode buffer named %BUFFER%.")
;;;; Faster version
(defun markdown-has-reference-definition-2 (reference)
"Find out whether Markdown REFERENCE is defined.
REFERENCE includes square brackets, like [this]."
(save-excursion
(goto-char (point-min))
(re-search-forward (concat "^ \\{0,3\\}"
(regexp-quote reference)
": [ ]?\\(.*?\\)\\(\"[^\"]+?\"\\)?$")
nil t)))
;;;; Kosher version
(defun markdown-has-reference-definition (reference)
"Find out whether Markdown REFERENCE is defined.
REFERENCE includes square brackets, like [this]."
(let ((reference (downcase reference)))
(save-excursion
(goto-char (point-min))
(catch 'found
(while (re-search-forward regex-reference-definition nil t)
(when (string= reference (downcase (match-string-no-properties 1)))
(throw 'found t)))))))
(defun markdown-get-undefined-refs ()
"Return a list of undefined Markdown references.
Result is an alist of pairs (reference . occurencies), where
occurencies 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 regex-link-reference nil t)
(let* ((label (match-string-no-properties 1))
(reference (match-string-no-properties 2))
(target (downcase (if (string= reference "[]") label reference))))
(unless (markdown-has-reference-definition target)
(let ((entry (assoc target missing)))
(if (not entry)
(add-to-list 'missing (cons target
(list (cons label (line-number-at-pos)))) t)
(setcdr entry
(append (cdr entry) (list (cons label (line-number-at-pos))))))))))
missing)))
(defun markdown-add-missing-ref-definition (ref buffer &optional recheck)
"Add blank REF definition to the end of BUFFER.
REF is a Markdown reference in square brackets, like \"[lisp-history]\".
When RECHECK is non-nil, BUFFER gets rechecked for undefined
references so that REF disappears from the list of those links."
(with-current-buffer buffer
(when (not (eq major-mode 'markdown-mode))
(error "Not available in current mdoe"))
(goto-char (point-max))
(indent-new-comment-line)
(insert (concat ref ": ")))
(switch-to-buffer-other-window buffer)
(goto-char (point-max))
(when recheck
(markdown-check-refs t)))
;; Button which adds an empty Markdown reference definition to the end
;; of buffer specified as its 'target-buffer property. Reference name
;; is button's label
(define-button-type 'markdown-ref-button
'help-echo "Push to create an empty reference definition"
'face 'bold
'action (lambda (b)
(markdown-add-missing-ref-definition
(button-label b) (button-get b 'target-buffer) t)))
;; Button jumping to line in buffer specified as its 'target-buffer
;; property. Line number is button's 'line property.
(define-button-type 'goto-line-button
'help-echo "Push to go to this line"
'face 'italic
'action (lambda (b)
(message (button-get b 'buffer))
(switch-to-buffer-other-window (button-get b 'target-buffer))
(goto-line (button-get b 'target-line))))
(defun markdown-check-refs (&optional silent)
"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 (eq major-mode 'markdown-mode))
(error "Not available in current mode"))
(let ((oldbuf (current-buffer))
(refs (markdown-get-undefined-refs))
(refbuf (get-buffer-create (replace-regexp-in-string
"%BUFFER%" (buffer-name)
markdown-refcheck-buffer t))))
(if (null refs)
(progn
(when (not silent)
(message "No undefined references found"))
(kill-buffer refbuf))
(with-current-buffer refbuf
(when view-mode
(View-exit-and-edit))
(erase-buffer)
(insert "Following references lack definitions:")
(newline 2)
(dolist (ref refs)
(let ((button-label (format "%s" (car ref))))
(insert-text-button button-label
:type 'markdown-ref-button
'target-buffer oldbuf)
(insert " (")
(dolist (occurency (cdr ref))
(let ((line (cdr occurency)))
(insert-button (number-to-string line)
:type 'goto-line-button
'target-buffer oldbuf
'target-line line)
(insert " "))) (delete-backward-char 1)
(insert ")")
(newline))))
(view-buffer-other-window refbuf)
(goto-line 4))))
(define-key markdown-mode-map "\C-c\C-cc" 'markdown-check-refs)
Последняя версия доступна в elisp/markdown-goodies.el в моём репо (свой репо — не сложно).
Хотел сначала описать как я всё это накодил (как я описал свой опыт с Semantic), но потом понял, что тут ничего интересного и забил.
Для отыскания ссылок в основном тексте используются объявленные в
markdown-mode регулярные выражения, guess why.
Send patches! :-)
Положить markdown-goodies.el куда-нибудь в load-path, прописать
его загрузку в ~/.emacs.el. Теперь в markdown-mode становится
доступной интерактивная функция markdown-check-refs (по умолчанию
привязанная к C-c C-c c), которая действует так, как описано ранее.
Комментарии:
вместо mozex лучше использовать It's All Text - https://addons.mozilla.org/en-US/firefox/addon/4125
И вправду удобней, спасибо!
Начиная с версии 1.6, описанный в этой статье код включён в markdown-mode.