vimからlivedoorwikiを更新する

以前からVimスクリプトに挑戦しよう、挑戦しようとは思ってたんですが、なかなかできなかった。
その一つの理由にVimスクリプトの文法がよくわからんってのがあったので、今回はこれからVimスクリプト書こうって人のために、できるだけ詳細に説明しようと思います。
もちろん、こちとらVimスクリプト歴1週間なので、わからんとこはいっぱいあるんですが、、、

で、今回作ろうと思ったのがLivedoor wikiVimから更新するスクリプト
まあ、ほぼほぼhatena.vimをパクったんですが、かなり勉強にもなりました。

構成・機能

構成は以下のような感じです。

もう全部hatena.vimを丸パクリです。笑
使いかたはplugin/livedoor.vimのs:ld_userとs:ld_wiki_idに自分のIDをセットし、.vimrcに「set runtimepath+=$VIM/livedoor」とかランタイムパスを通してやればOKです。

ただ、今すぐにでも直すべきところはてんこもり。笑
セキュリティとか微塵も考えてない。笑
例外処理も皆無、、、あとたぶんUnixの人しか動かない気がします。(Windows試してません。笑)
という状態なので、自己責任でお願いいたします。
土日にしかブログ書けないので、説明も超走り書きですが、今回は僕自身vimスクリプトがよくわかっていないので、アドバイスとかを早めにもらえたら、、、
とか、プログラムを公開することで何かいいことありそうって気がしたので、かなり不完全ですが、公開することにしました。

livedoor.vimその1

全部一気に説明するとかなり見にくいので、少しずつ分割して説明していきます。

scriptencoding utf-8

" ログイン
command! -nargs=0 LivedoorLogin call LivedoorLogin()
" 編集 wiki内容を取得
command! -nargs=1 LivedoorEdit call LivedoorEdit('<args>')
" 更新 wikiへ内容を送る
command! -nargs=0 LivedoorEditCommit call LivedoorEditCommit()
" カーソル上の単語のwikiページを編集
command! -nargs=? LivedoorCustomEdit call LivedoorCustomEdit('<args>')

" LivedoorCustomEditを「\le」で使用できるように  
nnoremap <Leader>le : LivedoorCustomEdit<CR>

" livedoor.vimのベースディレクトリを取得
if !exists('g:ld_base_dir')
    let g:ld_base_dir = substitute(expand('<sfile>:p:h'), '[/\\]plugin$', '', '')
endif

" curlのコマンド
let s:curl_cmd = 'curl -k --silent'

" LivedoorID
let s:ld_user = "hogehoge"
" LivedoorwikiのユーザID
let s:ld_wiki_id = '123456'
" LivedoorwikiのURL
let s:ld_wiki_url = 'http://wiki.livedoor.jp/'
" Livedoorwikiの管理画面のURL
let s:ld_cms_url = 'http://cms.wiki.livedoor.com/wiki/'
" LivedoorのログインURL
let s:ld_login_url = 'https://member.livedoor.com/login/index'
command

まずcommandですが、これでユーザ定義の関数をコマンドラインから使えるようにしています。
コマンドラインで「:LivedoorEdit 'hoge'」みたいな。
「-nargs」は引数の数を定義しています。
http://www.ac.cyberhome.ne.jp/~yakahaira/vimdoc/usr_40.html#40.2
」はコマンドラインで与えられた引数がそのままそこに入ることを意味しています。
「''」とすると、自動的に引数を文字列にしてくれます。
つまり、

command! -nargs=1 LivedoorEdit call LivedoorEdit(<args>)
は、コマンドラインより
:LivedoorEdit 'hoge'
としてやらんとだめなんですが、
command! -nargs=1 LivedoorEdit call LivedoorEdit('<args>')
:LivedoorEdit hoge
でいいわけです。
nnoremap

ノーマルモードの時のマップです。
ここでは「\ld」をコマンドラインのLivedoorCustomEditにマッピングしています。
」は「\」のことを意味しています。
nnormapとnmapの違いはどうも再マップされないようにする? よくわかりません。。。
http://www.ac.cyberhome.ne.jp/~yakahaira/vimdoc/usr_40.html#40.1

