;; hatena-diary-mode.el --- major mode for Hatena::Diary (http://d.hatena.ne.jp) ;; Created: Thu Jun 17 2004 ;; Keywords: blog emacs ;; author: hikigaeru ;; hirosandesu ;; ;; 公開ページ: http://sourceforge.jp/projects/hatena-diary-el/ ;; Special Thanks to : ;; http://d.hatena.ne.jp/hikigaeru/20040617 ;; http://d.hatena.ne.jp/dev-null ;; and all users ;; This program supports the update of your Hatena-Diary. ;; This program is Elisp program that operates by Emacs. ;; Copyright (C) 2010 hirosandesu ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published ;; by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ;; You should have received a copy of the GNU General Public License along with this program. ;; If not, see . (defconst hatena-version "2.0" "Version number of hatena.el") ;; ■インストール方法 ;; 1) 適当なディレクトリにこのファイルをおく. ;; (~/elisp/ 内においたとする). ;; ;; 2) .emacs に次の 4 行を追加する. ;; (setq load-path (cons (expand-file-name "~/elisp") load-path)) ;; (load "hatena-diary-mode") ;; (setq hatena-usrid "your username on Hatena::Diary") ;; (setq hatena-plugin-directory "~/elisp") ;; `hatena-use-file' を non-nil にするとパスワードを base64 で ;; 暗号化してファイルに保存しますが、"人間が見てすぐわからない"ぐらいの ;; 意味しかないので注意して下さい。 ;; ;; ■使い方 ;; ;; 1)日記を書く ;; `M-x hatena' で今日の日記が開きます. ただのテキストファイルです。 ;; タイトル を付けたい場合は、一行目に "title" と書いて、その後にテキストを ;; 続けてください。 ;; ;; 2)ポストする ;; 日記を書いたら, \C-c\C-p で send できます. ;; マークアップは、はてなの記法に従います。 ;; \C-ct で「更新」と「ちょっとした更新」を切りかえます。 ;; ;; 3)変数や関数 ;; ;; `hatena-change-trivial' "ちょっとした更新"かどうかを digit に変えます。 ;; `hatena-entry-type' エントリの "*" の動作を切りかえます。 ;; 0 で *pn* に、1 で *t* (タイムスタンプ)になります。 ;; ;; `hatena-submit' (\C-c\C-p) 日記をはてなにポストします ;; `hatena-delete-diary' その日の日記を web から削除. ;; `hatena-find-previous' (\C-c\C-b) ;; `hatena-find-followings' (\C-c\C-f). それぞれ、前の日と次の日の ;; 日記ファイルを開く。引数を与えるとその日数だけジャンプ。 ;; ( 例 \C 1 2 \C-c\C-b で12日前 ) ;; `hatena-exit' 日記 buffer を save して すべて kill ;; `hatena-browser-function' に 'browse-url とかやると日記をポスト ;; した後その日 url を引数としてブラウザを呼びます. ;; `hatena-insert-webdiary' はてなバッファで実行すると、現在 web に ;; アップされているファイルを取ってくる。 o ;; `hatena-twitter' 日記更新時にTwitterに通知するかどうかを変えます。 ;; ;; 4) 上位モード ;; hatena-diary-mode はデフォルトで html-mode に被せています。これを ;; html-helper-mode にしたければ、 ;; ;; -(define-derived-mode hatena-diary-mode html-mode "Hatena" ;; +(define-derived-mode hatena-diary-mode html-helper-mode "Hatena" ;; ;; として `eval-buffer' して下さい。 ;; ;; 5) hook について ;; hook とはライブラリを読込んだ時、初期化する時など、特定のタイミ ;; ングで呼び出したい関数を保持する変数です。hatena-diary-mode には以下の ;; hook があります ;; ;; `hatena-diary-mode-hooks' Hatena mode にした時に呼ばれる hook . ;; 例 .emacs に ;; (add-hook 'hatena-diary-mode-hooks ;; '(lambda () ;; (setq line-spacing 8) ;;行が詰まってるとイヤ、 ;; )) ;; ;; `hatena-diary-mode-submit-hook' 日記をポスト`hatena-submit' する直前に ;; 呼び出す関数です。例えば、連続しない改行をすべて除く、などの処理が考えられます。 ;; ;; (add-hook 'hatena-diary-mode-submit-hook ;; '(lambda () ;; (goto-char (point-min)) ;; (replace-regexp "\\([^\n]\\)\n\\([^\n]\\)" "\\1\\2"))) ;; (require 'hatena-vars) (require 'hatena-kw) (require 'font-lock) (require 'derived) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;本体 (if hatena-diary-mode-map () (setq hatena-diary-mode-map (make-keymap)) (define-key hatena-diary-mode-map "\C-c\C-p" 'hatena-submit) (define-key hatena-diary-mode-map "\C-c\C-b" 'hatena-find-previous) (define-key hatena-diary-mode-map "\C-c\C-f" 'hatena-find-following) (define-key hatena-diary-mode-map "\C-ct" 'hatena-change-trivial)) (defconst hatena-today-buffer nil) (defun hatena (&optional date) "Hatena::Diary ページを開く. " (interactive) (unless (file-exists-p hatena-directory) (make-directory hatena-directory t)) (if (not date) (progn ;;今日の日記のバッファを確認(cookie の管理のため) ;;存在しなければ、クッキーを取得する。 (let ((buffer-new-p t) (file-new-p t)) (if (memq hatena-today-buffer (buffer-list)) (setq buffer-new-p nil) (hatena-login)) (setq hatena-today-buffer (find-file (concat hatena-directory (hatena-today-date)))) ;;ファイル、バッファが存在しなければ、webの日記をチェック (if (file-exists-p (concat hatena-directory (hatena-today-date))) (setq file-new-p nil)) (if (and file-new-p buffer-new-p) (progn (message "日記ファイルもバッファもありません。Webをチェックします") (hatena-insert-webdiary))) ) ) (if (string-match hatena-fname-regexp date) (find-file (concat hatena-directory date)) (error "Not date")) ) ;;keyword-cheating (if hatena-kw-if (hatena-kw-init) nil) ) (define-derived-mode hatena-diary-mode html-mode "Hatena" "はてなモード. " (font-lock-add-keywords 'hatena-diary-mode (list (list "^\\(Title\\) \\(.*\\)$" '(1 hatena-header-face t) '(2 hatena-title-face t)) ;; 見出し (list "\\(<[^\n/].*>\\)\\([^<>\n]*\\)\\(\\)" '(1 hatena-html-face t) '(2 hatena-link-face t) '(3 hatena-html-face t)) ;; 見出し2 (list "^\\(\\*[^\n ]*\\) \\(.*\\)$" '(1 hatena-markup-face t) '(2 hatena-html-face t)) ;;特殊記法 (list "\\(\\[?\\(a:id\\|f:id\\|i:id\\|r:id\\|map:id\\|graph:id\\|g.hatena:id\\|b:id:\\|id\\|google\\|isbn\\|asin\\|http\\|http\\|ftp\\|mailto\\|search\\|amazon\\|rakuten\\|jan\\|ean\\|question\\|tex\\):\\(\\([^\n]*\\]\\)\\|[^  \n]*\\)\\)" '(1 hatena-markup-face t)) (list "^:\\([^:\n]+\\):" '(0 hatena-markup-face t) '(1 hatena-link-face t)) (list "^\\([-+]+\\)" '(1 hatena-markup-face t)) (list "\\(((\\).*\\())\\)" '(1 hatena-markup-face t) '(2 hatena-markup-Face T)) (list "^\\(>>\\|<<\\|><\\|>|?|\\||?|<\\)" '(1 hatena-markup-face t)) (list "\\(s?https?://\[-_.!~*'()a-zA-Z0-9;/?:@&=+$,%#\]+\\)" '(1 hatena-html-face t)))) (font-lock-mode 1) (set-buffer-modified-p nil) (run-hooks 'hatena-diary-mode-hook)) ;;hatena-diary-mode トグル (setq auto-mode-alist (append (list (cons (concat hatena-directory hatena-fname-regexp) 'hatena-diary-mode)) auto-mode-alist)) (defun hatena-today-date(&optional offset date) ;; date は任意の日付、offset は任意の時間、-24 で一日進む (let ( (lst (if date (progn (string-match "\\([0-9][0-9][0-9][0-9]\\)\\([0-9][0-9]\\)\\([0-9][0-9]\\)" date) (list 0 0 0 (string-to-int (match-string 3 date)) (string-to-int (match-string 2 date)) (string-to-int (match-string 1 date)) 0 nil 32400)) (decode-time (current-time))) )) (setcar (nthcdr 2 lst) (- (nth 2 lst) (if offset offset hatena-change-day-offset))) (format-time-string "%Y%m%d" (apply 'encode-time lst )))) (defun hatena-submit (&optional file userid) "はてな日記 http://d.hatena.ne.jp/ に post メソッドで日記を送る. curl を使う. " (interactive) (if file nil (setq file buffer-file-name) (save-excursion ;;"*t*" にするか "*pn*" にするか (cond ( (= hatena-entry-type 0) (progn (let ((i 0) (j 0)) (goto-char (point-min)) (while (re-search-forward "^\\*p\\([0-9]\\)\\*" nil t) (if (< i (setq j (string-to-int (match-string 1)))) (setq i j))) (goto-char (point-min)) (while (re-search-forward "^\\(\\*\\)\\([[ ]\\)" nil t) (replace-match (concat "*p" (format "%d" (setq i (1+ i))) "*\\2") ))))) ( (= hatena-entry-type 1) (progn (goto-char (point-min)) (while (re-search-forward "^\\(\\*\\)\\([[ ]\\)" nil t) (replace-match (concat "*t*\\2") )))) (t nil) ) ;;タイトルの*t*を時間に置きかえる (goto-char (point-min)) (let ((i 0)) (while (re-search-forward "^\\*t\\*" nil t) (replace-match (concat "*" (hatena-current-second i) "*") (setq i (1+ i)) )))) (save-buffer)) (if (not userid) (setq userid hatena-usrid)) (let ((filename (file-name-nondirectory file))) (if (string-match hatena-fname-regexp filename) (let* ((year (match-string 1 filename)) (month (match-string 2 filename)) (day (match-string 3 filename)) (date (concat year month day)) ;;はてなに通知するタイムスタンプ (timestamp (format-time-string "%Y%m%d%H%m%S" (current-time))) (baseurl (concat "http://d.hatena.ne.jp/" userid "/")) (referer (concat baseurl "edit?date=" date)) (nexturl (concat baseurl (concat year month day))) (url (concat baseurl "edit")) (title "") (send-file file) (full-body (with-temp-buffer (insert-file-contents send-file) ;; バッファを送る前に呼ばれる hooks (run-hooks 'hatena-diary-mode-submit-hook) (cond ( (string-match "\\`title[  ]*\\(.*\\)?\n" (buffer-string)) (progn (setq title (match-string 1 (buffer-string))) (substring (buffer-string) (length (match-string 0 (buffer-string)))) )) ;;古い実装 ( (string-match hatena-header-regexp (buffer-string)) (progn (setq title (match-string 1 (buffer-string))) (substring (buffer-string) (1+ (length (match-string 0 (buffer-string)))))) ) (t (buffer-string))))) (body (hatena-url-encode-string full-body hatena-default-coding-system)) (trivial (if hatena-trivial "1" "0")) (twit (hatena-url-encode-string hatena-twitter-prefix hatena-default-coding-system)) (post-data (concat "dummy=1" "&mode=enter" "&body=" body "&trivial=" trivial "&title=" title "&day=" day "&month=" month "&year=" year "&twitter_notification_enabled=" (if hatena-twitter-flag "1" "") "&twitter_notification_prefix=" twit ;; session ID for POST to hatena ;; this is a scheme of ensuring security in Hatena::Diary (concat "&rkm=" (let* ((md5sum (md5 (with-temp-buffer (insert-file-contents hatena-cookie) (re-search-forward "rk\\s \\([0-9a-zA-Z]+\\)") (concat (buffer-substring (match-beginning 1) (match-end 1)))) nil nil 'utf-8)) (p 0) (temp "")) (while (> (length md5sum) p) (setq temp (concat temp (char-to-string (string-to-number (substring md5sum p (+ p 2)) 16)))) (setq p (+ p 2))) (substring (base64-encode-string temp) 0 22))) ;; if "date" element exists , ;; command can't create the new page at hatena (if (hatena-check-newpage referer) (concat "&date=" date)) "×tamp=" timestamp ))) (with-temp-file hatena-tmpfile (insert post-data)) (message "%s => %s" filename referer) (call-process hatena-curl-command nil nil nil "-b" hatena-cookie "-x" hatena-proxy "--data" (concat "@" hatena-tmpfile) url) (message "posted") (and (functionp hatena-browser-function) (funcall hatena-browser-function nexturl)) ) (error "Not Hatena file: %s" file))) (setq hatena-twitter-prefix nil)) (defun hatena-login () (interactive) (if (file-exists-p hatena-cookie) (delete-file hatena-cookie)) (message (concat "logging in to \"" hatena-url "\" as \"" hatena-usrid "\"")) (let ((password (hatena-ask-password))) (call-process hatena-curl-command nil nil nil "-k" "-c" hatena-cookie "-x" hatena-proxy "-d" (concat "name=" hatena-usrid) "-d" (concat "password=" password) "-d" (concat "autologin=1") "-d" (concat "mode=enter") "https://www.hatena.ne.jp/login")) (message "Say HAPPY! to Hatena::Diary")) (defun hatena-check-newpage (urldate) "ページが作成済みかどうかチェック" (message "checking diary ....") (call-process hatena-curl-command nil nil nil "-o" hatena-tmpfile2 "-b" hatena-cookie urldate) (if (save-excursion (find-file hatena-tmpfile2) (prog1 (string-match "name=\"date\"" (buffer-string)) (kill-this-buffer))) (progn (message "modify diary") t) (message "make new diary") nil)) (defun hatena-diary-file-p(file) (let ((fname (file-name-nondirectory file))) (if (string-match hatena-fname-regexp fname) t nil))) (defun hatena-get-diary-string(&optional date) "はてなにある日記ファイルを取り、その文字列を返す。 ログインしていなければならない。" (if (not date) (error "not date")) (message "checking diary of %s ...." date) (let ((urldate (concat "http://d.hatena.ne.jp/" hatena-usrid "/edit?date=" date))) (call-process hatena-curl-command nil nil nil "-o" hatena-tmpfile "-b" hatena-cookie urldate)) (with-temp-buffer "*hatena-get*" (insert-file-contents hatena-tmpfile) ;;ここなんとか... (goto-char (point-min))(while (replace-string """ "\"")) (goto-char (point-min))(while (replace-string "&" "&")) (goto-char (point-min))(while (replace-string ">" ">")) (goto-char (point-min))(while (replace-string "<" "<")) (goto-char (point-min))(while (replace-string "'" "'")) (if (string-match "\n]*>\\(\\(\n\\|.\\)*?\\)" (buffer-string)) (match-string 1 (buffer-string)) nil))) (defun hatena-insert-webdiary(&optional date) "web の日記を挿入する。" (interactive) (if date nil (setq date (file-name-nondirectory buffer-file-name))) (if (string-match hatena-fname-regexp date) (insert (hatena-get-diary-string date)) (error "not date or hatena file"))) (defun hatena-delete-diary(&optional file userid) "日記を削除する。ローカルは削除しない。" (interactive) ;;バッファから読むと送信時間のところに"deleted" (if file nil (setq file buffer-file-name)) (if (not userid) (setq userid hatena-usrid)) (let ((filename (file-name-nondirectory file))) (if (string-match hatena-fname-regexp filename) (let* ((year (match-string 1 filename)) (month (match-string 2 filename)) (day (match-string 3 filename)) (date (concat year month day)) (baseurl (concat "http://d.hatena.ne.jp/" userid "/")) (referer (concat baseurl "edit?date=" date)) (url (concat baseurl "edit")) (edit (hatena-url-encode-string "この日を削除")) (post-data (concat "edit=" edit "&date=" date (concat "&rkm=" (let* ((md5sum (md5 (with-temp-buffer (insert-file-contents hatena-cookie) (re-search-forward "rk\\s \\([0-9a-zA-Z]+\\)") (concat (buffer-substring (match-beginning 1) (match-end 1)))) nil nil 'utf-8)) (p 0) (temp "")) (while (> (length md5sum) p) (setq temp (concat temp (char-to-string (string-to-number (substring md5sum p (+ p 2)) 16)))) (setq p (+ p 2))) (substring (base64-encode-string temp) 0 22))) "&mode=delete"))) (message "deleting %s" referer) (with-temp-file hatena-tmpfile (insert post-data)) (call-process hatena-curl-command nil nil nil "-b" hatena-cookie "-x" hatena-proxy "--data" (concat "@" hatena-tmpfile) url) (message "deleted")) (error "Not Hatena file: %s" file)))) (defun hatena-logout() (interactive) (call-process hatena-curl-command nil nil nil "-b" hatena-cookie "-x" hatena-proxy "http://d.hatena.ne.jp/logout") (message "logged out from d.hatena.ne.jp")) (defun hatena-ask-password() (let (pass str) (if (null hatena-use-file) (setq pass (read-passwd "password ? : ")) ;;ファイルが無かった場合は作る。 (if (not (file-exists-p hatena-password-file)) (append-to-file (point) (point) hatena-password-file)) (setq str (with-temp-buffer nil (insert-file-contents hatena-password-file) (buffer-string))) (if (string-match "[^ ]+" str) (setq pass (base64-decode-string (match-string 0 str))) (setq pass (read-passwd "password ? : ")) (with-temp-file hatena-password-file (insert (base64-encode-string (format "%s" pass))))) pass))) (defun hatena-exit() "hatena-fname-regexpにマッチするバッファをすべて保存して消去" (interactive) (if (yes-or-no-p "save all diaries and kill buffer ?") (progn (let ((buflist (buffer-list)) (i 0)) (while (< i (length buflist)) (let ((bufname (buffer-name (nth i (buffer-list))))) (if (string-match hatena-fname-regexp bufname) (progn (if (buffer-modified-p (nth i (buffer-list))) (save-buffer (nth i (buffer-list)))) (kill-buffer (nth i (buffer-list))))) (setq i (1+ i)))))))) (defun hatena-find-previous (&optional count file) "count 日前の日記を開く count が nil なら一日だけ戻る" (interactive "p") (hatena-find-pf (if count (- count) -1) (buffer-name))) (defun hatena-find-following (&optional count file) "count 日後の日記を開く count が nil なら一日だけすすむ" (interactive "p") (hatena-find-pf (if count count 1) (buffer-name))) (defun hatena-find-pf(count &optional file) (if (equal major-mode 'hatena-diary-mode) (if (not file) (setq file (buffer-name))) (error "not hatena mode")) (let ((find-previous (lambda (element count lst) (let* ((sublst (member element lst)) (result (+ (- (length lst) (length sublst)) count))) (if (or (null sublst) (< result 0)) nil (nth result lst))))) previous) (setq previous (funcall find-previous (file-name-nondirectory file) (if (not count) 1 count) (directory-files hatena-directory nil hatena-fname-regexp))) (if previous (find-file (concat (file-name-directory file) previous)) ;;見つからない時は、未来の日付を尋ねる。 (let ((filename (read-string "作成したい日付を入力: " (hatena-today-date (* -24 count) (buffer-name)) nil))) (if (string-match hatena-fname-regexp filename) (progn (find-file filename) (save-buffer)) (error "日付ファイルではありません!!")))))) (defun hatena-get-webdiary () "http://d.hatena.ne.jp/usrid/export を取ってきて変換。足りない日記分をファイルに足す。" (interactive) ;;exportをとってくる (call-process hatena-curl-command nil nil nil "-o" hatena-tmpfile "-b" hatena-cookie (concat "http://d.hatena.ne.jp/" hatena-usrid "/export" )) ;;export は utf-8 なので、hatena-default-coding-system に直す。 (let ((filelst (directory-files hatena-directory nil hatena-fname-regexp)) (title-regexp "\n\n") pt-start pt-end day title body) (with-temp-buffer "*hatena-get*" (insert-file-contents hatena-tmpfile) (set-buffer-file-coding-system hatena-default-coding-system) (hatena-translate-reverse-region (point-min) (point-max)) (while (re-search-forward title-regexp nil t) (setq day (concat (match-string 1) (match-string 2) (match-string 3))) (setq title (match-string 4)) (setq pt-start (match-end 0)) (re-search-forward "\n" nil t) (setq pt-end (match-beginning 0)) (setq body (buffer-substring pt-start pt-end)) (save-excursion (if (null (member day filelst)) (progn (hatena day) (set-buffer-file-coding-system hatena-default-coding-system) (message "creatig %s" day) (insert body) (save-buffer) (kill-buffer (current-buffer)))))) (message "finished")))) (defun hatena-url-encode-string (str &optional coding) "w3m-url-encode-string からコピー" (apply (function concat) (mapcar (lambda (ch) (cond ((eq ch ?\n) ; newline "%0D%0A") ((string-match "[-a-zA-Z0-9_:/.]" (char-to-string ch)) ; xxx? (char-to-string ch)) ; printable ((char-equal ch ?\x20) ; space "+") (t (format "%%%02x" ch)))) ; escape ;; Coerce a string to a list of chars. (append (encode-coding-string (or str "") (or coding buffer-file-coding-system 'iso-2022-7bit)) nil)))) (defun hatena-twitter-prefix-input (ts) "日記更新時の内容入力" (interactive "sTwitter prefix:") (setq hatena-twitter-prefix ts)) ;----------------特殊文字の変換---------------- ;;yahtml を改変 (defvar hatena-entity-reference-chars-alist '((?> . "gt") (?< . "lt") (?& . "amp") (?\" . "quot")) "translation table from character to entity reference") (defvar hatena-entity-reference-chars-regexp "[><&\\]") (defvar hatena-entity-reference-chars-reverse-regexp "&\\(gt\\|lt\\|amp\\|quot\\);") (defun hatena-translate-region (beg end) "Translate inhibited literals." (interactive "r") (save-excursion (save-restriction (narrow-to-region beg end) (let ((ct hatena-entity-reference-chars-alist)) (goto-char beg) (while (re-search-forward hatena-entity-reference-chars-regexp nil t) (replace-match (concat "&" (cdr (assoc (preceding-char) ct)) ";"))))))) (defun hatena-translate-reverse-region (beg end) "Translate entity references to literals." (interactive "r") (save-excursion (save-restriction (narrow-to-region beg end) (let ((ct hatena-entity-reference-chars-alist)) (goto-char beg) (while (re-search-forward hatena-entity-reference-chars-reverse-regexp nil t) ;(setq c (preceding-char)) (replace-match (string (car (rassoc (match-string 1) ct))))))))) (defun hatena-change-trivial () (interactive) (if (not hatena-trivial) (progn (message "ちょっとした更新モード") (setq hatena-trivial t)) (setq hatena-trivial nil) (message "更新モード"))) (defun hatena-twitter () (interactive) (if (not hatena-twitter-flag) (progn (message "twitterに通知") (setq hatena-twitter-flag t)) (setq hatena-twitter-flag nil) (message "twitterには通知しない"))) (defun hatena-current-second(number) "現在までの秒数を返す。emacs では整数がケタ溢れするので、浮動小数点で" (let* ((ct (current-time)) (high (float (car ct))) (low (float (car (cdr ct)))) str) (setq str (format "%f"(+ (+ (* high (lsh 2 15)) low) number))) (substring str 0 10) ;; )) (provide 'hatena-diary-mode) ;;;;;end of file