anpi-nlp.elとEmacsマイナーモードのつくり方

最近ブログに読書記録しか書いていないなぁ,ということでこのままだと読書ブログなので (違うの?) 技術系のブログ記事を書いてみる.

東日本大震災の被災者安否確認を支援するANPI_NLPというプロジェクト (詳細はこちら) にタグ付け戦士として参加した際に,@mhagiwaraさんと@unnonounoさんがEmacs Lispxyzzy Lispで書いたタグ付け支援関数があまりにも素敵だったので,これに手を加えてマイナーモードを作ってみることにした.

ANTI_NLPのタグ付けフェーズは終わったので,このマイナーモードは役に立つことはないだろうけれど,今後もタグづけをする機会は出てくるだろうし自分用メモ

anpi-nlp.el

まず,作成したEmacs lispコード.使い方 (いや,もう誰も使わないだろうけれど) も含めたソースコードは以下のとおり.

;;; anpi-nlp.el v.0.0.3 (2011-03-17 update)
;;;   by Yoshihiko Suhara ((at)sleepy_yoshi)
;;
;; * How to use?
;; 
;; (1) Put anpi-nlp.el into the directory in the load-path list
;;     or add the directory into the load-path. For example:
;;   (setq load-path
;;      (cons (expand-file-name "~/.elisp") load-path))
;; 
;; (2) Load anpi-nlp.el
;;   (load "anpi-nlp")
;;
;; (3) Use ANPI NLP minor mode by M-x or adding a key-binding setting in your .emacs
;;   M-x anpi-nlp-mode
;;   or
;;  (global-set-key "\C-c\C-a" 'anpi-nlp-mode)
;;
;; (4) Tag it!
;; 
;; * Key bindings:
;;   C-c p: Insert a <person> tag into the marked region.
;;   C-c l: Insert a <location> tag into the marked region.
;;   C-c o: Insert a <organization> tag into the marked region.
;;   C-c d: Delete a tag which embraces the point of the cursor.
;;   C-c t <tagname>: Insert <tagname> at the end of line.
;;   C-c C-l: Set ON/OFF to truncate lines.
;;
;; * MANY THANKS!
;;   These codes are mostly written by Hagiwara-san, Unno-san. Thank you!
;;
;; * Contact
;;   Any comments are always welcome! Please give (at)sleepy_yoshi a tweet. :)
;;

;; Insert a NER tag at the marked region
(defun sgml-insert-tag (tagname)
  (interactive)
  (let ( (pold (point)) )
    (goto-char (mark))
    (insert (format "<%s>" tagname))
    (goto-char (+ pold (length (format "<%s>" tagname))))
    (insert (format "</%s>" tagname))
    )
  )

(defun sgml-insert-person-tag () (interactive) (sgml-insert-tag "person"))
(defun sgml-insert-location-tag () (interactive) (sgml-insert-tag "location"))
(defun sgml-insert-organization-tag () (interactive) (sgml-insert-tag "organization"))


;; Delete a selected NER tag at the cursor point
(defun sgml-delete-tag ()
  (interactive)
  (save-excursion
    (if (re-search-backward "<[^/>]*>")
	(let ((start-begin (match-beginning 0))
	            (start-end (match-end 0)))
	    (if (re-search-forward "</[^>]*>")
		      (progn
			(delete-region (match-beginning 0) (match-end 0))
			(delete-region start-begin start-end)))))))


;; Insert an ANPI tag at the end of a line.
(defun insert-anpi-tag (tagname)
  (interactive)
  (move-end-of-line nil)
  (insert (format "\t%s" tagname)))

(defun insert-anpi-tag-i () (interactive) (insert-anpi-tag "I"))
(defun insert-anpi-tag-l () (interactive) (insert-anpi-tag "L"))
(defun insert-anpi-tag-p () (interactive) (insert-anpi-tag "P"))
(defun insert-anpi-tag-m () (interactive) (insert-anpi-tag "M"))
(defun insert-anpi-tag-h () (interactive) (insert-anpi-tag "H"))
(defun insert-anpi-tag-s () (interactive) (insert-anpi-tag "S"))
(defun insert-anpi-tag-o () (interactive) (insert-anpi-tag "O"))
(defun insert-anpi-tag-r () (interactive) (insert-anpi-tag "R"))
(defun insert-anpi-tag-u () (interactive) (insert-anpi-tag "U"))

;; Obsoleted (to reduce a hitting count of an enter key.)
; (defun insert-anpi-tag-interactive (tagname)
;   (interactive "sInput anpi tag: ")
;   (move-end-of-line nil)
;   (insert (format "\t%s" (upcase tagname))))


;; Set ON/OFF to truncate lines
(defun toggle-truncate-lines ()
  (interactive)
  (if truncate-lines
      (setq truncate-lines nil)
    (setq truncate-lines t))
  (recenter))


;; ANPI NLP minor mode
(easy-mmode-define-minor-mode anpi-nlp-mode
  "ANPI NLP mode, which helps ANPI NLP tagging."
  nil
  " ANPI NLP"

  ;; Initial key-map
  '(("\C-cp" . sgml-insert-person-tag)
    ("\C-cl" . sgml-insert-location-tag)
    ("\C-co" . sgml-insert-organization-tag)
    ("\C-cd" . sgml-delete-tag)

    ("\C-cti" . insert-anpi-tag-i)
    ("\C-ctl" . insert-anpi-tag-l)
    ("\C-ctp" . insert-anpi-tag-p)
    ("\C-ctm" . insert-anpi-tag-m)
    ("\C-cth" . insert-anpi-tag-h)
    ("\C-cts" . insert-anpi-tag-s)
    ("\C-cto" . insert-anpi-tag-o)
    ("\C-ctr" . insert-anpi-tag-r)
    ("\C-ctu" . insert-anpi-tag-u)

    ("\C-c\C-l" . toggle-truncate-lines)))

