はてなブログに自作VIMスクリプトで記事を書く
概要
VIMでメモをよく取る、このノリでブログを書きたい。 調べてみると、すでにはてなブログへ投稿するプラグインは存在する。
しかし、この仕組みは自作できるようになっていれば、他の開発でも役に立つはずと考え 自作してみることにした。ちなみに初vimscript。いちおうgithubで公開中hajimemat/hateblo.vim
行った調査は以下。
- vimとはてなブログはどうつながるか
- vimでどう実現するか
- 通信ツール math/webapi-vim
- 参考にしたスクリプト
- 既存のツール moznion/hateblo.vim
わかったこと
webapi-vimを使うとAtomPubのAPIと通信ができる。 HTTPリクエストを行い、XMLレスポンスをパースし、vimscript内で配列として受け取れる。
こんなスクリプトを書いて確認
api_url = "エントリポイント" user = "はてなID" api_key = "はてなAPIKEY echo webapi#atom#getFeed(api_url, user,api_key)
作るもの
これから作るものの仕様を考えてみた
- インターフェイスはuniteを使う
- 通常のVIMの編集作業と変わるようなコマンド、ショートカットは使わない
- VIMの保存 = サーバへの送信としたい
- 削除機能は実装しない
- 編集書式は常にMarkdownとする
コマンド
モード | CMD | - |
---|---|---|
ex | :Unite hateblo-list | 記事一覧を表示する+新規作成ができる |
ex | :w | サーバにデータを保存する |
書式
1行目にタグ、タイトルを記述する。 未公開の場合はタイトルの前にD:が表示される。 公開させるにはD:を削除して保存する。
# [:tag1,:tag2] タイトル # [:tag1,:tag2] D:タイトル
セッティング
let g:hateblo_settings = { \ 'user': 'はてなID', \ 'blog': 'はてなブログID', \ 'api_key': 'APIキー', \ }
新規作成
Uniteで起動したリストからNewを選択。 プロンプトでタイトルとカテゴリが聞かれる
以下のように入力
TITLE: タイトル
CATEGORIES: cat,cat2
- 追記[2017/01/01] ~~ 保存で送信はされるが、送信されたデータのentry_urlが取れなかったので、 一度保存したら、Uniteから再び開いて編集モードで送信しないと、 新規記事が何個もできてしまう。。。~~
webapi-vimのソースを読んだら、戻り値でentry_urlを返却していたので、 新規で開く保存 => 作成。 もう一度保存 => 上書き。 になるように処理フローを変更
function! webapi#atom#createEntry(uri, user, pass, entry, ...) abort let headdata = a:0 > 0 ? a:000[0] : {} let headdata["Content-Type"] = "application/x.atom+xml" let headdata["X-WSSE"] = s:createWsse(a:user, a:pass) let headdata["WWW-Authenticate"] = "WSSE profile=\"UsernameToken\"" let res = webapi#http#post(a:uri, s:createXml(a:entry), headdata, "POST") let location = filter(res.header, 'v:val =~ "^Location:"') if len(location) return split(location[0], '\s*:\s\+')[1] endif return '' endfunction
ソースコード一式
plugin/plugin.vim
let g:hateblo_draft_marker = "D:" " :wでサーバに送信する仕組み augroup hateble_env autocmd! autocmd BufWriteCmd hateblo:* call hateblo#editor#save() augroup END
autoload/hateblo/webapi.vim
" WEBAPI用の設定などを取得する " エントリポイントを取得する function! hateblo#webapi#getEndPoint() return g:hateblo['entry_point'] \ .'/' \ .g:hateblo['user'] \ .'/' \ .g:hateblo['blog'] endfunction " 記事用のエントリポイントを取得する function! hateblo#webapi#getEntryEndPoint() return hateblo#webapi#getEndPoint() \ .'/atom/entry' endfunction
autoload/hateblo/util.vim
" 文字列操作などのユーティリティを格納する function! hateblo#util#stripWhitespace(str) let l:str = substitute(a:str, '^\s\+', '', '') return substitute(l:str,'\s\+$', '', '') endfunction
autoload/hateblo/entry.vim
" エントリの処理 " エントリ一覧を取得する function! hateblo#entry#getEntries() return hateblo#entry#getEntriesWithURL(hateblo#webapi#getEntryEndPoint()) endfunction function! hateblo#entry#getEntriesWithURL(api_url) let l:feed = webapi#atom#getFeed(a:api_url, g:hateblo['user'],g:hateblo['api_key']) let b:hateblo_entries = l:feed['entry'] let b:hateblo_next_link = '' for l:link in l:feed['link'] if l:link['rel'] == 'next' let b:hateblo_next_link = l:link['href'] endif endfor return b:hateblo_entries endfunction function! hateblo#entry#getList() if !exists('b:hateblo_entries') call hateblo#entry#getEntries() endif let l:entries = b:hateblo_entries let l:list = [] for l:entry in l:entries if l:entry['app:control']['app:draft'] == 'yes' let l:word = '[draft] '.l:entry['title'] else let l:word = l:entry['title'] endif call add(l:list, { \ 'word': l:word, \ 'source': 'hateblo-list', \ 'kind': 'file', \ 'action__action': 'edit_entry', \ 'action__entry_url': l:entry['link'][0]['href'], \ 'draft': l:entry['app:control']['app:draft'] \}) endfor if b:hateblo_next_link != '' call add(l:list, { \ 'word': '### NEXT PAGE ###', \ 'source': 'hateblo-list', \ 'kind': 'file', \ 'action__action': 'next_page', \ 'action__url': b:hateblo_next_link \}) endif call add(l:list, { \ 'word': '### NEW ###', \ 'source': 'hateblo-list', \ 'kind': 'file', \ 'action__action': 'new', \}) call add(l:list, { \ 'word': '### Reflesh ###', \ 'source': 'hateblo-list', \ 'kind': 'file', \ 'action__action': 'reflesh', \}) return l:list endfunction function! hateblo#entry#getCategories(entry) let l:categories = [] for l:category in a:entry['category'] call add(l:categories, l:category['term']) endfor return l:categories endfunction
autoload/hateblo/editor.vim
" 編集 function! hateblo#editor#edit(entry_url) let l:entry = webapi#atom#getEntry(a:entry_url, g:hateblo_vim['user'],g:hateblo_vim['api_key']) let l:type = 'html' execute 'edit hateblo:'.fnameescape(l:entry['title']) execute ":%d" let b:entry_url = a:entry_url let b:entry_is_new = 0 if l:entry['app:control']['app:draft'] == 'yes' let b:entry_is_draft = 1 else let b:entry_is_draft = 0 endif call append(0, hateblo#editor#buildFirstLine(l:entry['title'],hateblo#entry#getCategories(l:entry))) call append(2, split(l:entry['content'], '\n')) execute ":2" if l:entry['content.type'] ==# 'text/x-markdown' let l:type = 'markdown' elseif l:entry['content.type'] ==# 'text/x-hatena-syntax' let l:type = 'hatena' endif execute 'setlocal filetype='.l:type.'.hateblo' endfunction " ファーストラインからタイトルとタグを取得する function! hateblo#editor#parseFirstLine(line) let l:matched = matchlist(a:line, '#\s\+\[\(.\+\)\]\s\+\(.\+\)') if len(l:matched) < 1 return [] endif let l:categories = [] for l:category in split(l:matched[1],',') call add(l:categories, hateblo#util#stripWhitespace(substitute(l:category, '^\s*:', '', ''))) endfor let l:title = l:matched[2] return { \ 'categories': l:categories, \ 'title': l:title \} endfunction " ファーストラインを作成する function! hateblo#editor#buildFirstLine(title,categories) let l:categories = [] for l:category in a:categories call add(l:categories, ":".l:category) endfor if b:entry_is_draft == 1 return "# [".join(l:categories, ',')."] ".g:hateblo_draft_marker.a:title else return "# [".join(l:categories, ',')."] ".a:title endif endfunction " 保存 function! hateblo#editor#save() let l:data = hateblo#editor#parseFirstLine(getline(1)) let l:categories = l:data['categories'] let l:title = l:data['title'] let l:contents = join(getline(3,'$'), "\n") if l:title[0:len(g:hateblo_draft_marker)-1] ==# g:hateblo_draft_marker let l:title = l:title[len(g:hateblo_draft_marker):] let l:is_draft = 'yes' else let l:is_draft = 'no' endif if b:entry_is_new == 1 call webapi#atom#createEntry( \ hateblo#webapi#getEntryEndPoint(), \ g:hateblo_vim['user'], \ g:hateblo_vim['api_key'], \ { \ 'title': l:title, \ 'content': l:contents, \ 'content.type': 'text/plain', \ 'content.mode': '', \ 'app:control': { \ 'app:draft': l:is_draft, \ }, \ 'category': l:categories \ }) echom "Created" execute(":q!") call hateblo#entry#getEntries() Unite hateblo-list else call webapi#atom#updateEntry( \ b:entry_url, \ g:hateblo_vim['user'], \ g:hateblo_vim['api_key'], \ { \ 'title': l:title, \ 'content': l:contents, \ 'content.type': 'text/plain', \ 'content.mode': '', \ 'app:control': { \ 'app:draft': l:is_draft, \ }, \ 'category': l:categories \ }) echom "Saved" endif endfunction function! hateblo#editor#create() let l:data = {} let l:data['title'] = input('TITLE: ') if len(l:data['title']) < 1 echom 'Canceled' return 0 endif let l:data['categories'] = split(input('CATEGORIES: '),',') execute 'tabe hateblo:'.fnameescape(l:data['title']) let b:entry_is_draft = 1 call append(0, hateblo#editor#buildFirstLine(l:data['title'], l:data['categories'])) " let l:data = hateblo#editor#parseFirstLine(getline(1)) " if len(l:data) < 1 " let l:data = {} " let l:data['title'] = input('TITLE: ') " if len(l:data['title']) < 1 " echom 'Canceled' " return 0 " endif " let l:data['categories'] = split(input('CATEGORIES: '),',') " execute 'tabe hateblo:'.fnameescape(l:data['title']) " call append(0, hateblo#editor#buildFirstLine(l:data['title'], l:data['categories'])) " else " execute 'tabe hateblo:'.fnameescape(l:data['title']) " call append(0, getline(0,'$')) " endif let b:entry_is_new=1 execute 'setlocal filetype=markdowm.hateblo' endfunction
autoload/unite/sources/hateblo_list.vim
let s:save_cpo=&cpo set cpo&vim " Unite用 let s:source = { \ 'name': 'hateblo-list', \ 'description': 'Entry list of hateblo', \ 'action_table': { \ 'on_choose': { \ } \ }, \ 'default_action': 'on_choose' \ } function! s:source.gather_candidates(args,context) return hateblo#entry#getList() endfunction function! s:unite_action_on_choose(candidate) "echo a:candidate.action__action if a:candidate.action__action == 'edit_entry' call hateblo#editor#edit(a:candidate['action__entry_url']) elseif a:candidate.action__action == 'next_page' "echo a:candidate call hateblo#entry#getEntriesWithURL(a:candidate.action__url) Unite hateblo-list elseif a:candidate.action__action == 'reflesh' "echo a:candidate call hateblo#entry#getEntries() Unite hateblo-list elseif a:candidate.action__action == 'new' call hateblo#editor#create() else echo 'not impl' endif endfunction function! s:source.action_table.on_choose.func(candidate) call s:unite_action_on_choose(a:candidate) endfunction function! unite#sources#hateblo_list#define() return s:source endfunction let &cpo = s:save_cpo unlet s:save_cpo