変数

変数とか基本的なことは下記に書いてありますので、参照下さい。
http://nanasi.jp/code.html
上記のlet ...というやつはこのスクリプトで使いまわす変数を定義しています。

expand

expandは特殊文字を展開します。
例えばコマンドラインで「:echo expand(%)」とすると、現在開いているファイル名が表示されると思います。そんな感じです。 組込み関数は下記参照。
http://www.ac.cyberhome.ne.jp/~yakahaira/vimdoc/eval.html#functions

livedoor.vimその2

" livredoorにログインする
function! LivedoorLogin()
    let ld_user = s:ld_user
    
    " クッキーを保存するファイル
    if has('win32')
        let cookie_file = g:ld_base_dir . '\cookies\' . ld_user
    else
        let cookie_file = g:ld_base_dir . '/cookies/' . ld_user
    endif

    " パスワードをプロンプトで入力
    let password = inputsecret('Password: ')

    if !len(password)
        echo 'キャンセルしました'
        return []
    endif

    " ログイン
    let g:content = system(s:curl_cmd . ' ' . s:ld_login_url . ' -d livedoor_id=' . ld_user . ' -d password=' . password . ' -c "' . cookie_file . '" -D -')

    " ログインできたらクッキーファイルのパスを返す
    if g:content =~ 'Location: '
        echo 'ログインしました'
        return cookie_file
    else
        echoerr 'ログインに失敗しました'
    endif
endfunction
inputsecret

inputsecretでユーザ対話型で入力を促せます。
secretなので、と同じように文字が「*」になります。
↑というかこれ通ってしまっていいのか?笑

system

外部コマンドを呼び出します。
ここではcurlを使用しています。

curl
  • -k
    • 証明書がちゃんとしていることを前提としてSSL接続
  • -d
    • Postとしてデータを送る
    • データはURLエンコードしてくれる。
      • 実はこれを知らなくて、printfとか使ってURLエンコードをがんばってたんですが、どうしてもUTF-8からEUC-JPに変換して、エンコードというのができなくて2日ほど消費しました...
      • Livedoorwikiでは該当ページの編集用URLを取得するのにどうしても編集ページ名をURLエンコードする必要が合ったので。
  • -c
    • クッキーデータをファイルに保存する
  • -D
    • ヘッダー情報をファイルに保存する。「-D -」で標準出力にだす。
=~

左辺が右辺に正規表現でマッチすればTrue、やと思う。笑 上記はログインできた時のみ「Location:」でリダイレクトするので、それをログインの成功判定条件としている。

livedoor.vimその3

function! LivedoorCustomEdit(...)
    if exists(a:0)
        call LivedoorEdit(a:0)
    else
        let cursor_word = matchstr(expand('<cWORD>'), '[^\[].*[^\]]')
        call LivedoorEdit(cursor_word)
    endif
endfunction
a:0

変数にはスコープがあります。
「a:hoge」は関数の引数の変数という意味です。
この関数ではLivedoorCustomEdit(...)というように引数が何個も取れるようになっています。
「a:0」というのは、その何個も取れる引数のうちの一つめという意味です。
ちなみにLivedoorCustomEditは引数を必要としないので、そもそも上記はおかしいのですが。笑

expandで展開できる特殊な単語の内の一つです。
詳しくは下記参照。
http://www.ac.cyberhome.ne.jp/~yakahaira/vimdoc/cmdline.html#:%3Ccword%3E
適当な単語の上にカーソルをあわせた後、コマンドラインで「:echo expand('')」とかすると感動できます。(僕は感動しました。笑)

matchstr

正規表現で文字列を検索取得しています。
Livedoorwikiでは他のページにリンクさせるのを「hoge」というwiki記法で表現します。
で、上記ではwikiを編集中、「hoge」にカーソルをあわせて上記関数を実行すると、変数cursol_wordには「hoge」が入ったりするわけです。
冒頭で、「nnoremap le : LivedoorCustomEdit」としているのでカーソルを合わせたまま「\le」でそのページが編集できたりします。

livedoor.vimその4

