Monthly Archives: 9月 2024

2024-09-23

Emacs LispでXMLを解析する方法(名前空間あり)

libxml-parse-xml-region

「emacs lisp parse xml」などでWeb検索すると真っ先に出てくるのはEmacs Lispのマニュアルでしょう。

Parsing HTML/XML (GNU Emacs Lisp Reference Manual)

そこではlibxml-parse-xml-regionという関数で解析が出来るというようなことが書かれています。早速試してみましょう。

まずは次のXMLファイルを用意します。これは今適当にでっち上げたXMLで特に意味はありません。これをexample.xmlというファイル名で保存します。

<?xml version="1.0" encoding="UTF-8"?>
<my-nanikano-data
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:dc="http://purl.org/dc/elements/1.1/">
  <my-file-list>
    <file name="a0001.wav">
      <text><p xmlns="http://www.w3.org/1999/xhtml">これはテストですてすとてすと<strong>0001</strong>です。</p></text>
      <rdf:Description rdf:about="" xmp:Rating="5" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
        <dc:creator>太郎</dc:creator>
      </rdf:Description>
    </file>
    <file name="b0001.wav">
      <text><p xmlns="http://www.w3.org/1999/xhtml">こんにちは<strong>0002</strong>です!</p></text>
      <rdf:Description rdf:about="">
        <dc:creator>花子</dc:creator>
      </rdf:Description>
    </file>
  </my-file-list>
</my-nanikano-data>

そしてlibxml-parse-xml-regionを使って解析してみます。

(with-temp-buffer
  (insert-file-contents "example.xml")
  (libxml-parse-xml-region))

結果は次のようになりました。

(my-nanikano-data nil
                  (my-file-list nil
                                (file ((name . "a0001.wav"))
                                      (text nil
                                            (p nil
                                               "これはテストですてすとてすと"
                                               (strong nil "0001")
                                               "です。"))
                                      (Description
                                       ((about . "") (Rating . "5"))
                                       (creator nil "太郎")))
                                (file ((name . "b0001.wav"))
                                      (text nil
                                            (p nil "こんにちは"
                                               (strong nil "0002")
                                               "です!"))
                                      (Description ((about . ""))
                                                   (creator nil "花子")))))

名前空間が消えてしまっています。これはいけません。これをベースに内容を修正して書き戻したらmy-nanikano-dataを読み込むアプリが正しく動かなくなってしまいます(まぁ、そんなものはありませんが)。

