トップ 追記

KeN's GNU/Linux Diary


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を出していただいたほうがよいでしょう。


2017年12月17日

_ [computer] OpenBlocks AX3のストレージ交換

Debianをインストールして基幹に使っているOpenBlocks AX3のストレージが不穏なメッセージを出し始めた。

 ata1.00: exception Emask 0x0 SAct 0x6000 SErr 0x0 action 0x6 frozen
 ata1.00: failed command: WRITE FPDMA QUEUED
 ata1.00: cmd 61/08:68:39:1d:00/00:00:00:00:00/40 tag 13 ncq 4096 out
          res 40/00:00:00:00:00/00:00:00:00:00/00 Emask 0x4 (timeout)
 ata1.00: status: { DRDY }

smartctl -x /dev/sdaで見てもPRE-FAILなヤバそうなレポートが出ており、緊急にストレージ交換をする必要がある。

AX3のストレージ付き製品を使用しているので、SATAで2.5インチあるいはハーフスリムであれば交換はできるようだった。ただ、ハーフスリムのものはもう各社とも撤退ぎみで、ぷらっとオンラインでも納期応相談という状態のため、若干放熱に心配はあるが2.5インチでいいだろうと踏んでサンディスクの256GB SSDを調達した(今はメモリ系は品薄で高いねぇ……)。

いくつかこちらの事情でハマりどころはあったものの、AX3のOSの仕掛けがよくできていて実際の停止時間は極めて短く済ませることができた。

交換ストレージ準備

新しいSSDにどう現状のOS状態をコピーするかだが、AX3のDebianはフラッシュメモリにブートローダとカーネル、最低限の基本構成が書き込まれており、外部ストレージは/.rwとしてマウントされて、aufsで/etcや/usr等の各システムがオーバレイしている仕掛けになっている。フラッシュメモリが壊れたら元も子もないが、外部ストレージのほうは/.rwの内容をコピーすればよく、ブートローダやカーネルといった起動にあたってちょっと面倒くさい部分を考えなくていい。

手元にRATOCのリムーバブルケース+eSATA・USBの外ケースを持っていたので、これに新SSDを3.5インチマウントアダプタ(裸族のインナー CRIN2535)を使って接続する。ミリネジも買っておいたのだが、インナーのほうに付いていた。

あとはこれをAX3のeSATAにつなげて……としたのだが、AX3のeSATAコントローラはつなげると一応反応らしきものはするものの、OSを再起動しないとディスクを扱えないようだ(scanもできなかった)。元ストレージがいつ天に召されるかわからない中での再起動は怖いので、USBにして接続。今度は普通に/dev/sdbとして見えたので、cfdiskで/dev/sdb1をLinux形式で作成し、「mkfs.ext4 /dev/sdb1」でext4形式にする。