" 編集
function! LivedoorEdit(wiki_name)

    " クッキーファイルのパスを取得していない時のみログイン
    if !exists('b:ld_login_cookie')
        let ld_login_cookie = LivedoorLogin()
        if !len(ld_login_cookie)
            return
        endif
    else
        let ld_login_cookie = b:ld_login_cookie
    endif

    " ログインし直さないでいいように
    let b:ld_login_cookie = ld_login_cookie
    " 該当ページのページID, wikiのバージョン, 編集コンテンツ、編集クッキー情報取得
    let [page_id, wiki_version, content, ld_edit_cookie] = LivedoorLoadContent(a:wiki_name, ld_login_cookie)

    " 編集コンテンツをVimに表示するための一時ファイル
    let tmpfile = tempname()
    " 一時ファイルを開く
    execute 'edit!' tmpfile
   
    " バックアップファイルは作らない
    setlocal noswapfile
    " LivedoorwikiはEUC-JPなので
    let &fileencoding = 'euc-jp'
    " 一時ファイルのタイトル
    let &titlestring = 'livedoor wiki ' . a:wiki_name
    " Livedoorwiki記法のsyntax
    set filetype=livedoor

    " 必要な情報を新らしく開いたバッファに渡す
    let b:ld_login_cookie = ld_login_cookie
    let b:page_id = page_id
    let b:wiki_version = wiki_version
    let b:ld_edit_cookie = ld_edit_cookie

    " これよくわからんけど必要
    let nopaste = !&paste   
    set paste
    " 取得したコンテンツを貼り付け
    execute 'normal i' . content
    if nopaste
        set nopaste
    endif
    " 貼り付けた状態では、まだ編集していない状態にする
    set nomodified

endfunction
b:hoge

「b:hoge」はバッファをスコープとした変数です。
このスコープがバッファの意味ですが、要はVimのウィンドウをスコープとしているようです。(たぶん)
gvimだと「:split」とかでウィンドウを分割できますが、ウィンドウ分割すると、分割する前に参照できたb:hogeは参照できなくなります。
ここではクッキーファイルのパスをバッファ変数とし、その変数がある限りはログインする必要のないようにしています。

tmpfile

LivedoorLoadContentで取得してきた内容を書き込むために一時ファイルを作成しています。
「execute 'edit! tmpfile'」でVimで開いています。
これはコマンドらインで「:e! tmpfile」とかしたのと同じことをしています。
で、この時点で新しいウィンドウを開いたので、b:ld_login_cookieとかに再度セットして、新たに開いたファイルで変数が参照できるようにしています。

nomodified

これは現在の開いているバッファを変更なしの状態とします。
gvimだと最初にファイルを開いたときから何か変更を加えると、コマンドラインの上のところに「[+]」というマークが出ると思います。
これが出ると保存しないで「:q」でウィンドウを閉じようとすると、保存してから閉じるべし、といわれるんですが、nomodifiedをセットするとあたかも変更をしていないかのようにできます。

livedoor.vimその5
" 編集するコンテンツを取得
function! LivedoorLoadContent(wiki_name, login_cookie)
    " LivedoorwikiはEUC-JPなので
    let enc_wiki_name = iconv(a:wiki_name, &enc, 'euc-jp')

    " まず、編集ページのURLを取得するためのHTMLを取得
    let content = system(s:curl_cmd . ' "' . s:ld_wiki_url . s:ld_user  . '/d/' . enc_wiki_name . '" -b "' . a:login_cookie . '"')
    let enc_content = iconv(content, 'euc-jp', &enc)

    " 編集ページのURLを取得
    let edit_url = matchstr(enc_content, '<li class="menu-02"><a href="\zs.\{-}\ze">')
    " 騙取するページIDを取得
    let page_id = matchstr(edit_url, '&id=\zs.\{-}\ze$')

    let cookie_file = g:ld_base_dir . '/cookies/' . s:ld_user . '_edit'
    " 編集するページのHTMLを取得
    let edit_content = system(s:curl_cmd . ' "' . edit_url . '" -b "' . a:login_cookie . '" -c "' . cookie_file . '" -D -')
    let enc_edit_content = iconv(edit_content, 'euc-jp', &enc)

    " 編集するページのバージョン管理番号を取得
    let wiki_version = matchstr(enc_edit_content, 'name="id" type="HIDDEN">\n<input value="\zs.\{-}\ze" name="ver" type="HIDDEN">')
    " 編集するコンテンツを取得
    let edit_body = matchstr(enc_edit_content, '<textarea id="inputBody".\{-}>\zs.\{-}\ze</textarea>')

    return [page_id, wiki_version, edit_body, cookie_file]
