root/lang/elisp/simple-hatena-mode/simple-hatena-mode.el @ 405

Revision 405, 17.9 kB (checked in by kentaro, 6 years ago)

lang/elisp/simple-hatena-mode: improved the documentation

  • Property svn:mime-type set to text/plain; charset=utf-8
Line 
1;;; simple-hatena-mode.el --- Emacs interface to Hatena::Diary Writer
2;; -*- coding: utf-8; mode:emacs-lisp -*-
3
4;; Copyright (C) 2007 Kentaro Kuribayashi
5;; Author: Kentaro Kuribayashi <kentarok@gmail.com>
6;; Keywords: blog, hatena, はてな
7
8;; This file is free software; you can redistribute it and/or modify
9;; it under the terms of the GNU General Public License as published
10;; by the Free Software Foundation; either version 2, or (at your
11;; option) any later version.
12
13;; This file is distributed in the hope that it will be useful, but
14;; WITHOUT ANY WARRANTY; without even the implied warranty of
15;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16;; General Public License for more details.
17
18;; You should have received a copy of the GNU General Public License
19;; along with GNU Emacs; see the file COPYING.  If not, write to the
20;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21;; Boston, MA 02111-1307, USA.
22
23;;; Commentary:
24
25;; * simple-hatena-mode.elについて
26
27;; このパッケージは、「はてなダイアリーライター」をEmacsから使えるよう
28;; にし、はてなダイアリー/グループ日記を簡単に更新するためのメジャーモー
29;; ド、simple-hatena-modeを提供します。simple-hatena-modeは、
30;; html-helper-modeの派生モードとして定義されていますので、
31;; html-helper-modeが提供する各種機能も利用できます。
32;;
33;; インストール、設定方法等については、以下のページをご覧ください。
34;; http://coderepos.org/share/wiki/SimpleHatenaMode
35
36;;; Code:
37
38;;;; * ヴァージョン
39
40(defconst simple-hatena-version "0.01"
41  "simple-hatena-mode.elのヴァージョン。")
42
43(defun simple-hatena-version ()
44  "simple-hatena-mode.elのヴァージョンを表示する。"
45  (interactive)
46  (let ((version-string
47         (format "simple-hatena-mode-v%s" simple-hatena-version)))
48    (if (interactive-p)
49        (message "%s" version-string)
50      version-string)))
51
52;;;; * ユーザによるカスタマイズが可能な設定
53
54;; カスタマイズ変数
55(defcustom simple-hatena-bin "hw.pl"
56  "はてなダイアリーライターのパスを指定する。")
57
58(defcustom simple-hatena-root "~/.hatena"
59  "はてなダイアリーライターのデータを置くディレクトリのルートを指
60定する。")
61
62(defcustom simple-hatena-default-id nil
63  "はてダラで使うデフォルトのはてなidを指定する。
64
65この変数が設定されている場合、simple-hatenaあるいは
66simple-hatena-group実行時に、設定されたidが使われるため、idを選択
67する必要がない。
68
69このidを変更するには、simple-hatena-change-default-idを実行する。")
70
71;; はてダラにわたすオプション
72(defcustom simple-hatena-option-useragent (simple-hatena-version)
73  "はてなダイアリーライターのユーザエージェントオプションを指定す
74る。実行時に、-aオプションとして使われる。")
75
76(defcustom simple-hatena-option-debug-flag nil
77  "はてなダイアリーライターを、デバッグモードで実行するか否かを示
78すフラグ。
79
80はてなダイアリーライター実行時に、-dオプションとしてわたされ、また、
81その場合、実行結果をバッファに表示する。
82
83デバッグモードをオン/オフするには、
84simple-hatena-toggle-debug-modeを実行する。")
85
86(defcustom simple-hatena-option-timeout "30"
87  "はてなダイアリーライターのタイムアウトを指定する。実行時に、-T
88オプションとして使われる。")
89
90(defcustom simple-hatena-option-cookie-flag t
91  "はてなダイアリーライターのログインに、cookieを利用するかどうか
92を指定するフラグ。実行時に、-cオプションとして使われる。")
93
94;; キーバインド
95(setq simple-hatena-mode-map (make-keymap))
96
97(define-key simple-hatena-mode-map (kbd "C-c C-v") 'simple-hatena-version)
98(define-key simple-hatena-mode-map (kbd "C-c C-p") 'simple-hatena-submit)
99(define-key simple-hatena-mode-map (kbd "C-c C-c") 'simple-hatena-trivial-submit)
100(define-key simple-hatena-mode-map (kbd "C-c   *") 'simple-hatena-timestamp)
101(define-key simple-hatena-mode-map (kbd "C-c C-i") 'simple-hatena-change-default-id)
102(define-key simple-hatena-mode-map (kbd "C-c C-n") 'simple-hatena-find-diary-for)
103(define-key simple-hatena-mode-map (kbd "C-c C-b") 'simple-hatena-go-back)
104(define-key simple-hatena-mode-map (kbd "C-c C-f") 'simple-hatena-go-forward)
105(define-key simple-hatena-mode-map (kbd "C-c C-d") 'simple-hatena-toggle-debug-mode)
106(define-key simple-hatena-mode-map (kbd "C-c C-e") 'simple-hatena-exit)
107
108;; フック
109(defvar simple-hatena-mode-hook nil
110  "simple-hatena-mode開始時のフック。")
111(defvar simple-hatena-before-submit-hook nil
112  "日記を投稿する直前のフック")
113(defvar simple-hatena-after-submit-hook nil
114  "日記を投稿した直後のフック")
115
116;; フォントロック
117
118(defvar simple-hatena-font-lock-keywords nil)
119(defvar simple-hatena-slag-face 'simple-hatena-slag-face)
120(defvar simple-hatena-subtitle-face 'simple-hatena-subtitle-face)
121(defvar simple-hatena-inline-face 'simple-hatena-inline-face)
122(defvar simple-hatena-markup-face 'simple-hatena-markup-face)
123(defvar simple-hatena-link-face 'simple-hatena-link-face)
124
125(defface simple-hatena-slag-face
126  '((((class color) (background light)) (:foreground "IndianRed"))
127    (((class color) (background dark)) (:foreground "wheat")))
128  "小見出しの*タイムスタンプorスラッグ*部分のフェイス。")
129
130(defface simple-hatena-subtitle-face
131  '((((class color) (background light)) (:foreground "DarkOliveGreen"))
132    (((class color) (background dark)) (:foreground "wheat")))
133  "小見出しのフェイス。")
134
135(defface simple-hatena-inline-face
136  '((((class color) (background light)) (:foreground "MediumBlue" :bold t))
137    (((class color) (background dark)) (:foreground "wheat" :bold t)))
138  "id記法や[keyword:Emacs]等のface")
139
140(defface simple-hatena-markup-face
141  '((((class color) (background light)) (:foreground "DarkOrange" :bold t))
142    (((class color) (background dark)) (:foreground "IndianRed3" :bold t)))
143  "はてなのマークアップのフェイス。")
144
145(defface simple-hatena-link-face
146  '((((class color) (background light)) (:foreground "DeepPink"))
147    (((class color) (background dark)) (:foreground "wheat")))
148  "リンクのフェイス。")
149
150;;;; * 実装
151
152(eval-when-compile
153  (require 'cl)
154  (require 'derived)
155  (require 'font-lock)
156  (require 'html-helper-mode))
157
158(defconst simple-hatena-filename-regex
159   "/\\([^/]+\\)/\\(diary\\|group\\)/\\([^/]+\\)?/?\\([0-9][0-9][0-9][0-9]\\)-\\([01][0-9]\\)-\\([0-3][0-9]\\)\.txt"
160  "日記ファイルの正規表現。マッチした場合、以下のインデックスによ
161りファイル情報を取得できる。
162
163  0. マッチした全体
164  1. はてなid
165  2. diary/group
166  3. 2がgroupの場合は、グループ名。そうでない場合はnil
167  4. 年(YYYY)
168  5. 月(MM)
169  6. 日(DD)")
170
171;; simple-hatena-modeを、html-helper-modeの派生モードとして定義する。
172(define-derived-mode simple-hatena-mode html-helper-mode "Simple Hatena"
173  "はてなダイアリーライターを、Emacsから利用するためのインタフェイ
174スを提供するモード。
175
176設定方法や使い方については、以下を参照のこと。
177http://coderepos.org/share/wiki/SimpleHatenaMode"
178
179  ;; 現在開いているバッファの情報
180  (make-local-variable 'simple-hatena-local-current-buffer-info)
181  (make-local-variable 'simple-hatena-local-current-buffer-id)
182  (make-local-variable 'simple-hatena-local-current-buffer-type)
183  (make-local-variable 'simple-hatena-local-current-buffer-group-name)
184  (make-local-variable 'simple-hatena-local-current-buffer-year)
185  (make-local-variable 'simple-hatena-local-current-buffer-month)
186  (make-local-variable 'simple-hatena-local-current-buffer-day)
187
188  (if (string-match simple-hatena-filename-regex (buffer-file-name))
189      (progn
190        (setq simple-hatena-local-current-buffer-info
191              (match-string 0 (buffer-file-name)))
192        (setq simple-hatena-local-current-buffer-id
193              (match-string 1 (buffer-file-name)))
194        (setq simple-hatena-local-current-buffer-type
195              (match-string 2 (buffer-file-name)))
196        (setq simple-hatena-local-current-buffer-group-name
197              (match-string 3 (buffer-file-name)))
198        (setq simple-hatena-local-current-buffer-year
199              (match-string 4 (buffer-file-name)))
200        (setq simple-hatena-local-current-buffer-month
201              (match-string 5 (buffer-file-name)))
202        (setq simple-hatena-local-current-buffer-day
203              (match-string 6 (buffer-file-name))))
204    (error "Current buffer isn't related to Hatena::Diary Writer data file"))
205
206  ;; フォントロック
207  (font-lock-add-keywords 'simple-hatena-mode
208    (list
209     (list  "^\\(\\*[*a-zA-Z0-9_-]*\\)\\(.*\\)$"
210            '(1 simple-hatena-slag-face t)
211            '(2 simple-hatena-subtitle-face t))
212     ;; 必ず[]で囲まれていなければならないもの
213     (list "\\[[*a-zA-Z0-9_-]+\\(:[^\n]+\\)+\\]"
214           '(0 simple-hatena-inline-face t))
215     ;; 必ずしも[]で囲まれていなくてもよいもの
216     (list "\\[?\\(id\\|a\\|b\\|d\\|f\\|g\\|graph\\|i\\|idea\\|map\\|question\\|r\\|isbn\\|asin\\)\\(:[a-zA-Z0-9_+:-]+\\)+\\]?"
217           '(0 simple-hatena-inline-face t))
218     (list  "^\\(:\\)[^:\n]+\\(:\\)"
219            '(1 simple-hatena-markup-face t)
220            '(2 simple-hatena-markup-face t))
221     (list  "^\\([-+]+\\)"
222            '(1 simple-hatena-markup-face t))
223     (list  "\\(((\\).*\\())\\)"
224            '(1 simple-hatena-markup-face t)
225            '(2 simple-hatena-markup-face t))
226     (list  "^\\(>>\\|<<\\|><!--\\|--><\\|>|?[^|]*|\\||?|<\\|=====?\\)"
227            '(1 simple-hatena-markup-face t))
228     (list  "\\(s?https?://\[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#\]+\\)"
229            '(1 simple-hatena-link-face t))))
230  (font-lock-mode 1)
231
232  (use-local-map simple-hatena-mode-map)
233  (run-hooks 'simple-hatena-mode-hook))
234
235;; はてダラデータにsimple-hatena-modeを適用する
236;;
237;; - ~/.hatena/hatena-id/diary/YYYY-MM-DD.txt
238;; - ~/.hatena/hatena-id/group/group-name/YYYY-MM-DD.txt
239;;
240;; というファイルを開いたら、simple-hatena-modeにする
241(add-to-list 'auto-mode-alist
242             (cons simple-hatena-filename-regex 'simple-hatena-mode))
243
244;;;; * コマンド
245
246(defun simple-hatena-setup ()
247  "ディレクトリ配置をセットアップする。")
248
249(defun simple-hatena (id)
250  "実行日現在の日付のファイルを開く。"
251  (interactive
252   (list
253    (if simple-hatena-default-id
254        simple-hatena-default-id
255      (simple-hatena-internal-completing-read-id simple-hatena-root))))
256  (simple-hatena-internal-safe-find-file (concat
257               simple-hatena-root
258               "/"
259               id
260               "/diary/"
261               (simple-hatena-internal-make-diary-file-string 0))))
262
263(defun simple-hatena-group (id group)
264  "実行日現在の日付の、指定されたグループに投稿するためのファイル
265を開く。"
266  (interactive
267   (if simple-hatena-default-id
268       (list
269        simple-hatena-default-id
270        (simple-hatena-internal-completing-read-group simple-hatena-default-id))
271     (let ((id (simple-hatena-internal-completing-read-id simple-hatena-root)))
272       (list
273        id
274        (simple-hatena-internal-completing-read-group id)))))
275  (simple-hatena-internal-safe-find-file (concat
276              simple-hatena-root
277              "/"
278              id
279              "/group/"
280              group
281              "/"
282              (simple-hatena-internal-make-diary-file-string 0))))
283
284(defun simple-hatena-change-default-id ()
285  "現在のデフォルトidを変更する。"
286  (interactive)
287  (setq simple-hatena-default-id
288        (simple-hatena-internal-completing-read-id simple-hatena-root))
289  (message "Changed current default id to %s" simple-hatena-default-id))
290
291(defun simple-hatena-submit ()
292  "はてなダイアリー/グループに投稿する。"
293  (interactive)
294  (simple-hatena-internal-do-submit))
295
296(defun simple-hatena-trivial-submit ()
297  "はてなダイアリー/グループに「ちょっとした更新」で投稿する。"
298  (interactive)
299  (simple-hatena-internal-do-submit "-t"))
300
301(defun simple-hatena-timestamp ()
302  "実行位置に、「*タイムスタンプ*」を挿入する。"
303  (interactive)
304  (insert (format-time-string "*%s*" (current-time))))
305
306(defun simple-hatena-find-diary-for (date)
307  "指定された日付の日記バッファを表示する。"
308  (interactive "sDate(YYYY-MM-DD): ")
309  (if (equal major-mode 'simple-hatena-mode)
310      (if (string-match "^[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9]$" date)
311          (simple-hatena-internal-safe-find-file
312           (concat (file-name-directory (buffer-file-name))
313                   (concat date ".txt")))
314        (error "Invalid date"))
315    (error "Current major mode isn't simple-hatena-mode")))
316
317(defun simple-hatena-go-forward ()
318  "前の日付へ移動する。"
319  (interactive)
320  (simple-hatena-internal-go-for 1))
321
322(defun simple-hatena-go-back ()
323  "次の日付へ移動する。"
324  (interactive)
325  (simple-hatena-internal-go-for -1))
326
327(defun simple-hatena-toggle-debug-mode ()
328  "デバッグモードをオン/オフする。"
329  (interactive)
330  (if simple-hatena-option-debug-flag
331      (progn
332        (setq simple-hatena-option-debug-flag nil)
333        (message "Debug mode off"))
334    (progn
335      (setq simple-hatena-option-debug-flag t)
336      (message "Debug mode on"))))
337
338(defun simple-hatena-exit ()
339  "simple-hatena-modeの適用されているバッファを全て削除する。"
340  (interactive)
341  (dolist (buffer (buffer-list))
342    (when (and
343           (buffer-file-name buffer)
344           (string-match simple-hatena-filename-regex (buffer-file-name buffer)))
345      (when (buffer-modified-p buffer)
346        (progn
347          (save-current-buffer
348            (set-buffer buffer)
349            (save-buffer))))
350      (kill-buffer buffer)))
351  (message "simple-hatena-mode has been exited"))
352
353;;;; * 内部関数
354
355(defun simple-hatena-internal-safe-find-file (filename)
356  "新しいヴァージョンのhtml-helper-modeは、デフォルトでスケルトン
357を作ってウザいので、阻止する。"
358  (let ((html-helper-build-new-buffer nil))
359    (find-file filename)))
360
361(defun simple-hatena-internal-make-diary-file-string (i &optional date)
362  "dateが指定されていない場合は、実行日現在の日付を起点にした日記ファイル名を生成する。
363
364   0: 今日
365   1: 明日
366  -1: 昨日
367
368指定されている場合は、その日付を起点にした日記ファイル名を生成する。"
369  (apply (lambda (s min h d mon y &rest rest)
370           (format-time-string "%Y-%m-%d.txt"
371                               (encode-time s min h (+ d i) mon y)))
372         (if date
373             (append '(0 0 0) date)
374           (decode-time))))
375
376(defun simple-hatena-internal-go-for (i)
377  "引数の数だけ前後の日付のファイ名バッファへ移動する。"
378  (simple-hatena-internal-safe-find-file
379   (concat
380    (file-name-directory (buffer-file-name))
381    (simple-hatena-internal-make-diary-file-string
382     i
383       (list (string-to-number simple-hatena-local-current-buffer-day)
384             (string-to-number simple-hatena-local-current-buffer-month)
385             (string-to-number simple-hatena-local-current-buffer-year))))))
386
387(defun simple-hatena-internal-list-directories (dir)
388  "dir下にあるディレクトリをリストにして返す。"
389  (let ((dir-list nil))
390    (dolist (file (directory-files dir t "^[^\.]") dir-list)
391      (if (file-directory-p file)
392          (progn
393            (string-match "\\([^/]+\\)/?$" file)
394            (setq dir-list (cons (match-string 1 file) dir-list)))))))
395
396(defun simple-hatena-internal-completing-read-id (dir)
397  "dir以下からはてなidを抽出し、補完入力させる。"
398  (completing-read
399   "Hatena id: " (simple-hatena-internal-list-directories simple-hatena-root) nil t))
400
401(defun simple-hatena-internal-completing-read-group (id)
402  "dir以下からグループ名を抽出し、補完入力させる。"
403  (completing-read
404   "Group: " (simple-hatena-internal-list-directories
405              (concat simple-hatena-root "/" id "/group")) nil t))
406
407(defun simple-hatena-internal-do-submit (&optional flag)
408  "はてなダイアリ/グループへ日記を投稿する。"
409  (let ((max-mini-window-height 10)) ; hw.plが表示するメッセージを、
410                                     ; echoエリアに表示させるため。
411    (run-hooks 'simple-hatena-before-submit-hook)
412    (save-buffer)
413    (shell-command (simple-hatena-internal-build-command flag)
414                   "*SimpleHatenaOutput*" "*SimpleHatenaError*")
415    (run-hooks 'simple-hatena-after-submit-hook)))
416
417(defun simple-hatena-internal-build-command (flag)
418  "実行可能なコマンド文字列を作成する。"
419  (let ((flag-list (list flag)))
420    (if simple-hatena-option-debug-flag  (setq flag-list (cons "-d" flag-list)))
421    (if simple-hatena-option-cookie-flag (setq flag-list (cons "-c" flag-list)))
422    (simple-hatena-internal-join
423     " "
424     (cons simple-hatena-bin
425           (append (simple-hatena-internal-build-option-list-from-alist) flag-list)))))
426
427(defun simple-hatena-internal-build-option-list-from-alist ()
428  "引数を取るオプションのリストを作成する。"
429  (let ((opts nil))
430    (dolist (pair
431             `(("-u" . ,simple-hatena-local-current-buffer-id)
432               ("-a" . ,simple-hatena-option-useragent)
433               ("-T" . ,simple-hatena-option-timeout))
434             opts)
435      (if (cdr pair)
436           (setq opts (append opts (list (car pair) (cdr pair))))))))
437
438(defun simple-hatena-internal-join (sep list)
439  "車輪の再発明なんだろうけど、見つからなかったのでjoin実装"
440  (if (<= (length list) 1)
441      (car list)
442    (concat (car list) sep (simple-hatena-internal-join sep (cdr list)))))
443
444(provide 'simple-hatena-mode)
445
446;;; simple-hatena-mode.el ends here
Note: See TracBrowser for help on using the browser.