タグの挿入 by @mhagiwaraさん

マークした位置から現在の位置までをタグで囲むという関数.

(defun sgml-insert-tag (tagname)
  (interactive)
  (let ( (pold (point)) )
    (goto-char (mark))
    (insert (format "<%s>" tagname))
    (goto-char (+ pold (length (format "<%s>" tagname))))
    (insert (format "</%s>" tagname))
    )
  )

これを眺めてみると,やっていることはとてもわかりやすい.Emacs lispを知らない人のために日本語でロジックを解説.

  1. 現在の位置を(point)で取得してpoldに保存
  2. (mark)でマークした位置を取得し,(goto-char)で移動
  3. tagnameを という形で挿入
  4. poldにの長さを加えた位置に移動
  5. tagnameを という形で挿入

こういうEmacs lispを書いたことがなかったので,とても参考になる.

タグの除去 by @unnonounoさん

@unnonounoさんはXyzzy lispで書いていたので,Emacs lispに移植.処理系が違うと用意された関数とその仕様も違うので少し苦労.やっぱりやっていることは簡単.

;; Delete a selected NER tag at the cursor point
(defun sgml-delete-tag ()
  (interactive)
  (save-excursion
    (if (re-search-backward "<[^/>]*>")
	(let ((start-begin (match-beginning 0))
	            (start-end (match-end 0)))
	    (if (re-search-forward "</[^>]*>")
		      (progn
			(delete-region (match-beginning 0) (match-end 0))
			(delete-region start-begin start-end)))))))

処理の流れは以下のとおり

  1. save-excursionで終了後に現在位置に戻れるようにしておく
  2. (re-search-backward) で現在位置から直前の を見つける
  3. 見つかった場合
    1. マッチ位置の先頭をstart-beginとする
    2. マッチ位置の最後をstart-endとする
    3. (re-search-forward) で現在位置から直後の を見つける
    4. 見つかった場合
      1. (delete-region)でマッチ位置の先頭から最後まで削除する (を削除する)
      2. (delete-region)でstart-beginからstart-endまでを削除する


このスクリプトにはいくつか改善点がある.今回は一行ずつの作業であり,タグの入れ子もなかったので今回は問題ではないが,別の作業で利用する場合には以下の点で注意が必要.

  • 行という概念を利用していないので,直前にがない位置で実行すると前の行のタグが除去される
  • 正規表現というものを利用しているので,タグの中で実行すると,ひとつ前のタグを消してしまう.
  • tagnameの一致を見ていないので,ほげふがというタグになっていた際に,ほげの位置で実行するとほげふがまでが削除されてしまう.
    • 今回はタグの入れ子が存在しなかったので問題なし

easy-mmodeを使ったマイナーモードのつくり方

以下のページを参考にした.

;; マイナーモードの定義
 (easy-mmode-define-minor-mode test-mode
   ;; ドキュメント
   "This is Test Mode."
   ;; 初期値
   nil
   ;; on の時のモード行への表示 (先頭にスペースがないと見づらい)
   " TestMode"
   ;; マイナーモード用キーマップの初期値
   '(("C-cf" . test-function))

折り返しのON/OFF

今回扱ったファイルは一行が長かったので,行折り返しをOFFにしたかった.

M-x set-variables
truncate-lines f

という方法もあるけれど,下記の方法でも切り替えることができる.

;; Set ON/OFF to truncate lines
(defun toggle-truncate-lines ()
  (interactive)
  (if truncate-lines
      (setq truncate-lines nil)
    (setq truncate-lines t))
  (recenter))

補足: requireとloadの違い

.emacsに読み込みの記述を追加するときに出てきた疑問.

requireは二重読み込みを防ぐ効果がある.ただし,そのためには,.elファイルにprovide関数を記述しなければならない.イメージとしては,Cのヘッダファイルにおける

#ifndef __HOGE_H__
#define __HOGE_H__
...
#endif

と同じノリ.なお,シンボルを引数とするか,文字列を引数とするかという点も違う.

(require 'hoge)
(load "hoge")

まぁ,世の中に配られている.elがrequireだったり,loadだったりマチマチなので,あまり気にしなくていいのではないかというのが現在の認識.

まとめ

Emacs lispは以前にすこーしだけかじったことがあるけれど,どういうところで使うのか長らく疑問だった.メーラがWanderlustということもあり,飲み会の会計計算をするときにメール作成のバッファでS式書いて計算,そのまま送信ということができるので非常に便利だとは思っていたけれど,便利な計算機以上のものではなかった.

というわけで今回のような使い道があるということと,マイナーモードは簡単に作れるのでglobal-keyに設定したくない場面で,オレ様マイナーモードを作っておくといろいろうれしいことがあるかもしれない.

基本的にtexを書くときはYatexにお世話になっているけれど,自分用関数を用意するときにマイナーモードを作ってかぶせるなど,ほんのちょっとの知識で夢が膨らむ.

Emacs lispは操作全てに対応する関数が存在するため,プログラミングするというよりかは,操作マクロをひとつずつあてはめていくというイメージでプログラミングするのが近い気がする.昔触ったことがあるLOGOというプログラミング言語もそんな感じだったなぁということをふと思い出した.同い年でLOGOを使ったことある人なんていないだろうなぁ...

参考文献