トップ 追記

KeN's GNU/Linux Diary


2018年04月24日

_ [computer] 写真テスト - 端末編

とりあえずインスタに保持するとして…

メイン利用のぜんぽん3。これはひどい……

あいぽん6。うーん、まぁまぁ許容範囲?

もとぜっと。ぜんぽんよりはマシだが、なんか暗い。

ということで、この中では事前に想像していたとおりあいぽんが一番マシ? 設定すればうんぬんなどもあるのかもしれないが。イチデジからアップロードしたい……。

_ [computer] 埋め込み

そういえばFlashAirだとどうなんだっけ、と久々に入れてみたが使い方をすっかり忘れてしまい、パスフレーズから悩むことに。

そうそう、これは小さなAP&Webサーバで、スマホをWiFi接続するんだった。つなげはしたものの、画像を表示はできるがダウンロードもなんもできない……。

公式アプリのほうを入れないとダメ、というオチだった。

いざ写真を選んで共有経由でアップロードをしようとすると、カメラAPはインターネットにつながっていないので当然接続できず、普段のAPにつなぎ直し……でガチャガチャと面倒なことに。よくよく見直したら、インターネット用に同時接続するAPをFlashAir側に設定できるので、これに入力すればよいだけだった。 そういえば昔そんな設定をしたことがあったなーと思い出してきた。

ずぼんさんにツッコミを受けたが、tDiaryでのInstagramプラグインだとスマホのような狭い画面で悲惨なことになるようだ。たしかに見てみたら切れ切れで悲惨だった。Instagramのメニューで「埋め込み」というのがあるが、これでやってみるとどうなるだろう?……XSSチェックでだめだった。

tDiary Instagramプラグインの「別の書き方」を試してみる。…、もそもそも短縮URLを取る手段がなさそうでだめだな。

WordPress由来だけど、div囲みして調整するという手法があるようだ。 というかそもそもWordPressにすればいいのではという話はある。


2018年04月23日

_ [computer] 写真テスト

Picasa APIも封じられたので「写真を手軽にアップロードする」の手段に困る。 ローカルに置くのは負荷が大きくてサーバが死ぬので外部に置きたい。

現状は

イチデジカメラ→SD取り出し→PCにSDをイン→DigiKamに読み込み→ブラウザでGoogle Photosのアルバムにアップロード→tDiaryの「Googleフォト」のプレビューから探して選択→ちょっとコメント書きして「追記」→PCからSD取り出し→イチデジにSDをイン

というプロセスになる。旅行記みたいにがっつりしたものならまぁこれでもなんとかなるのだが、毎日の料理記録となると億劫この上ない。

これまではSDイン→自作スクリプトでPicasa APIでPicasa/Google Photosにアップロード兼記事書き としていたのでまだマシだったのだが、まずOAuth対応が必要になり、それからAPIが消え、ということでお手上げになった。

なお、カメラからWiFiで送れるFlashAirも試してみたのだが、アクセス手続きが煩雑でフォルダに写真がたくさんあると使い勝手が悪すぎるのと、上記のとおりそもそもGoogle Photosにアクセスしづらい問題がある。

イチデジ経由はあきらめるとしてスマフォ(これもiPhoneは売ってしまったので今はろくなカメラ付きのがないのだが……うまくフローを作れるなら適当な型落ちiPhoneを買ってもいいかも)でInstagramを使ってはどうかなとちょっと実験をしてみている。

Instagramへのアップロード自体は簡単だった(スマフォがダメなので写真画質は悲惨だが)。コメントもその場で入れることはできる。

で、これをtDiaryのinstagram.rbプラグインで読ませてみる。

これはなかなか厳しいな……。フレームで入るのでだいぶジャマい気がする。コメントも取り出すことはできなさそうだ。

あとはURLのID部分を切り出さないといけないのだが、スマフォ上でそれをやるのは難しい。Instagram側から「メールでシェア」にして、メールメッセージを解析し、ついでにコメントも本文に書いておいてそこから拾うということはできなくはなさそう。

ごはん記録くらいならこれでもいいかなぁ。