endfunction
iconv

LivedoorwikiはEUC-JPなので、文字コードを変更します。
「&enc」は現在の文字コードです。(たぶん正確には違いますが。。。)

curl
  • -b
    • 保存してあるログインクッキーを参照させます
\zs.\{-}\ze

  • \zs \ze
    • このふたつで挟まれた文字列をで取得します。
  • \{-}
    • 最短一致の0回以上の繰り返し
    • 上記では、「.\{-}」ということで、任意の文字列の0回以上の繰り返しという意味です。
最短一致についてはこちら vim正規表現の詳しくは書きを参照下さい。
http://www.ac.cyberhome.ne.jp/~yakahaira/vimdoc/pattern.html#pattern

livedoor.vimその5

" Livedoorwikiへ更新する
function! LivedoorEditCommit()
    
    " 変更されていれば、保存する
    if &modified
        write
    endif

    " 今開いている一時ファイルのフルパスを取得
    let body_file = expand('%')

    " 更新
    let result = LivedoorPost(b:ld_login_cookie, b:page_id, b:wiki_version, body_file, b:ld_edit_cookie)
    let enc_result = iconv(result, 'euc-jp', &enc)

    if result =~ 'Location: http://wiki.livedoor.jp/'
        echo '更新しました'
    endif
    
endfunction
&modified

バッファが変更されたかどうかのフラグです。
上記は変更があれば保存という処理(:w)です。

livedoor.vimその6

" LivedoorwikiへPostする
function! LivedoorPost(ld_login_cookie, page_id, wiki_version, body_file, ld_edit_cookie)

    " multipart対応の-FでPostする
    let post_data = ' -F wiki_id=' . s:ld_wiki_id
                    \ . ' -F id=' . a:page_id
                    \ . ' -F ver=' . a:wiki_version
                    \ . ' -F reload=0'
                    \ . ' -F full='
                    \ . ' -F "content=<' . a:body_file . '"'
                    \ . ' -F tags= -F file= -F description= '
                    \ . ' -F .save="' . iconv('保存する', &enc, 'euc-jp') . '"'

    " リファラ
    let referer = s:ld_cms_url . 'edit?wiki_id=' . s:ld_wiki_id . '&id=' . a:page_id

    " Post
    return system(s:curl_cmd . ' ' . s:ld_cms_url . 'edit -b "' . a:ld_login_cookie .'" -e "' . referer . '"' . post_data . ' -D -')

endfunction
curl
  • F
    • multipartでフォームデータをPostする
  • ' -F "content<' . a:body_fiele . '"'
    • a:body_fileにはファイルパスが入っており、それを標準入力として、contentにいれています。
  • e
    • リファラをセットする
    • Livedoorwikiでは別にリファラがなくてもPostできるっぽいですが、セキュリティ対策でそのうちリファラがないと送信できなくなるかもしれないので念のため。

ちなみにめちゃめちゃはまったんですが、Livedoorwikiは「.save」というsubmitボタンもセットしてやらないとちゃんと更新してくれませんでした。

syntax/livedoor.vim

こっちは上記よりも精査できていません(というかほぼ名前変えただけ。笑)が、一応ほぼ使えるっぽいです。

syntax include @Html syntax/html.vim

syn match LDHeading         +^\*\{1,3}\(\w\+\*\)\=.\+$+         contains=LDHeadingName,LDLink
syn match LDHeadingName     +^\*\{1,3}[^*]\{-1,}\*\|^\*\{1,3}+  contained nextgroup=LDCategory
syn match LDCategory        +\(\[.\{-}\]\)\++                   contained

