1. 目的
HTMLでリンク先を新しいタブで開くにはa要素の属性として target="_blank"
を指定します。全ての文書で常に新しいタブで開くのはどうかと思いますが、特定の文書(ページ)内は一律そうして欲しいケースもあると思います。
org-modeのリンクをHTMLでエクスポートしたとき、デフォルトではもちろんそのような属性は付きません。リンクの前に #+attr_html: :target _blank
のような指定を入れれば実現可能ですが沢山のリンクがあると面倒ですし入れ忘れも生じます。また、一つの段落に複数のリンクがある場合、全てのリンクに属性が反映されないという問題もあります。
http://example.com/]]は別タブで開きます。二つ目のリンク[[http://example.org/]]は開きません。[[
というわけで今回はorg-modeのフィルタシステムを使用して自動的に全てのリンクに新しいタブで開くための属性を入れてみたいと思います。
2. フィルタ関数の作成
まずフィルタ関数を作成します。
(defun org-newtab-link-filter (s backend info) (if (and ;;@todo Support <a data-ex=">" href=...> (org-export-derived-backend-p backend 'html) ;; html only (not (string-match "\\`[^>]* target=\"" s)) ;; has no target= (not (string-match "\\`[^>]* rel=\"" s)) ;; has no rel= (string-match "\\`[^>]* href=\"[^#]" s) ;;not internal link (string-match "\\`<a " s)) ;; a tag (replace-match "<a target=\"_blank\" rel=\"noopener\" " t t s) s))
この関数はHTMLのa要素にtarget属性とrel属性を挿入します。ただし追加するのは次の条件が全て満たされているときのみです。
- HTMLバックエンドかその派生のバックエンドでエクスポート中であること
target=
やrel=
がまだ指定されていないこと- リンク先が
#
で始まるページ内リンクではないこと
3. フィルタ関数の登録
次にこのフィルタがエクスポート時に使われるようにするのですが、これには色々な方法があります。
3.1. 変数 org-export-filter-link-functions に設定する方法
org-export-filter-TYPE-functions
という名前の変数にフィルタ関数を登録すると、構文要素 TYPE
のエクスポート結果を再処理できます。リンク要素に対するフィルタは org-export-filter-link-functions
という変数へ登録します。(参考: Advanced Export Configuration (The Org Manual))
この方法で個別のファイルにフィルターを設定する方法については以前書きました。
個別のorg-modeファイルにエクスポート時のフィルターを設定する
例えば次のようにソースブロックを使ってエクスポートするたびにバッファローカル変数を設定するようにします。
#+BEGIN_SRC emacs-lisp :exports results :results none (setq-local org-export-filter-link-functions '(org-newtab-link-filter)) #+END_SRC [[https://example.com]]
#+BIND:
を使っても良いのですが org-export-allow-bind-keywords
が t
でないと動作しないので、それならソースブロックにして評価するかyes/no確認を入れた方が良いと思います。
3.2. 派生したバックエンドを作成する方法
次のコードはhtmlバックエンドから派生したnewtab-link-htmlバックエンドを作成するものです。(参考: Advanced Export Configuration (The Org Manual))
;; エクスポートオプションで有効/無効を切り替えるしくみ(後述) (defvar org-newtab-link-enabled nil) ;;tでデフォルト有効 (defvar org-newtab-link-options-alist '((:newtab-link-enabled "HTML_LINK_NEWTAB" nil org-newtab-link-enabled))) (defun org-newtab-link-filter-opt (s backend info) (if (not (member (plist-get info :newtab-link-enabled) '(nil "" "nil" "no"))) (org-newtab-link-filter s backend info) s)) ;; HTMLから派生したバックエンドを作る (defun org-newtab-link-define-backend () (require 'ox-html) (org-export-define-derived-backend 'newtab-link-html 'html :filters-alist '((:filter-link . org-newtab-link-filter-opt)) :options-alist org-newtab-link-options-alist :menu-entry '(?n "Export to HTML (Enable org-newtab-link)" ((?N "As HTML buffer" org-newtab-link-export-as-html) (?n "As HTML file" org-newtab-link-export-to-html) (?o "As HTML file and open" (lambda (a s v b) (if a (org-newtab-link-export-to-html t s v b) (org-open-file (org-newtab-link-export-to-html nil s v b))))))))) (defun org-newtab-link-export-as-html (&optional async subtreep visible-only body-only ext-plist) (interactive) (org-export-to-buffer 'newtab-link-html "*Org Go Game HTML Export*" async subtreep visible-only body-only ext-plist (lambda () (set-auto-mode t)))) (defun org-newtab-link-export-to-html (&optional async subtreep visible-only body-only ext-plist) (interactive) (let* ((extension (concat "." (or (plist-get ext-plist :html-extension) org-html-extension "html"))) (file (org-export-output-file-name extension subtreep)) (org-export-coding-system org-html-coding-system)) (org-export-to-file 'newtab-link-html file async subtreep visible-only body-only ext-plist))) (org-newtab-link-define-backend)
エクスポートメニューにもエントリーを追加するので、 C-c C-e n n
といったキー操作でフィルタを有効にしたエクスポートが可能です。
しかしこの方法にはいくつか欠点があります。
- HTMLから派生した他のバックエンドには適用されない。
- メニューが煩雑になる。
- エクスポート時に明示的にバックエンドを選ばなければならない。
こういった問題があることを考えるとバックエンドを派生させるというアイデアはそれほど良いものとは思えなくなってきます。
3.3. HTMLバックエンドを修正する方法
派生したバックエンドを作るよりも既存のHTMLバックエンドを修正した方が使い勝手は良いものになります。ただし、他にもバックエンドを修正するコードがあるかもしれないので干渉しないように注意しましょう。
(defun org-newtab-link-modify-html-backend () (require 'ox-html) (let ((backend (org-export-get-backend 'html))) ;; org-newtab-link-options-alistで定義されているオプションをバックエンドへ追加 (let ((backend-options (org-export-backend-options backend)) (new-option-names (mapcar #'car org-newtab-link-options-alist))) (setf (org-export-backend-options backend) (nconc (seq-remove (lambda (elem) (memq (car elem) new-option-names)) backend-options) org-newtab-link-options-alist))) ;; フィルタ関数org-newtab-link-filter-optをバックエンドの:filter-linkへ追加 (let ((filter-link (assq :filter-link (org-export-backend-filters backend)))) ;; null => (:filter-link . ()) (when (null filter-link) (push (setq filter-link (list :filter-link)) (org-export-backend-filters backend))) ;; (:filter-link . function) => (:filter-link . (function)) (when (functionp (cdr filter-link)) (setcdr filter-link (list (cdr filter-link)))) ;; Add my filter function (when (not (memq 'org-newtab-link-filter-opt (cdr filter-link))) (push 'org-newtab-link-filter-opt (cdr filter-link)))))) (org-newtab-link-modify-html-backend)
こうすると通常のHTMLバックエンドによるエクスポートでリンクをフィルタできるようになります。
4. フィルタの有効/無効を切り替える
リンクを常に新しいタブで開くのなら良いのですが、たいていの場合目的に応じて切り替える必要があるでしょう。プロジェクトやディレクトリ、ファイルによって柔軟に切り替えられるようにしておく必要があります。
4.1. ファイルローカル変数・ディレクトリローカル変数を使用する方法
一番簡単なのはファイルローカル変数という仕組みを使用して、ファイルやディレクトリ毎に org-export-filter-link-functions
変数を変更することです。
まずバッファローカル変数にフィルタを設定する org-newtab-link-enable
という関数を作ります。
(defun org-newtab-link-enable () (setq-local org-export-filter-link-functions (cons #'org-newtab-link-filter org-export-filter-link-functions)))
そしてそれをファイルローカル変数として安全に評価できるように設定します。ファイルを開くたびに警告が出るのは鬱陶しいので。
(add-to-list 'safe-local-eval-forms '(org-newtab-link-enable))
その上で次のように .dir-locals.el
を作成します(M-x add-dir-local-variable
、 org-mode
、 eval
、 (org-newtab-link-enable)
と入力等)。
;;; Directory Local Variables ;;; For more information see (info "(emacs) Directory Variables") ((org-mode . ((eval . (org-newtab-link-enable)))))
するとそのディレクトリ下にあるorgファイルを開いたときに、自動的に変数 org-export-filter-link-functions
にフィルタ関数がバッファローカルとして設定されます。
org-modeはエクスポート時にフィルタ変数の値を読み取ってバックエンドオブジェクトのプロパティにコピーするので、エクスポート用に一時的に作られるバッファ上でも設定したフィルタが正しく使われます。下手に新しく専用のバッファローカル変数を作成すると、その変数はエクスポート用のバッファにコピーされないので正しく動作しません。
4.2. エクスポートオプションを追加する方法
#+HTML_LINK_NEWTAB: t
のような記述でファイル毎に制御できると便利です。
これを実現するにはバックエンドオブジェクトにオプション定義を追加する必要があります。そのための仕組みは既に上のバックエンドを定義する方法の所に書いてあります。以下一部を再掲します。
(defvar org-newtab-link-enabled nil) ;;tでデフォルト有効 (defvar org-newtab-link-options-alist '((:newtab-link-enabled "HTML_LINK_NEWTAB" nil org-newtab-link-enabled))) (defun org-newtab-link-filter-opt (s backend info) (if (not (member (plist-get info :newtab-link-enabled) '(nil "" "nil" "no"))) (org-newtab-link-filter s backend info) s)) ;; これらをバックエンドに仕込む方法は派生させる場合と修正する場合で異なります。
この方法は変数 org-newtab-link-enabled
でもフィルタの有効性を制御できるようにします。この変数をファイルローカル変数やディレクトリローカル変数にすることもできます。そうしたい場合、安全な変数に指定しておくと煩わしい警告が出なくて便利です。
(put 'org-newtab-link-enabled 'safe-local-variable #'booleanp)
この方法はバックエンドに何らかの方法(派生させるなり修正するなり)で手を入れる必要があります。もしバックエンドに関わりたくないのであれば、フィルタ関数の中でオプション記述を検索するという方法もあり得ます。
5. 使う
以下は実際の使用例です。
* Chapter1 [[https://example.com/]]や[[https://example.org/]]は新しいタブで開きます。 [[https://example.com/]]のように明示的にtarget属性が指定されているようなリンクには適用されません。[[https://example.org/]]のような段落中の二つ目のリンクは ~#+attr_html:~ の効果が及ばないので新しいタブで開きます。 [[*Chapter1][Chapter1]]のような内部リンクには適用されません。
6. 他の方法
今回はorg-modeのフィルタという仕組みを使用しましたが他にも様々なやり方が考えられます。
- エクスポートフックを使う方法 (参考: Hooks - Advanced Export Configuration (The Org Manual))
- Transcodeの段階で対処する方法 (参考: Extending an existing back-end - Advanced Export Configuration (The Org Manual))
- htmlのbase要素を使う方法 (参考: https://emacs.stackexchange.com/a/46383)
- JavaScriptで何とかする方法 (参考: https://emacs.stackexchange.com/a/37585)
- org-modeの関数をadvice等で書き替える方法
- エクスポート後に置換する方法
7. 終わりに
org-modeのエクスポートシステムは複雑でカスタマイズできるポイントも限られているのでちょっとしたことでもいつもどう実現しようか悩んでしまいます。この記事が何か同じようなカスタマイズをする際の参考になれば幸いです。
8. まとめ
;; リンクのフィルタ関数 (defun org-newtab-link-filter (s backend info) (if (and ;;@todo Support <a data-ex=">" href=...> (org-export-derived-backend-p backend 'html) ;; html only (not (string-match "\\`[^>]* target=\"" s)) ;; has no target= (not (string-match "\\`[^>]* rel=\"" s)) ;; has no rel= (string-match "\\`[^>]* href=\"[^#]" s) ;;not internal link (string-match "\\`<a " s)) ;; a tag (replace-match "<a target=\"_blank\" rel=\"noopener\" " t t s) s)) ;; エクスポートオプション (defvar org-newtab-link-enabled nil) ;;tでデフォルト有効 (put 'org-newtab-link-enabled 'safe-local-variable #'booleanp) (defvar org-newtab-link-options-alist '((:newtab-link-enabled "HTML_LINK_NEWTAB" nil org-newtab-link-enabled))) (defun org-newtab-link-filter-opt (s backend info) (if (not (member (plist-get info :newtab-link-enabled) '(nil "" "nil" "no"))) (org-newtab-link-filter s backend info) s)) ;; HTMLバックエンドの修正 (defun org-newtab-link-modify-html-backend () (require 'ox-html) (let ((backend (org-export-get-backend 'html))) ;; org-newtab-link-options-alistで定義されているオプションをバックエンドへ追加 (let ((backend-options (org-export-backend-options backend)) (new-option-names (mapcar #'car org-newtab-link-options-alist))) (setf (org-export-backend-options backend) (nconc (seq-remove (lambda (elem) (memq (car elem) new-option-names)) backend-options) org-newtab-link-options-alist))) ;; フィルタ関数org-newtab-link-filter-optをバックエンドの:filter-linkへ追加 (let ((filter-link (assq :filter-link (org-export-backend-filters backend)))) ;; null => (:filter-link . ()) (when (null filter-link) (push (setq filter-link (list :filter-link)) (org-export-backend-filters backend))) ;; (:filter-link . function) => (:filter-link . (function)) (when (functionp (cdr filter-link)) (setcdr filter-link (list (cdr filter-link)))) ;; Add my filter function (when (not (memq 'org-newtab-link-filter-opt (cdr filter-link))) (push 'org-newtab-link-filter-opt (cdr filter-link)))))) (with-eval-after-load "ox-html" (org-newtab-link-modify-html-backend))
一応GitHubにも置いておきます。
misohena/org-newtab-link: Open org-mode links exported as HTML in a new tab.
[…] 昔からそうだったけ?とモヤモヤしながらもう長いこと経つのですが、org-modeの書き方を例示することが多い私はよくこの問題に引っかかります(最近ではこういうのとかこういうのとか)。 […]