ほかの候補はFlickrですかね。


2018年01月02日

_ [reviewml] Re:VIEW Hacks (4) 〜Bookオブジェクトの深遠

あけましておめでとうございます。今年のRe:VIEWはTeXまわりの強化を考えていますが、後方互換性を派手に壊しそうなのでどうしたものかと……。

さて、前回に残していたBookやIndex、あとChapterについてです。……がかなり複雑なのでBookだけで終わりそうです。

ビルダとコンパイルについて説明してきましたが、「本」としての関係性定義はどこにあるのかというと、lib/review/book内の各クラスにあります。

前回少し言及した@bookは、lib/review/book/base.rbのReVIEW::Book::Baseから作られています。@bookはReVIEW::Book.load(<ディレクトリ>)で作られますが、self.loadが呼び出されたときに内部ではself.update_rubyenvが呼び出され、ここでreview-ext.rbが読み込まれてモンキーパッチによる自己書き換えが行われます(これは第1回で説明しました)。その後、ReVIEW::Bookオブジェクトが新たに作成される、という運びです。このあたりは本当に、青木さん巧みだなぁと思いますね。

 module ReVIEW
   module Book
     class Base
     …
     def self.load(dir = '.')
       update_rubyenv dir
       new(dir)
     end

     …
     def self.update_rubyenv(dir)
         …
           Kernel.load File.expand_path("#{dir}/review-ext.rb")
         …
     end

     def initialize(basedir)
       @basedir = basedir
       @parts = nil
       @chapter_index = nil
       @config = ReVIEW::Configure.values
       @catalog = nil
       @read_part = nil
       @warn_old_files = {} # XXX for checking CHAPS, PREDEF, POSTDEF
     end

クラス内には、本を構成する各種のパラメータ取得代わりに使うインスタンスメソッドが定義されています。少々注意すべきは、initialize時(newでオブジェクトを作るとき)にはconfig.yml YAML設定の読み込み(@config)くらいしかされていないという点です。

「章一覧がほしい」「このIDの表のカウンタはいくつ?」といった要求を受けて初めて、該当のオブジェクトを探す→まだないな→必要な読み込みを行って該当のオブジェクトを作成→以降はそのオブジェクトを利用、というプロセスを踏みます。軽量で拡張しやすくはありますが、結果的に読み込み部分があちこちのメソッドに存在することになるため、わかりにくいかもしれません。

review-compileで見てみると、

 book = ReVIEW::Book::Base.load(basedir)
 book.config = config # needs only at the first time
 ARGV.each do |item|
   chap_name = File.basename(item, '.*')
   chap = book.chapter(chap_name)

が呼び出し部分で、book.chapterにファイルIDを渡して章を表すChapterオブジェクトの取得を要求しています。

ここからlib/review/book/base.rbを追ってみると、

 def chapter(id)
   chapter_index[id]
 end

とchapter_indexメソッドが返す連想配列を戻すようになっており、その実装は

 def chapter_index
   return @chapter_index if @chapter_index

   contents = chapters
   parts.each { |prt| contents << prt if prt.id.present? }
   @chapter_index = ChapterIndex.new(contents)
 end

となっています。@chapter_indexをすでに構築済みならそれを返し、そうでないならChapterIndexオブジェクトを構築します。新たにchapters、partsの呼び出しが登場しました。

  def chapters
    parts.map(&:chapters).flatten
  end

  def parts
     @parts ||= read_parts
   end

このように、chaptersを作るには結局partsを使っていました。parts.map(&:chapters).flattenは、partsで返る部配列の各部に対して.chaptersで章の配列を取り出し、flattenで平坦化して結果的に章の一覧の配列を得ます。

@partsはPartオブジェクトの配列です。構築済みならそれを返し、そうでないならread_partsメソッドで構築します。

  def read_parts
     list = parse_chapters
     # NOTE: keep this = style to work this logic.
     if pre = prefaces
       list.unshift pre
     end
     if app = appendix
       list.push app
     end
     if post = postscripts
       list.push post
     end
     list
   end