syn match LDList            +^[+-]\++

syn match LDDefinition      +^:.\{-1,}:.\+$+    contains=LDDefColon
syn match LDDefColon        +:+                 contained

syn match LDTable           +^|\(.\{-}|\)\++    contains=LDTableHeader,LDTableSeparator
syn match LDTableHeader     +\*[^|]\++          contained
syn match LDTableSeparator  +|+                 contained

syn match LDLink            +\[\(http\|google\(:news\|:image\)\=\|amazon\|rakuten\):.\{-}\]+    contains=LDLinkSpecial
syn match LDLinkURL         +https\=://[-!#$%&*+,./:;=?@0-9a-zA-Z_~]\++
syn match LDLinkSpecial     +:title\(=[^]]*\)\=\ze\]\|:barcode\|:image+

syn match LDKeyword         +\[\[.\{-}\]\]+
syn region LDCancelLink     matchgroup=LDBlockDelimiter start=+\[\]+ end=+\[\]+ oneline

syn match LDFootNote        +(\@<!(((\@!.\{-}))+
syn match LDCancelFootNote  +)((+   contains=LDCanceledParen
syn match LDCanceledParen   +(+     contained

syn match LDReadMore        +=====\?+
syn match LDTex             +\[tex:.\{-}\]+
syn match LDUkulele         +\[uke:.\{-}\]+

syn region LDCancelP        matchgroup=LDBlockDelimiter start=+>\ze<+ end=+<$+ contains=@Html

syn match LDHTMLTag         +<+ contains=@Html

syn cluster LDSpecials      contains=LDDefColon,LDTableSeparator,LDCanceledParen

" 自動リンク
syn match LDAutoLinkDiary       +\(d:\)\=id:[A-Za-z]\w*\(:\d\{6}\|:\d\{8}\|:archive\(:\d\{6}\)\=\|:about\)\=+
syn match LDAutoLinkQuestion    +question:\d\{10}\(:title\|:detail\|:image\(:small\)\=\)\=+
syn match LDAutoLinkQuestion    +\[question:\d\{10}:title=.\{-}\]+
syn match LDAutoLinkSearch      +\[search:\(keyword:\|question:\|asin:\)\=.\{-}\]+
syn match LDAutoLinkAntenna     +a:id:[A-Za-z]\w*+
syn match LDAutoLinkBookmark    +b:id:[A-Za-z]\w*\(:\d\{8}\|:favorite\|:asin\)\=+
syn match LDAutoLinkBookmark    +\[b:id:[A-Za-z]\w*:t:.\{-}\]+
syn match LDAutoLinkBookmark    +\[b:\(keyword\|t\):.\{-}\]+
syn match LDAutoLinkFotolife    +f:id:[A-Za-z]\w*\(:favorite\|:.\{-}:image\(:small\|:[wh]\d\+\)\=\)\=+
syn match LDAutoLinkGroup       +g:[A-Za-z]\w*\(:id:[A-Za-z]\w*\(:\d\{6}\|:\d\{8}\|:archive\)\=\)\=+
syn match LDAutoLinkGroup       +\[g:[A-Za-z]\w*:keyword:.\{-}\]+
syn match LDAutoLinkIdea        +idea:\d\+\(:title\)\=+
syn match LDAutoLinkIdea        +i:id:[A-Za-z]\w*+
syn match LDAutoLinkIdea        +\[i:t:.\{-}\]+
syn match LDAutoLinkRSS         +r:id:[A-Za-z]\w*+
syn match LDAutoLinkGraph       +graph:id:[A-Za-z]\w*+
syn match LDAutoLinkGraph       +\[graph:id:[A-Za-z]\w*\(:.\{-}\(:image\)\=\)\=\]+
syn match LDAutoLinkGraph       +\[graph:t:.\{-}]+
syn match LDAutoLinkMap         +map:x[0-9.]\+y[0-9.]\++
syn match LDAutoLinkMap         +\[map:\(t:\)\=.\{-}\]+
syn match LDAutoLinkKeyword     +\[keyword:.\{-}\(:graph\(\(:refcount\|:refrank\|:accessrank\)\(:\d\+[dwmy]\)\=\)\=\)\=\]+
syn match LDAutoLinkISBN        +isbn:\d\{10}\(:title\|\(:image\|:detail\)\(:small\|:large\)\=\)\=+
syn match LDAutoLinkASIN        +asin:[0-9A-Z]\+\(:title\|\(:image\|:detail\)\(:small\|:large\)\=\)\=+
syn match LDAutoLinkJANEAN      +\(jan\|ean\):\d\+\(:title\|\(:image\|:detail\)\(:small\|:large\)\=\|:barcode\)\=+
syn match LDAutoLinkJANEAN      +\[\(jan\|ean\):\d\+:title=.\{-}\]+
syn cluster LDAutoLinks         contains=LDAutoLinkDiary,LDAutoLinkQuestion,LDAutoLinkSearch,LDAutoLinkAntenna,LDAutoLinkBookmark,LDAutoLinkFotolife,LDAutoLinkGroup,LDAutoLinkIdea,LDAutoLinkRSS,LDAutoLinkGraph,LDAutoLinkMap,LDAutoLinkKeyword,LDAutoLinkISBN,LDAutoLinkASIN,LDAutoLinkJANEAN

syn match LDBlockQuoteCite +>\@<=https\?:.\{-}>$+ contained contains=LDLinkURL

syn region LDBlockQuote matchgroup=LDBlockDelimiter start=+^>>$+  end=+^<<$+ contains=ALLBUT,@LDSpecials,LDBlockQuoteCite
syn region LDBlockQuote matchgroup=LDBlockDelimiter start=+^>\zehttps\?:.\{-}>$+ end=+^<<$+ contains=ALLBUT,@LDSpecials
syn region LDPre        matchgroup=LDBlockDelimiter start=+^>|$+  end=+^|<$+ contains=ALLBUT,@LDSpecials,LDBlockQuoteCite
syn region LDSuperPre   matchgroup=LDBlockDelimiter start=+^=|[^|]*|$+ end=+^||=$+

" コメント
syn region LDComment        start=+<!--+    end=+-->+

hi link LDHeading           Title
hi link LDCategory          Label
hi link LDHeadingName       Identifier
hi link LDList              Statement
hi link LDDefColon          Statement
hi link LDTableSeparator    Statement
hi link LDTableHeader       Title
hi link LDKeyword           Underlined
hi link LDLink              Underlined
hi link LDLinkURL           Underlined
hi link LDLinkSpecial       Special
hi link LDAutoLinkDiary     Underlined
hi link LDAutoLinkQuestion  Underlined
hi link LDAutoLinkSearch    Underlined
hi link LDAutoLinkAntenna   Underlined
hi link LDAutoLinkBookmark  Underlined
hi link LDAutoLinkFotolife  Underlined
hi link LDAutoLinkGroup     Underlined
hi link LDAutoLinkIdea      Underlined
hi link LDAutoLinkRSS       Underlined
hi link LDAutoLinkGraph     Underlined
hi link LDAutoLinkMap       Underlined
hi link LDAutoLinkKeyword   Underlined
hi link LDAutoLinkISBN      Underlined
hi link LDAutoLinkASIN      Underlined
hi link LDAutoLinkJANEAN    Underlined
hi link LDFootNote          Identifier
"hi link LDCanceledParen    Special
"hi link LDCancelLink        Special
hi link LDError             Error
"hi link LDBlockQuote       String
"hi link LDPre              String
"hi link LDSuperPre         String
hi link LDBlockQuoteCite    Delimiter
hi link LDBlockDelimiter    Delimiter
hi link LDReadMore          Special
hi link LDComment           Comment
hi link LDCancelP           Delimiter

今回のブログは

もちろんhatena.vimを使って更新しました!!
ここまで一週間。もう毎日こればっかりやってて、昨日とかパソコンの前に半日以上座ってがんばってここまで。
そこまで時間かけてこれってものも悲しいのですが、色々あるとは思いますが、アドバイス等頂けたら幸いです。
ぱっと見すぐ直せる場所もたくさんあるので、明日から地味に直していきます。。。