Yearly Archives: 2020

2020-06-02

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

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

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

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

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

misohena/phscroll: Enable partial horizontal scroll in Emacs

使い方は

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

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

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

既知の問題:

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

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

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

2020-05-26 ,

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

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

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

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

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

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

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

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

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

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

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

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

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

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

(defvar org-altcaption--link nil)

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

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


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

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

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

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

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

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

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

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

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

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

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

おいしそうなすいか
図1: おいしそうなすいか
2020-05-26 ,

org-modeのインライン画像のサイズを制限する(最大サイズを指定する)

前回の続きでorg-modeのインライン画像の気に入らないところその2。画像の最大サイズを指定できないところ。

Org文書中からEmacsのフレームよりも大きい画像へリンクを張ることがあります(特にOrg2Blogを使っていると画像はサーバ側で様々なバリエーションを自動生成するので元画像は大きめになります)。そんなときに org-toggle-inline-images でインライン画像を表示するとフレームから画像がはみ出して目も当てられません。

フレームからはみ出す画像
図1: フレームからはみ出す画像

org-modeには org-image-actual-width という変数があり、ImageMagickが有効なときは指定したサイズへスケーリングできます。しかし全ての画像が指定したサイズで表示されてしまうため、小さな画像も大きく拡大されてしまいます。 #+ATTR_HTML: :width 300 のような属性指定を元にサイズを決める機能もありますが、Emacs内での表示のためだけにこの指定をあちこちに入れるのは面倒です。それに用途によってはエクスポートしたときの属性値はEmacs内での表示サイズとは別のものが必要になるでしょう(追記:そういう用途には #+ATTR_ORG: :width を併用するようです> How do I re-scale inline org-mode images to specific widths? - Stack Overflow)。

あくまで欲しいのは可能な限り元のサイズで表示し、設定した最大サイズを超えた場合のみ縮小して表示する機能です。

org-modeにこの機能が無いのはおそらくEmacsのcreate-image関数にそれをサポートする機能が無いためではないでしょうか。create-image関数は幅を指定して読み込む機能(:widthプロパティ)はありますが指定された幅より大きい場合にのみ縮小するような機能(例えば:max-widthとか)はありません(max-image-sizeという変数はありますが、これは読み込む最大のサイズを指定します。それを超えた画像は読み込まれません!)。

幸い画像のサイズを取得するimage-size関数はあるので自分でサイズを計算して読み込むことは可能です。ただしimage-size関数がどのように実装されているかは確認していません。ヘッダーだけ読み込んでサイズを返すならばそれなりに速い動作が期待できますが、画像全体を読み込んでからサイズを返すようだと二度手間になるので速度は低下することでしょう。

それは覚悟の上で最大サイズを指定できるようにするのが次のコードです。

訂正: ImageMagickを使った読み込みでは :max-width、:max-heightが指定できました!(ImageMagick Images - GNU Emacs Lisp Reference Manual) それらを使用して最大サイズを指定できるようにするのが次のコードです。