少々長いですが、後半は前付・付録・後付時の対処で、ここではparse_chaptersメソッドの呼び出しをしているということに注目します。部を読んでいたはずなのに章読みになったのは、部の処理が後から付け足したことのせいだった気がします(すでに忘却の彼方)。

 def parse_chapters
   part = 0
   num = 0

   if catalog
     return catalog.parts_with_chaps.map do |entry|
       if entry.is_a?(Hash)
         chaps = entry.values.first.map do |chap|
           chap = Chapter.new(self, num += 1, chap, "#{@basedir}/#{chap}")
           chap
         end
         Part.new(self, part += 1, chaps, read_part.split("\n")[part - 1])
       else
         chap = Chapter.new(self, num += 1, entry, "#{@basedir}/#{entry}")
         if chap.number
            num = chap.number
          else
            num -= 1
         end
         Part.new(self, nil, [chap])
       end
     end
   end
 …
 end

全体でreturnするには詰め込みすぎている感じがありますが、

  • 「catalog」は変数ではなく、章構成を定義しているcatalog.ymlから読み込みを行うcatalogメソッド。Catalogオブジェクトが入っている(lib/review/catalog.rb)。
  • Catalogオブジェクト内で部と章を表すオブジェクト変数は階層構造になっているが、ひとまずCatalog#parts_with_chaps内でCHAPS情報の配列にする。
  • 配列の各要素を見ていって、Hashなら部、そうでない(String)なら章と見なせる。部の場合は部内の各章のChapterオブジェクトを作成し、最後に部のPartオブジェクトを作る。章だけなら章のChapterオブジェクトを作成する。
  • part=0、num=0の初期状態からオブジェクトを作るたびにインクリメントすることで、採番をしている。chap.numberを入れない設定にしている(見出し指定で[nonum]を付けているなど)なら、採番を1つ戻す
  • 全体をmapにしているとおり、最初はただのHashとStringの配列だったものが、PartオブジェクトとChapterオブジェクトの配列に書き変わる。

やれやれ。自分でも書いていてよくわからなくなりますね。ともあれ、これでようやくreview-compile内のbook.chapterに対して返せるChapterオブジェクトができました。

read_partsで前付・付録・後付時の対処については飛ばしていましたが、prefaces, appendix, postscriptsではそれぞれ、

  1. mkpart_from_namelist→mkchap_ifexistでChapterオブジェクトを作成
  2. mkpartで空の部を作成してその中にChapterオブジェクトを格納
  3. 部を全体の配列に挿入

という流れになっています。いやはや。

実は、これらの流れからRe:VIEWのカタログ記法の制約がわかります。

  • 前付・付録・後付は部構成にすることはできない
  • 「部」「章」以外の細分化はできない。AsciiDocやSphinxのように節や項の単位で細分化した原稿にしておいて結合という形はできない

後者は編集者という立場で言えば原稿が散逸するのは手間なので実現に気乗りはしないのですが、こういう書き方をするほうが好みというユーザーもいると思うのでどうしたものかというところです。今の時点では分散原稿のマージは、#@mapfileとreview-preprocを使うのが推奨される手段です。

追跡で疲れたので今日はここまで。


2017年12月25日

_ [computer] 文字のマスキング置き換え作業を支援してみる

「原書データがビットマップの図版しかない」「しかしその英文箇所を日本語文にしたい」という案件がたまに発生する。というか今まさに発生していて160点ほど必死に作成しなければならない状況に置かれている。

比較的単純で単色の図版が多いので、Illustratorでの作業自体はさほど難しいわけではない。

  • ①大きすぎる原書図版を版面幅に合わせる。
  • ②-A レイヤーを上に乗せて適切な書体・サイズの日本語文を置き、英文箇所は下地の色でマスクする。
  • ②-B 不透明なアミボックスに適切な書体・サイズの日本語文を乗せ、英文の近くに配置する。

(②でAかBかどちらになるかは指示があるので悩む必要がないものとする。)

しかし、手順が難しくなければ手間がかからないのかというとまた別の話で、これをちまちまやるのは正直かなり精神に来るものがある。

