Author Archives: misohena

2024-10-21

XMPメタデータをimage-dired内で使う

前回に引き続き、el-xmpの話。

xmp-image-dired.elを追加し、image-dired内で画像ファイルのメタデータを操作できるようにしました。具体的には次のことが可能です。

  • マークしたファイルまたは現在のポイントが指すファイルに対するプロパティの設定
  • 現在のポイントが指すファイルからのプロパティの取得
  • サムネイルのプロパティ値に基づくフィルタ(一時的な非表示)

現在対応しているプロパティは次の通りです。

  • xmp:Rating
  • xmp:Label
  • dc:subject
  • dc:title
  • dc:description
  • dc:creators

image-diredにはタグやコメントを付ける機能が既にありますが、それでは物足りないという人のためのものです(標準機能のタグやコメントは.emacs.d内に独自形式で保存されるので他のアプリと連携できないという問題もあります)。

参考までに設定例としてxmp-setup.elを追加したので、それを使えば簡単にこれらの機能をdiredとimage-diredの両方から利用できるようになります(フィルタはimage-diredのみ。diredではマーク操作が可能)。次のキーが使えるようになります。

  • プロパティ設定
    • ' s r : Set Rating
    • ' s l : Set Label
    • ' s s : Set Subjects
    • ' a s : Add Subjects
    • ' r s : Remove Subjects
    • ' s t : Set Title
    • ' s d : Set Description
    • ' s c : Set Creators
  • プロパティ取得
    • ' g a : Get All properties
    • ' g r : Get Rating
    • ' g l : Get Label
    • ' g s : Get Subjects
    • ' g t : Get Title
    • ' g d : Get Description
    • ' g c : Get Creators
  • マーク(diredのみ)
    • ' m r : Mark by Rating
    • ' m l : Mark by Label
    • ' m s : Mark by Subjects
    • ' m t : Mark by Title
    • ' m d : Mark by Description
    • ' m c : Mark by Creator
  • フィルタ(image-diredのみ)
    • ' f r : Filter by Rating
    • ' f l : Filter by Label
    • ' f s : Filter by Subjects
    • ' f - : Clear Filter

例えば ' s r でレーティングを設定して、 ' f r で特定のレーティングのみを表示する、といった使い方ができます。 ' f - でフィルタを解除します。HydraやらTransientやらで使いやすくした方が良いと思いますが、とりあえず設定例ということで。

ここで言うLabel(xmp:Labelプロパティ)は、ファイルに対して一つだけ設定できる短い文字列です。定義については https://developer.adobe.com/xmp/docs/XMPNamespaces/xmp/ を参照してください。Adobe Lightroom等ではカラーラベルを設定するとこのプロパティにRedやBlueといった色名が設定されるらしいです。

Subject(dc:subject)は、ファイルの主題を表すキーワード等のリストです。定義については https://developer.adobe.com/xmp/docs/XMPNamespaces/dc/https://www.dublincore.org/specifications/dublin-core/dcmi-terms/terms/subject/ を参照してください。実質的にはある種のタグとして使用してしまって良いのではないかと思います。主題なので、ファイルが何を表現しているのかといった観点から付けた方が良いのだと思います。何か作業の状態等を付けるのは憚られる感じかと思います。

フィルタ表示機能は元々image-diredが対応していないものを無理矢理実現しているので、Emacsのバージョンアップに伴って機能しなくなる可能性があります。気がついたら直すつもりです。

image-dired内でマークしたファイルにdc:subjectプロパティを設定している所
図1: image-dired内でマークしたファイルにdc:subjectプロパティを設定している所
2024-10-19

EmacsからファイルのXMPメタデータを取得・設定する

XMP(Extensible Metadata Platform)というメタデータを記述するための仕様があります。

https://developer.adobe.com/xmp/docs/

https://developer.adobe.com/xmp/docs/XMPSpecifications/

メタデータというと例えばタイトル、作成者、作成日、内容説明、ラベルといったものが代表的ですが、それに限らずあらゆる関連情報を表現することが出来る拡張可能な枠組みがXMPです。

例えば XMPSpecificationPart1.pdf というPDFファイル(バイナリ)の最後の方には次のようなXMLテキストが含まれています。

