Yearly Archives: 2020

2020-06-03 ,

org-modeで幅の広い表を折り返しモード時でも折り返さず水平スクロールさせる

昨日の続き。

org-mode ではデフォルトで truncate-lines がtとなっています(org-startup-truncated で変更できます) 。つまり、折り返さずに表示するのがデフォルトです。これはおそらく折り返すと幅が広い表(テーブル)が正しく表示されないためではないでしょうか。

しかしそれだと普通のテキストも折り返さずに表示されて大変読みづらくなってしまいます。fill-paragraph 等を使って自分で適当な位置に改行を入れていくことになるのですが多少問題もあります。私はよくHTMLでエクスポートするのですが、文字と文字との間にわずかに空白が入ってしまうことがあります。確認してみるとそれは文章の途中にある改行のせいだったりします。なので私は必要に応じて M-x toggle-truncate-lines で切り替えていたのですが、やっぱり面倒くさい。

というわけで、先日作った部分的に水平スクロールできるようにするコードをorg-modeの表部分に適用してみました。

ソースコードはGitHubにあります。

misohena/phscroll: Enable partial horizontal scroll in Emacs

使い方は (load "org-phscroll.el") するだけ。スクロールは C-x < や C-x > でも可能です。

それと truncate-lines はnilにしないと意味がありません。私は org-startup-truncated をnilに変更してしまいました。

表全体を消した場合自動的にスクロールエリアも削除されますが、表が表で無くなった場合(先頭の|を消すなどして)、スクロールエリアが残ってしまいます。 M-x phscroll-delete-at でポイントがある位置のスクロールエリアを削除できます。

org-phscroll.elの使用例
図1: org-phscroll.elの使用例