ということで、ざくっとした支援スクリプトを作った。いつか機械学習でそのうちかっこいいのができるのだろうけれども、今必要なのは当座の問題の解決である。

操作説明

InDesign Hacking with JavaScriptのlibCommon.jsx・dialogConfigureFigureStringSupport.jsx・dialogFigureStringSupport.jsxの3つのファイルで構成される。スクリプト用に適当なフォルダを作り、SPAiで呼び出すのがよいだろう。なお、macOS+CC 2017環境での挙動しか確認していない。

photo

SPAiの画面はこんな感じ。IllustratorでもInDesignのスクリプトウィンドウのように一覧からスクリプトを簡単に呼び出せる。

photo

dialogConfigureFigureStringSupport.jsxで各種の設定を行う。先の①に相当する規定最大幅や、②のA・Bそれぞれのパターンを定義する。アミにチェックを入れると文字の下にアミ/マスクが置かれるようになる。色や余白は右側の欄で設定する。グループ化にチェックを入れると、文字とアミがグループ化される。②-Aのようにマスクの場合はグループ化しないほうが使いやすいが、②-Bのように文字の背景として使う用途ではグループ化したほうが移動などの際に便利だろう。「保存」のクリックで設定が保存される。

photo

まずは画像ファイルをIllustratorで開く。ここではブログ用の例示サンプルとしてDATA USA Los Angeles Countyのデータを使う(ここだと生データやPDFも取れるので本当は必死に上書きする必要もないけれども)。

デフォルト紙面サイズでも余裕ではみ出していることがわかる。画像が選択されている状態(開いた時点でそうなっているはず)で、dialogFigureStringSupport.jsxを実行する。

photo

画像のリサイズ、アートボードのフィットが行われ、画像レイヤーはロックされて文字乗せ用の新しいレイヤーができる。

photo

置き換え用のテキストを用意し、適当なテキストエディタで開いておく。1行1項目にしておく必要がある。

photo

テキストエディタからIllustratorにコピーペーストする。デフォルトの書体・サイズでとりあえず貼り込まれる。

photo

テキストが選択されている状態でdialogFigureStringSupport.jsxを実行すると、先に設定した文字定義1、2どちらを使うか尋ねられる。なぜ「はい」「いいえ」にしたかというと専用ダイアログではなくてJavaScriptのconfirmを使っているからなのだが、1つ利点として、OSのショートカットを有効にしておけばEnter(はい)とスペース(いいえ)とマウスを使うことなく簡単に1キーで選択できる。

photo

「はい」を選び、上記設定(デフォルト)の定義1が使われるとこんな感じ。文字が指定の書体・サイズとなり、各行にバラされる。やや大きめの白いマスクが付随しているが、グループ化はしていない。

photo photo

たとえばこのように文字とマスクを上乗せしたいところに移動し、必要なら書体やサイズを調整して、マスクで英文を隠す。マスクを選択している状態でスポイトツールで地の色をクリックすれば、マスクの出来上がり。

photo photo

デフォルトの定義2設定(②-B相当)だと、文字とアミがグループ化されており、簡単に配置したいところに移動できる。

私感で当社比5〜10倍ほどの精神的健康と生産性向上が見られている。

コーディング

InDesignに比べるとIllustratorはまだスクリプト開発の手習い中で、無駄なことをしている可能性が高い。

  • document.fitArtboardToSelectedArtは引数オプショナルとなっているが、なしにしたらクラッシュした。
  • ElementPlacement enumの情報がオブジェクモデルビューアにないような……? Webで探してかねむさんの記事を参考にした。
  • 合成フォントの名前はTextFont.nameでは引き出せず、familyのほうに入っている。ATCだったら合成フォントと見なしてfamilyを取る、というロジックにしてみた。プレフィクスに「(合成)」としたのはソートしたときに合成フォントが頭になるようにしたかったから。
  • しかし、ScriptUIでフォントやカラーのピッカーコンポーネントがほしいものである。
  • 座標系は全部ptに統一する必要がある。
  • 行分割はテキストばらしAIを流用させていただいた。
  • グループ登録の処理、マジわかりにくい。ElementPlcementが必要なのにその情報が見つけにくいというのはどうなのか……。
  • 設定のシリアライズはもっとうまい方法はありそうだけれども、ひとまずこれでも簡潔なので。
  • 使い手を選ぶだろうから、UIのエラーチェックはほとんどしていない。
  • ScriptUIの座標設定ツラい。

