Author Archives: AKIYAMA

2020-12-12 ,

org-modeで文字をエスケープする方法

org-modeで文字が意図しない解釈をされてしまったことはないでしょうか。「_が下付きになっちゃった!」「テーブルの中に|を書いたらフィールドが分かれてしまった!」等々。

起きるたびに調べて場当たり的に対処してきたのですが、今日はorg-modeにおける文字のエスケープ、意図したとおりに解釈させる方法についてまとめてみました。(Org9.3.7時点)

Org entities

最も筋の良い方法はentityを使うことです。

entityというのはLaTeX風の記法で記号を文書中に挿入できる仕組みです。HTMLで言うところの実体参照のようなものです。

参考: Special Symbols (The Org Manual)

例えば_の場合、 \under{} と書くことで _ に置換されます。 {} の部分は無くても良いのですが、あった方が区切りが明確になって助かる場合があります。例えば a_b が下付きで表示されて困る場合は a\under{}b と書きます。org-modeではこのような書き方が出来る文字が多数定義されています。

M-x org-entities-help を実行するとその書き方の一覧が見られます。実際にやってみると多数の記号が表示されます。

これだとよく引っかかる文字が分かりづらいので、org-modeで実際に特殊な意味で使われている記号をピックアップし、それに対応するentityを表にしてみました。

