Home About Contact
Emacs , Emacs Lisp

Emacs Lisp JSON 文字列のパースしてハイパーリンクボタンを生成

make text buttons

ハイパーリンクのつくり方がわかったので サーバから取得した JSON に記述されている 名前とURL の一覧情報から ハイパーリンクの一覧情報をつくろうと思う。

次のような JSON データがあったとして、これをパースして、 Emacs のバッファにボタン(ハイパーリンク)をつくる lisp を書きたい。

{
  "list": [
    {
      "name": "google",
      "url": "https://www.google.co.jp/"
    },
    {
      "name": "apple",
      "url": "https://www.apple.com/"
    }
  ]
}

JSON 文字列のパース

このページ https://www.gnu.org/software/emacs/manual/html_node/elisp/Parsing-JSON.html を見ると、 json-parse-string という関数が書いてある。 ただ使い方がよくわからない。

代わりに json-read-from-string を使ってみる。

(require 'json)
(json-read-from-string "{\"list\": [1, 2, 3]}")

foo.el ファイルなどでこれを記述して C-x C-e する(それぞれの行で)。 するとミニバッファに次が出力される。

((list . [1 2 3]))

これは (list . [123]) という key-value のオブジェクトがひとつだけ入ったリストになっているので、 たとえば、 elt を使って先頭の要素(オブジェクト)を取り出す。

(elt
 (json-read-from-string "{\"list\": [1, 2, 3]}") 0)

C-x C-e するとミニバッファに (list . [1 2 3]) が出力される。

または、assq というキーを指定して該当要素をみつける関数を使えば:

(assq 'list
      (json-read-from-string "{\"list\": [1, 2, 3]}"))

C-x C-e すると同じ結果が得られる。

ここから car すれば list が cdr すれば [1 2 3] を得ることができる。

(car (assq 'list (json-read-from-string "{\"list\": [1, 2, 3]}"))) ; => list
(cdr (assq 'list (json-read-from-string "{\"list\": [1, 2, 3]}"))) ; => [1 2 3] 

もう少し具体的な JSON データを考える。

{
  "list": [
    {
      "name": "macbook"
    },
    {
      "name": "mac mini"
    }
  ]
}

これを実行すると(1番目の要素を取得)

(elt
 (cdr (assq
       'list
       (json-read-from-string
        "{\"list\": [{\"name\":\"macbook\"}, {\"name\":\"mac mini\"}]}"))) 0)

これが得られる。

((name . "macbook"))

これを実行すると(2番目の要素を取得)

(elt
 (cdr (assq
       'list
       (json-read-from-string
        "{\"list\": [{\"name\":\"macbook\"}, {\"name\":\"mac mini\"}]}"))) 1)

これが得られる。

((name . "mac mini"))

道のりは遠い。

いよいよ冒頭に書いた JSON をパースする

これ:

{
  "list": [
    {
      "name": "google",
      "url": "https://www.google.co.jp/"
    },
    {
      "name": "apple",
      "url": "https://www.apple.com/"
    }
  ]
}

まず、これを link.json に保存しておく。

json-read-from-string の代わりに json-read-file を使う。

(json-read-file "link.json")

C-x C-e するとミニバッファに次のが出た:

((list . [(... ...) (... ...)]))

key が list の値を assq を使って取得する:

(assq 'list (json-read-file "link.json"))

C-x C-e すると(もう面倒なので以後は実行するとと書く):

(list . [((name . "google") (url . "https://www.google.co.jp/")) ((name . "apple") (url . "https://www.apple.com/"))])

ドットで対になっているデータが来ている。 今は list には興味がなく値の方だけほしいので cdr する。

(cdr (assq 'list (json-read-file "link.json")))

実行するとミニバッファに次のが出た:

[((name . "google") (url . "https://www.google.co.jp/")) ((name . "apple") (url . "https://www.apple.com/"))]

角括弧で囲まれて表現されたこれはなんなのか不明なのだが、つまり、パーレンの場合はリストだけど、これはリストではない。 ためしに listp してみる。

(listp (cdr (assq 'list (json-read-file "link.json"))))

実行すると nil になる。 よくわからないのだが、なんかのシーケンスらしい。ここでは角括弧のやつと呼ぶ。

シーケンスの処理例: https://www.gnu.org/software/emacs/manual/html_node/elisp/Sequence-Functions.html

(cdr (assq 'list (json-read-file "link.json"))) して得た結果(再掲載):

[((name . "google") (url . "https://www.google.co.jp/")) ((name . "apple") (url . "https://www.apple.com/"))]

これを google, apple と順に取得するにはどうすればいいのか? elt を使えばよい。このように:

(elt (cdr (assq 'list (json-read-file "link.json"))) 0) ; => ((name . "google") (url . "https://www.google.co.jp/"))
(elt (cdr (assq 'list (json-read-file "link.json"))) 1) ; => ((name . "apple") (url . "https://www.apple.com/"))

しかし、今は2個の情報があることを知っているが、実際は何個のデータが来るかは不明だ。n 個のデータに対処できるようにコードを書きたい。 どうすればいいのか?

とりあえずは、今何個あるかを調べるには length を使えばよい。

(length (cdr (assq 'list (json-read-file "link.json")))) ; => 2

この 2 から (0 1) というリストを作り出そう。 make-number-list を書く。

ちなみに、ここでやろうとしている方法よりもっとよい方法があるに違いないのでその点了承ください。

この辺に答えがありそう。 https://www.gnu.org/software/emacs/manual/html_node/elisp/Iteration.html dotimes 使えばいいだけなのかも。

どうでもいいけど Kotlin だったら 0.until(2) するだけの話。

(defun make-number-list (i)
  (cond
   ((eq i 0) nil)
   (t (cons (1- i) (make-number-list (1- i))))))

意図通り動くか試す。

(make-number-list 0) ; => nil
(make-number-list 1) ; => (0)
(make-number-list 2) ; => (1 0)
(make-number-list 3) ; => (2 1 0)

作動した。ただイメージと逆なので reverse した方がいいな。

(reverse (make-number-list 3)) ; => (0 1 2)

では reversemake-number-list 使えば (Kotlin で言うところの) 0.until(n) ができるようになった。

コードが長くなってきたので補助関数 item-seq , item-len を書く。

(defun item-seq (filename)
  (cdr
   (assq 'list (json-read-file filename))))

(defun item-len (filename)
  (length (item-seq filename)))

これ item-len を使えば、 link.jsonlist に何件のハイパーリンクデータが入っているか計算できる。 そこからインデックスリストをつくたければ、次のようにすればよい。

(reverse (make-number-list (item-len "link.json"))); => (0 1)

できた。

あとは、このインデックスリストを使って dolist すればよい。

ハイパーリンクボタン

まず json の各ハイパーリンク情報から url を取り出す。 そのために補助関数 get-link-obj をつくる。

本当は url と name を取り出したいのだが、力尽きた。 ここでは url だけを処理します。

(defun get-link-obj (xs index)
  (elt xs index))
  
(get-link-obj (item-seq "link.json") 0) ; => ((name . "google") (url . "https://www.google.co.jp/"))
(get-link-obj (item-seq "link.json") 1) ; => ((name . "apple") (url . "https://www.apple.com/"))

あとは car, cdr を駆使すれば次のようにして url 情報を取得できる。

(cdr (car (cdr (get-link-obj (item-seq "link.json") 0)))) ; => "https://www.google.co.jp/"
(cdr (car (cdr (get-link-obj (item-seq "link.json") 1)))) ; => "https://www.apple.com/"

取り出した url を使ってハイパーリンクボタンをつくる

どうやってつくるんだっけ? これを参考に make-hyper-link-button を書く。

(defun my-open-url (u)
  (shell-command (concat "xdg-open " u)))

(defun make-hyper-link-button (url)
  (with-current-buffer (get-buffer-create "*urllist*")
    (insert-text-button
     url
     'action
     (lambda (b)
       ( my-open-url (button-label b)) ))
    (insert "\n")
    ))

今回は 複数のボタンを配置するため insert-text-button したあとにさらに (insert "\n") して直後に改行を入れています。

あとは先ほどの関数と組み合わせるだけです。

dolist を使って 0 と 1 の index を持つハイパーリンク情報を link.json から取得し、 そこから url 情報を取り出したら make-hyper-link-button 関数に渡しています。

(dolist (index '(0 1))
  (make-hyper-link-button (cdr (car (cdr (get-link-obj (item-seq "link.json") index))))))

今は試しで '(0 1) と書いていますが、 その部分は (reverse (make-number-list (item-len "link.json"))) に置きかえます。

(dolist (index (reverse (make-number-list (item-len "link.json"))))
  (make-hyper-link-button (cdr (car (cdr (get-link-obj (item-seq "link.json") index))))))

できました。

まとめ

(require 'json)

(defun make-number-list (i)
  (cond
   ((eq i 0) nil)
   (t (cons (1- i) (make-number-list (1- i))))))

(defun item-seq (filename)
  (cdr
   (assq 'list (json-read-file filename))))

(defun item-len (filename)
  (length (item-seq filename)))

(defun get-link-obj (xs index)
  (elt xs index))

(defun my-open-url (u)
  (shell-command (concat "xdg-open " u)))

(defun make-hyper-link-button (url)
  (with-current-buffer (get-buffer-create "*urllist*")
    (insert-text-button
     url
     'action
     (lambda (b)
       ( my-open-url (button-label b)) ))
    (insert "\n")
    ))

(defun main (filename)
  (dolist (index (reverse (make-number-list (item-len filename))))
    (make-hyper-link-button (cdr (car (cdr (get-link-obj (item-seq filename) index)))))))

(main "link.json")

全体をリージョンに指定して、 M-x eval-region する。 (M-x eval-current-buffer がなぜか使えなかった。)

そして、**urllist** バッファに移動すると、 link.json に記載されているURLを使ったハイパーリンクボタンが作成されている。

make text buttons

以上です。