実装にあたっては、例によってfont-lockのタイミングで表を探して水平スクロール領域を適用しています。他のタイミングでも良いのですがfont-lock処理の直後、特にorg-fontify-meta-lines-and-blocksの直後ならばfaceがorg-tableかどうかで表かどうかを判別できるのでそこにしました。単純に行頭に|があるかどうかで判別すると、各種ブロック(#+BEGIN_~#+END_)の中にあるものまで拾ってしまうので。

phscroll.el自体の問題も色々修正したのでそこそこ使えるようになってきましたが、大きな表は少し重いかもしれません。できるだけウィンドウ内だけ更新するように作ってありますが、折りたたんだ場所に沢山の表が入っている場所がウィンドウ内に入ると、それらの表を全て一度に整形することになるので時間がかかります。

今のところオーバーレイやテキストプロパティを一文字毎にチェックしているので、それをある程度まとめてやれば少し速くなるかもしれません。面倒くさいですけど。

2020-06-02

Emacsで部分的な水平スクロールを実現する

Emacsでは toggle-truncate-lines を使えばバッファ全体の水平スクロールが可能です。行の折り返しをやめて左右端で切り詰めて表示できるので、幅の長い整形済みテキストを折り返さずに見たい場合には有効です。

しかしこのモードを使うと当然ながら文書全体で折り返しが無くなってしまいます。文書内の通常の文章は折り返して表示し、整形済みの部分だけ水平スクロールを適用する、そんな器用な機能は見当たりませんでした。

一方Emacsにはテキストプロパティオーバーレイという機能でテキストの一部を非表示にできます。となると、行の先頭部分を非表示にすることで擬似的に部分的な水平スクロールを実現できないでしょうか。

というわけで実装したのがこちら:

misohena/phscroll: Enable partial horizontal scroll in Emacs

使い方は

  1. elispをロードする 例:(require 'phscroll)
  2. スクロールさせたいリージョンで囲む
  3. M-x phscroll-region

これで選択した部分だけ水平スクロール出来るようになります。

ただし、truncate-lines が t (折り返さず表示するモード)のときは加工せずそのまま表示します。Emacs標準の水平スクロール機能を使用してください。 M-x toggle-truncate-lines で切り替えると、それを検出して表示を更新します。

既知の問題:

  • 遅い : すでに適用されているテキストプロパティやオーバーレイを調べて各行の幅を計算する部分やオーバーレイを適用して一部を非表示にする部分がかなり遅いみたいです。頑張って必要な部分だけ(windowで表示する範囲内や編集した部分だけ)更新するようにしてみましたが、それでもまだ遅いみたいです。
  • 複数ウィンドウ対応 : 複数のウィンドウで水平スクロール部分を見たときに表示が乱れることがあります。特にフレームを左右に分割したときに左右の幅が異なる場合は選択している方のウィンドウ幅でレイアウト計算するので、他のウィンドウではウィンドウの幅を超えてしまうことがあります。
  • display, invisibleプロパティの対応形式: 既にdisplayプロパティinvisibleプロパティで加工されているテキストも水平スクロールの対象にできますが、正しく幅を計算できる形式は限られています。単純に文字列に置き換えている場合やただ消している場合、それとrelative-spaceは対応しましたがそれ以外は未対応です。
phscroll.el使用例
図1: phscroll.el使用例

これだけだといちいちスクロールさせるエリアを手動で設定しなければならず面倒なので、次回は自動的にエリアを設定する方法について書こうと思います。

ふと思ったのですが、これとは逆に通常部分のみ折り返すというのは可能なのでしょうか。truncate-lines tで通常部分だけオーバーレイで折り返す。オーバーレイで改行ってできるのかな。でも変更があるたびに改行位置を維持するのは大変そうですね。

2020-05-26 ,

org-modeのHTMLエクスポート時にimgタグのalt属性をcaptionからつける

org-mode文書をHTMLでエクスポートしたとき、imgタグのalt=属性(代替テキスト)はデフォルトで画像のファイル名になります。

<img src="./suica.jpg" alt="suica.jpg" />

altがファイル名というのはなんとも気が利かない感じがします。 altを任意の文字列にする方法は Images and XHTML export に書かれていて、画像リンクの前に #+ATTR_HTML: :alt 文字列 と書きます。

#+ATTR_HTML: :alt おいしそうなすいか
[[file:./suica.jpg]]

一方画像にはキャプションを付けることが可能で、画像リンクの前に #+CAPTION: 文字列 と書くと画像の下に「図1:文字列」のようなキャプションがつきます。

#+CAPTION: おいしそうなすいか
#+ATTR_HTML: :alt おいしそうなすいか
[[file:./suica.jpg]]

キャプションには大抵は画像の内容を要約した文が書かれますから、altもキャプションと同じで良いのではないでしょうか? キャプションがあるのだからそもそも代替テキストは不要なのではないか(画像が表示されないときはキャプションを読めば良いのではないか)という気がしなくもありませんが、一応つけるとしたらキャプションと同じ内容で十分なことは多いでしょう。

#+CAPTION:#+ATTR_HTML: の両方を書くのは面倒ですしミスも発生します。私は先に #+CAPTION: だけ使って文書を書き上げてから正規表現置換で #+ATTR_HTML: を生成していたのですが、生成後に #+CAPTION: を修正したときに #+ATTR_HTML: を修正し忘れることが何度もありました。

というわけでエクスポート時にaltを #+CAPTION: から決めるようにするのが次のコードです。

(defun org-altcaption--get-caption (paragraph info)
  "段落に設定されているキャプション文字列を取得する。

org-html-paragraph関数内の「;; Standalone image.」の部分より。"
  (if paragraph
      (let ((raw (org-export-data (org-export-get-caption paragraph) info)))
        (if (org-string-nw-p raw) raw nil))))

(defun org-altcaption--get-link-parent (link info)
"linkの親要素を取得する。ただし、linkが最初のリンクでない場合はnil。

org-html-link関数内より。"
  ;; Extract caption from parent's paragraph.  HACK: Only
  ;; do this for the first link in parent (inner image link
  ;; for inline images).  This is needed as long as
  ;; attributes cannot be set on a per link basis.
  (let* ((parent (org-export-get-parent-element link))
         (link (let ((container (org-export-get-parent link)))
                 (if (and (eq 'link (org-element-type container))
                          (org-html-inline-image-p link info))
                     container
                   link))))
    (and (eq link (org-element-map parent 'link #'identity info t))
         parent)))

(defvar org-altcaption--link nil)

(defun org-altcaption--org-html-link (old-func link desc info)
  "org-html-linkに対するaround advice"
  ;; Pass link to org-altcaption--org-html--format-image function
  (let ((org-altcaption--link link))
    (funcall old-func link desc info)))

(defun org-altcaption--org-html--format-image (old-func source attributes info)
  "org-html--format-imageに対するaround advice"
  ;; Add alt attribute if link has caption
  (if (and org-altcaption--link (null (plist-get attributes :alt)))
      (let ((caption (org-altcaption--get-caption (org-altcaption--get-link-parent org-altcaption--link info) info)))
        (when caption
          (setq attributes (plist-put attributes :alt caption))
          ;;(message "caption=%s" caption)
          )))
  ;; Call original function
  (funcall old-func source attributes info))


(defun org-altcaption-activate ()
  (interactive)
  (advice-add #'org-html-link :around #'org-altcaption--org-html-link)
  (advice-add #'org-html--format-image :around #'org-altcaption--org-html--format-image))

(defun org-altcaption-deactivate ()
  (interactive)
  (advice-remove #'org-html-link #'org-altcaption--org-html-link)
  (advice-remove #'org-html--format-image #'org-altcaption--org-html--format-image))

org-altcaption-activate で有効化、 org-altcaption-deactivate で無効化します。adviceを使って既存の関数をフックしているのでorgのバージョンが上がると動かなくなるかもしれません(9.3.6で確認)。

こんなこと簡単に実現できるだろう、と思いきや結構難しいんですこれが。Orgの文法のうまくできていないところに片足を突っ込んでいる感じです。

Orgでは個別のリンク一つ一つにプロパティを指定することが困難です。例えば段落中のリンクに target="_blank" 属性を設定したい(リンク先を別のウィンドウで開きたい)場合次のように書くのですが

#+ATTR_HTML: :target _blank
山といえば [[https://www.pref.yamanashi.jp/][山梨県]] と [[http://www.pref.shizuoka.jp/index.html][静岡県]] 。富士山にまたがるこの二つの県は……

この場合最初のリンク(山梨県)にしか target="_blank" は設定されません。次のように書くと当然二つの段落に分かれてしまいます。

#+ATTR_HTML: :target _blank
山といえば [[https://www.pref.yamanashi.jp/][山梨県]] と
#+ATTR_HTML: :target _blank
[[http://www.pref.shizuoka.jp/index.html][静岡県]] 。富士山にまたがるこの二つの県は……

属性はリンクに設定されるのではなく段落に対して設定されます。リンクをエクスポートするときは、リンクを包む親要素(段落)に対する属性指定を調べてそれを適用します。属性はリンクについているのではなく親要素についています。これはキャプションも同じで、親要素を調べなければキャプション文字列は分かりません。さらに属性が適用されるのは段落中の最初のリンクのみ。二つ目以降のリンクには適用されない仕様です。キャプションの場合はスタンドアロンな画像(行内にある画像ではなくブロックを形成する画像)にしかキャプションは付けられない仕様なのであまり気にする必要は無いのかもしれませんが、いちおう二つ目の画像リンクには適用しない方がいいでしょう。

ということをするのが org-altcaption--get-link-parent 関数になります。このロジックは org-html-link 関数(ox-html.el)の中にあるものを拝借しました。

リンクそのものにプロパティを指定する文法の提案をどこかで見かけたような気がするのですがうろ覚え。

おいしそうなすいか
図1: おいしそうなすいか