Полезное дополнение для markdown-mode в Emacs

Markdown-mode для Emacs — замечательная вещь. Длинные посты™ для блога я всегда пишу в Emacs (при помощи Mozex + emacsclient(1) или просто копируя финишный Markdown-документ в браузер).

Проблема

Я заметил, что иногда бывает такой тупой облом: пишешь-пишешь, делаешь ссылки в виде [label][id] и забываешь в конце прописать [id]:, в результате чего в соответствующем месте уже парсеного документа получается [вот такая][жопа] Я обычно ещё долго обнаруживаю неопределённые ссылки через некоторое время после отправки поста.

Можно конечно во время написания делать C-c C-c m и искать в тексте квадратные скобочки, но это имхо некошерно. В хорошем Длинном посте™ может быть несколько десятков ссылок и легко запариться, проверяя все.

Решениеъ

Поэтому я нашкодил такую плюшку-расширение для markdown-mode: с её помощью можно грабить корованы вывести список всех неопределённых ссылок в тексте в отдельный буфер с такими удобствами: ссылка выводится как кнопка, при нажатии на которую в исходный Markdown-буфер добавляется пустое определение ссылки. Также кнопочками выводится список строк, в которых применяется ссылка (если вдруг (а такое бывает) во время написания Длинного поста™ забывается контекст, в котором использовалась ссылка). Имена ссылок приводятся к нижнему регистру, так как эталонные реализации Markdown не различают регистр у имён ссылок.

Скриншот, а то непонятно:

Emacs, markdown-mode goodies

Вот код:

;;; 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), которая действует так, как описано ранее.

Комментарии:

mkmks, 10.01.2008

вместо mozex лучше использовать It's All Text - https://addons.mozilla.org/en-US/firefox/addon/4125

Sphinx, 10.01.2008

И вправду удобней, спасибо!

Дмитрий Джус ← сам, 28.06.2009

Начиная с версии 1.6, описанный в этой статье код включён в markdown-mode.

Оставить комментарий:





1∙x−5−8=-8,

Бесконечно много решений? F5!

← F-spot Aliens vs. Predator: Requiem →