いろいろわかったこともあったので、社内で困っている事案があればAIのスクリプティング開発も進めてみよう。


2017年12月21日

_ [reviewml] Re:VIEW Hacks (3) 〜ビルダの仕組み

誰も見ていない気はしますが、自分の覚え書きでもあるので引き続き。

前回のコンパイラから実際の表現としてのビルダが呼び出されます。lib/review/なんとかbuilder.rbがそれで、現時点で存在するのは

 $ ls *builder.rb
 builder.rb      idgxmlbuilder.rb    md2inaobuilder.rb  topbuilder.rb
 epubbuilder.rb  latexbuilder.rb     rstbuilder.rb
 htmlbuilder.rb  markdownbuilder.rb  textbuilder.rb

review-epubmakerやreview-webmaker経由のときにはhtmlbuilder、review-pdfmaker経由のときにはlatexbuilder、「review-compile --target=なんとか」を使ったときには「なんとかbuilder」が使われます。

textbuilder.rbはtopbuilder.rbと同一、epubbuilder.rbはhtmlbuilder.rbと同一です。builder.rbは各ビルダの基底クラスなので、全ビルダに波及する何かを定義したり上書きしたりしたいときにはこのビルダのクラス(ReVIEW::Builderクラス)を対象にすることになるでしょう。