(defcustom org-limit-image-size '(0.99 . 0.5) "Maximum image size") ;; integer or float or (width-int-or-float . height-int-or-float)

(defun org-limit-image-size--get-limit-size (width-p)
  (let ((limit-size (if (numberp org-limit-image-size)
                        org-limit-image-size
                      (if width-p (car org-limit-image-size)
                        (cdr org-limit-image-size)))))
    (if (floatp limit-size)
        (ceiling (* limit-size (if width-p (frame-text-width) (frame-text-height))))
      limit-size)))

(defvar org-limit-image-size--in-org-display-inline-images nil)

(defun org-limit-image-size--create-image
    (old-func file-or-data &optional type data-p &rest props)

  (if (and org-limit-image-size--in-org-display-inline-images
           org-limit-image-size
           (null type)
           ;;(image-type-available-p 'imagemagick) ;;Emacs27 support scaling by default?
           (null (plist-get props :width)))
      ;; limit to maximum size
      (apply
       old-func
       file-or-data
       (if (image-type-available-p 'imagemagick) 'imagemagick)
       data-p
       (plist-put
        (plist-put
         (org-plist-delete props :width) ;;remove (:width nil)
         :max-width (org-limit-image-size--get-limit-size t))
        :max-height (org-limit-image-size--get-limit-size nil)))

    ;; default
    (apply old-func file-or-data type data-p props)))

(defun org-limit-image-size--org-display-inline-images (old-func &rest args)
  (let ((org-limit-image-size--in-org-display-inline-images t))
    (apply old-func args)))

(defun org-limit-image-size-activate ()
  (interactive)
  (advice-add #'create-image :around #'org-limit-image-size--create-image)
  (advice-add #'org-display-inline-images :around #'org-limit-image-size--org-display-inline-images))

(defun org-limit-image-size-deactivate ()
  (interactive)
  (advice-remove #'create-image #'org-limit-image-size--create-image)
  (advice-remove #'org-display-inline-images #'org-limit-image-size--org-display-inline-images))

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

変数 org-limit-image-size には最大サイズを指定します。一つの数値で指定した場合は幅と高さの最大値は同じになります。ドット対 (width . height) の形で二つの数値を指定した場合は幅と高さの最大値は別々になります。数値は整数の場合はピクセル数となります。浮動小数点数の場合はフレームのサイズに対する比率となります。デフォルトは (0.99 . 0.5) で、幅の最大値はほぼフレーム一杯、高さの最大値はフレームの半分くらいまでとしています。

実装にあたっては org-display-inline-images 関数の途中をいじりたかったのですがうまく分割できないので create-image をフックして org-display-inline-images 経由で呼び出されたときだけ動作を変えています。

正直このくらいのことはorg-mode標準で対応して欲しいなぁという気もします。やってることは結局create-imageに:max-widthと:max-heightを付加することだけなので。フックするためのコードが馬鹿らしいですよね。ハマリどころとしては :width nil はダメってことです。Emacs27は分かりませんが、ImageMagickでは :width nil で :max-widthを指定すると画像が出ません。不要な:widthプロパティは削除する必要があります。

追記: Emacs27対応について。Emacs27ではImageMagick対応がデフォルト無効になるそうです。その代わりスケーリングは標準対応になるとNEWS.27に書いてあります(Windowsでも対応するのか不安……)。image.cやdisplay.texiを見る限り:max-widthや:max-heightも標準対応していそう。なのでimagemagickの有無を判定して処理を変えている部分を少し修正して、無くても:max-width、:max-heightプロパティを付加するようにしてみました。ちゃんと対応するなら新しく追加される関数image-transform-pを使用して判別した方が良いかもしれません。org-mode側ではImageMagickの有無で:widthプロパティを設定するかどうかを判別しているので、org-image-actual-widthや#+ATTR_*でのサイズ指定は今のところImageMagick必須です。

2020-05-25 ,

org-modeのインライン画像をリアルタイムで更新する

(追記: https://github.com/misohena/org-inline-image-fix/blob/master/org-flyimage.el にソースコードを上げました。org-flyimage-modeというマイナーモードにしてあります)

org-modeには画像へのリンクをインライン表示する機能(org-toggle-inline-images (C-c C-x C-v))がありますが、私はたまにしか使っていませんでした。

理由は色々あるのですが、その一つが画像を瞬時に更新してくれないこと。画像のへのリンクを修正したり追加したりしても自動的に画像を更新してくれません。コメントアウトしても表示されたまま。org-redisplay-inline-images (C-c C-x C-M-v)で更新できるのですが、これはいったん全ての画像を消してから再表示するため画像が多いととても時間がかかります。それだといっそのこと画像なんて表示せずに編集して、まとまったところでhtmlにでもエクスポートして確認すれば済んでしまいます。

というわけで編集するそばから自動的に画像を更新させるのが次のコードです。

(defvar org-flyimage-in-activate-links nil)

(defun org-flyimage-activate-links (old-func limit)
  (let ((org-flyimage-in-activate-links t))
    (funcall old-func limit)))

(defun org-flyimage-remove-inline-images-in (beg end)
  ;; is faster to use (overlays-in beg end) ?
  (loop for ov in org-inline-image-overlays
        if (let ((ovbeg (overlay-start ov))
                 (ovend (overlay-end ov)))
             (and (numberp ovbeg) (numberp ovend)
                  (< ovbeg end) (> ovend beg)))
        do (org-display-inline-remove-overlay ov t nil nil)))

(defun org-flyimage-remove-flyspell-overlays-in (old-func beg end)
  (funcall old-func beg end)

  (if t ;;(not org-flyimage-in-activate-links) ;;in t, reflect #+attr_html: :width immediately. but slow
      (org-flyimage-remove-inline-images-in beg end))

  (if org-flyimage-in-activate-links
      (org-display-inline-images nil t beg end)))

(defun org-flyimage-activate ()
  (interactive)
  (advice-add #'org-activate-links :around #'org-flyimage-activate-links)
  (advice-add #'org-remove-flyspell-overlays-in :around #'org-flyimage-remove-flyspell-overlays-in))

(defun org-flyimage-deactivate ()
  (interactive)
  (advice-remove #'org-activate-links #'org-flyimage-activate-links)
  (advice-remove #'org-remove-flyspell-overlays-in #'org-flyimage-remove-flyspell-overlays-in))

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

仕組みとしてはfont-lockのタイミングでインライン画像用のオーバーレイを作成・削除しています。

org-modeでは、リンクのハイライトは org-activate-links で行われています。この関数の中ではリンクの種別が判定されてリンク文字列の範囲に様々なテキストプロパティが設定されます。

;; org.elより
(defun org-activate-links (limit)
  "Add link properties to links.
This includes angle, plain, and bracket links."
  (catch :exit
    (while (re-search-forward org-link-any-re limit t)
      (let* ((start (match-beginning 0))
             (end (match-end 0))
             (visible-start (or (match-beginning 3) (match-beginning 2)))
             (visible-end (or (match-end 3) (match-end 2)))
             (style (cond ((eq ?< (char-after start)) 'angle)
                          ((eq ?\[ (char-after (1+ start))) 'bracket)
                          (t 'plain))))
        (when (and (memq style org-highlight-links)
                   ;; Do not span over paragraph boundaries.
                   (not (string-match-p org-element-paragraph-separate
                                        (match-string 0)))
                   ;; Do not confuse plain links with tags.
                   (not (and (eq style 'plain)
                             (let ((face (get-text-property
                                          (max (1- start) (point-min)) 'face)))
                               (if (consp face) (memq 'org-tag face)
                                 (eq 'org-tag face))))))
          (let* ((link-object (save-excursion
                                (goto-char start)
                                (save-match-data (org-element-link-parser))))
                 (link (org-element-property :raw-link link-object))
                 (type (org-element-property :type link-object))
                 (path (org-element-property :path link-object))
                 (properties            ;for link's visible part
                  (list
                   'face (pcase (org-link-get-parameter type :face)
                           ((and (pred functionp) face) (funcall face path))
                           ((and (pred facep) face) face)
                           ((and (pred consp) face) face) ;anonymous
                           (_ 'org-link))
                   'mouse-face (or (org-link-get-parameter type :mouse-face)
                                   'highlight)
                   'keymap (or (org-link-get-parameter type :keymap)
                               org-mouse-map)
                   'help-echo (pcase (org-link-get-parameter type :help-echo)
                                ((and (pred stringp) echo) echo)
                                ((and (pred functionp) echo) echo)
                                (_ (concat "LINK: " link)))
                   'htmlize-link (pcase (org-link-get-parameter type
                                                                :htmlize-link)
                                   ((and (pred functionp) f) (funcall f))
                                   (_ `(:uri ,link)))
                   'font-lock-multiline t)))
            (org-remove-flyspell-overlays-in start end)
            (org-rear-nonsticky-at end)
            (if (not (eq 'bracket style))
                (add-text-properties start end properties)
              ;; Handle invisible parts in bracket links.
              (remove-text-properties start end '(invisible nil))
              (let ((hidden
                     (append `(invisible
                               ,(or (org-link-get-parameter type :display)
                                    'org-link))
                             properties)))
                (add-text-properties start visible-start hidden)
                (add-text-properties visible-start visible-end properties)
                (add-text-properties visible-end end hidden)
                (org-rear-nonsticky-at visible-start)
                (org-rear-nonsticky-at visible-end)))
            (let ((f (org-link-get-parameter type :activate-func)))
              (when (functionp f)
                (funcall f start end path (eq style 'bracket))))
            (throw :exit t)))))         ;signal success
    nil))

この org-activate-links を修正してテキストプロパティを設定するのと同時に画像のオーバーレイも生成しよう、と最初は思ったのですが、それだと消す方はどうするのだろうと疑問が浮かびました。現状でもリンク自体を修正すると画像は消えるようにできています。オーバーレイにmodification-hooksが設定されているので。でもリンク自体を修正せずにリンクでは無くなる場合、例えば画像へのリンクの前に# を入れてコメントアウトするとfaceはリンクの色からコメントの色に変わります。それと一緒に画像も消えて欲しいわけです。テキストプロパティは変わるわけですから、どこかでその処理があるはずです。ここで設定したテキストプロパティを消しているのはどこなんだろう……。

詳しく追うのは大変そうだな……と思ったときにふと目に入ったのが (org-remove-flyspell-overlays-in start end) の部分。これはおそらくflyspellのオーバーレイを削除するためのものでしょう。org.el内を検索するとあちこちで org-remove-flyspell-overlays-in が呼ばれていました。コメントの色づけをすると思わしき場所でも。おそらく範囲(start end)の分類が変わるときにはこの関数を呼び出して表示をリセットするのではないでしょうか。

そこで (org-remove-flyspell-overlays-in start end) をフックしてstartからendの間にあるオーバーレイを削除してみると、無事コメントアウトで画像が削除されました(笑)。

org-activate-links も内部で org-remove-flyspell-overlays-in を呼び出しますから、 org-activate-links ではなく org-remove-flyspell-overlays-in の方で指定された範囲の画像を更新してしまえば良いでしょう。 org-activate-links 経由かどうか判定できるようにして、そのときだけ処理を変えるようにしました。

org-flyimage-remove-flyspell-overlays-in 内のコメントがある部分ですが、tだとリンク更新時に必ずオーバーレイを作り直すので少しだけ遅くなります。ただ、 #+ATTR_HTML: :width で画像サイズを調整するときにリアルタイムで反映される(注意:org-image-actual-widthとimagemagickサポートによる)のでとりあえずこうしてあります。本来は org-display-inline-images の側でサイズの変更を検出して作り直すかどうか判断して欲しいところです。

今回の件とは直接関係ないですが、 org-display-inline-imagesorg-image-actual-width には画像の最大サイズを指定する機能が欲しいです。先に画像サイズを読み込んで計算してから再度画像を読み込まなければならないと思うので速度低下が心配ですが。

font-lock時に画像を読み込むのは速度低下の心配がありますが、極端に大きな画像で無ければ私の所では今のところ問題ないようです。気になるようなら遅延読み込みをした方が良いかもしれません。

大量の画像を整理して不要なものをコメントアウトしたりサイズを調整したりする作業
図1: 大量の画像を整理して不要なものをコメントアウトしたりサイズを調整したりする作業
2020-05-23

NTEmacsのWanderlustでメール送信時に時々SMTP errorが出る問題が解消?

Windows上のEmacsで Wanderlust を使っていて長年悩まされていたのがSMTP error。送信時に SMTP error と出て送信が中断してしまいます。何度か繰り返すと成功するのですが、たまに失敗時にも送れてしまうことがあるらしく何通も相手に送ってしまっている場合があります。

長年の経験で原因はどうも SSL/TLS まわりにあるらしいことが分かっています。SSL/TLSを使わないと発生しないので。Emacs組み込みのgnutlsを使うか外部のコマンドを使うかでも挙動が変わってきます。SSL/TLSを使うと受信時にも時々エラーが発生して何度か繰り返すとうまく行くことがあります。

この問題が解決するならばとGnusへの移行を試してみたこともあるのですが同じ問題が発生したので無意味でした。

今日少しこの問題を調査してみたのですが、結論から言うと

(setq gnutls-log-level 5)

を設定したらSMTP errorが出なくなりました。

な、なんだってー!

このコードはgnutlsのログレベルを上げて沢山のログを出すようにしただけです。

gnutls-log-levelを2にして出たログが次です。

Sending...
gnutls.c: [1] (Emacs) connecting to host: ****.****.ne.jp
gnutls.c: [1] (Emacs) allocating credentials
gnutls.c: [2] (Emacs) allocating x509 credentials
gnutls.c: [2] (Emacs) using default verification flags
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=CORP\\srv-build-cd.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=****.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=****.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=CORP\\srv-build-cd.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: OU=Copyright (c) 1997 Microsoft Corp.,OU=Microsoft Corporation,CN=Microsoft Root Authority.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=****.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: C=US,ST=California,L=Newark,O=Logitech Inc,CN=Logitech Inc.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: C=US,O=MSFT,CN=Microsoft Authenticode(tm) Root Authority.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=CORP\\srv-build-cd.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=CORP\\srv-build-cd.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=****.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=Root Agency.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [audit] There was a non-CA certificate in the trusted list: CN=dsalocal.intel.com.
gnutls.c: [1] (Emacs) setting the trustfile:  c:/****/certs/ca-bundle.crt
gnutls.c: [1] (Emacs) gnutls callbacks
gnutls.c: [1] (Emacs) gnutls_init
gnutls.c: [1] (Emacs) got non-default priority string: NORMAL:%DUMBFW
gnutls.c: [1] (Emacs) setting the priority string
gnutls.c: [2] added 6 protocols, 29 ciphersuites, 18 sig algos and 9 groups into priority list
gnutls.c: [2] Keeping ciphersuite 13.02 (GNUTLS_AES_256_GCM_SHA384)
gnutls.c: [2] Keeping ciphersuite 13.03 (GNUTLS_CHACHA20_POLY1305_SHA256)
gnutls.c: [2] Keeping ciphersuite 13.01 (GNUTLS_AES_128_GCM_SHA256)
gnutls.c: [2] Keeping ciphersuite 13.04 (GNUTLS_AES_128_CCM_SHA256)
gnutls.c: [2] Keeping ciphersuite c0.2c (GNUTLS_ECDHE_ECDSA_AES_256_GCM_SHA384)
gnutls.c: [2] Keeping ciphersuite cc.a9 (GNUTLS_ECDHE_ECDSA_CHACHA20_POLY1305)
gnutls.c: [2] Keeping ciphersuite c0.ad (GNUTLS_ECDHE_ECDSA_AES_256_CCM)
gnutls.c: [2] Keeping ciphersuite c0.0a (GNUTLS_ECDHE_ECDSA_AES_256_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite c0.2b (GNUTLS_ECDHE_ECDSA_AES_128_GCM_SHA256)
gnutls.c: [2] Keeping ciphersuite c0.ac (GNUTLS_ECDHE_ECDSA_AES_128_CCM)
gnutls.c: [2] Keeping ciphersuite c0.09 (GNUTLS_ECDHE_ECDSA_AES_128_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite c0.30 (GNUTLS_ECDHE_RSA_AES_256_GCM_SHA384)
gnutls.c: [2] Keeping ciphersuite cc.a8 (GNUTLS_ECDHE_RSA_CHACHA20_POLY1305)
gnutls.c: [2] Keeping ciphersuite c0.14 (GNUTLS_ECDHE_RSA_AES_256_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite c0.2f (GNUTLS_ECDHE_RSA_AES_128_GCM_SHA256)
gnutls.c: [2] Keeping ciphersuite c0.13 (GNUTLS_ECDHE_RSA_AES_128_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite 00.9d (GNUTLS_RSA_AES_256_GCM_SHA384)
gnutls.c: [2] Keeping ciphersuite c0.9d (GNUTLS_RSA_AES_256_CCM)
gnutls.c: [2] Keeping ciphersuite 00.35 (GNUTLS_RSA_AES_256_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite 00.9c (GNUTLS_RSA_AES_128_GCM_SHA256)
gnutls.c: [2] Keeping ciphersuite c0.9c (GNUTLS_RSA_AES_128_CCM)
gnutls.c: [2] Keeping ciphersuite 00.2f (GNUTLS_RSA_AES_128_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite 00.9f (GNUTLS_DHE_RSA_AES_256_GCM_SHA384)
gnutls.c: [2] Keeping ciphersuite cc.aa (GNUTLS_DHE_RSA_CHACHA20_POLY1305)
gnutls.c: [2] Keeping ciphersuite c0.9f (GNUTLS_DHE_RSA_AES_256_CCM)
gnutls.c: [2] Keeping ciphersuite 00.39 (GNUTLS_DHE_RSA_AES_256_CBC_SHA1)
gnutls.c: [2] Keeping ciphersuite 00.9e (GNUTLS_DHE_RSA_AES_128_GCM_SHA256)
gnutls.c: [2] Keeping ciphersuite c0.9e (GNUTLS_DHE_RSA_AES_128_CCM)
gnutls.c: [2] Keeping ciphersuite 00.33 (GNUTLS_DHE_RSA_AES_128_CBC_SHA1)
gnutls.c: [2] Advertizing version 3.4
gnutls.c: [2] Advertizing version 3.3
gnutls.c: [2] Advertizing version 3.2
gnutls.c: [2] Advertizing version 3.1
gnutls.c: [2] HSK[0000000005c5d140]: sent server name: '****.****.ne.jp'
gnutls.c: [1] (Emacs) non-fatal error: Resource temporarily unavailable, try again. [73 times]
gnutls.c: [audit] FFDHE groups advertised, but server didn't support it; falling back to server's choice
gnutls.c: [1] (Emacs) non-fatal error: Resource temporarily unavailable, try again. [295 times]
gnutls.c: [2] (Emacs) Deallocating x509 credentials
Invalid response: ... Recipient ok
Invalid response: RCPT TO:<****@****.jp>
wl-draft-send-mail-with-smtp: SMTP error
SMTP error

「non-fatal error: Resource temporarily unavailable, try again.」というのが73回+295回も繰り返されているのが気になります。

このエラーメッセージはgnutlsのGNUTLS_E_AGAINに対応するもののようです。

GNUTLS_E_AGAINはあちこちで発生するようです。

もう少し詳細なログが出ないかgnutls-log-levelを5にして再度試してみたところ、何回繰り返してもエラーが発生しません。

ログを見ると次のように大量のretryメッセージが出ていました。

gnutls.c: [3] ASSERT: ../../../gnutls-3.6.10/lib/nettle/mpi.c[wrap_nettle_mpi_print]:60
gnutls.c: [4] HSK[00000000021cc030]: CLIENT KEY EXCHANGE was queued [262 bytes]
gnutls.c: [4] REC[00000000021cc030]: Sent ChangeCipherSpec
gnutls.c: [5] REC[00000000021cc030]: Initializing epoch #1
gnutls.c: [5] REC[00000000021cc030]: Epoch #1 ready
gnutls.c: [4] HSK[00000000021cc030]: Cipher Suite: GNUTLS_DHE_RSA_AES_256_GCM_SHA384
gnutls.c: [4] HSK[00000000021cc030]: Initializing internal [write] cipher sessions
gnutls.c: [4] HSK[00000000021cc030]: recording tls-unique CB (send)
gnutls.c: [4] HSK[00000000021cc030]: FINISHED was queued [16 bytes]
gnutls.c: [5] REC[00000000021cc030]: Preparing Packet Handshake(22) with length: 7 and min pad: 0
gnutls.c: [5] REC[00000000021cc030]: Sent Packet[2] Handshake(22) in epoch 0 and length: 12
gnutls.c: [5] REC[00000000021cc030]: Preparing Packet Handshake(22) with length: 262 and min pad: 0
gnutls.c: [5] REC[00000000021cc030]: Sent Packet[3] Handshake(22) in epoch 0 and length: 267
gnutls.c: [5] REC[00000000021cc030]: Preparing Packet ChangeCipherSpec(20) with length: 1 and min pad: 0
gnutls.c: [5] REC[00000000021cc030]: Sent Packet[4] ChangeCipherSpec(20) in epoch 0 and length: 6
gnutls.c: [5] REC[00000000021cc030]: Preparing Packet Handshake(22) with length: 16 and min pad: 0
gnutls.c: [5] REC[00000000021cc030]: Sent Packet[1] Handshake(22) in epoch 1 and length: 45
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[get_last_packet]:1168
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[_gnutls_io_read_buffered]:589
gnutls.c: [3] (Emacs) retry: Resource temporarily unavailable, try again.
gnutls.c: [1] (Emacs) non-fatal error: Resource temporarily unavailable, try again.
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[get_last_packet]:1168
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[_gnutls_io_read_buffered]:589
gnutls.c: [3] (Emacs) retry: Resource temporarily unavailable, try again.
gnutls.c: [1] (Emacs) non-fatal error: Resource temporarily unavailable, try again.
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[get_last_packet]:1168
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[_gnutls_io_read_buffered]:589
gnutls.c: [3] (Emacs) retry: Resource temporarily unavailable, try again.
gnutls.c: [1] (Emacs) non-fatal error: Resource temporarily unavailable, try again.
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[get_last_packet]:1168
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[_gnutls_io_read_buffered]:589
gnutls.c: [3] (Emacs) retry: Resource temporarily unavailable, try again.
gnutls.c: [1] (Emacs) non-fatal error: Resource temporarily unavailable, try again.
gnutls.c: [3] ASSERT: ../../gnutls-3.6.10/lib/buffers.c[get_last_packet]:1168
....繰り返し

これは推測なのですが、リトライ時のメッセージ出力量が増えたことによってリトライの間隔が延びたことが影響しているのではないでしょうか。これらのメッセージは逐一ミニバッファに出るので体感できるくらいには遅くなります。

(setq gnutls-log-level 5) を設定したまま1~2日ほど使ってみましたが今のところエラーは発生していません。

これ以上深くは追っていないのですが、またエラーが発生したら調べてみようと思います。

2020-06-21追記:未だにエラーが出ません。

2020-05-06

JavaScriptとCSSの遅延読み込み

ブログにJavaScriptものを貼るときは色々気を使うんですよね。ブログ全体のheadにscriptタグを直接書くのは嫌ですし、エントリーにscriptタグを直接書いても良いのですが同じスクリプトを使うエントリーが複数同じページに表示されたときに二重に読み込んでしまうのは困ります。また、使用箇所がまだ表示されていないのに読み込んでしまうとサイトが重くなってしまいます。このサーバ、かなり遅いみたいですし。

というわけで遅延読み込みの仕組みを作ってみました。要素が画面内に入ったら指定されたスクリプトやcssを読み込みます。

//
// 一応IE11でも動くように作っています。IE8はaddEventListenerがないので動きません。
//
(function(w, d){
    // Array.prototype.forEach.call(a, f)の代わり
    var each=function(a,f){for(var i=0;i<a.length;++i){f(a[i]);}};
    // Promiseもどき。対応しているならPrms=Promiseでも良い。
    //var Prms = Promise;
    function Prms(f){
        var thenCb;//複数必要ならthens=[]で。thenではthens.push(cb)、succではeach(thens,function(thenCb){...})
        this.then=function(cb){
            thenCb=cb;
            return new Prms(function(succ){
                cb.nextCb=succ;
            });
        };
        var succ=function(result){
            setTimeout(function(){
                if(typeof thenCb=="function"){
                    var next=thenCb(result);
                    if(next&&thenCb.nextCb){next.then(thenCb.nextCb);}
                }
            },0);
        };
        f(succ);
    }
    Prms.resolve = function(){
        return new Prms(function(succ){succ();});
    };
    Prms.all = function(arr){
        return new Prms(function(succ){
            var count = arr.length;
            function onSucc(){
                if(--count == 0){
                    succ();
                }
            }
            each(arr,function(e){e.then(onSucc);});
        });
    };

    // 指定されたurlを読み込むタグ(cssならlink、それ以外ならscript)を
    // headへ追加して読み込みが終わったら解決するPromiseを返します。
    //
    // 配列を指定した場合は sequentially の指定によって処理が変わります。
    // sequentially が false なら同時に読み込みます。
    // sequentially が true なら先頭から順番に読み込みます。
    //
    // 配列内の配列も読み込みますが、sequentiallyが反転します。
    // 例えばload([a, [c, d, [e, f]], g], false)の場合、
    // - a, c, gは同時
    // - dはcの後
    // - e, fはdの後
    // に読み込みます。
    //
    function load(url, sequentially){
        //console.log("load(" + url + " " + (sequentially ? "sequentially" : "parallel") + ")");
        if(typeof url=="string"){
            return new Prms(function(succ){
                // 既に追加されている<link rel=stylesheet>、<script>要素を列挙する。
                //
                // この関数が追加した要素には.isUrlLoadingが設定されていて、
                // trueなら読み込み中。falseなら読み込み済み。
                // 他で追加した要素は読み込み済みか判定する方法が見当たらない
                // ので、読み込み済みと判断する。
                //
                // loadのたびに毎回探し直す必要がある。
                // sequentiallyの場合は前のloadが実行されるタイミングで
                // <script>や<link>が追加されるので。
                var es={};//elements
                each(d.getElementsByTagName("link"),function(link){if(link.getAttribute("rel")=="stylesheet"){es[link.getAttribute("href")]=link;}});
                each(d.getElementsByTagName("script"),function(script){es[script.getAttribute("src")]=script;});
                var head=d.head||d.getElementsByTagName("head")[0];

                var e=es[url];//既に追加済みのelementがあるなら取得
                if(e){
                    //console.log("already added " + url);
                    // すでに追加されている場合
                    if(e.isUrlLoading){
                        // 読み込み中の場合
                        var old = e.onload;
                        e.onload = old ? function(){old(); succ();} : succ; //フックする
                    }
                    else{
                        // 読み込み済みまたは不明な場合
                        //console.log("already loaded? " + url);
                        succ();
                    }
                }
                else{
                    if(/\.css/.test(url)){
                        // .cssの場合
                        e=d.createElement("link");
                        e.isUrlLoading=true;
                        e.rel="stylesheet";
                        e.type="text/css";
                        e.href=url;
                    }
                    else{
                        // その他は.jsと仮定
                        e=d.createElement("script");
                        e.isUrlLoading=true;
                        e.type="text/javascript";
                        e.src=url;
                    }
                    function onLoad(ev){
                        if(e.isUrlLoading){
                            console.log("loaded: " + url); //この関数で追加した要素が読み込み完了。
                            e.isUrlLoading=false;
                            succ();
                        }
                    }
                    e.onload = onLoad;
                    //e.onreadystatechange= はIE11のエミュレーションによればIE9以降不要。IE8はaddEventListenerに対応していないほどなのでいいや。
                    head.appendChild(e);
                }
            });
        }
        else if(url instanceof Array){
            // 配列の場合
            if(sequentially){
                // 先頭から一つずつ読み込み
                return new Prms(function(succ){
                    function next(){
                        if(url.length == 0){
                            succ();
                        }
                        else{
                            //console.log("start load " + url[0]);
                            load(url.shift(), false).then(next);
                        }
                    }
                    next();
                });
            }
            else{
                // 同時に読み込み
                // mapが使えるならreturn Prms.all(url.map(u=>load(u, true)));
                var prmss = [];
                each(url, function(u){
                    //console.log("start load " + u + " parallel");
                    prmss.push(load(u, true));});
                return Prms.all(prmss);
            }
        }
        /*
          else if(typeof url=="function"){
          // 関数の場合、実行したらPromiseを返すものと仮定
          return url();
          }
          else if(url instanceof Prms){
          // Promiseはそのまま
          return url;
          }
        */
        else{
            throw new Error("Unknown url type");
            return null;
        }
    }

    function onViewport(elem){
        return new Prms(function(succ){
            // scrollイベントを使う。本当はIntersectionObserverを使いたい。
            function onScroll(ev){
                var MARGIN=50;
                var rect=elem.getBoundingClientRect();
                if(rect.bottom+MARGIN>=0&&rect.top-MARGIN<=(w.innerHeight||d.documentElement.clientHeight)){
                    w.removeEventListener("load",onScroll,false);
                    w.removeEventListener("scroll",onScroll,false);
                    succ(elem);
                }
            }
            w.addEventListener("load",onScroll,false);
            w.addEventListener("scroll",onScroll,false);
        });
    }

    function loadScriptOnViewport(elem, urls){
        if(typeof elem == "string"){
            elem = d.getElementById(elem);
        }
        return new Prms(function(succ){
            onViewport(elem).then(function(elem){
                load(urls).then(function(){succ(elem);});
            });
        });
    }
    w.loadScriptOnViewport = loadScriptOnViewport;
})(window, document);

これを次のように使います。

var div = document.createElement("div");
document.currentScript.parentNode.appendChild(div);
//divが画面内に入ったらigo.css, igo.js, igo_view.jsを読み込む。igo.jsとigo_view.jsは順番に読み込む。
loadScriptOnViewport(div, ["igo.css", ["igo.js", "igo_view.js"]]).then(){
   div.appendChild((new GameView()).rootElement);
};

ファイルは配列で読み込む順番を指定出来ます。cssとjsは同時に読み込んでも問題ありませんがjsは順番を守る必要があるケースが多々あるので。

IEを切り捨てて良いなら色々と短くできる箇所があると思います。Promiseもアロー関数も使えますし。読み込むスクリプトがIE非対応ならここで対応する意味はありません。少し切り詰めてminifyかけたら1260文字くらいになりました。昨日の詰碁で使用しています。

2020-05-06 ,

オシツブシ

実戦でオシツブシが決まると気持ちいいね! 囲碁クエスト9路盤本日の対局より。

それで実際の所これ活きてたの?

(注意:↑↑に画面内に入ったらigo.js等を読み込んで、読み込み終わったら盤面を表示するスクリプトが仕込んであります。表示まで時間がかかるのはサーバが重いんじゃないかな……?)

2020-05-02 ,

JavaScript碁盤

JavaScript碁盤を書きました。

実行
ソースコード
https://github.com/misohena/js_igo

最近囲碁クエストにはまっているのですが、ブラウザ版には終局後の検討機能がありません。検討機能は終局後に巻き戻して途中から自分で好きに石を並べられるモードです。終局後に「あそこは何かあったんじゃないかな」「防ぐ手はなかったかな」などと思うことは良くありますが、ブラウザでプレイするとそれができません。Android版にはちゃんと検討機能がありますし過去の対局を振り返って検討することもできるのですが、デスクで作業しているときにいちいちスマホに手を伸ばしたくありません。

幸い棋譜(SGF)のエクスポート機能(ブラウザのテキスト領域に表示してコピーできる)はあるので、何か適当なソフトで読み込めば検討はできます。CGobanを入れてみたのですが、コピーしたSGFテキストを一度ファイルに保存してから読み込まなければならず面倒です。同じブラウザでSGFをペーストして読み込める碁盤があれば便利だなと思ったので作ることにしました。同じようなものは探せばいくらでもあるみたいですが、探すのも面倒ですし作るのも勉強になりますので。

基本的なロジックは10年以上前に作ったものがあったので流用しつつ現代風にアレンジ&機能追加しました。

盤面のデータ構造はUint32Arrayに1交点2ビットずつ詰め込む形を採用。9路盤を2*9*9=162ビット、162/32=5.0625で6dwords(24バイト)で表現出来ます。最小を目指すなら3(空点、黒、白)の倍数で記録していくべきなのですがさすがにそれはやりすぎかなと。盤外を表す点を用意すべきか等色々トレードオフがあってよく分からないのですが、盤面を沢山複製して保持するときにできるだけ小さい方がいいだろう、ということで。ハッシュも計算しやすそうですし。ただ、今回の用途までならどうやっても問題なし。

画像で表示するのも馬鹿らしいのでSVGで表示。グラデーションと影をつけてまあまあの見た目になりました。

履歴は最初からツリー構造を採用。巻き戻してから他の場所に打ったときは別のノードを作って記録します。分岐はA, B, C,…と盤面に表示できるようにしました。他のツールのようにツリー構造全体を図で表示出来たらかっこよかったのですが、これでも十分実用にはなります。

SGFインポート/エクスポート機能は今回の目的では必須。SGFフォーマットのページを見たらEBNFが書いてあったのでそれを元に手書きで解析器を作成。プロパティは、囲碁クエストのSGFを読むだけならSZ, B, Wくらいに対応すれば十分。後から詰碁を表示したいと思ってPL, AB, AW, AE, C, その他マーク等にも対応しました。

詰碁に対応するためにコメント機能、フリー編集機能、先番設定機能を追加。

一応スマホでも使えるようにタッチイベントに対応。19x19はさすがに小さいですね。ボタン類も一緒に小さくなってしまうので盤面部分だけ拡大縮小出来るようになるとよいのですが……。(→追記:対応しました)

あ、結果の判定機能はありません。死活判定しなければならないので難しいですよね……。死に石を指定出来るようにするとか、純碁みたいに全部打ち切るなら判定出来るのですが……。今後の大きな課題です。

純碁と言えば王銘エン先生の「こんなに面白い 世界の囲碁ルール」は面白かったです。オススメ。

ちなみに囲碁クエストをはじめたのは、知り合いが将棋ウォーズをやっていたからです。将棋は子供の頃に父親にボコボコにされたトラウマがあるのでやらないことにしているのです。なので囲碁ウォーズを試してみたのですが、Android版アプリの出来がすこぶる悪い。何回やっても起動しなくなるのです。時々思い出したようにすんなり起動するのですが、アプリを終了するとまた起動しなくなります。何が原因なのかまったく分かりません。調べてみると囲碁クエストという別のアプリの方が安定しているようなのでそちらをはじめました。

どちらもメインは9路盤のようです。19路のオンライン対局は大変なのでとてもやる気が起きませんが、9路なら短時間でプレイできるのでオススメです。

一問一答! 囲碁・9路盤の手筋 ~基本定石からヨセまで~ (囲碁人ブックス)」という本も買ってみたのですが、これはちょっと難しいですね。難易度が低い問題が分からなくて気を落としていたら難易度が中くらいのものがあっさり分かってしまったり。巻末の引き分け定石は参考になります。手順番号の着いた棋譜を脳内再生するのは苦手なのですが、今回作った碁盤を使って勉強してみようと思います。

Web上も探すと9路の情報が結構ありますね。Youtube動画もあります。

2020-04-22

2020年春の新番組

かぐや様2期くらいかなぁ。

印象 開始日時 チャネル タイトル
03/26(木) Netflix 7SEEDS 第2期
03/27(金) 24:00 TOKYO MX GRANBLUE FANTASY The Animation Season2
03/28(土) 11:00 YouTube アイカツオンパレード!
03/30(月) 18:10 NHK Eテレ 忍たま乱太郎 第28シリーズ
04/01(水) 24:30 TOKYO MX 神之塔 -Tower of God-
04/01(水) 07:30 テレビ東京系 みっちりわんこ!あにめ~しょん
04/01(水) 18:00 NHK Eテレ おじゃる丸 第23シリーズ
04/02(木) 17:20 NHK Eテレ のりものまん
04/02(木) 22:30 TOKYO MX 八男って、それはないでしょう!
× 04/02(木) 23:30 TOKYO MX 球詠
04/02(木) 24:00 TOKYO MX かくしごと
04/03(金) 16:00 公式ポータルサイト ベイブレードバースト スパーキング
04/03(金) 17:00 YouTube(爆丸公式Ch) 爆丸アーマードアライアンス
04/03(金) 18:20 NHK Eテレ あはれ!名作くん シーズン5
× 04/03(金) 22:00 TOKYO MX 新サクラ大戦 the Animation
04/03(金) 25:23 テレビ東京 文豪とアルケミスト
04/03(金) 25:55 TBS LISTENERS リスナーズ
04/03(金) 26:25 TBS 波よ聞いてくれ
04/04(土) 07:30 テレビ東京系 遊☆戯☆王 SEVENS
04/04(土) 09:00 NHK Eテレ おしりたんてい 新シリーズ
04/04(土) 17:35 NHK Eテレ MAJOR 2nd 第2シリーズ
04/04(土) 22:00 TOKYO MX アルテ
04/04(土) 25:00 TOKYO MX ギャルと恐竜
04/04(土) 25:30 TOKYO MX 乙女ゲームの破滅フラグしかない悪役令嬢に転生してしまった…
04/04(土) 25:30 テレビ朝日 イエスタデイをうたって
04/05(日) 07:30 テレビ東京系 ディズニー・サンデー ラプンツェル ザ・シリーズ
04/05(日) 09:00 フジテレビ デジモンアドベンチャー:
× 04/05(日) 09:30 テレビ東京系 トミカ絆合体 アースグランナー
04/05(日) 10:30 テレビ東京系 ミュークルドリーミー
04/05(日) 19:00 NHK Eテレ もっと!まじめにふまじめ かいけつゾロリ
04/05(日) 22:00 TOKYO MX 継つぐもも
04/05(日) 22:30 TOKYO MX アイドリッシュセブン Second BEAT!
04/05(日) 23:30 TOKYO MX グレイプニル
04/05(日) 24:15 NHK総合 キングダム 第3シリーズ
04/05(日) 24:30 TOKYO MX 社長、バトルの時間です!
04/05(日) 25:00 TOKYO MX 俺の指で乱れろ。~閉店後二人きりのサロンで…~
04/06(月) 07:05 テレビ東京系 ガル学。~聖ガールズスクエア学院~
× 04/06(月) 17:55 テレビ東京系 ファンファンキティ!
04/06(月) 22:30 TOKYO MX 白猫プロジェクト ZERO CHRONICLE
04/06(月) 22:50 NHK Eテレ 銀河英雄伝説 Die Neue These(NHK版)
04/06(月) 23:29 BS日テレ ぽっこりーず
04/06(月) 24:30 TOKYO MX プリンセスコネクト!Re:Dive
04/06(月) 25:00 チバテレビ あの世のすべては、おばけぐみ
04/06(月) 25:15 TOKYO MX 邪神ちゃんドロップキック'
04/06(月) 25:30 テレビ東京 フルーツバスケット 新作 2nd season
04/07(火) 07:30 テレビ東京系 おりがみにんじゃ コーヤン
04/07(火) 17:55 テレビ東京系 シャドウバース
04/07(火) 24:30 TOKYO MX 放課後ていぼう日誌
04/07(火) 25:35 TOKYO MX 無限の住人-IMMORTAL-
04/08(水) 07:20頃 テレビ東京系 おばけずかん
04/08(水) 22:00 TOKYO MX 本好きの下剋上 司書になるためには手段を選んでいられません 第2部
04/08(水) 23:29 BS日テレ 困ったじいさん
04/08(水) 24:55 フジテレビ BNA
04/09(木) 25:10 フジテレビ 富豪刑事 Balance:UNLIMITED
04/09(木) 25:28 TBS ノー・ガンズ・ライフ 第2期
04/09(木) 25:58 TBS やはり俺の青春ラブコメはまちがっている。完
  04/10(金) 21:00 YouTube 魔神英雄伝ワタル 七魂の龍神丸
04/10(金) 20:00 ニコニコCh ざしきわらしのタタミちゃん
04/10(金) 22:30 TOKYO MX 天晴爛漫!
04/10(金) 24:30 TOKYO MX 食戟のソーマ 豪の皿
04/10(金) 25:25 TBS系 アルゴナビス from BanG Dream! ANIMATION
04/11(土) 17:30 日本テレビ系 ハクション大魔王2020
04/11(土) 19:00 TOKYO MX ガンダムビルドダイバーズRe:RISE 2nd Season
04/11(土) 23:30 TOKYO MX かぐや様は告らせたい?
04/12(日) 23:00 TOKYO MX ULTRAMAN
04/13(月) 23:00 TOKYO MX 啄木鳥探偵處
04/16(木) 19:53 テレビ東京 テレビ野郎 ナナーナ 怪物クラーケンを追え!
04/20(月) 21:54 TOKYO MX オリンピア・キュクロス
04/23(木) Netflix 攻殻機動隊 SAC_2045
04/25(土) 08:00 テレビ東京系 ヴァンガード 外伝 イフ-if-
04/25(土) 24:00 TOKYO MX ソードアート・オンライン-アリシゼーション- War of Underworld -THE LAST SEASON-
–/–(日) 08:30 テレビ東京系 デュエル・マスターズキング
2020-04-15 ,

org-mode文書をタグなどで検索したときにエントリの中身(本文)を全部表示する(Org 9.3.6)

org-modeで書かれた文書を org-match-sparse-tree を使ってをタグで検索する(Sparse Tree を作る)とマッチしたヘッドラインだけが表示されるのですが、そのとき本文も全て一括で表示して欲しかったのでどうしたらよいのか調べてみました。

例えば DIARY タグで検索して日記を通して読みたいときにいちいち一つ一つのヘッドラインをTABキーで展開してまわるのは面倒です。かといってS-TABだとマッチしていない部分の本文も展開されてしまいます。

マニュアルのSparse Treeの項目を見てもマッチしたエントリを移動する操作くらいしか書いていませんでした。

Sparse Trees (The Org Manual)

しかたがないので org-match-sparse-tree 関数(org.el内)を調べてみることにしました。

org-match-sparse-tree の亜種を作る

org-match-sparse-tree は内部で org-scan-tags を呼んでいます。 org-scan-tags はその名の通りの文書をスキャンしてマッチする部分を巡回する関数です。マッチしたところで何をするかは引数 action で指定することになっていて、 org-match-sparse-tree では 'sparse-tree を渡してマッチした場所をハイライトしたりヘッドラインを表示したりしています。 action には任意の関数も渡せるようになっているので、そこにサブツリーを表示する関数を渡してマッチした部分を子孫も含めて全て表示させることにしました。

org-match-sparse-tree のコードをベースに修正してみます。

;; org-match-sparse-treeをベースに修正
(defun org-match-sparse-tree-show-subtree (&optional todo-only match)
  "Create a sparse tree according to tags string MATCH.

MATCH is a string with match syntax.  It can contain a selection
of tags (\"+work+urgent-boss\"), properties (\"LEVEL>3\"), and
TODO keywords (\"TODO=\\\"WAITING\\\"\") or a combination of
those.  See the manual for details.

If optional argument TODO-ONLY is non-nil, only select lines that
are also TODO tasks."
  (interactive "P")
  (org-agenda-prepare-buffers (list (current-buffer)))
  (let ((org--matcher-tags-todo-only todo-only))

    ;; 修正点ここから
    ;; まずはトップレベル以外は全て非表示にする。ハイライトももしあれば解除する。
    (org-overview)
    (org-remove-occur-highlights)
    ;; 修正点ここまで

    (org-scan-tags
     ;; 修正点ここから
     ;; マッチしたところをハイライトしてからサブツリーを表示する
     (lambda ()
       (and org-highlight-sparse-tree-matches
            (org-get-heading) (match-end 0)
            (org-highlight-new-match
             (match-beginning 1) (match-end 1)))

       (org-show-subtree) )
     ;; 修正点ここまで
     (cdr (org-make-tags-matcher match))
     org--matcher-tags-todo-only)))

元々の (org-scan-tags 'sparse-tree ....) では、マッチした場所で (org-show-context 'tags-tree) を呼び出しており、 (org-show-context)(org-show-set-visibility) を呼び出してその場所を表示状態にします。どのような表示を行うかは org-show-context-detail 変数でカスタマイズできるのですがサブツリー全体を表示するというオプションはありません。 'local が一番近いのですが、なぜか次のエントリーのヘッドラインまで表示してしまいます(ドキュメントにもそう書いてあるので意図した動作のようです)。他のオプションには if point is not on headline, also show entry and all children 等と書いてあり、ヘッドライン外にポイントがないと内容は表示してくれません。しかたがないので (org-show-context) の代わりに直接 (org-show-subtree) を呼ぶようにしてあります。

一時的に org-show-context をオーバーライド

org-match-sparse-tree の実行中、 (org-show-context) を一時的にオーバーライドしても結果は同じです。こちらの方がシンプルでしょうか?

(require 'cl)
(defun org-match-sparse-tree-show-subtree (&optional todo-only match)
  (interactive "P")
  (flet ((org-show-context (&optional key) (org-show-subtree)))
    (org-match-sparse-tree todo-only match)))

fletで一時的にorg-show-contextをオーバーライドしてみました。

マッチしたところだけ後から展開する

これでマッチした全エントリーの中身(サブツリー)を表示出来たのですが通常の動作との使い分けが面倒なことに気がつきました。最初から本文を全部読みたいと思って検索するなら上で定義した (org-match-sparse-tree-show-subtree) を使えば良いのですが「とりあえずタグで検索 → 一覧が出る → 一覧を全部展開」という手順の方が自然な気がします。

本当に欲しかったのはS-TABでマッチした(ハイライトした)エントリだけをvisibility cyclingすることなのではないでしょうか。

マッチした部分は (org-occur-next-match) という関数で巡回出来ます(M-g n や M-g p で移動するときに使われます)。マッチした部分のハイライトは (org-highlight-new-match beg end) で行われています。これは org-occur のためにある関数のようですが指定された範囲にオーバーレイを適用して、オーバーレイを org-occur-highlights リストに追加します。検索後にC-c C-cでハイライトが消えるのは (org-ctrl-c-ctrl-c) 内で org-occur-highlights リストに何か入っているときは (org-remove-occur-highlights) を呼んでいるからだったりします。 (org-occur-next-match) で次のマッチ、前のマッチに移動出来るのはオーバーレイの 'org-type プロパティに値 'org-occur が設定されているからで、その値を検索して移動しているようです。

なので (org-occur-next-match) でハイライト部分を巡回して (org-show-subtree) しまくればマッチしたエントリーの内容をサブツリーを含めて表示出来ます。

(defun org-show-occur-highlights-subtree ()
  (interactive)
  (save-excursion
    (goto-char (point-min))

    (while (ignore-errors (org-occur-next-match 1))
      (org-show-subtree))))

本当は S-TAB をオーバーライドしてハイライトがあるときはハイライトされている部分だけを展開したり折りたたんだりできればいいのですが現在の展開状態を調べる必要があるので面倒そうなので今日はここまでにしておきます。

(2020-04-16追記)S-TABでマッチしたエントリのみを展開したり閉じたりする

ここまでで一応検索結果(Sparse Tree)の本文を一括で表示(展開)できるようになったのですが、やっぱり S-TAB に割り当てて検索結果を開いたり閉じたりしたい! ということで引き続き調べてみました。

まず、既存の S-TAB がどのようになっているのか調べる必要があります。

調べてみた結果、次のようなフローになっていることが分かりました。複雑ですね。

S-TAB => org-shifttab => org-global-cycle => (org-cycle '(4)) => org-cycle-internal-global

C-u 引数によって多少流れが変わることもあるかもしれませんが最終的には org-cycle-internal-global が呼ばれるようです。

org-cycle-internal-global関数は切り替えの処理を行いますが最初はOVERVIEWで表示し、同じコマンドが連続して呼ばれたときに => CONTENTS => ALL => OVERVIEW … の順で表示を切り替える実装になっています。私はこれまで気がついていなかったのですが現在の状態から次の状態へ変わるわけではないんですね。例えば他の作業をした後にS-TABを3回押せばどんな状態からでも必ずALLになるわけです。

実際の表示状態の変更は次の関数が行います。

org-overview
トップレベルのヘッドラインだけ表示
org-content
全てのヘッドラインだけを表示
org-show-all
内容も含めて全て表示(ドロワーなど一部を除く)

ハイライトがあるとき(Sparse Tree表示時)の動作はこれらの関数を修正すべきでしょう。S-TABだけでなくこれらの関数を直接呼び出すこともあるでしょうし。

……と思ったのですが、これらの関数がハイライトがあるときにどのような動作をすべきか考えたのですがなかなか一貫性のある挙動を決めることができませんでした。

org-overview
このまま? ハイライトされている部分だけを表示?(デフォルトの表示って事?)
org-content
ハイライトされているエントリ以下の全てのヘッドラインを表示? org-occurでマッチしたヘッドライン以外の部分はどうする?
org-show-all
ハイライトされているエントリ以下の全てを表示?

org-occurがあるのでハイライトされるのはヘッドラインだけとは限らないところが問題を難しくします。Sparse Treeを表示した直後に1回S-TABを押したときに同じ表示になる(何も変わらない)のも避けたいところです。

仕方が無いので既存の動作との一貫性は脇に置いてSparse Tree表示時の特殊な挙動として仕様を決めることにしました。

org-sparse-tree-show-matched
マッチしたところを表示(デフォルト)
org-sparse-tree-show-all
サブツリーを含めて全て表示

ハイライトがあるときは、この二つと既存の三つをS-TABで切り替えることにします。

OVERVIEW => SPARSE-TREE-SHOW-MATCHED => SPARSE-TREE-SHOW-ALL => CONTENTS => ALL

つまり2回S-TABすればマッチした箇所(タグ検索の場合はヘッドラインのみ)が、3回S-TABすれば本文まで展開してくれるわけです。既存の操作感覚に割と近くなっていると思います。マッチした部分に限定されたくない場合に備えて、4回S-TABすればCONTENTS、5回S-TABすればALLとなるようにしました。

;; org-sparse-tree-cycle.el
;; https://misohena.jp/blog/2020-04-15-show-contents-of-entry-when-sparse-tree-in-org-mode.html
;;

(defun org-sparse-tree-show-all ()
  "マッチした部分をサブツリーを含めて表示します。"
  (interactive)
  (save-excursion
    (org-overview)

    (goto-char (point-min))
    (while (ignore-errors (org-occur-next-match 1))
      (org-show-context 'tags-tree)
      (org-show-subtree))))

(defun org-sparse-tree-show-matched ()
  "マッチした部分を表示します。"
  (interactive)
  (save-excursion
    (org-overview)

    (goto-char (point-min))
    (while (ignore-errors (org-occur-next-match 1))
      (org-show-context 'tags-tree))))

;; この関数は org-cycle-internal-global を元に修正しています。
(defun org-sparse-tree-cycle-internal ()
  "Do the sparse tree cycling action."
  ;; Rotate overview => sparse-tree-show-matched => sparse-tree-show-all => contents => show all

  ;; Hack to avoid display of messages for .org  attachments in Gnus
  (let ((ga (string-match "\\*fontification" (buffer-name))))
    (cond
     ((and (eq last-command this-command)
           (eq org-cycle-global-status 'sparse-tree-show-all))
      ;; We just created the overview - now do table of contents
      ;; This can be slow in very large buffers, so indicate action
      (run-hook-with-args 'org-pre-cycle-hook 'contents)
      (unless ga (org-unlogged-message "CONTENTS..."))
      (org-content)
      (unless ga (org-unlogged-message "CONTENTS...done"))
      (setq org-cycle-global-status 'contents)
      (run-hook-with-args 'org-cycle-hook 'contents))

     ((and (eq last-command this-command)
           (eq org-cycle-global-status 'contents))
      ;; We just showed the table of contents - now show everything
      (run-hook-with-args 'org-pre-cycle-hook 'all)
      (org-show-all '(headings blocks))
      (unless ga (org-unlogged-message "SHOW ALL"))
      (setq org-cycle-global-status 'all)
      (run-hook-with-args 'org-cycle-hook 'all))

     ((and (eq last-command this-command)
      ;; sparse tree show matched
           (eq org-cycle-global-status 'overview))
      (org-sparse-tree-show-matched)
      (setq org-cycle-global-status 'sparse-tree-show-matched))

     ((and (eq last-command this-command)
           (eq org-cycle-global-status 'sparse-tree-show-matched))
      ;; sparse tree show all
      (org-sparse-tree-show-all)
      (setq org-cycle-global-status 'sparse-tree-show-all))

     (t
      ;; overview
      (run-hook-with-args 'org-pre-cycle-hook 'overview)
      (org-overview)
      (unless ga (org-unlogged-message "OVERVIEW"))
      (setq org-cycle-global-status 'overview)
      (run-hook-with-args 'org-cycle-hook 'overview)))))

(defun org-sparse-tree-shifttab-advice (oldfun arg)
  (if org-occur-highlights
      ;; ハイライトがある
      (org-sparse-tree-cycle-internal)
    ;; 元の関数を呼ぶ
    (funcall oldfun arg)))

;; org-shifttabをオーバーライドする
(advice-add 'org-shifttab :around #'org-sparse-tree-shifttab-advice)

うん、これでSparse Treeが大分使いやすくなりました。