...バイナリ略...
<?xpacket begin="(略)" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 4.0-c316 44.253921, Sun Oct 01 2006 17:14:39">
   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
      <rdf:Description rdf:about=""
            xmlns:dc="http://purl.org/dc/elements/1.1/">
         <dc:format>application/pdf</dc:format>
         <dc:title>
            <rdf:Alt>
               <rdf:li xml:lang="x-default">XMP Specification Part 1: Data Model, Serialization, and Core Properties</rdf:li>
            </rdf:Alt>
         </dc:title>
         <dc:creator>
            <rdf:Seq>
               <rdf:li>Adobe Systems Incorporated</rdf:li>
            </rdf:Seq>
         </dc:creator>
      </rdf:Description>
      <rdf:Description rdf:about=""
            xmlns:xap="http://ns.adobe.com/xap/1.0/">
         <xap:CreateDate>2012-03-21T08:55:04Z</xap:CreateDate>
         <xap:CreatorTool>FrameMaker 8.0</xap:CreatorTool>
         <xap:ModifyDate>2012-03-21T08:57:17-07:00</xap:ModifyDate>
         <xap:MetadataDate>2012-03-21T08:57:17-07:00</xap:MetadataDate>
      </rdf:Description>
      <rdf:Description rdf:about=""
            xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
         <pdf:Producer>Acrobat Distiller 8.1.0 (Windows)</pdf:Producer>
         <pdf:Copyright>Copyright 2012, Adobe Systems Incorporated. All rights reserved.</pdf:Copyright>
      </rdf:Description>
      <rdf:Description rdf:about=""
            xmlns:pdfx="http://ns.adobe.com/pdfx/1.3/">
         <pdfx:Copyright>Copyright 2012, Adobe Systems Incorporated. All rights reserved.</pdfx:Copyright>
      </rdf:Description>
      <rdf:Description rdf:about=""
            xmlns:xapMM="http://ns.adobe.com/xap/1.0/mm/">
         <xapMM:DocumentID>uuid:3806c052-1c0d-4599-b3ff-38c0441bbb3a</xapMM:DocumentID>
         <xapMM:InstanceID>uuid:abc82103-c360-4b74-8812-ff4c81eceaea</xapMM:InstanceID>
      </rdf:Description>
   </rdf:RDF>
</x:xmpmeta>
...空白文字略...
<?xpacket end="w"?>
...バイナリ略...

これによって、このPDFのタイトルが「XMP Specification Part 1: Data Model, Serialization, and Core Properties」、作成者が「Adobe Systems Incorporated」、作成日時が「2012-03-21T08:55:04Z」であることを示しているわけです。

XMPはJPEGファイルの中にも埋め込まれていることがあり、様々な撮影情報が記録されていることがあります。

写真のメタデータと言えばExifが代表的ですが、私の使っているカメラではカメラ内で設定したレーティング(写真の評価)だけがXMPとして記録されるようになっていました(それ以外の撮影情報は全てExifで記録されていました)。

また、写真編集ソフトや各種ビューアもこのXMPを利用するものが多いです。沢山ある写真を評価したり分類したりしたときに、その情報をサイドカーファイルと呼ばれるXMLファイル(IMG0001.JPG.xmpのようなファイル名)に保存するようになっているソフトが多くあります。darktableにいたっては現像のための全てのパラメータをサイドカーファイルに保存します。サイドカーファイルは少々煩わしい存在ですが、メタデータをネットワークストレージを介して共有したり他のソフトと交換したりするのには最適でしょう。

というわけで、EmacsからもこのXMPで書かれたメタデータを読み書きするためのライブラリを作成しました。

misohena/el-xmp: Emacs XMP (Extensible Metadata Platform) Library

今のところ基本的な情報を読み書きするためのコマンドやDired上でメタデータを元にマークするコマンドくらいしかありません。

サイドカーファイル(拡張子が.xmpのXMLテキスト)からの読み取りはもちろん、JPEG内のAPP1セグメント内にあるXMPパケットも読み取れるようになっています。その他の場合は汎用的なパケットスキャン(<?xpacket を探す)が使われますが、確実に読み取れるとは限りません。

保存は全てサイドカーファイルに対して行われます。一応パケット内に書き戻すことも可能なのですが、危ないのでデフォルトでは無効になっています。

Diredでマークするときなど、数千ファイルくらいあると読み取りにかなりの時間がかかります。一応Emacsのセッション中だけ有効なキャッシュ機構があるので、二回目以降は速くなるはずです。

永続的なファイルメタデータキャッシュ機構もそのうち作りたいと考えています。

Diredでレーティングを元にjpgファイルをマークしてimage-diredで表示させた例
図1: Diredでレーティングを元にjpgファイルをマークしてimage-diredで表示させた例
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は使えば使うほど直したいところが出てくるのでまたそのうち。

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プレビュー機能を使ってみたところ

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