各クラスのメソッドはその性質上おおむね似通っており、次のようなメソッドがあります。

  • builder_init(no_error=false):ビルダオブジェクトのinitialize後呼び出される、ビルダ固有の初期化メソッド。
  • builder_init_file():コンパイラやドキュメントに束縛(ReVIEW::Builder#bind)された後に呼び出されるメソッド。コンパイラやドキュメント情報を基に初期値設定などをしたりする。
  • result():変換結果文字列の表示。@outputに変換結果の文字列がStringで格納されている。
  • ブロックタグ名(引数):Re:VIEWの//list、//imageなどに対応して「def list」「def image」といったメソッドが対応付けられて用意されている。引数はコンパイラで定義したものに依存するが、代表的なものとしてはlines(内包する行のString配列。通常はインラインタグを展開済み)、id(ID文字列)、caption(キャプション文字列)がある。ブロックタグの処理結果は、printまたはputsで書き出すようにする(※print/putsはオーバライドされており、標準出力の代わりに@output変数に追加される)。
  • inline_インラインタグ名(引数):Re:VIEWの@<b>、@<list>などに対応して「inline_b」「inline_list」といったメソッドが対応付けられて用意されている。引数はRe:VIEWの書式どおり当然ながら単一の文字列。インラインタグの処理結果はprint/putsではなくメソッドの戻り値として返す。
  • headline(level, label, caption):見出し処理。
  • nonum_begin(level, label, caption)、nonum_end(level):見出しの亜種で、「==[nonum] 〜」のようにしていたときにはこちらが使われる。nonum_endは「==[/nonum]」のように明示的に閉じられるか同レベルか上位レベルの別の見出しが登場したときに呼び出され、nonum環境の終了処理を行う。
  • notoc_begin(level, label, caption)、notoc_end(level)、nodisp_begin(level, label, caption)、nodisp_end(level):「==[notoc]」「==[nodisp]」の処理。
  • column_begin(level, label, caption)、column_end(level):コラムの処理。仕組みはnonum_beginなどと同じ。
  • ul_begin()、ul_item_begin(lines)、ul_item_end()、ul_end():●箇条書きの処理。ul_begin、ul_endが全体を囲む定義、ul_item_beginとul_item_endが各箇条書きを囲む定義となる。
  • ol_begin()、ol_item(lines, num)、ol_end():番号箇条書きの処理。
  • dl_begin()、dt(line)、dd(lines)、dl_end():説明付き箇条書きの処理。
  • paragraph(lines):段落の処理。
  • image_image(id, caption, metric), image_header(id, caption):実際に画像ファイルがあるときの図版処理。キャプション処理はimage_headerメソッドで定義する。
  • image_dummy(id, caption, metric):画像ファイルが見つからなかったときの代替処理。
  • image_ext():あまりちゃんと使っている気がしないが、ビルダ内での画像ファイル拡張子のデフォルト。
  • extname():変換結果の推奨拡張子。

初期設定系の定義をbuilder_initかbuilder_init_fileのどちらに追加するかですが、通常は後者のbuilder_init_fileのほうでよいでしょう。

束縛が行われた後であるbuilder_init_file()ほか各タグ処理メソッドでは、コンパイラオブジェクトや現在の処理ドキュメントのオブジェクトを参照して利用できます。

  • @compiler:コンパイラオブジェクト。
  • @chapter:現在処理中の章を表すChapterオブジェクト。
  • @book:Re:VIEWの設定やブック情報であるBookのオブジェクト。たとえば@book.config HashオブジェクトでYAML設定の内容の参照や設定ができる。

最後にビルダ挙動を変更する例として、LaTeXビルダで@<b>を\textbf(太字にする命令で、ゴシックにするわけではない)の代わりにゴシック体+太字にする例を示します。

 module ReVIEW
   module LATEXBuilderOverride
     def inline_b(str)
       "{\\sffamily\\bfseries\\gtfamily #{escape(str)}}"
     end
   end

   class LATEXBuilder
     prepend LATEXBuilderOverride
   end
 end

実際はこのままだとコード内でbを使ったときにはttfamilyにすべきで困るため、レイアウトsty側で定義したほうがよいのですが、カスタマイズの一例とお考えください。

前回、インラインタグはブロックタグに渡される前に展開されていると述べたとおり、今はinline_bの中で「コードブロック内かどうか」を判断する術がありません。PR#858を入れると@doc_statusから拾えるようになりますが、負債にもなりかねないので、このパッチはもうちょっと精査したほうがよいのかもしれません。

あとはBookやIndexについて説明が必要ですが、今日はこのへんで。


2017年12月19日

_ [reviewml] Re:VIEW Hacks (2) 〜コンパイラの仕組み

review-ext.rbでRe:VIEWの挙動を上書きするためには、当然ながらまずはRe:VIEWの仕掛けを理解しておく必要があります。実際のところ、私も青木さんの元コードを読んだりしながら習っているところがあり、完全には全容を理解しきれているとは言えません。

まずbin/にあってユーザーから呼び出されるreview-epubmaker, review-pdfmaker, review-compileあたりはオプション解析をする程度で、実体コードのほとんどはlib/reviewの中のライブラリ側に置いています。コマンド挙動を上書きするのはあまり意味がない(生成結果を加工処理するのはフック利用を期待している)ので、ここでは説明しません。

review-epubmaker, review-pdfmakerはReVIEW::Converter→ReVIEW::Compilerという呼び出し経路、review-compileはReVIEW::Compilerを直に呼び出して、与えられたRe:VIEW形式コンテンツを処理します。

ReVIEW::Compiler(lib/review/compiler.rb)は、以下の2つの重要な役割を担います。

  • 基本のインラインタグ、ブロックタグの宣言
  • ドキュメントパーサ

宣言は次のような形です。

 defblock :read, 0  (ブロックタグの宣言。1つめが名前、2つめが引数の数)
 defblock :lead, 0
 defblock :list, 2..3
 defblock :emlist, 0..2 (0〜2個の可変の引数をとる)
 ……
 defsingle :footnote, 2 (1行で使用する({〜//}を持たない)ブロックタグの宣言)
 defsingle :noindent, 0
 ……
 definline :chapref (インラインタグの宣言)
 definline :chap
 ……

あくまでもCompilerでは宣言するのみで、表現の実体は変換先、Re:VIEWで「ビルダ」と呼んでいる実装側に任されます。パーサで処理する際にどのようなタグが存在するのかわからないと困るので、このように設計されています。

パーサの中は、歴史的経緯もあって、現時点ではドキュメントオブジェクトモデルを構築するのではなく、正規表現で必死に処理しています(@takahashimさんがPEG版を開発していますが、正規表現パーサやビルダのあいまいさを消化するのに苦戦されているようです)。

 def do_compile
   f = LineInput.new(StringIO.new(@chapter.content))
   @strategy.bind self, @chapter, Location.new(@chapter.basename, f)
   tagged_section_init
   while f.next?
     case f.peek
     when /\A\#@/
       f.gets # Nothing to do
     when /\A=+[\[\s\{]/
       compile_headline f.gets
     when /\A\s+\*/
       compile_ulist f
     when /\A\s+\d+\./
       compile_olist f
     when /\A\s*:\s/
       compile_dlist f
     when %r{\A//\}}
       f.gets
       error 'block end seen but not opened'
     when %r{\A//[a-z]+}
       name, args, lines = read_command(f)
       syntax = syntax_descriptor(name)
       unless syntax
         error "unknown command: //#{name}"
         compile_unknown_command args, lines
         next
       end
       compile_command syntax, args, lines
     when %r{\A//}
       line = f.gets
       warn "`//' seen but is not valid command: #{line.strip.inspect}"
       if block_open?(line)
         warn 'skipping block...'
         read_block(f, false)
       end
     else
       if f.peek.strip.empty?
         f.gets
         next
       end
       compile_paragraph f
     end
   end
   close_all_tagged_section
 end

入れ子の処理などができないというのはこれに起因するものですが、実装としては把握しやすいですね。コアのパーサロジックがたったこれだけの行で済んでいるというのはちょっと驚きます。@strategyはビルダのオブジェクトです。

このパーサの時点ではインラインタグは処理せず、コメントの無視化、特例的なタグである見出し・箇条書き、ブロックタグ、そして残ったものを段落に、という区分けをしています。

それぞれの処理の中では、ビルダ(@strategy)に渡す情報を整理します。その際、テキストと見なされる箇所はtextメソッドでインラインタグを処理します。これもだいぶ必死な正規表現ですね。

 def text(str)
    return '' if str.empty?
    words = replace_fence(str).split(/(@<\w+>\{(?:[^\}\\]|\\.)*?\})/, -1)
    words.each { |w| error "`@<xxx>' seen but is not valid inline op: #{w}" if w.scan(/@<\w+>/).size > 1 && !/\A@<raw>/.match(w) }
    result = @strategy.nofunc_text(words.shift)
    until words.empty?
      result << compile_inline(words.shift.gsub(/\\\}/, '}').gsub(/\\\\/, '\\'))
      result << @strategy.nofunc_text(words.shift)
    end
    result.gsub("\x01", '@')
  rescue => err
    error err.message
  end
 ……
 def compile_inline(str)
   op, arg = /\A@<(\w+)>\{(.*?)\}\z/.match(str).captures
   raise CompileError, "no such inline op: #{op}" unless inline_defined?(op)
   raise "strategy does not support inline op: @<#{op}>" unless @strategy.respond_to?("inline_#{op}")
   @strategy.__send__("inline_#{op}", arg)
 rescue => err
   error err.message
   @strategy.nofunc_text(str)
 end

\x01という謎なものがありますが、これはフェンス記法のサポート(replace_fence)の副作用です。インラインタグでリテラルな}を使おうとすると\}とエスケープする必要があった(たとえば@<m>{\frac{1\}{2\}})のですが、これはTeX数式を書く際などにとても煩雑なため、@<m>$\frac{1}{2}$あるいは@<m>|\frac{1}{2}|といった代替の表記方法を提供します(あくまでも「代替」です)。このときに@が間に挟まっているといろいろと奇妙な動作を起こしてしまうため、いったん\x01に退避してから戻すという回避策をとりました(@znzさんのご提案)。インラインタグは以下の箇所のとおりビルダ側の「inline_タグ名」のメソッドで変換処理されます。

 @strategy.__send__("inline_#{op}", arg)

さて、こうしてできあがった文字列をビルダに渡します。たとえば見出しなら

  @strategy.headline level, label, caption

段落なら

  @strategy.paragraph buf

ブロックなら

  @strategy.__send__(syntax.name, (lines || default_block(syntax)), *args)

といった具合です。インラインタグが処理済みのものが渡されるという点に注意が必要です。

長くなりましたので今日はここまで。次はビルダの中に入っていきましょう。


2017年12月18日

_ [reviewml] Re:VIEW Hacks (1) 〜review-ext.rbの基本構成

Re:VIEWはRubyで書かれていますが、作業フォルダにreview-ext.rbという名前のファイルを置いておくと、コンパイル実行前に読み込んで内部挙動を置き換えることができます。

ある程度まとまったら電子書籍にでもしようと思っていますが、このreview-ext.rbや各種フォルダを使ったRe:VIEWのカスタマイズやハックについて、気の向いたときにパラパラとメモがてら日記にまとめていこうと考えています。

Rubyは既存のクラスの動的な変更や拡張、すなわちモンキーパッチを許容しており、先に定義したクラスやそのメソッドを後から別の定義によって上書きできます(定数など一部の例外はあります)。作業フォルダに置いたreview-ext.rbは、起動後すぐにlib/review/book/base.rb#self.update_rubyenv内のKernel.loadで読み込まれ、Re:VIEWの実装挙動を上書きします。

なお、第三者向け提供のシステムに備えて環境変数REVIEW_SAFE_MODEの数値で2ビット目が立っているときにはロードしないという仕組みは一応用意しているのですが、ほかにもいろいろ悪さをできるところはあるはずで、あまり期待はしないでください(PullRequest歓迎です)。

ではreview-ext.rbのシンプルな例を示しましょう。

 module ReVIEW
   module HTMLBuilderOverride
     def builder_init_file
       super
       STDERR.puts "Initialize!"
     end
   end

   class HTMLBuilder
     prepend HTMLBuilderOverride
   end
 end

ReVIEW::HTMLBuilderはHTML変換ビルダのクラスです。review-compile --target=htmlやreview-epubmakerで使われます。このビルダの挙動を変えるために、HTMLBuilderOverride(名前は何でもよいのですが)というモジュールを用意し、HTMLBuilderではprependメソッドで取り込むように設定します。

メソッドを上書きするのに私は以前はHTMLBuilderの下に実装を記述していたのですが、@takahashimさんに「prepend」を教えていただきました。prepend導入の最大のメリットは、上書きモジュールのメソッド内で「super」を使ってクラスの元々のメソッドを呼び出せることです。「初期化に少し加えたい」「結果の文字列を少しいじりたい」といった場面のために元々のメソッドの内容を全部コピーして修正するのは、馬鹿馬鹿しい上にRe:VIEW本体の実装が変わったときに厄介な問題を孕んでしまうので、これがすっきりするのはとても便利です。

上記の例では、ビルダの初期化を担うbuilder_init_fileメソッドをオーバライドし、元々のメソッドをsuperで実行した後に「Initialize!」とエラー出力に表示するようにしてみました。引数付きメソッドなら「super(引数, ...)」を使えばよいし、値返しのメソッドなら「s = super(引数, ...)」のように変数に格納させてから加工すればよいでしょう。

review-compile --target htmlでreファイルを変換してみると、最初に「Initialize!」と表示されるはずです。

Re:VIEWは今も改良を続けているプロダクトであり、外形上の変化は互換性をできるだけ保って影響が少ないように努めてはいるものの、内部のコードは頻繁に書き変わっています。つまり、Re:VIEW側の実装バージョンアップ次第でモンキーパッチが動かなくなる可能性は常にある、というリスクは念頭に置いておきましょう。良い機能であるという確信があれば、モンキーパッチで対処するよりもGitHubにissueなりPRを出していただいたほうがよいでしょう。