「emacs libxml namespace」でWeb検索するとこの問題に対するバグ報告と思わしきものがヒットしますが、libxml2のバグでEmacsのバグじゃ無いよ、ということでクローズされているみたいです(#59537 - `libxml-parse-xml-region` strips out the namespace information, and namespace prefix in the DOM representation - GNU bug report logs)。そんなわけないでしょう(笑)。libxml2はノードの解析結果をxmlNode構造体で返すようですが、libxml-parse-xml-regionの実装はその中のnsを一切触っていませんからね。

まぁ、バグを見つける→バグ報告を見つける→よく分からない理由で却下されている、という流れはEmacsでは良くあることです。Emacsは「あるがまま」の状態で提供されているわけで、自分で直して使えない人にはお勧めできません。

とは言えここはC言語で書かれている部分なので修正のハードルは高いですね。Emacs Lispレベルで何とかならないでしょうか?

xml-parse-file (オプション無し)

探してみるとxml-parse-fileというものが見つかりました(xml.el内)。早速使ってみましょう。

(xml-parse-file "example.xml")
((my-nanikano-data
  ((xmlns:rdf . "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
   (xmlns:dc . "http://purl.org/dc/elements/1.1/"))
  "\n  "
  (my-file-list nil "\n    "
                (file ((name . "a0001.wav")) "\n      "
                      (text nil
                            (p
                             ((xmlns . "http://www.w3.org/1999/xhtml"))
                             "これはテストですてすとてすと"
                             (strong nil "0001") "です。"))
                      "\n      "
                      (rdf:Description
                       ((rdf:about . "") (xmp:Rating . "5")
                        (xmlns:xmp . "http://ns.adobe.com/xap/1.0/"))
                       "\n        " (dc:creator nil "太郎") "\n      ")
                      "\n    ")
                "\n    "
                (file ((name . "b0001.wav")) "\n      "
                      (text nil
                            (p
                             ((xmlns . "http://www.w3.org/1999/xhtml"))
                             "こんにちは" (strong nil "0002") "です!"))
                      "\n      "
                      (rdf:Description ((rdf:about . "")) "\n        "
                                       (dc:creator nil "花子")
                                       "\n      ")
                      "\n    ")
                "\n  ")
  "\n"))

パッと見良さそうに見えます。名前空間の宣言や接頭辞の情報が失われず残っています。空白文字が全部残っているのは気になりますが、まぁ、何とでもなります。

問題は要素名や属性名が接頭辞を含む一つのシンボルで表現されていることです。その接頭辞がどの名前空間名(URI)を表しているか(あるいは省略されている場合にデフォルト名前空間が何か)は自分で調べなければならないでしょう。接頭辞が慣用的に決まっていてそれしか対応する必要が無いならこれでも良いのでしょうが、私はちょっと気になります。

xml-parse-file (parse-ns引数を使用)

改めてxml-parse-fileの関数ドキュメントを見ると、parse-ns引数を非nilにするとQNAMES(修飾名)を展開できると書かれています。早速やってみましょう。

(xml-parse-file "example.xml" nil t)
((("" . "my-nanikano-data")
  ((("http://www.w3.org/2000/xmlns/" . "rdf")
    . "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
   (("http://www.w3.org/2000/xmlns/" . "dc")
    . "http://purl.org/dc/elements/1.1/"))
  "\n  "
  (("" . "my-file-list") nil "\n    "
   (("" . "file") ((("" . "name") . "a0001.wav")) "\n      "
    (("" . "text") nil
     (("http://www.w3.org/1999/xhtml" . "p")
      ((("http://www.w3.org/2000/xmlns/" . "")
        . "http://www.w3.org/1999/xhtml"))
      "これはテストですてすとてすと"
      (("http://www.w3.org/1999/xhtml" . "strong") nil "0001")
      "です。"))
    "\n      "
    (("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "Description")
     ((("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "about") . "")
      (("" . "Rating") . "5")
      (("http://www.w3.org/2000/xmlns/" . "xmp")
       . "http://ns.adobe.com/xap/1.0/"))
     "\n        "
     (("http://purl.org/dc/elements/1.1/" . "creator") nil "太郎")
     "\n      ")
    "\n    ")
   "\n    "
   (("" . "file") ((("" . "name") . "b0001.wav")) "\n      "
    (("" . "text") nil
     (("http://www.w3.org/1999/xhtml" . "p")
      ((("http://www.w3.org/2000/xmlns/" . "")
        . "http://www.w3.org/1999/xhtml"))
      "こんにちは"
      (("http://www.w3.org/1999/xhtml" . "strong") nil "0002")
      "です!"))
    "\n      "
    (("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "Description")
     ((("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "about") . ""))
     "\n        "
     (("http://purl.org/dc/elements/1.1/" . "creator") nil "花子")
     "\n      ")
    "\n    ")
   "\n  ")
  "\n"))

おお、これまで要素名や属性名が入っていた部分がconsセルになっています。 (名前空間名 . ローカル名) という形式になっており、これはXML名前空間仕様における展開名(expanded name)と同じです。これなら接頭辞がどの名前空間名に対応するかを自分で調べなくても良さそうですね。

……と思ったのですが、よく見るとおかしな所があります。

それは (("" . "Rating") . "5") と書かれている部分。これは (("http://ns.adobe.com/xap/1.0/" . "Rating") . "5") でないといけないはずです。

少し追試をしてみましょう。試しに次のようなXMLを作ってみました。

<?xml version="1.0" encoding="UTF-8"?>
<rdf:Description
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    xmlns:xmp="http://ns.adobe.com/xap/1.0/"
    rdf:about=""
    xmp:Rating="3"
    xmp:CreateDate="2024-09-23">
  <xmp:Thumbnails>
    <rdf:Alt>
      <rdf:li rdf:parseType="Resource">
        ....
      </rdf:li>
    </rdf:Alt>
  </xmp:Thumbnails>
</rdf:Description>

これを同様に解析してみます。

(xml-parse-file "example-2.xml" nil t)

結果は次の通り。

((("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "Description")
  ((("http://www.w3.org/2000/xmlns/" . "rdf")
    . "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
   (("http://www.w3.org/2000/xmlns/" . "xmp")
    . "http://ns.adobe.com/xap/1.0/")
   (("" . "about") . "") (("" . "Rating") . "3")
   (("" . "CreateDate") . "2024-09-23"))
  "\n  "
  (("http://ns.adobe.com/xap/1.0/" . "Thumbnails") nil "\n    "
   (("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "Alt") nil
    "\n      "
    (("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "li")
     ((("http://www.w3.org/1999/02/22-rdf-syntax-ns#" . "parseType")
       . "Resource"))
     "\n        ....\n      ")
    "\n    ")
   "\n  ")
  "\n"))

属性名である rdf:aboutxmp:Ratingxmp:CreateDate の名前空間名が "" になってしまっています。しかし前にある要素名 rdf:Description や、その後にある要素名(xmp:Thumbnails)や属性名(rdf:parseType)には正しい名前空間名が展開されています。つまり名前空間を宣言した要素に指定されている属性名だけが正しく展開されていないように見えます。

そこでxml.el内にあるxml-parse-fileの実装を読んだところ、原因はxml-parse-tag-1関数内にあることが分かりました。 ;; opening tag と書いてあるあたり、開始タグの解析を行っているあたりです。xml-nsという変数が接頭辞と名前空間名との対応表(alist)になっているようですが、その更新と使用の順序に問題があります。全属性の解析と属性名の展開、名前空間宣言(xmlns:??=)の反映、要素名の展開の順に行われているため、属性名を展開する段階ではまだその要素で行われている名前空間宣言が反映されていません。従って、名前空間宣言をした要素にある属性名は正しく展開されないというわけです。せっかく便利な機能があるのにこれでは使えません。

nxml-parse-file

他にもEmacs内にXMLを解析している部分は無いかと探したところ、nxml-parse-fileという関数を見つけました(nxml-parse.el内)。

早速使ってましょう。

(nxml-parse-file "example.xml")
("my-nanikano-data"
 (((:http://www.w3.org/2000/xmlns/ . "rdf")
   . "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
  ((:http://www.w3.org/2000/xmlns/ . "dc")
   . "http://purl.org/dc/elements/1.1/"))
 "\n  "
 ("my-file-list" nil "\n    "
  ("file" (("name" . "a0001.wav")) "\n      "
   ("text" nil
    ((:http://www.w3.org/1999/xhtml . "p")
     (((:http://www.w3.org/2000/xmlns/ . "xmlns")
       . "http://www.w3.org/1999/xhtml"))
     "これはテストですてすとてすと"
     ((:http://www.w3.org/1999/xhtml . "strong") nil "0001") "です。"))
   "\n      "
   ((:http://www.w3.org/1999/02/22-rdf-syntax-ns\# . "Description")
    (((:http://www.w3.org/1999/02/22-rdf-syntax-ns\# . "about") . "")
     ((:http://ns.adobe.com/xap/1.0/ . "Rating") . "5")
     ((:http://www.w3.org/2000/xmlns/ . "xmp")
      . "http://ns.adobe.com/xap/1.0/"))
    "\n        "
    ((:http://purl.org/dc/elements/1.1/ . "creator") nil "太郎")
    "\n      ")
   "\n    ")
  "\n    "
  ("file" (("name" . "b0001.wav")) "\n      "
   ("text" nil
    ((:http://www.w3.org/1999/xhtml . "p")
     (((:http://www.w3.org/2000/xmlns/ . "xmlns")
       . "http://www.w3.org/1999/xhtml"))
     "こんにちは"
     ((:http://www.w3.org/1999/xhtml . "strong") nil "0002") "です!"))
   "\n      "
   ((:http://www.w3.org/1999/02/22-rdf-syntax-ns\# . "Description")
    (((:http://www.w3.org/1999/02/22-rdf-syntax-ns\# . "about") . ""))
    "\n        "
    ((:http://purl.org/dc/elements/1.1/ . "creator") nil "花子")
    "\n      ")
   "\n    ")
  "\n  ")
 "\n")

問題は特に見当たらないと思います。

特徴:

  • (要素名や属性名の中で)名前空間名(URI等)を表現するのにキーワードを使う(:http://www.w3.org/2000/xmlns/ 等)。
  • 要素名や属性名はconsセル (<名前空間名キーワード> . <ローカル名文字列>) で表現される。ただし、名前空間無しの場合はローカル名単体の文字列となる。
  • デフォルト名前空間の宣言(xmlns=<名前空間名>)の解析結果は ((:http://www.w3.org/2000/xmlns/ . "xmlns") . "<名前空間名>") の形で表現される。 xmlns の部分は :http://www.w3.org/2000/xmlns/ と展開されているのだからローカル名部分は空文字列にでもなりそうなものだが、なぜか "xmlns" が入っている。これだと xmlns: という接頭辞の名前空間を宣言する(xmlns:xmlns=<名前空間名>)のと同じ形になってしまうので、注意が必要。

要素名の展開表現とdom.elの問題

xml-parse-file(parse-nsが非nilの場合)やnxml-parse-fileを使った場合、要素名がconsセルで表現されるようになるわけですが、そうするとdom.elが使えなくなるという問題が発生します。dom.elの各関数には処理対象のノードを指定する引数(nodeやdom)があるわけですが、それらはなぜか引数がノードのリストである可能性を考慮しています。引数nodeがノードのリストであった場合、そのリスト内の最初のノードを処理対象にするコードが各関数に入っています。引数nodeがノードのリストかどうかをチェックするコードは (consp (car node)) という形になっています。nxml-parse-file等を使用して生成されたノードは要素名部分、つまりリストの最初の要素がconsセルになりますから、このチェックにひっかかってしまいます。そしてノードのリストと勘違いされて、その先頭要素が取り出され、構造が破壊されてしまいます。

この問題に対する対策ですが、dom.elは大したことはやっていませんし少々使いづらい所もあるので、自分で独自のノード操作関数を作成するのが一番だと思います。要素の内部表現はいずれで解析した場合でも、 (<要素名> . (<属性alist> . <子供リスト>)) の形になっています。要素名や属性名(属性alistのキー部分)の部分が文字列だったりconsセル (<名前空間名> . <ローカル名>) だったりするだけです。やっていることは単純なので簡単に作れるでしょう。el-easydrawなんかでも最初のうちはdom.elやsvg.elを使用していましたが、今は使わなくなってしまいました。

別の方法としては、解析した後に名前部分を補正してしまう手があります。つまり、ツリーを巡回して名前部分を一つのシンボルなり文字列なりに結合してしまうわけです(:http://www.w3.org/1999/02/22-rdf-syntax-ns\#::Description のように)。そうすればdom.elは引き続き使えます。

まとめ

名前空間をちゃんと扱いたいならnxml-parse-fileが一番良さそうです。この場合、解析結果そのままだとdom.elが使えないので何らかの対処が必要になります。

接頭辞が決め打ちでも構わないような用途なら(parse-ns引数を使わない)xml-parse-fileも使えそうです。parse-ns引数は不具合があるので止めましょう。

名前空間を使わない、もしくは消えても構わないような用途ならlibxml-parse-xml-regionが使えます。

Emacsの中をくまなく探したわけでもありませんし紹介した関数の中もくまなく調べたわけでもありませんので、何か気がついていない問題やより良い方法があるかもしれません。

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は使えば使うほど直したいところが出てくるのでまたそのうち。