あとは「mount /dev/sdb1 /media」のように適当にマウントし、「rsync -av /.rw/* /media/」で現状をコピー。16GBしかなかったので一瞬で終わった。

最後にAX3固有だがDebianのシステムパーティションとして見えるように、「e2label /dev/sdb1 DEBIAN」でDEBIANラベルを付けておく。「umount /dev/sdb1」で解除し、交換の日に備えて社内外に連絡等の諸々の準備を行う。

ターミナル

AX3のコンソールはRS-232Cのシリアルターミナルであり、これまではLinuxデスクトップ+ELECOMのUC-SGTでモニタリングをしていた。その後、機器群の整理をしてAX3の近くからLinuxデスクトップがなくなって、手元のラップトップは皆macにしてしまった(デスクトップは今でもどれもDebianだけど)ため、UC-SGTがmacではうまく認識されなくて困る、という事実に思い至る。

鯨井さんの記事を見ながらやってみたのだが、タイミングが悪いのかOS X 10.10で署名の問題を解決できず(古いのを読ませてから新しい設定を入れる、というのも試してはみた)。

あまりこれに拘泥しているのも時間が惜しいので、https://www.mac-usb-serial.com/ の検査でサポートされていることを確認して、7.90ユーロでドライバを購入。インストールして接続してみたところ、問題なく/dev/以下にttyデバイスファイルができた。

cuやscreenはどうも挙動が変だったので、homebrewでminicomをインストールし、「minicom -D /dev/ttyデバイスファイル」で接続。AX3のDebianのログインプロンプトが表示され、普通にコンソールとして操作できる。

ストレージ交換

左右4つの小ネジ、後ろのネジを外し、ツメを押しながら上に引き上げる。ツメがちょっと外しずらかった。引き上げるときに変にナナメに力を加えるとSATAのライザーカードが折れかねないので注意。

photo

本体からライザーカードを抜き取り、ディスクマウンタに付いているハーフスリムSSD基盤を取り外す。剥き出し基盤なのでスペーサーが付けられており、この取り外しにはかなり小さな精密ドライバが必要。mSATA化の方向に行く中でハーフスリムがどれだけ入手性があるのかはともかく、なくさないように一式まとめて保存しておく。

2.5インチSSDのほうは特にスペーサーはいらないので、普通にネジで取り付ける。あとは本体にライザーカードごと刺し直し、蓋をして交換は終了。

再起動

交換してシリアルから送られてくるコンソールを見ながら緊張と祈りの再起動……特に何ということもなく、あっさり起動して必要なサービスが復活した。すばらしい。

放熱についても、冬のせいもあるかもしれないが以前より上昇しているという傾向も見られなかった。

さて、16GBから256GBになると、いろいろと欲が出てもくるが、あまり無茶なことをすると基幹に影響が出るので、あくまでもネットワーク監視系特化で何か考えよう。


2017年12月08日

_ [life] 皆さまからいただいた原稿はこう加工されますというお話

昔からお世話になっているモーリさんから編集とライティングにまつわるアレコレAdvent Calendar 2017に書け、という有形無形のプレッシャーが……昨日のアドベントカレンダーご担当はmktredwellさんでした。

本日は某制作プロダクション会社の編集者が、著者さまや訳者さまからいただいた原稿をどう加工して紙面化しているのかを記してみます。編集者の方々や、執筆・翻訳をして出版社から出版しよう、という方にも参考になれば幸いです。

背景として、私自身は基本的に企画やライティングはせず、クライアントである版元さま(=出版社。ほぼ技術書系)が企画して著者さま・訳者さまが執筆された原稿を、版元さまとともに編集・校正し、紙面化して確認をいただき、最終的に印刷所にお渡しする、という編集のお仕事をしています(企画やライティングのお仕事のほうが多い同僚もいます)。

また、OSS開発者兼技術書執筆者でもあり、自分の執筆と仕事に欠かせない環境としてRe:VIEWというマークアップドキュメント変換システムの開発保守をずっと続けています(Re:VIEWのオリジナルは青木峰郎さんが作成されました)。このRe:VIEWと商業組版ソフトInDesignやフリーの組版ソフトLaTeXと組み合わせた「自動組版」を使って、紙面化の作業も兼務しています。

よって、最初にエクスキューズしておくと、ここからの内容はあくまでも某制作プロダクション会社の中の私のチームが採用している手順というだけで、弊社ですべての書籍をこうしているとか世間一般にこうである、というわけではないことにご注意ください。

イテレーション、イテレーション、イテレーション

世の中にはMicrosoft Word(以下Word)をはじめ、一太郎、Excel、PowerPoint、LaTeX、HTML、XML、Markdown、Sphinx、Re:VIEW、AsciiDocなど、たくさんのドキュメント形式があります。

著者さまや訳者さまがどのようなファイル形式で書かれるかは、ある程度版元さまから指示があるのが普通ですが、「なんでも書きやすいものでいいですよ」と放任になっていることもあります(実際それを加工しないといけないのは当方なんですけどね……)。

ともあれ、どのようなデータであろうと、最終的に紙(≒印刷所入稿用PDF)にする、というゴールには変わりありません。

 原稿→編集・校正→組版(紙面化)→紙面に編集・校正→組版反映(紙面化)→……→印刷所入稿

しかし、現ラムダノートの鹿野さんが「どんな原稿形式でもそれをマスターデータとしてがんばる」といったことを書かれていたかおっしゃっていた気がしますが、もし原稿がせっかくマークアップテキスト形式で書かれているのならば、私もなるべくそれを生かしてできるだけ制作終盤ギリギリまでそれを使っていきたいと常々考えています(Wordや一太郎のようなバイナリデータ原稿の場合はノーチャンスです)。つまり、

 原稿→編集・校正→組版(紙面化)→原稿修正→編集・校正→組版(紙面化)→……

のように、元原稿をあたかもソースコードのように捉え、編集・校正でコード修正、組版というコンパイルを経て紙面を生成するのを繰り返す(ここではイテレーションと呼びます)ことをしたいわけです。

実際のところそのようなイテレーション型の作り方ができるかどうかは、妥当な費用・時間、紙面デザイン、版元さまや著者さま・訳者さまの意向、といった要素に依存します。

まず、イテレーションを実現するには、一般的な「DTPオペレータが手作業で原稿を見ながらDTPソフトInDesign上で紙面要素に割り当てていき(手組み)、その後は紙(ゲラ)上で赤入れしたものを目視で反映していく=InDesign DTPデータが最新データで、原稿ファイルは乖離した遺物」というやり方ではなく、何らかの「自動組版」の仕組みが必要です。そのために、Re:VIEWあるいはXMLとInDesignを組み合わせたり、LaTeXを採用したりといった自動組版を設計し運用しています。しかし、これには紙面デザインやツールを調整する費用・時間のコストがかかります。ともかく初校を早く安く! という案件の場合には向きません。

また、Re:VIEW+InDesignにせよLaTeXにせよ、自動組版の場合はInDesignの手組みほどのレイアウトの自由度は提供できません(正確には、できなくはないけれども妥当なコストで実現しづらく、イテレーションもやりづらいものになります)。流し込み、ある程度の白スペース許容、紙面の後からの見た目の調整は最小限、といった制約がかかってきます。LaTeXを使った組版であれば完全な無人オペレーションのためいくらでもイテレーションはかけられるのですが、InDesignの自動組版の場合はどうしても人手が介在するので、コスト的に多くても3、4回以内のイテレーションとなるのが普通です。

そしてこれらを踏まえた上で、あとはどれだけこのようなやり方を採用したいかどうかの問題となります。結局著者さま・訳者さまなり、版元編集者さまなりが「原稿ソースで」文章校正を管理したいという欲求がないと、なかなかうまくは運びません。もちろん、そういった要望がなくても、イテレーション作業が妥当と社内で判断したときには、対外的には従来どおりゲラを出しつつ内部ではイテレーションを採用していることもあります(特にLaTeX採用の場合)。なお、初校以降から要素のデザイン調整が全面的に入る、制作終盤に本格的な編集を始める、といった傾向の著者さま・版元さまとは完全に相性が悪い仕組みなので、その場合は当初からこの方法は除外しています。

コウカンサレルモノ—テキストマークアップ形式

前述のとおり、ドキュメント形式は多種多様ですが、いただく原稿は概して次のいずれかの記法のどれかになろうかと思います。順序はおおむね私の業務範囲での案件数に沿っています。

  • Markdown(実際はGitHub Flavor)
  • Re:VIEW
  • Word
  • LaTeX
  • プレインテキスト+独自マーク記号
  • HTML
  • XML(DocBookまたは独自スキーマ)
  • AsciiDoc
  • Sphinx(reStructuredText)

いずれにせよ、手組みの場合は最終的に社内タグを付けたプレインテキスト、自動組版の場合はXMLまたはLaTeXに帰結する必要があります。社内タグは次のような感じです(Re:VIEWのtopbuilderで吐き出したものがほぼそれです)。

 ■H1■第4章 InDesignで自動DTP
 ◆→開始:リード←◆
 この章ではいよいよ、IDGXMLドキュメントをレイアウトに割り付け、…
 ◆→終了:リード←◆

 ■H2■4.1 作業方針
 前章で作成したXMLドキュメントをInDesignのレイアウトに割り付けるにあたり、…

 1       レイアウトテンプレートの△master.indd☆ファイルをInDesignで開き、…

変換の流れについて言葉で説明すると長くなるので、ドキュメント形式ごとにどうしているかを図示しましょう。

photo (クリックで拡大)

実際には変換したあとに変換漏れ対処や細かなカスタマイズのためのフィルタ(Rubyコードあるいはシェルスクリプト)を通すこともありますが、大きな流れとしてはこのようになっている、とお考えください。

登場しているツールのリンクも示しておきます。

Markdownは紙面表現には絶望的に機能が不足しているのですが、どうしてもこの形式を保持して自動組版と組み合わせたいという場合は、Re:VIEWタグがところどころに埋め込まれた原稿を作っていくことになります(以下は『サイバーセキュリティプログラミング』(オライリージャパン、2015)より)。

 パッケージがインストールされたら、7章で作成する@<hidx>{GitHub}GitHubを使ったトロ
 イの木馬をビルドするために使うモジュールをインストールできるか試してみよう。ター
 ミナルに次のコマンドを入力する。

 ```
 root@kali:~#: @<ttb>{pip install github3.py}
 ```

なお、Wordについては当初大いにこの記事に書き連ねたのですが、呪詛の言葉しか並ばずまったくクリスマスにそぐわないため、ばっさり削除しました。1つ申し上げておくと、いくらWordで見た目を綺麗に作っていただいても、(原稿の内容ではなく)Wordドキュメント形式に起因する諸々の問題で信頼性に疑義があるため、図にあるとおりまずプレインテキストにしてから編集します。Wordのスタイルを活用してツールによる初校作成を実装したのは、長大・要素膨大・多数の編集者が関与という条件のあった『できる大事典Excel VBA』(インプレス、2017)ほか数冊のシリーズくらいです。

最近はWord数式の含まれたものについてはPandoc、あるいはdocx2texの変換も試したりしていますが、まだ全幅の信頼を置くには至っていないため、たいていは手でLaTeX数式に置き換えています。

食ったパンの枚数だって数えられる—バージョン管理

手組みにせよ、イテレーションの自動組版にせよ、必ずバージョン管理サービスは利用します。ローカルマシンのストレージほど信用ならないものはないですし、私の勤務体系はだいぶフリーダムなので、オフィスでも自宅でも外出先でもデータに容易にアクセスし、履歴をたどれる必要があります。

著者さま・訳者さま・版元さまが供与されているならGitHub・GitLab・Backlog・GitBucketなど基本的にはどのサービスでもそれを使うようにしています。こちらから提供するときにはGitHubか自前で運用しているSubversionあるいはGitBucketです。

実際のところ、InDesignデータや図版データなどはバイナリで数十MB〜数百MBと大きく、リポジトリを圧迫する上に、そんな生データはDTPの担当者以外にとって無用の長物なため、gitとは相性がよくない傾向です。gitを使うプロジェクトでも、それらのデータのほうはSubversionで管理しています。Subversionは内部的にはディレクトリベースの管理構造なので、1つのリポジトリで編集側・DTP側のデータを完全に分割管理できます。

プログラムソースコードと違って原稿の場合は変更によって「壊れる」ことはなく、むしろ並行作業を長く続けると「競合の発生」と「マージの負担」のコストが大きくなるので、「1章を終えた」という大きな単位ではなく、たとえば「今日はここまでやった」といった小さな単位でコミットしています。GitHubでもブランチを切ってマージして……は省略してmasterに直接入れることもよくありますし、rebaseもほぼ使いません。

人の目、機械の目—編集作業

さて、原稿が揃い、バージョン管理にひとまずいろいろとつっこんで方針の策定ができたら原稿の編集加工作業に入ります。本編から逸れて長くなりそうなので手短かにしますが、おおむね以下のような作業が挙げられます。

  • 用字用語の統一(版元さまごとのルールあり)
  • 誤字・脱字の訂正
  • 読みにくい・意味がわかりづらい箇所の改変あるいは提案
  • 見出しレベルや採番の確認(Re:VIEWの場合はおおむね自動任せ)
  • 必要に応じて数式箇所のLaTeX記法化
  • 使用図版の整理、場合によってはラフ描き
  • 引用出典元の確認
  • (料金や時間次第ですが)動作検証

章立てや節・項にまたがって全面的に書き直すというタイプの編集者も世に多くいますが、私は文章というのは著者さまが責任を持つべき範囲であると考えます。段落を壊さない範囲での読みやすさ改善は積極的に施しますが、文章をごっそり入れ替えたり作文したりすることはあまりせず、「ここが不足しているのでは」「この説明順序だと変では」という提案をする程度に留めています。いずれにせよ、編集着手前に「どの程度直してよいか」を著者さまに確認することが重要です(「たとえ誤脱字であってもすべからくお伺いを立てるべし」というケースもあります!)。

エディタはDebian GNU/Linux+Emacs+SKK+編集作業専用モードを利用し続けています。モードとしてはjaspace.elをマイナーモードで取り込み、Re:VIEWの場合は自作のreview-mode.el、LaTeXの場合はauctex、それ以外のテキストの場合はhensyu-mode.el(自作)を使います。これらのモードを使う主な目的は、タグのショートカット入力とカラーリング(タグや、全角半角スペース/タブ、多種存在して混同しやすい二重引用符やハイフン/マイナスの記号を見分ける)です。フォントには等幅かつ可読性に優れたVLGothicを愛用しています。

photo (クリックで拡大)

単文節変換の日本語入力機構であるSKKを使っているのは1993年以来常用して慣れているからというのもありますし、版元さまごとに送り仮名や漢字の開きなどが異なるルールの下、常時3〜4冊の編集を抱える中で一般的な連文節変換型の日本語入力ではルールの違いに耐え得ないことに依ります。たとえば午前には「組込み」「例えば」を使うA社案件、午後には「組み込み」「たとえば」を使うB社案件、という具合です。

表記統一にはEmacs上でgrepやquery-replaceを多用するのは当然として、ある程度原稿整理の済んだところで日本語の表記揺れ発見に優れたJustRight!(ジャストシステム社、Windows版)での検査も行います。最近はRedPenに同種の機能が実装されつつあるので楽しみに見ています。

誤脱字についてはいちおうJustRight!、RedPen、あるいはtextlintなどの検査ツールはありますが、技術書に対してはキーワードなどに対するfalse alarmが多く、私はあまり使用していません。目視とありがちミスに対するgrep(1つ誤字を発見したら全体をgrepするなど)を使うことがほとんどです。

「読みにくさ」というのは極めて主観的な事柄なものの、私は文のリズムからそれを判断します。読点はブレス、句点はふーと息をつき、段落の終わりはそこで一旦休んで次の段落に備えるところです。リズムが崩れている、テンポが悪いと感じた箇所にはたいてい、誤脱字があったり、文が日本語としてそもそも破綻していたり、段落内の文が長すぎたり短すぎたり、他者の文章の借用であったりが潜んでいます。共著・共訳の場合は全体のタームやトーンをなるべく整える必要もあります。

「意味のわかりづらさ」もまた主観的判断ですが、本には想定読者というものを必ず立てるわけで、一度自分の持つ知識をリセットして、そのペルソナを被って読んでみます。すると、読者にとって「唐突にわけのわからない内容が出てきた」、あるいは「当たり前のことを長々と書くよりこっちを説明してほしい」といったことが見えてきます(とはいえ、自身の知識レベル以上には変身できませんが)。単著の場合は思い込みに過ぎるところはないか、共著の場合は互いで矛盾していたり同じことを繰り返したりしていないかも注意します。

レベル、採番確認は、マークアップシステムを利用するものなら変換結果をおおむね信頼できるのですが、プレインテキストやWordから変換したテキストのように、リテラルで番号が振られたものについては、grepやwcを使って検査するシェルスクリプトを作って確認しています。1行内で複数回登場するパターンへのチェックなど、より重厚な確認が必要な場合はRubyで書くこともあります。

 #!/bin/sh
 # check.sh: 見出しを一覧し、図・表・リストの一覧とそれぞれの総数をカウントする例
 # (番号と総数がずれていたら途中が抜けている可能性がある)
 egrep "■H" $*
 echo
 egrep "^図[1-9]" $*
 egrep "^図[1-9]" $* | wc -l
 echo
 egrep "^表[1-9]" $*
 egrep "^表[1-9]" $* | wc -l
 echo
 egrep "^リスト[1-9]" $*
 egrep "^リスト[1-9]" $* | wc -l

図版については書くとまた長くなるので割愛します。なお、「キャプチャに矢印や囲みを入れた結果の画像しかない」という原稿ですと、

 ◆→著者様:お手数ですが加工前の原図をご支給ください -編集者←◆

というコメントを初校で目にすることになるでしょう。

初校作成までにおおむね2〜3回程度見直した上で、組版に進みます。

組版周りについても、とても長くなるので割愛します(自動組版の詳細は『Re:VIEW+InDesign制作技法』電子版をご参照ください)。組んだPDFが正しくできているかを照合する(内校)のも編集の仕事の1つですが、本来この時点で文章はすでにほぼ完成している「はず」なので、紙面要素が正しく適用されているか、作図に間違いがないかといった箇所を重点的に確認します。

そうしてできあがったものがついに著者さま・訳者さま、版元さまに届き、晴れて校正を受けるときがやってきます。マークアップ原稿によるイテレーションを採用しているなら文字校正は直接元原稿の修正を、そうでなく手組みならゲラなりPDFなりに赤入れを、進めていくことになるでしょう。

まとめに代えて

いろいろと書き連ねてはみましたが、どのような原稿であろうとも、紙面化は可能です。ただし、標準的なテキストマークアップ形式で書いていただき、版元さまを通じてご要望を出していただければ、原稿をソースとしたイテレーションを導入できる見込みは高いです。特にRe:VIEW形式で書いていただけるといろいろと捗るので、ぜひ検討してみてください! ご質問にはTwitter @kmuto でお答えできることもあります。

以上、12月8日、こんな感じで私はいただいた原稿を編集加工して紙面化しています、というお話でした。良い週末を。明日のご担当は51_arayaさんです。

編集とライティングにまつわるアレコレAdvent Calendar 2017