2024-09-17 ,

org-modeでエクスポートしたHTMLから図番を消す方法

と言うわけで昨日の続きなのですが、captionから図番を消す方法について。

昨日紹介した "#+CUSTOM_TRANSLATION: "Figure %d:" "" という指定で図番を消すのは、指定方法が間接的すぎてイマイチだなという話でした。もっと直接的に「図番を消せ」と指定したい所です。

なので今度は #+OPTIONS: fignum:nil という指定で消せるようにしてみます。

#+OPTIONS: fignum:nil

これは何かの写真です。

#+CAPTION: 何かの写真
[[file:example-1.jpg]]

これは別の写真です。

#+CAPTION: 別の写真
[[file:example-2.jpg]]

といってもやることは結局翻訳を書き替えるだけです。

そもそも図番を消すのになぜ翻訳を書き替えているのかというと、ox-html.elでこの図番の文字列を生成している部分が非常に入り組んだ場所にあって、advice等で動作を修正しづらいからという理由があります。

;; ox-html.elより
(defun org-html-paragraph (paragraph contents info)
  "Transcode a PARAGRAPH element from Org to HTML.
CONTENTS is the contents of the paragraph, as a string.  INFO is
the plist used as a communication channel."
  (let* ((parent (org-element-parent paragraph))
         (parent-type (org-element-type parent))
         (style '((footnote-definition " class=\"footpara\"")
                  (org-data " class=\"footpara\"")))
         (attributes (org-html--make-attribute-string
                      (org-export-read-attribute :attr_html paragraph)))
         (extra (or (cadr (assq parent-type style)) "")))
    (cond
     ((and (eq parent-type 'item)
           (not (org-export-get-previous-element paragraph info))
           (let ((followers (org-export-get-next-element paragraph info 2)))
             (and (not (cdr followers))
                  (org-element-type-p (car followers) '(nil plain-list)))))
      ;; First paragraph in an item has no tag if it is alone or
      ;; followed, at most, by a sub-list.
      contents)
     ((org-html-standalone-image-p paragraph info)
      ;; Standalone image.
      (let ((caption
             (let ((raw (org-export-data
                         (org-export-get-caption paragraph) info))
                   (org-html-standalone-image-predicate
                    #'org-html--has-caption-p))
               (if (not (org-string-nw-p raw)) raw
                 (concat "<span class=\"figure-number\">"
                         (format (org-html--translate "Figure %d:" info) ;;★★ここ!!★★ (org-html--translateはorg-export-translateを呼んでるだけです)
                                 (org-export-get-ordinal
                                  (org-element-map paragraph 'link
                                    #'identity info t)
                                  info nil #'org-html-standalone-image-p))
                         " </span>"
                         raw))))
            (label (org-html--reference paragraph info)))
        (org-html--wrap-image contents info caption label)))
     ;; Regular paragraph.
     (t (format "<p%s%s>\n%s</p>"
                (if (org-string-nw-p attributes)
                    (concat " " attributes) "")
                extra contents)))))

paragraphのtranscodeを行う関数の奥深くに埋め込まれてしまっているんですね。もっと関数を細かく分けようよ……などと言っても仕方がありません(Emacs Lispでは良くあることです)。なのでそこから呼び出されているorg-html--translate(org-export-translate)の動作を変えることを考えたわけです。

最初は次のようにしてうまく行くことを確かめました。

(defun my-org-html--translate:no-figure-number (s info)
  (when (and (stringp s) (string= s "Figure %d:"))
    ""))
(advice-add 'org-html--translate :before-until ;;←nilを返したら元の関数を呼ぶ指定
            'my-org-html--translate:no-figure-number)

これだと常に図番が消えてしまうので、何か切り替える方法が必要です。必要かどうかは文書によって変わるのでバッファ内オプションで指定出来るのが望ましいです。

それならいっそのこと翻訳全般をバッファ内オプションでカスタマイズ出来るようにしてはどうか? と思い作成したのが昨日のコードでした。まぁ、ちょっとやり過ぎだったみたいです。

なので昨日のコードをベースにして、オプションの指定方法とその反映部分を修正してみましょう。

まずはエクスポートオプション fignum を追加します。全てのバックエンドに共通するオプションはorg-export-options-alist変数に格納されています。その定義は次のようになっています。

;; ox.elより
(defconst org-export-options-alist
  '((:title "TITLE" nil nil parse)
    (:date "DATE" nil nil parse)
    (:author "AUTHOR" nil user-full-name parse)
    ...
    (:creator "CREATOR" nil org-export-creator-string)
    (:headline-levels nil "H" org-export-headline-levels)
    (:preserve-breaks nil "\\n" org-export-preserve-breaks)
    (:section-numbers nil "num" org-export-with-section-numbers)
    ...

ここに登録しておくと、エクスポート時に自動的にplistの形で全オプションの値を集めてくれます。alistのキー(:title等)は後からplistのキーとして使うキーワードです。2番目の文字列は #+TITLE: のような形のオプションで使います。3番目の文字列は #+OPTIONS: H:5 のような形のオプションで使います。4番目はデフォルト値、5番目は複数のオプションが指定されたときにどうするかを指定します。

今回は #+OPTIONS: fignum:nil のように指定させたいので (:figure-number nil "fignum" t) のような要素をorg-export-options-alistに追加すれば良いでしょう。

(setf (alist-get :figure-number org-export-options-alist)
      '(nil "fignum" t))

こういう時私はいつもsetfとalist-getを使用しています。なんでalist-setみたいなものが無いんでしょうね?

後は翻訳辞書を書き替えるだけです。

今回私はマニュアルのエクスポートプロセスを見てから少し実験した上で、org-export-filter-options-functionsに登録する関数でその書き替え処理を行うことにしました。理由は、この段階になるとオプションが収集し終わっていること、そしてトランスコードが始まる前であることです。加えて、その後一貫してカレントバッファが一時コピーであることも確認しました(ソースコードを追った上での確認はしていないので、そうならないケースがあったらスミマセン)。

(add-to-list 'org-export-filter-options-functions
             'org-figure-number-filter-options)

(defun org-figure-number-filter-options (options _backend &rest _rest)
  (unless (plist-get options :figure-number)
    (org-figure-number-override-dictionary options))
  options)

翻訳辞書(org-export-dictionary)の一時的な変更はローカル変数化することで実現しています。少し実験した限り、エクスポート処理中は元のorg-modeバッファをコピーした一時バッファが常にカレントバッファになっているようだったので、それで十分かなと思いました(間違っていたらスミマセン)。

(defun org-figure-number-override-dictionary (options)
  (setq-local
   org-export-dictionary
   (nconc
    (org-figure-number-make-dictionary
     ;; #+LANGUAGE:の指定がある場合にも対応。
     (or (plist-get options :language) org-export-default-language))
    org-export-dictionary)))

(defun org-figure-number-make-dictionary (lang)
  (list
   (list
    "Figure %d:"
    (list lang :default ""))))

というわけで最終的には次のようになります。

;;; org-figure-number.el --- Remove figure numbers   -*- lexical-binding: t; -*-

;; init.el:
;; (with-eval-after-load "ox" (require 'org-figure-number))

(require 'cl-lib)
(require 'ox)

(defun org-figure-number-make-dictionary (lang)
  (list
   (list
    "Figure %d:"
    (list lang :default ""))))

(defun org-figure-number-override-dictionary (options)
  (setq-local
   org-export-dictionary
   (nconc
    (org-figure-number-make-dictionary
     (or (plist-get options :language) org-export-default-language))
    org-export-dictionary)))

(defun org-figure-number-filter-options (options _backend &rest _rest)
  (unless (plist-get options :figure-number)
    (org-figure-number-override-dictionary options))
  options)

(defun org-figure-number-setup ()
  (setf (alist-get :figure-number org-export-options-alist)
        '(nil "fignum" t))
  (add-to-list 'org-export-filter-options-functions
               'org-figure-number-filter-options))

(org-figure-number-setup)

(provide 'org-figure-number)

と、ここまで書いてふと気がついたのですが、他のバックエンドではどうなっているのかな? と。……ox-latex.elなんかだと図番はまた別の方法で生成されているみたいですね。そういえばTeXって処理系が連番振るものでしたね……。

というわけで、以上はHTMLでエクスポートする際の話でした。

2024-09-16 ,

org-modeでエクスポート時の翻訳をバッファ内オプションで変更する

「エクスポート時の翻訳ってなんじゃ?」とお思いの方もいると思いますが、org-modeには人間が読むための短い文字列を英語以外の言語へ翻訳するための仕組みがあります。詳しくはorg-export-dictionary変数を見るのが手っ取り早いと思います。 "Author""著者""Date""日付""Figure %d:""図%d: ""Listing""ソースコード" 等々、色々定義されています。そしてそういった訳がイマイチしっくりこないという事はありませんか? それも文書によって適したものは変わってくることもあります。そこで今回はこの翻訳をバッファ内のオプションでカスタマイズする方法を用意してみました。

前準備:

  1. 末尾掲載のorg-custom-translation.elをload-pathが通っているところに配置。
  2. init.elに次のコードを追加。
(with-eval-after-load "ox"
  (require 'org-custom-translation))

使い方:

単純に置き換える例。

#+TITLE: むかしのはなし
#+AUTHOR: おおむかしのかたりべ
#+CUSTOM_TRANSLATION: ja Author さくしゃ
#+CUSTOM_TRANSLATION: ja Created つくったにちじ

むかしむかしあるところにおじいさんとおばあさんがいました。

めでたしめでたし。

図番を消す例。次のようにすれば「図1:」のような番号を消すことも出来ます。

#+CUSTOM_TRANSLATION: "Figure %d:" ""

これは何かの写真です。

#+CAPTION: 何かの写真
[[file:example-1.jpg]]

これは別の写真です。

#+CAPTION: 別の写真
[[file:example-2.jpg]]

いや、本当はこの図番を消すために作ったのですが、いざ作り終えてみると正直この方法で消すのはイマイチかなぁ、と。もしorg-modeの更新で "Figure %d:" の部分が変わってしまったら効果が無くなってしまいますからね。例えば ":" の部分は別途付け加えるようになるとか。

なので後でもうちょっと違うやり方を考えてみようと思いますが、せっかく作ったのでここに残しておきます。

;;; org-custom-translation.el --- Customize export translations -*- lexical-binding: t; -*-

;; Copyright (C) 2024 AKIYAMA Kouhei

;; Author: AKIYAMA Kouhei <misohena@gmail.com>
;; Keywords: 

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Change the translation dictionary used during export from the
;; in-buffer options.

;; Add the export option "#+CUSTOM_TRANSLATION:" to temporarily change
;; org-export-dictionary during export.

;; * Preparation
;; Put the following in your init.el.
;;   (with-eval-after-load "ox"
;;     (require 'org-custom-translation))

;; * Option Syntax
;; #+CUSTOM_TRANSLATION: [<language>] <src> <dst>

;; * Examples
;; Translate to Japanese romanization.
;; #+CUSTOM_TRANSLATION: ja Author Sakusha
;; #+CUSTOM_TRANSLATION: ja Date Hizuke

;; Remove figure number.
;; #+CUSTOM_TRANSLATION: "Figure %d:" ""

;;; Code:

(require 'cl-lib)
(require 'ox)

(defun org-custom-translation-split-option-value (str)
  (cl-loop with index = 0
           while (progn
                   (string-match
                    " *\\(\\(\"\\([^\"]*\\)\"\\)\\|\\([^ \t\"]+\\)\\)\\|"
                    str index)
                   (match-beginning 1))
           collect (or (match-string 3 str)
                       (match-string 4 str))
           do (setq index (match-end 0))))

(defun org-custom-translation-make-dictionary (lines current-language)
  (cl-loop for line in lines
           for values = (org-custom-translation-split-option-value line)
           when (>= (length values) 2)
           collect (let ((lang (if (>= (length values) 3)
                                   (pop values)
                                 current-language))
                         (src (car values))
                         (dst (cadr values)))
                     (list src (list lang :default dst)))))

(defun org-custom-translation-override-dictionary (options)
  (setq-local
   org-export-dictionary
   (append
    (org-custom-translation-make-dictionary
     ;;(cdar (org-collect-keywords '("CUSTOM_TRANSLATION")))
     (when-let ((lines-str (plist-get options :custom-translation)))
       (split-string lines-str "\n"))
     (or (plist-get options :language) org-export-default-language))
    org-export-dictionary)))

(defun org-custom-translation-filter-options (options _backend &rest _rest)
  (org-custom-translation-override-dictionary options)
  options)

(defun org-custom-translation-add-export-option ()
  (setf (alist-get :custom-translation org-export-options-alist)
        '("CUSTOM_TRANSLATION" nil nil newline)))

(defun org-custom-translation-setup ()
  (org-custom-translation-add-export-option)
  (add-to-list 'org-export-filter-options-functions
               'org-custom-translation-filter-options))

(org-custom-translation-setup)

(provide 'org-custom-translation)
;;; org-custom-translation.el ends here

基本的な処理は全て org-export-filter-options-functions を経由して呼び出される org-custom-translation-filter-options で行っています。

org-export-filter-options-functions は本来オプションをフィルタするためのものですが、ここで CUSTOM_TRANSLATION オプションの値に応じて org-export-dictionary を変更してしまいます。ここが呼ばれる時のカレントバッファは元のorg-modeバッファをコピーした一時的なバッファのようなので、ローカル変数として設定してしまいます。後は自然に新しい訳が使われるようになります。

2024-09-13

Emacs 30.0.91を試す(MS-Windows)

たまにはpretestの段階で触ってみる。

Windows版のバイナリは既にある。仕事が速い。

https://alpha.gnu.org/gnu/emacs/pretest/windows/emacs-30/

ネイティブコンパイルの設定は以前と同じでOKだった(一部のdllファイルはすでに含まれていた)。MSYS2は最近pacman -Suyしたので最新のはず(国内のミラーって無くなってたのね)。

lib/gdk-pixbuf-2.0(画像ローダーdll)はコピーしなくてもSVG内のimage要素が表示されるみたい。環境変数PATHをSystem32だけにしたりmsys64ディレクトリを一時的にリネームしたりしても表示されるので、密かに画像ローダーが見つけられてしまっているわけでも無さそう。喜ばしいことではあるけど何でだろう。「librsvg decode image」でGoogle検索したらLibrsvg will use Rust-only image decoders starting on 2.58.0 - Federico's Blogというのが出てきた。ひょっとしてこれのおかげ? 去年の12月の記事だし、emacs-30.0.91.zipに入っているlibrsvgのバージョンも2.58.0になっている。bmpも表示できなくなっているので多分間違いない(対応しているのはJPEG、PNG、GIF、WebPのみ:Do not load images with gdk-pixbuf; use Rust loaders instead (!904) · マージリクエスト · GNOME / librsvg · GitLab)。

.emacs.dを29と分けるため、runemacs.exeへのショートカットに --init-directory= オプションを含めて ~/.emacs.d.30 を指すようにした(私はいつもrunemacs.exeへのショートカットをスタートメニューにemacs-xx.xという名前で入れていて、「Ctrl+ESC em RET」でEmacsを起動している。バージョンアップするときはいつもこのショートカットを入れ替える作業をしている)。29と一緒だとやはりバイトコンパイル済みのelispに問題が生じる。

ソースコード(https://alpha.gnu.org/gnu/emacs/pretest/emacs-30.0.91.tar.xz)をダウンロードしてfind-function-C-source-directoryがそれのsrcディレクトリを指すようにする。

image-diredが色々変更されているのでそれに追従する。私のカスタマイズと衝突しているので修正。まずは29から30へのlisp/imageディレクトリのdiffを取って変更点を理解する。image-dired-insert-thumbnail関数の引数が一つ減っているので、とりあえずそこだけ対応したらエラーは出なくなった。他にも何かあるかもしれない。

w32image-create-thumbnailという関数が追加された。image-diredはサムネイル作成用のプログラム(ImageMagickやGraphicsMagick)が存在しない場合はこの関数を使うようになった。パフォーマンスはどちらが良いのか分からない。0.05秒のタイマーで繰り返しているのは気になる。試しに (setq image-dired-cmd-create-thumbnail-program "hoge") などと存在しないプログラムを指定することで使ってみた。縦横比を無視して正方形のサムネイルが生成されてしまう。ExifのOrientationも考慮されなかった。パフォーマンスはそう大きく変わらないように感じたが、多分サムネイル生成部分以外が遅そうなので後で調べてみる。

(追記:パフォーマンス以前にw32image-create-thumbnailを使ってサムネイルを生成するimage-dired-thumb-queue-run関数の後半はタイマーの使い方が間違っていてサムネイルが全部出来るまで操作不能になってしまう。直るまで使わない方が良い)

Windowsでは、convert.exeがImageMagickのものかSystem32のものかを /? オプションを付けて実際に実行して判別するコードが追加された。これがqueue-runで呼び出されているのはパフォーマンス的に良くないと思う。というかいい加減convertなんてやめてmagickコマンドを使えば良いのに。

サムネイルのファイル名をファイル内容の先頭4096バイトのSHA-1にできる設定が追加されているが、これはどうなんだろう。どうもファイルを移動してもサムネイルが使い回せることが狙いのようだが、先頭4096バイトがたまたま同一な別の画像というのは普通にあり得るのではないだろうか。特にbmpのような圧縮しない形式においては。

2024-09-12

最近Emacs関連でやったカスタマイズ

tramp-default-methodをplinkにする

久しぶりにMSYS2をアップグレードしたせいか分からないけれど、なぜか突然trampがscpxで繋がらなくなった。少しだけ原因を調べてみたけどよく分からなかったので諦めてplinkを使うことに。これまで頑なにplinkを使うことを避けていたのだけど、使ってみたら超快適! なぜいままで使っていなかったのかと。パスフレーズの記憶がssh-agentと一緒にならないのは良くないところだけど。

暇があったらなぜ動かなくなったのか調べてみたい。そもそもtrampはどうやって動いているのかよく知らない。

Emacsの標準機能でメールを送れるようにする

これまでメールと言えばWanderlustばっかり使っていたんだけど、Emacsの標準機能だけでメールを送れるようにしてみた(Sending Mail (GNU Emacs Manual (Japanese Translation)))。

(setq smtpmail-default-smtp-server "smtp.gmail.com"
      smtpmail-smtp-server "smtp.gmail.com"
      smtpmail-smtp-service 587
      smtpmail-stream-type 'starttls
      smtpmail-local-domain nil
      smtpmail-smtp-user "<username>")

後は.authinfoにごにょごにょと。面倒だからアプリパスワードも使っちゃう。この辺りは人によってセキュリティ的に許容できないだろう。

ちなみにEmacsに標準で組み込まれているメール作成のためのパッケージ(MUA)には次のものがある(define-mail-user-agentで検索):

また、メールを送信する方法には次のものがある:

上の設定はsmtpmailで送るためのもの。

image-diredでサムネイルが横になるのを直す

ExifのOrientationで回転させている写真が回転していないので直した。

image-dired-cmd-create-thumbnail-options に "-auto-orient" オプションを追加しただけ。

image-diredのサムネイルサイズを変更

前々からどのくらいのサムネイルサイズが最適なんだろうかと疑問に思っていた。小さすぎると内容が分からないし、大きすぎると一度に沢山の画像を並べられない。私が普段使っているテキスト領域の幅は640pxなので、横に3つ並ぶサイズということで image-dired-thumb-size を 196 にした。サムネイル毎の余白に案外スペースを取られる。

縦画像のサムネイルが左寄せで表示されるので、そのうち中央寄せされるように直したい。

image-diredのキー割り当てを変更

サムネイルを表示するバッファ( *image-dired* バッファ)のキーマップがdiredと違っていて戸惑うことがよくあるので、できるだけdiredに近づけるようにした。特に外部ビューアで開くキー。

次表示(SPC)、前表示(DEL)は良くある操作方法だけど、DEL(私はC-hをDELにしている)が押しづらいのでnとpに割り当ててしまった。fとbの方が良いのかもしれないけれど、dired上でファイルを移動するのがnとpだし、next-lineではなくnext-imageだと考えればそこまでおかしくもない気がする。いや、やっぱりfとbの方が使いやすいだろうか。そもそもC-f C-m、C-b C-mと押すのがそんなに面倒だろうか。SPCはそれほど苦では無いのだから、bだけ割り当てれば良いのかもしれない。

image-diredは使えば使うほど直したいところが出てくるのでまたそのうち。

2024-06-22 ,

org-gotoで日本語で検索(インクリメンタルサーチ)できない不具合を修正

org-modeで C-c C-j (org-goto) を実行し、日本語(マルチバイト文字)を入力すると次のようなエラーが出てインクリメンタルサーチできない(Org9.7.3時点)。

funcall-interactively: Wrong type argument: stringp, [12375]

consult-org-heading なり consult-outline なりを使えば良いような気もするけど気持ち悪いので一応直しておく(org-goto-local-auto-isearchに対する修正)。

;; org-gotoで日本語検索が出来ない問題を修正
(with-eval-after-load "org-goto"
  ;; 関数を置き換える。
  (defun org-goto-local-auto-isearch ()
    "Start isearch."
    (interactive)
    (let ((keys (this-command-keys)))
      (when (eq (lookup-key isearch-mode-map keys) 'isearch-printing-char)
        (isearch-mode t)
        ;; ここから修正
        ;; 元: (isearch-process-search-char (string-to-char keys))
        (cond
         ((vectorp keys)
          (when (< 0 (length keys))
            (let ((ch (aref keys 0)))
              (when (integerp ch)
                (isearch-process-search-char ch)))))
         ((stringp keys)
          (isearch-process-search-char (string-to-char keys))))
        ;; ここまで修正
        (font-lock-ensure)))))

this-command-keys はキーシーケンスを文字列で返すこともあればvectorで返すこともあるのだとか(Strings of Events (GNU Emacs Lisp Reference Manual))。

2024-06-19

xrefでメソッドへジャンプできなくなる問題に対処する

Emacs Lispをいじっているときに、cl-defmethodで定義したメソッドへM-.(xref-find-definitions)でジャンプできなくなることがたまにあったので次のように対処しました。(Emacs29時点)

;; xrefでメソッドに飛べなくなるのを回避するハック。
;; defgenericを使わずにdefmethodをloadし直すと起きる問題に対処する。
;; `elisp--xref-find-definitions:around'から`find-lisp-object-file-name'を
;; 呼び出したときの挙動を変更する。
(defun my-elisp--xref-find-definitions:around (old-fun &rest args)
  (cl-letf* ((flofn-old (symbol-function 'find-lisp-object-file-name))
             ((symbol-function 'find-lisp-object-file-name)
              (lambda (object &rest flofn-args)
                (or (apply flofn-old object flofn-args)
                    ;; 本来のfind-lisp-object-file-nameの結果がnullでかつ
                    ;; OBJECTがgeneric関数シンボルなら
                    ;; 空のファイル名を返すことで
                    ;; elisp--xref-find-definitions内の
                    ;; メソッド列挙部に到達させるハック。
                    ;; 本来ならその部分のFIXMEにも書いてある通り
                    ;; その部分を`elisp-xref-find-def-functions'に分離
                    ;; すべきだと思われる。
                    (when (and (symbolp object) (cl--generic object))
                      "")))))
    (apply old-fun args)))
(advice-add 'elisp--xref-find-definitions :around
            'my-elisp--xref-find-definitions:around)

根本的な原因は私がcl-defgenericせずにcl-defmethodしているからなのですが、まぁ、そこは良いんです。かったるくてやってられないから省いているだけので。それがダメというならmy-defmethodでも作らにゃなりません。しかし、まぁ、そのツケが回ってきたというだけの話ではあります。

xref等はsymbol-file関数を使ってシンボルを定義したファイルを特定しますが、そのsymbol-fileload-history変数に記録されたファイルとシンボル定義の対応表(alist)を参照します。elファイルをloadするとその過程で定義されたシンボルがこの変数に記録されるわけですが、cl-defmethodは既にgeneric関数が定義されているときには新たにgeneric関数を定義しないので、同じelファイルを2回ロードすると暗黙的に作成されたgeneric関数のファイル名が消えてしまいます。elisp--xref-find-definitionsはgeneric関数のファイル名が取得できないとメソッドのファイル名も列挙しないので、結果定義されているはずのメソッドに飛べなくなるわけです。

上の変更では、generic関数のファイル名が特定できなかった場合に空文字列のファイル名を返すことで特定できたことにして、メソッドの列挙部分に無理矢理処理を通します。幸いなことにxrefは空文字列のファイル名を無視してくれる(ジャンプ先候補に出さない)ようです。

ろくなもんじゃありませんけど、とりあえずはこれで。

ちゃんとやるならelisp--xref-find-definitionsのFIXMEコメントに書かれているようにgeneric&method列挙部分をelisp-xref-find-def-functions変数に登録する関数としてくくりだした上で、その中でgeneric関数のファイル名が特定できなくてもメソッドのファイル名を列挙するようにするのが良さそうです。

今回はシンボルと定義ファイル名の割り出し方法に関するお勉強でした。

2024-06-08 ,

org-inline-image-fixのorg-mode 9.7対応

org-modeでインライン画像化する画像形式を限定するの続き。

org-mode 9.7がリリースされたので関係する変更点を調べてorg-inline-image-fixに必要な修正を加えました。

9.7のインライン画像周辺の変更点としては次のものが見つかりました:

いくつかは前回対応しましたし、取りこむ必要が無いものもあります。

  • org-image-max-widthの追加
  • org-image-alignの追加

の二つは一応取りこんでおくことにしました。

org-image-max-width は org-limit-image-size と機能が被りますが、それぞれ独立して機能するのでどちらを使っても問題ありません。私は高さの制限が出来る後者を使い続けます。まれに縦長の画像に出くわすことがあるので。

org-image-align の方は、私は中央寄せや右寄せの指定を普段しないので多分使わないと思います。一応試したらこんな感じになりました:

Org9.7のインライン画像alignプレビュー機能を使ってみたところ
図1: Org9.7のインライン画像alignプレビュー機能を使ってみたところ

一箇所、インライン画像の右側にある空白を画像のオーバーレイに含めてしまう(空白を消して表示する)修正に疑問があったので、本家とは違う修正をしました。おそらく中央寄せや右寄せにするときに右側に空白があると完全な位置に寄らないのでそれを解消する意図があるのだと思いますが、個人的には空白が(存在するのに)勝手に消えて表示されるのはあまり好ましくないような気がします。とりあえず左寄せの時は従来通りに必ず空白を残すようにしておきました。

2024-06-08 ,

インポートとジェネレータ

el-easydraw(以下edraw)の最近の変更の中で比較的大きかったのはインポート機能でしょうか。

これは元々edrawが一部のSVGを正しく読み込むことが出来ないという問題を指摘されたことから開発に至った機能です。

元々edrawは膨大なSVG仕様の全てに対応する気はさらさら無く、edrawによって作成・出力したSVGのみが再編集可能です。edrawが出力したSVGには <g id="edraw-body">...</g> という形のグループ要素がありその中が唯一編集可能な領域になっています。idがedraw-bodyなグループ要素(つまりg#edraw-body)の中にはedrawが対応しているものしか入っていない前提で作られているため、無理矢理そこに何かを入れたとしても動作は保証されません(いや、まぁ、どのみち誰も何も保証しませんが)。とは言えその方は他のツールで作成したSVGを持ってきたかったらしく、そのための仕組みとして最終的にインポート機能を作ることになったわけです。

特にedraw-modeを使うと任意のSVGファイル(を開いたバッファ)で作図エディタを起動することが可能だったのですが、そんなことをしても当然そのSVGの中にあった図形を編集できるわけも無く、せいぜいその上に新しい図形を乗せていくことくらいしか出来ませんでした。

現在では、他で作った(g#edraw-bodyが存在しない)SVGでM-x edraw-modeを実行するとエラーが出て、諦めるか自己責任で M-x edraw-convert-buffer-to-edraw-svg-xml を実行して変換するよう勧めるメッセージが表示されます。実際に edraw-convert-buffer-to-edraw-svg-xml を使用すると、バッファ内のSVGがedrawが扱えそうな形に変換されます。対応していない要素は除去されたり警告が出たりします。そして再度 M-x edraw-mode を実行すると、運が良ければ編集できることでしょう。

作図エディタの中からでもメインメニューの「ドキュメント」→「ファイルからインポート...」を選択して任意のSVGファイルを選べばそれを変換・取りこむことができます。

試しにInkscapeで作成したSVGを読み込んでみましたが、まぁ、思ったよりは取りこめる感じでした。他にもGraphvizで作成したSVGを取りこんだりも出来ました(ただしグラフ構造を手軽に再編集できるようなものではありません)。その方はdvisvgmの出力を取りこみたかったようですが、それも最終的にはうまく取りこめるようになりました。

dvisvgmを使用すればlatexで生成したものをSVGに変換できます。つまり、例えば数式をSVGに変換することが可能です。そしてそれを取りこめるようになったわけです。

しかしこのままでは数式を図の中に取りこむには手間がかかりすぎます。まずlatexのソースを書いて、latexでdviを作成し、それをdvisvgmでsvgへ変換し、edrawでインポートしなければなりません。もっと効率よく出来る仕組みが必要です。

私も数式を図に取りこめたら良いだろうなと思ったことはあったので、そのための仕組みを作ることにしました。

「数式ツール」のようなものを作っても良かったのですが、それでは直接的すぎて面白くありません。そこで考えたのが「ジェネレータ」です。

ジェネレータは何らかの設定から図形を生成するような(図形として配置可能な)オブジェクトです。ジェネレータツール(生成ツール)で配置できます。

ジェネレータにはプロパティとして生成の元(ソース)となるような情報を指定出来ます。まず第一に生成タイプがあり、今のところlatexかgridが指定出来ます。次に生成ソースを指定するプロパティがあり、タイプがlatexの時はそこにLaTeXのソースコードを記述できます。最後に生成オプションがあり、これはタイプによって異なる追加のプロパティを色々設定できます。

そして生成ボタンを押すとそれらのソース情報を元に図形が生成されるという寸法です。latexの場合は、preamble部分(カスタマイズ変数で変更可能)などと合成されてlatex、dvisvgmコマンドが呼び出され、生成されたSVGがedraw用に変換されてジェネレータの子要素として挿入されます(もちろんlatexとdvisvgmが必要です。私はTeX Liveでインストールしましたが、MSYS2にPATHが通っていると色々ハマるのでご注意を)。

latexジェネレータで数式を生成したところ
図1: latexジェネレータで数式を生成したところ

生成タイプのgridは格子状の線を生成するものです。生成タイプがlatexだけでは格好が付かないので適当に作ってみました。線が太くて気に入らないという方は位置を0.5ピクセルずらしてみて下さい。「これもうソースコード要らないじゃん!」ということが発覚してちょっと後悔しています。

数式をSVGに入れるならMathMLを使うという手もあります。実際latexからMathMLに変換してforeignObject要素として取りこむような生成タイプを作ることもできるでしょう。しかし一番のネックはlibrsvgが対応していないのでEmacsの中で表示されないことでしょうね。

latexの中で(tikzで)グラフを生成することもできるみたいです。

latexの図を生成したところ
図2: latexの図を生成したところ

これらの機能で簡単に複雑な図を表現できるようになったのは良いところですが、反面データサイズが膨らみがちなことが困りそうなところでしょうか。特に [[edraw:data= 形式のリンクはデータがOrgファイル内(特に一行に)埋め込まれるので、あまり大きいと何か問題を引き起こすかもしれません。単純にインライン画像をoffにしたときに見栄えが悪いというのもあります(今更ですが)。そういう場合はインライン画像を右クリックして [[edraw:file= 形式へ変換して下さい。

2024-06-07 ,

複数サブパス(複合パス)への対応

別のプロジェクトで簡単な絵が必要になったので自作の作図ツールを使って描いていたら色々不満があったので最近はちょくちょくいじっていました。そのプロジェクトはそっちのけで(笑)。

misohena/el-easydraw: Embedded drawing tool for Emacs

特に複数のサブパスを含むパスへの対応をいい加減やらなきゃな、と。

複数のサブパスとは

複数のサブパスというのは、一つのパスデータ(path要素のd属性:<path d=>)の中に複数の内部的なパス(サブパス)が表現されているような状況のことです。例えば <path d="M0,-100 L100,100 L-100,100 Z M0,-50 L-50,50 L50,50 Z" /> と書くと一つのpath要素で大小二つの三角形が表現できます。実際に表示してみるとこんな感じになります。

図1: d="M0,-100 L100,100 L-100,100 Z M0,-50 L-50,50 L50,50 Z"

複数のパスを表現したいなら複数のpath要素を使えばいいじゃないかと思うかもしれませんが、上のような「穴あき」を表現するには一つのパスデータの中の複数のパス(サブパス)が必要になります。複数のpath要素を重ねて配置しただけでは塗りつぶした大きな三角形の上に小さな三角形を塗りつぶすことしか出来ません。一つのパスの中に大小の三角形が一緒に入っていてはじめてこのような「穴あき」が作れます。

一つのパスの中に二つのパスが並んでいるだけでなぜ中が抜けるんだろうかと疑問に思う方もいると思いますが、これはこの図形をレンダリングすることを考えれば分かります。レンダリングの基本は画像を上から下へ一行ずつ、そして一行の中を左から右へ1ピクセルずつ色を決めて点を打っていくことです。例えばこの図形の真ん中らへんの一行(1ライン)をレンダリングするとします。画像の左端から右端へ向かって1ピクセルごと処理していきます。このとき各線分(segment)との交差判定をしながら進んでいくことがポイントです。一番最初は図形の外なので点を打ちません。右へ向かっていくと、最初の線分を跨ぎます。なので、それ以降は灰色のfill色を打ちます。さらに進むとまた線分を跨ぎます。なので、それ以降は図形の外にあると認識して点を打ちません。さらに進むとまた線分を跨ぐ(奇数回目)ので色を塗り始めます。そしてまた線分を跨いだら(偶数回目)塗るのを止めます。と、このように図形の内外を判定しながらレンダリングしていくのですが、このアルゴリズムで真ん中に穴あきがあるドーナツ形を作るには外側の線も内側の線も同じ判定対象(線分集合)の中に存在していなければなりません。凹形なら一つのパスでも作れるのでその上側をくっつけてしまうという回避策もありますが、strokeを指定するとボロが出たりと問題もあります。

ということで、一つのパスの中に複数のパス(サブパス)が必要になるわけです。

対応してこなかった理由

これまで対応してこなかった理由はひとえにデータ構造の悪さからでした。SVGのd=属性を解析して内部的な表現に変換してから編集するのですが、その内部的な表現の構造が悪すぎました。その表現は、d属性を解析したほぼそのままのコマンドリストでした。なので、一つアンカーを打てばMコマンド(最初の点を指定する)、Lコマンド(直線)、Cコマンド(カーブ)、Zコマンド(閉じる)を前後の状況に応じて判別して追加するような複雑なことをしなければなりませんでした。もちろんその複雑さを緩和するような層があって、上の層は下の層に処理を投げるだけなのですが、複数のサブパスに対応するには下の層を改善しなければならず、そしてそれは複雑なのでやりたくなかったわけです。

この構造のまま複数のサブパスにも(ユーザーからの様々な操作に対してちゃんと)対応しようとすると細かい条件分けが複雑すぎて大変でした。実は実際最後まで書き切ったのですが、どこかにバグがあっても不思議ではない、これからの改善も全くしたくないようなコードにしかなりませんでした。ウンザリしてすぐにその辺りのを構造(edraw-path.el)を書き直すことにしました。

新しい構造と内部・外部表現の変換

新しい構造はパス(パスデータ)、サブパス、アンカー、ハンドルを素直に表現したものとなりました。これによって様々な処理が驚くほどシンプルに素直に書けるようになりました。

そして新しい構造とSVGのd属性(コマンド文字列)との間の変換処理も用意して、必要に応じて変換します。これは従来の構造(コマンドリスト)でもそのような作りになっていました。

ただし、内部的な構造と実際のd属性の文字列(外部表現)との変換は必要最小限にする必要があります。それはどちらの方向でも失う情報があるからです。

SVGのd属性で表現できないもの

内→外で失うものとしては、開サブパスの端点の外側のハンドルがあります。何を言っているのかよく分からないかもしれませんが、次図の通りです。

閉(サブ)パス開(サブ)パス端点端点の外側のハンドル端点を持たない
図2: 開サブパスの端点の外側のハンドル

つまり、線の端っこのさらに外にあるハンドルです。実際のところこれは描画には全く影響を及ぼしません。なのでSVGのd属性で表す方法がありません。しかし編集においては、その端点の次にアンカー点を打ったときに、その端点と新しい端点とを結ぶ曲線の曲がり具合に影響します。描画側から見たら「まだ存在しない曲線の属性なんて知らん」ですが、編集側から見たら「いやいや、線がその端点を通過したときの出て行く先を示すんだからその端点の属性だろ」というわけです。というわけで、編集用の内部表現をd属性へ変換するとそのハンドルは失われます。まぁ、独自の属性に持たせるといった方法もありますが。

新しい構造で表現できないもの

外→内で失うものとしては、各コマンドの細かいニュアンスだと思います。

SVGのパスデータには沢山のコマンドが用意されています。

M m Z z L l H h V v C c S s Q q T t A a

これだけのコマンドがあります(Paths ― SVG 2を参照のこと)。

これだけあると同じ形を描画するのにも沢山の表現方法があります。これらのコマンドの使い分けは、ほとんどの場合データサイズを削減することが目的だと思われますが、誰かがそこにそれ以外の意図を込めていないと言い切れるでしょうか。相対表現(小文字)は前の点からの相対関係を維持してほしいという意図があったり、水平線や垂直線はその性質を維持してほしいと考えてはいないでしょうか。Aコマンドは特に意図が現れやすいです。しかし内部表現に変換すれば、それは全て無味乾燥な三次ベジェ曲線のアンカーとハンドルに集約されてしまいます。

私が一番頭を悩ませていたのは一つのMコマンドが複数のサブパスで共有されうることです。例えば次のようなd属性があった場合:

M0,0 L40,-20 L40,20 Z L20,40 L-20,40 Z L-40,20 L-40,-20 Z L0,-40

(ちなみにこれは分かりやすくするためにあえて無駄な書き方をしていますが、実際には次のように書けます)

M0 0 40-20V20ZL20 40H-20ZL-40 20V-20ZV-40
図3: 一つのMコマンドが複数のサブパスで共有されている例

これは上のような図形ですが、中央の点、つまり0,0の表記はd属性中に一つしか現れていません。このd属性の中には4つのサブパスが含まれています。しかしその開始点は最初に現れる M0,0 ただ一つにまとめられています。

これは単なるケチ表現なのかもしれません。しかし同じ点になっていることには何か必然的な理由があるのかもしれません。その点をドラッグしたとき、4つ全てのサブパスの開始点が動いた方が親切かもしれません。

まぁ、そんなことを全てのコマンドに対してやっていたら大変なので諦めることにしたわけです。

そして今

今では上のようなデータも正しく編集できるようになりました。もちろんバラバラの4つのサブパスとしてです。

私が間違った設計判断をしてしまった原因はこの割り切りをためらったからかもしれません。SVGパスデータの元々の表現の性質をできるだけ残しておきたかったわけです。しかしすでに相対表現やら二次ベジェ、水平線、垂直線などは絶対表現のLとCに変換してしまっていましたから、今更なわけですが。

実はこの問題は作り始めのかなり初期から気がついていました。でも頑張れば何とかなるだろうと思っていたわけです。

私は近年山を歩くことがありますが、登山道を歩いていると不意に道がおかしいな? と気がつくことがあります。妙に道が荒れている、障害となる草木や岩石が多い、越えられないことはないが一般登山道のレベルとは思えない、など。もちろんそこまで至るまでには「分岐など見当たらなかった」「正しい道を進んでいる」と思っているものです。しかしそういった異変に気がついたら地図を確認してすぐに引き返すことです。頑張れば行ける。そう思って先に行くと取り返しの付かないことになるかもしれません。

2024-03-23

Emacs Lispで文字列内の特定の位置が正規表現にマッチしているか判定する

Emacs Lispで、文字列の中の特定の位置が正規表現とマッチしているか判定するにはどうしたらよいでしょうか。

例えば "{ name: 'Taro' }" という文字列があったとして、3文字目の位置が正規表現 "\\([a-z]+\\)" にマッチしているか判定するにはどうしたら良いでしょうか(もちろんこの文字列や位置は色々と変わるものとします)。

Emacs Lispの正規表現マッチングの関数には大きく分けてバッファ用と文字列用があります。POSIX版ではない標準的なマッチング関数だと次のものがあります(Emacs 29.2時点でのRegexp Search (GNU Emacs Lisp Reference Manual)より):

  • バッファ用
    • re-search-forward
    • re-search-backward
    • looking-at
    • looking-at-p
    • looking-back
  • 文字列用
    • string-match
    • string-match-p

バッファ上のテキストであれば、現在のポイントから続くテキストが正規表現とマッチしているかを判定するにはlooking-atやlooking-at-pが使えます。他にも正規表現にポイントの位置とマッチするバックスラッシュ記法 \= があるので、それを使って (re-search-forward "\\=[a-z]+") などと書くことも出来ます(計測してみるとre-search-forwardの方が若干遅いようです)。

しかしながら文字列版の方にはそのような関数がありません。string-matchは文字列版のre-search-forwardです。検索を開始する位置こそ指定出来ますが、末尾までの間に正規表現がマッチする部分を探し、見つかったらその先頭位置を返します。例えば (string-match "\\([a-z]+\\)" "{ },{name: 'Taro' }" 2) を評価したら、 },{ の部分を飛ばして6文字目にマッチしてしまうわけです。指定した位置そのものがマッチしているかを判定するようなstring-looking-atのような関数はありません。 \= もバッファのポイントに対するもので、文字列には効きません。文字列の先頭を示す \` は本当に文字列の先頭(index=0)にしかマッチしませんし、 ^ も文字列の先頭か行頭(\nの直後)にしかマッチしません。

Emacs Lispというのは基本的にはバッファ用の関数の方が充実していて文字列用の関数が貧弱な傾向にあるような気がします。

仕方ないのでこれまで私はどうしていたかというと、

(eq (string-match "\\([a-z]+\\)" text pos) pos)

のようにしてマッチした位置が検索を開始した位置と同じであることを確認したり、

(string-match "\\`\\([a-z]+\\)" (substring text pos))

substringを使って検索開始位置を文字列の先頭に持ってきた上で \\` を使うなどしていたわけです(実際には前者ばかりで後者はほとんどしていないと思います)。

後はまぁ、

(string-match (format "\\`.\\{%d\\}\\([a-z]+\\)" pos) text)

みたいな手はあるかもしれません。

しかし、最近edraw-dom-svg.elでstyle属性の解析をしていたときに次のようなコードを書きました。

(defconst edraw-css-re-token
  (concat
   edraw-css-re-comment "*"
   "\\(?:\\(" edraw-css-re-ws "\\)"
   "\\|\\(" edraw-css-re-string "\\)"  ;; " '
   "\\|\\(" edraw-css-re-hash "\\)"  ;; #
   "\\|\\(" edraw-css-re-at-keyword "\\)"  ;; @
   "\\|\\(" edraw-css-re-dimension "\\)"
   "\\|\\(" edraw-css-re-percentage "\\)"
   "\\|\\(" edraw-css-re-number "\\)"
   "\\|\\(" edraw-css-re-function "\\)"
   "\\|\\(" edraw-css-re-ident "\\)"
   "\\|\\(" "[]({}),:;[]" "\\)"
   "\\|\\(" "." "\\)" ;; delim
   "\\)"))

(defun edraw-css-token (str pos)
  (unless (eq (string-match edraw-css-re-token str pos) pos)
    (error "CSS Syntax Error: %s `%s'" pos str))
  (cond
   ((match-beginning 1) ... )
   ((match-beginning 2) ... )
   ((match-beginning 3) ... )
   ...))

そのときに、「あれ、これって最後に . が入ってるんだから(末尾以外)必ずposの位置でマッチするんじゃないの?」と思ったわけです。

正規表現の最後が . ではなく空文字列の "略\\|" で終わっていれば、本当に必ずposの位置でマッチすることになります。

なので、最初の問いの答えは、

(progn
  (string-match "\\([a-z]+\\)\\|" text pos)
  (match-beginning 1))

でいいじゃないかということになったわけです。要するに必ずマッチさせてしまうわけです。これなら末尾まで無駄な検索は起こりませんし、substringで新しい文字列を作る必要もありません(substringがCopy-on-Writeだったりはしませんよね?)。で、実際に肝心の部分がマッチしているかは (match-beginning 1) が非nil(この場合はpos)を返すかどうかで確かめればいいわけです。正規表現に括弧が無いなら (match-end 0) がpos(つまり (match-beginning 0))と同じかどうかで確認しても良いでしょう。

分かっている人から見ればなんだ当たり前じゃ無いか、こんなことが分からないなんてバカなんじゃないか? と思われるかもしれませんが、何だかキツネにつままれたような不思議な気分になってしまいました。

大丈夫ですよね? 一応計測もしていてパフォーマンスも悪くは無さそうです。

ちなみにEmacsの \| は左がダメだったら右を試すという意味です。実際の実装がどうなってるのかは知りませんので、左が優先されると言った方が良いでしょうか。どちらか長い方という意味ではないのでご注意ください。

ところでel-easydrawの方は現在インポートまわりの作業をしていて、dvisvgmやdot、Inkscapeが出力したSVGを取りこんで遊んだりしています。org-babelのような仕組みが出来ると良いのですが……。