記号 entity 使われているところ(マニュアルの該当箇所)
[   timestamp, link, image, foot note, priority, subtask fraction, checkbox
< lt timestamp, link target, column group, column width
> gt column group
| vert table
#   comment, block, meta, table row
\   latex, line break, entity
.   list
$ dollar latex, table row
: colon drawers, property, fixed-width area, tag, list
-   list, horizontal line, shy hyphen, dashes, dots
^ asciicirc superscript, table row
@   export snippets
{   macro
* star bold, headline, list, table row
/ slash italic, table row
_ under underline, subscript, table row
+ plus strike through, list, table.el
= equal verbatim, table formula
~ tilde code
!   table row
%   agenda
,   block

こうしてみると対応する書き方がない記号が目立ちます。特に \ (バックスラッシュ。円記号に見えていますか?)に対応する書き方がないのが致命的ですね。将来的に増えてくれれば良いのですが。

ユーザー定義のentityを加えることも出来るようですが文書の交換に支障が出るケースもあります。例えばGithubにREADME.orgとして上げる場合は org-entities-user に何かを加えたところで解決にはならないでしょう。

HTMLの数値文字参照のように任意のコードポイントを指定できるような機能があれば良いのですが。

何かで包む

~(code) や =(verbatim) で囲むと文字の解釈を抑制できる場合があります。

~\under{}~ と書くことで最初の\はそのまま表示されます。

codeやverbatimの意味的に使いたくないケースはあるでしょうし、中に入る文字次第では抑制できません。

他にもソースブロックやexampleブロックで文字の解釈を抑制できる場合があります。

#+begin_example
exampleの中に \under と書くと最初の\はそのまま表示されます。
#+end_example

しかし代わりに別の文字(行頭の# , *)が特殊な解釈をされることになります。

ちなみにこれらのブロックの中では、行頭に,を入れることで行頭の#や*をエスケープできます。

#+begin_src org
,#+begin_example
exampleの中に \under と書くと最初の\はそのまま表示されます。
,#+end_example
#+end_src

fixed-widthエリアは行指向なので比較的問題が起きにくい気がします。

: \underと書けるし
: :コロンも先頭に書ける

設定で回避する

一部の文字は設定で解釈を変更できます。

オプション指定 意味
#+OPTIONS: ^:nil a^b a_b を変換しない
#+OPTIONS: ^:{} a^{b} a_{b} の書き方のみ許容
#+OPTIONS: *:nil *word* _word_ /word/ +word+ を変換しない
#+OPTIONS: -:nil \- -- --- ... を変換しない
#+OPTIONS: tex:verbatim \begin $ 等のTeX表記を変換しない

参考: Export Settings (The Org Manual)

Export snippetsを使う

Export snippets は #+begin_export のインライン版です。 @@NAME:VALUE@@ という書き方で、NAMEで指定したバックエンドでエクスポートする内容をVALUEで指定できます。

参考:

文字をエスケープする良い方法はないかと探していたところ、Stack Exchangeにこの仕組みを使ったやり方が載っていました。

参考:

例えば次のように書くことで A[[B]]C になります(LaTeX, HTML両方でそうなるはずですがLaTeXは未確認)。

A@@latex:\char91\char91@@@@html:&#91;&#91;@@B@@latex:\char93\char93@@@@html:&#93;&#93;@@C

これだとさすがにつらいので、マクロを併用して次のようにします。

#+MACRO: BO @@latex:\char91@@@@html:&#91;@@
#+MACRO: BC @@latex:\char93@@@@html:&#93;@@

A{{{BO}}}{{{BO}}}B{{{BC}}}{{{BC}}}C

もちろんリンクの中でも使えます。

[​[https://google.com/search?q=%5d][Search result of {{{BC}}}]]

次のようなマクロを定義して直接コードポイントを指定することも可能です。

#+MACRO: char @@html:&#$1;@@
{{{char(169)}}}Misohena Laboratories

zero width spaceを挟む

見えない空白を入れることで無理矢理解釈を変更できます。

参考: Escape Character (The Org Manual)

見えない空白を挿入するには、 C-x 8 RET zero width space と入力します。

例えば[と[の間に見えない空白を入れることでリンクと解釈されることを防げます。

検索に支障を来すこともあり得ますし正直あまり良い方法とは思えませんが、強力な方法ではあると思います。

Export snippetsを使う(2)

状況にも依りますが、zero width spaceの代わりにexport snippetsで空の文字列を挿入するという手もあります。

[@@ascii:@@[https://example.com/]] ←ブラケットリンクにならない!

バックエンドにはasciiを指定していますが、これだけでどのバックエンドでも効果があります。何なら @@-:@@ でも大丈夫なようです。

最初はマクロで空文字列を挿入しようと思ったのですが、残念ながら効果が無くリンクと解釈されてしまいました。

#+MACRO: empty-str (eval "")
[{{{empty-str}}}[https://example.com/]] ←ブラケットリンクになる

マクロを使うなら、マクロでexport snippetsを生成するのが良いでしょう。

#+MACRO: empty @@ascii:@@
[{{{empty}}}[https://example.com/]] ←ブラケットリンクにならない!

[{{{empty}}}[https{{{empty}}}://example.com/]] ←リンクにならない!

最後に

軽量マークアップ言語では限られた記号を何に使用するかが設計上の最大のポイントになります。そして何かの意味に使用した記号を元の意味で使うための逃げ道をどう用意するかも工夫のしどころとなります。

org-modeの文法はこの辺りがうまく設計されていないなと前々から感じていました。今回調べてみてもまだ釈然としない部分が残っています。文脈によってエスケープする必要のある文字やエスケープする方法は変わってくるので、全てを網羅するにはもっとちゃんとした調べ方をしないとダメそうです。

2020-12-08

Emacs Lispでポップアップメニューを出す方法(x-popup-menuの使用例)

EmacsでGUIプログラミングをしていると何かをクリックしたときにポップアップメニューを出したくなることがあります。メニューボタンを押したとき、対象を右クリックしてコンテキストメニューを出したいとき、といった具合です。

そんなときに使えるのが x-popup-menu という関数です。

x-popup-menu関数の概要

29.17 Pop-Up Menus - Emacs Lisp Manual

Function: (x-popup-menu position menu )

position にはメニューの表示位置を指定します。具体的な位置を指定するリスト ((XOFFSET YOFFSET) WINDOW-OR-FRAME) の他、マウスイベントやtを指定出来ます。tの場合現在のマウス位置に表示します。nilの時は実際には表示せず何かを事前計算するらしいのですがよく分かりません(やってみると単にnilが返ってきます)。クリックに応じてポップメニューを出したい場合は、last-input-event(interactive "e")で取得したイベント、または t を指定すると良さそうです。プルダウンメニューのように位置が決まっているものは具体的な位置を計算した上指定する必要があるのでしょう。

menu にはキーマップ(またはキーマップのリスト)か、独自の形式のリスト(ペインのリスト)を指定できます。

x-popup-menuにペインのリストを指定する

menu にペインのリストを指定するのが一番簡単ですが出来ることは限られます。

例えば次のコードを評価すると……

(x-popup-menu t '("Menu Title"
                  ("Pane Title1"
                   ("Item1-1" . 11)
                   ("Item1-2" . 12))
                  ("Pane Title2"
                   ("Item2-1" . 21)
                   ("Item2-2" . 22))))

次のようなポップアップメニューが表示されます。

x-popup-menuに複数のペインを含むリストを指定した場合の表示
図1: x-popup-menuに複数のペインを含むリストを指定した場合の表示

項目を選択すると項目の値がそのまま返ってきます。上の例では「Item1-1」を選択すると11が返ってきます。選択しなかった場合はC-gを押したときのようにQuitシグナルが発生します。

複数のペインを持ちたくない場合はペインを一つだけ指定することになりますが、その場合ペインのタイトルは表示されません。次の例では「Pane Title1」は表示されません。

(x-popup-menu t '("Menu Title"
                  ("Pane Title1"
                   ("Item1-1" . 11)
                   ("Item1-2" . 12))))
x-popup-menuに一つのペインを含むリストを指定した場合の表示
図2: x-popup-menuに一つのペインを含むリストを指定した場合の表示

x-popup-menuにキーマップを指定する

x-popup-menuの機能を完全に利用するにはキーマップを指定する必要があります。

例えば次のコードを評価すると……

(defun test-item-1 () (interactive) (message "Menu Item 1"))
(defun test-item-2 () (interactive) (message "Menu Item 2"))
(setq test-item-toggle-selected t)
(defun test-item-toggle () (interactive) (setq test-item-toggle-selected (not test-item-toggle-selected)))
(setq test-radio-choice 1)
(defun test-item-radio-1 () (interactive) (setq test-radio-choice 1))
(defun test-item-radio-2 () (interactive) (setq test-radio-choice 2))
(defun test-submenu-1-2 () (interactive) (message "hello"))

(let* ((menu '(keymap "Menu Title"
                      ;;イベント型 拡張メニュー 項目名 コマンド プロパティ...
                      (test-item-1 menu-item "Item1" test-item-1)
                      (test-item-2 menu-item "Item2" test-item-2 :keys "HogeHogeKey")
                      (3 menu-item "Item3" test-item-3 :enable nil)
                      (4 menu-item "Item4" test-item-4 :visible nil)
                      (5 menu-item (concat "Item" "5") test-item-3 :enable nil)
                      (test-item-toggle menu-item "ItemToggle" test-item-5 :button (:toggle . test-item-toggle-selected))
                      (separator-1 menu-item "--")
                      (test-item-radio-1 menu-item "ItemRadio1" test-item-radio-1 :button (:radio . (= test-radio-choice 1)))
                      (test-item-radio-2 menu-item "ItemRadio2" test-item-radio-2 :button (:radio . (= test-radio-choice 2)))
                      (separator-2 menu-item "--")
                      (text-1 menu-item "選択できない項目")
                      (separator-3 menu-item "--")
                      (10 menu-item "About Emacs" about-emacs) ;; :keysを書かなくてもglobal-mapから割り出してくれる
                      (submenu-1 menu-item "Submenu" (keymap "Submenu Title"
                                                             (submenu-1-1 menu-item "SubItem1" test-subitem-1)
                                                             (test-submenu-1-2 menu-item "SubItem2" test-subitem-2)))))
       (result (x-popup-menu t menu)))
  ;; 結果の最後の要素が関数なら呼び出す
  (if (and (symbolp (car (last result))) (fboundp (car (last result))))
      (funcall (car (last result)))
    ;; 関数でなければそのまま表示してみる
    (message "result=%s" result)))

次のようなポップアップメニューが表示されます。

x-popup-menuにkeymapを指定した場合の表示
図3: x-popup-menuにkeymapを指定した場合の表示

このように様々な事が出来るようになっています。

詳しくはマニュアルを参照のこと。

少しだけ補足。

:keys は通常書く必要はありません。上の例では HogeHogeKey なんて存在しないキーを表示させてましたが、あくまで何でも書けるか試しただけです。特筆すべきは About Emacs のところ。何も指定していなくても C-h C-a と表示されています。コマンド名からキーシーケンスを自動的に割り出して表示してくれます。グローバルマップやローカルマップだけでなく、(ポイントがそこにあれば)テキストプロパティやオーバーレイのkeymapプロパティまで考慮してくれます。

x-popup-menuは選択結果を返すだけでコマンドの呼び出し等は行いません。

easymenu.elというのが標準で入っていてもう少し簡単にメニュー用のキーマップが定義できるのですが、easy-menu-defineでキーマップを作ると項目の一番最初の要素は項目名の文字列をシンボルにしたものになるのでx-popup-menuで選択するとその人間向けな感じのシンボルが返ってきてしまいます。そこから実際にコマンドを呼び出す方法が今ひとつ分かりません。おそらく返ってきたシンボルを使ってキーマップから関数を割り出して呼び出せということなんだと思いますが、それなら最初から関数シンボルが返ってきた方が楽そうですよね……。そうでもないのかな? キーマップの意味論的に最初の要素はキー、というかイベント(シンボルはその名前のファンクションキーを押したというイベントという意味)なのだから、ルックアップを挟むべきってことなのでしょうか。それならば、こんな感じ?

(easy-menu-define test-menu nil
  "This is a Test Menu."
  '("Test Menu Title"
     ["Forward word desu!" forward-word] ;;x-popup-menuの結果は '(Forward\ word\ desu!)
     ["Backward word desu!" backward-word] ;;結果は '(Backward\ word\ desu!)
     ["Fun" (lambda () (interactive) (message-box "Fun!"))] ;;結果は '(Fun)
     ("Sub"
      ["FunFun" (lambda () (interactive) (message-box "FunFun!"))] ;;結果は'(Sub FunFun)
     )))

(let* ((events (x-popup-menu t test-menu))
       (cmd (lookup-key test-menu (apply 'vector events))))
  (call-interactively cmd))

例えば Sub → FunFun と選ぶとx-popup-menuは (Sub FunFun) というリストを返してきます。これはSubキーを押してからFunFunキーを押すという架空のキーシーケンスを意味します。そのキーシーケンスを元にlookup-keyでキーマップtest-menuからコマンドを割り出しています。

lookup-keyはリストを受け付けてくれなかったのでvectorに変換してから渡しています。これで良いのかな……。

他にも探したら popup-menu なんていう関数がmenu-bar.elにありました。内部でx-popup-menuを使っているようですがちゃんとコマンド呼び出しまでやってくれます。Emacs Lispのマニュアルに書いてない関数だけどこれでいいっぽい?

(popup-menu test-menu)
2020-11-28

Emacs Lispで画像を表示する方法(Emacs 27.1で確認)

参考

画像の表示方法については、Emacs LispマニュアルのImagesの所で説明されている。

手順

Emacs Lispで画像を表示するには次の二つのステップを踏む。

  1. Image Descriptorを作成する
  2. Image Descriptorをオーバーレイまたはテキストプロパティに設定する

まず第一にImage Descriptorを作成する。Image Descriptorとは画像をどのように表示するかを指定するリストで41.17.2 Image Descriptorsに書かれている形式で記述する。自分で組み立てても良いが41.17.8 Defining Imagesに書かれている create-image 等の関数を使うこともできる。SVG画像を作る場合は41.17.6 SVG Imagesに書かれている関数を使うこともできる。

次に作成したImage Descriptorをオーバーレイ(41.9 Overlays)またはテキストプロパティ(33.19 Text Properties)の、displayプロパティ(41.16 The display Property)に設定する。それによってはじめて実際に画像が表示される。自分でオーバーレイやテキストプロパティを設定しても良いが41.17.9 Showing Imagesに書かれている insert-image 等の関数を使うこともできる。この関数はバッファ内に適当な(" "等の)文字列を挿入してそのテキストプロパティのdisplayプロパティにImage Descriptorを設定する。

実例

それではやってみよう。

まず適当な画像ファイルを用意して、それを読み込むようなImage Descriptorを作成する。次の式を評価するとImage Descriptorが返ってくる。

(create-image (expand-file-name "~/tmp/karasawa.jpg"))

結果は次のようなリストになる。 :scale 1 の部分は高DPI環境では異なるかもしれない(高DPI環境でscaleを大きくする機能がcreate-imageに備わっている)。

(image :type jpeg :file "/home/hoge/tmp/karasawa.jpg" :scale 1)

この時点ではまだ画像は読み込まれない。

次のようにすると実際に画像が表示される。

(insert-image (create-image (expand-file-name "~/tmp/karasawa.jpg")))

(2021-10-11追記: create-imageに引き渡すファイル名は絶対パスに変換する必要がある。相対パスを指定するとdata-directoryやx-bitmap-file-pathの中から画像を探すことになり、設定によっては画像が読み込まれない)

画像ファイルを表示
図1: 画像ファイルを表示

関数insert-imageは適当な文字列(" ")をバッファに挿入し、その文字列がある範囲のdisplayテキストプロパティにImage Descriptorを設定する。

なので、次のようにしても同じ結果が得られる。

(progn
  (insert " ")
  (put-text-property (1- (point)) (point) 'display (create-image (expand-file-name "~/tmp/karasawa.jpg"))))

適当な画像を用意するのが面倒ならばSVG画像を作る方法もある。次のコードを評価するとSVGデータを含んだImage Descriptorが返ってくる。

(require 'svg)
(let ((svg (svg-create 400 300)))
  (svg-rectangle svg 0 0 400 300 :fill "#69f")
  (svg-circle svg 200 150 100 :fill "#eee")
  (svg-image svg))
(image :type svg :data "<svg width=\"400\" height=\"300\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"> <rect width=\"400\" height=\"300\" x=\"0\" y=\"0\" fill=\"#69f\"></rect> <circle cx=\"200\" cy=\"150\" r=\"100\" fill=\"#eee\"></circle></svg>" :scale 1)

実際に表示させてみる。

(require 'svg)
(insert-image
 (let ((svg (svg-create 400 300)))
   (svg-rectangle svg 0 0 400 300 :fill "#69f")
   (svg-circle svg 200 150 100 :fill "#eee")
   (svg-image svg)))
SVG画像を表示した結果
図2: SVG画像を表示した結果

画像の変換

表示する際に簡単な画像処理も可能。

(insert-image (create-image (expand-file-name "~/tmp/karasawa.jpg") 'jpeg nil :width 300 :rotation 90 :relief -10))
縮小、回転、レリーフ
図3: 縮小、回転、レリーフ
(insert-image (create-image (expand-file-name "~/tmp/karasawa.jpg") 'jpeg nil :width 400 :conversion 'disabled))
disabled効果
図4: disabled効果

これらの変形・効果はSVG画像にも適用できる。おそらくSVGをレンダリングした後のビットマップに適用している。

この他にも41.17.2 Image Descriptorsにはいくつかのプロパティが説明されている。また、最終的な表示のされ方はsliceなど他のdisplayプロパティの影響も受ける。

GIFアニメーションの表示も可能。(see:41.17.10 Multi-Frame Images)

画像のクリック

画像がクリックされたときに何か処理を行うこともできる。

(require 'svg)

(let (;;適当な文字列("A")を追加しその範囲をstart,endとする。
      (start (point))
      (end (progn (insert "A") (point))))

  ;; displayプロパティにSVG画像を設定する。
  (put-text-property
   start end 'display
   (let ((svg (svg-create 200 40)))
     (svg-rectangle svg 0 0 200 40 :rx 10 :ry 10 :stroke "#888" :fill "#fff")
     (svg-text svg "Button" :x 100 :y 30 :font-size 35 :text-anchor "middle")
     (svg-image svg)))

  ;; keymapプロパティにマウスクリックの際に呼ばれる関数を設定する。
  (put-text-property
   start end 'keymap
   (let ((km (make-sparse-keymap)))
     (define-key km [mouse-1]
       (lambda (event)
         (interactive "e")
         ;;(message "(x . y)=%s" (posn-object-x-y (event-start event)))
         (message "clicked event=%s" event)))
     km))

  ;; pointerプロパティをhandにすることでマウスカーソルの形を変える。
  (put-text-property
   start end 'pointer 'hand))
画像を表示してクリックした結果
図5: 画像を表示してクリックした結果

マウスの左クリックは[mouse-1]をキーマップに設定することで検出できる。テキストプロパティのkeymapプロパティを設定することで、画像がある場所にだけ効果があるキーマップを設定できる。

クリックした位置などイベントの詳細情報はInteractive Codeの"e"か、 last-input-event で受け取れる。

イベントの詳細情報はリストの形で記録されている。(22.7.4 Click Events)

そこから画像内でのクリックされた位置を取り出すには次のようにする。(22.7.15 Accessing Mouse Events)

(let ((xy (posn-object-x-y (event-start event))))
  (format "x=%s y=%s" (car xy) (cdr xy)))

画像の一部をクリック

画像内の一部がクリックされたことを判定したい場合イベントの座標から割り出しても良いがImage Descriptor:mapプロパティを使うこともできる。

(require 'svg)

(let (;;適当な文字列("A")を追加しその範囲をstart,endとする。
      (start (point))
      (end (progn (insert "A") (point))))

  ;; displayプロパティにSVG画像を設定する。
  (put-text-property
   start end 'display
   (let ((svg (svg-create 400 300)))
     (svg-circle svg 100 150 30 :fill "red")
     (svg-circle svg 200 150 30 :fill "blue")
     (svg-circle svg 300 150 30 :fill "green")
     ;; SVG画像を作る。同時にImage Descriptorに:mapプロパティを付加する。
     (svg-image svg :map '(((circle . ((100 . 150) . 30)) red-area (pointer hand help-echo "red"))
                           ((circle . ((200 . 150) . 30)) blue-area (pointer hand help-echo "blue"))
                           ((circle . ((300 . 150) . 30)) green-area (pointer hand help-echo "green"))))))

  ;; keymapプロパティにマウスクリックの際に呼ばれる関数を設定する。
  (put-text-property
   start end 'keymap
   (let ((km (make-sparse-keymap)))
     (define-key km [red-area mouse-1] (lambda () (interactive) (message "red!")))
     (define-key km [blue-area mouse-1] (lambda () (interactive) (message "blue!")))
     (define-key km [green-area mouse-1] (lambda () (interactive) (message "green!")))
     km)))
mapプロパティを伴う画像をクリックした結果
図6: mapプロパティを伴う画像をクリックした結果

矩形、円、任意の多角形で当たり判定の領域を設定できる。

画像の自動スケーリング

関数 create-image には高解像度モニターに備えて画像を自動的に拡大する機能がある。

拡大率は変数 image-scaling-factor で設定できる。デフォルトは 'auto になっており、ウィンドウサイズとフォントサイズの比率によって計算する。

具体的な計算は関数 image-compute-scaling-factor で行われている。まず、ウィンドウのピクセル幅を横方向の文字数(桁数)で割った値を計算する(つまり、平均文字幅ピクセル数)。その値が10以上の時、10からどのくらい大きくなったかの比率が画像の拡大率となる。そうして計算された値がcreate-imageが返すImage Descriptorの:scaleプロパティに付加される。

create-imageのPROPS引数に :scale プロパティが指定されている場合はこの処理は行われない。

注意すべき点は、この拡大率はマウスイベントの座標には一切影響を及ぼさないということ。イベントから得られる座標はこの拡大率の影響を受けないフレーム上の純粋な座標となる。また、:mapプロパティもこの拡大率の影響を受けて調整されることは ない 。従ってこの自動スケーリングが機能してしまうと、座標や領域の位置が画像と食い違ってしまう。これが問題になる場合、 (image-compute-scaling-factor image-scaling-factor) の計算値を使ってマウス座標や:mapプロパティを変換する必要がある。