シェルスクリプト Forced Todo

Forced Todoはなかなか進まない進捗に対して、一定の時間でTODOを読み上げ、進捗を迫るものである。 あまりに進捗しないので作ってみた。

Gist

Systray

Notifyを開始するとSystrayに常駐し、Systrayから終了できるようにした。

Systrayへの常駐はMailDeliver2同様、yadを使うようにした。

notifypid=$!

yad --notification --image=player-time --text="TODO NOW!" --command="yad --text-info --timeout=15 --title=TODO --width 300 --height 300 --filename=$HOME/.yek/forcedtodo/todotext" --menu='Edit!'"$task_editor $HOME/.yek/forcedtodo/todotext"'|Quit!quit' --tooptip="Immidiately Task" 
kill $!

Yadのオプションについて分析してみよう。

オプション 意味
--notification Systrayに常駐する
--image=player-time Systrayのアイコンを選択
--text="TODO NOW!" Notificationの名前(ポップアップに影響)
--command 左クリック時の実行コマンド
--menu 右クリックメニュー。項目名とコマンドを!で分けている。quitで終了

ネストされているYadは次の通り

オプション 意味
--text-info テキストダイアログ
--timeout=15 15秒でダイアログが閉じる
--title=TODO ウィンドウタイトル
--width 300 ウィンドウの幅
--height 300 ウィンドウの高さ
--filename=... テキストの内容となるファイル

Zenityとは結構な違いがあることに注意。

Yadは通常ブロックするため、Yadが終了しなければ戻らない。 そのため、Notifyするためのプログラムをサブシェルでバックグラウンドプロセスとして実行し、そのプロセスID($!)を取得し、Yadが戻ったらKillすれば良い、ということになる。

Notify (Open JTalk)

Notifyは単一のプロセスIDを振るためにサブシェルで実行し、バックグラウンドプロセスとすることでループさせる。

(
  while sleep $notify_interval
  do
  perl -pe 's/\n/。/g' ~/.yek/forcedtodo/todotext | open_jtalk "$jtalk_tuning[@]" -m $hts_voicefile -x $mecab_dictdir -ow $jtalk_tmpfile
  play $jtalk_tmpfile >| /dev/null &|
  notify-send -t $notify_time "TODO" "$(<$HOME/.yek/forcedtodo/todotext)"
  done
) &

単純なsleepによるループである。

基本的な手順としては

  1. Open JTalkで音声ファイルを作成
  2. 合成した音声ファイルをバックグラウンドプロセスとして再生
  3. Notify Sendを使って通知に表示

となる。

ここで大きな情報はOpen JTalkだろう。

Open JTalkはスピーチシンセサイザーだが、eSpeak, SVOX Pico, Festivalといったソフトウェアと比べて非常に自然で、日本語に対応している。 ただし、かなり複雑なソフトウェアだ。

Manjaro(Arch)では、AURにあるopen-jtalkパッケージをインストールする。

NAIST JdicファイルはAURにも存在していないが、辞書ディレクトリとして/usr/share/open-jtalk/dic/を使用することができる。精度は異なるかもしれない。

NAIST JDicをインストールするためには、mecabをインストールし、さらにNaist JDicで/usr/libexecが指定されている部分を/usr/libに変更しなくてはいけない。

女声を使用するためにはopen-jtalk-voice-meiをインストールする。

Open JTalkは非常に自然で、Twitterの読み上げにも使えそうだが(twとの組み合わせがいいだろう)、複数行を扱うことができないため、Perlで変換している。 また、Open JTalkは標準出力に吐くことはできないようだ。

様々なブラウザと様々なプロファイルを切り替えて使うスクリプト

基本解説

GitHub

これまでFirefox Latestスクリプトを使ってきたが、これではFirefoxしか利用できなかったり、異常終了やフリーズすると問題が生じる可能性があった。

Firefox Latestの仕組みは次のようなものだ。

Gist

要はシンボリックリンクで.mozillaを切り替えて起動→終了したらシンボリックリンク修正、をしているだけだ。

ちなみに、もともとはMageiaがFirefoxをLTS版を採用していて、それが気に入らなかったのでこのようにしている。
だが、Manjaroはその必要はないので、Firefoxだけでなく、任意のブラウザを任意のプロファイルで起動できるようなスクリプトを書いた。

多くの関数が定義されているが、まずはプロファイル切り替えの方法をまとめる必要があった。

Chromium系は--user-data-dirオプションを使用する。

これに該当するのは

  • Google Chrome
  • Chromium
  • Opera (Blink系)
  • Vivaldi
  • SRWare Iron
  • Maxthon

Maxthon for LinuxはあまりにもOUT OF DATEが過ぎるが。

Firefox系は-P profile-profile pathのふたつ。設定全体を切り替えることはできず、プロファイルのみの保存切り替えだが、問題はあまりなさそうだった。これに該当するのは

  • Firefox
  • SeaMonkey
  • Palemoon

それ以外

  • Midoriは-c directory
  • qupzillaは-p directory
  • Rekonqは--config directory

この5タイプに対応した上で、それぞれのブラウザに対応した関数を用意し、簡単に起動できるようにしている。

例えば

mybrowsers[shopping]="chr $HOME/.browser-settings/chromium/shopping"

とした上で、

$ mybrowser.zsh shopping

とすれば、$HOME/.browser-settings/chromium/shoppingを設定ディレクトリとしてChromiumが起動することになる。

Zsh Assoc

今回のスクリプトではZshの連想配列を2つ使用している。

ひとつは、ブラウザの起動設定を行うためのものだ。これは

eval "$mybrowsers[$1]"

という部分で作用している。

もうひとつは、ブラウザのコマンドを修正するものだ。Chromeはgoogle-chromeだったりするかと思うが、Archではgoogle-chrome-stableだったりもする。

そこでこれを修正するため、ブラウザの各コマンドは"${${modify_browsers[$browser]}:-$browser}"とすることで修正可能にした。

PureBuilder2 TopicPath機能の追加

変更点

最新の変更@Gist

概要

これまでTopicPath機能はPureBuilderで提供されて来なかった。
そのため、個別のドキュメントとテンプレートにおいて実装可能な機能として紹介されてきたが、一般的な要望であるため今回の変更で取り込んだ。

このTopicPath機能は整形されたHTMLを返すわけではなく、シンボルと文字列からなる配列を返す。
PureBuilderの本来の設計に従い、サイトの階層構造に等しいディレクトリ構成を採用し、かつドキュメントごとにTopicPathが固定される状態であれば、かなり楽にTopicPathが生成できる。

これまではTopicPathはドキュメントあたりで設定することが勧められていた。これは、ACCS indexが組み込みで機能するためだ。

この機能を使えば設定ファイルによって、そのディレクトリの親パスを定義し、そして文書タイトルがStringとして追加される。

詳細

つまり、ディレクトリで設定されているのが

[ :Foo, :Bar ]journal/

で、文書タイトルがBazであるならば、

[ :Foo, :Bar, "Baz" ]

となる。

別にシンボルである必要はない。だが、シンボルを推奨している。マップを使用することが推奨されているためだ。
だが、特にシンボルであることを期待しているコードは組み込まれていない。

だから、例えば

[ { Address: "http://example.com/foo/", Title: "Foo" }, "Baz ]

のような構造にしても構わない。

実際にそれを使うコードとしては

% tp = DOC.mktopicpath
% tp.each do |i|
%   if i.kind_of?(String)
        <li><%= i %></li>
%   else
        <li><a href="<%= 	DOC.pbenv[:TopicPathMap][i][1] %>"><%= DOC.pbenv[:TopicPathMap][i][0] %></a></li>
%   end
% end if tp

のようになる。

ただし、Indexの場合はディレクトリで定義されている階層は今いるページなのであるから、その場合はtitleとpathの最終エレメントは重複しているはずだ。

そこで、:Indexが定義されている場合は、最終エレメントを取り除くことにした。

過渡期のコード

タイトルは"title"なのか"Title"なのか:Titleなのか、といったところに揺れがある。

正式には"title"を使用することになっている。だが、おそらくはreservedなキーは大文字で始めることにしたほうがいいだろう。

この互換性を維持するコードにしてある。

また、すでにページあたりでTopicPathを設定している場合に備え、現状はdevelブランチのみの対応だ。


途中で放置したため、書くべきことを忘れました。
ゴメンナサイ。

PureDocとKramdownのhn要素のidを統一する

「Markdownで書いたページがTOCのリンクが切れている」という問題に気づき、急遽対処を行った

問題は、Kramdownが生成するヘッダーIDが、単純にテキストを用いるものではなかった、ということだ。

KramdownはどうしてもIDにACSIIのみを使うようになっており、そのためになかなか複雑な処理をしている。

これを統一するため、Kramdownのこの処理をしているところを探したところ、base.rbにあるKramdown::Converter::Base#generate_idであることがわかった。

これを、常にテキストを用いるようにオーバーライトする。ほんとはオーバーライドしたかったのだが、Kramdownの構造的にそれは割と難しい。

PureBuilderのコードをいじれば、一応動くようにはなる。これは、そのままのテキストを使うようにして、PureDocと揃えたためだ。

Gist

PureDocのほうも複数の形式をサポート。その中でデフォルトは今までどおりテキストを使う形式だが、Kramdownに合わせ、重複した場合の対処を加えた。

これによってテキストだけではIDを判断できなくなったので、IDもTOCの中に含めるようにした。

Gist

サイト構築で画像関連機能

ウォーターマークをいれた画像を半自動で用意する

Gist

ウォーターマークのベースはさすがに自動生成できるレベルにはなかったので、Inkscapeで作成した。
縦と横の二種類を用意している。

fig/にはオリジナルサイズの、thumb/にはサムネイルサイズの画像を配置するが、この時にcompositeを使ってウォーターマークを合成している。

また、幅1600pxを越える画像はリサイズしている。

@figbaseuriの不完全さ

PureDocにもわずかな変更を加えたが、これはattrを排除するためにすぎない。

大きいのはParser::PureDocにおける

        # Other settings for PureDoc
        begin
        if config[:purebuilder_config][:puredoc_tune].kind_of? Proc
          config[:purebuilder_config][:puredoc_tune].call(::DOC)
        end
        rescue
          STDERR.puts "!!ERROR in PureDoc TUNE:" + $!
        end

という変更だろう。

これによって、@config[:puredoc_tune]を使って各PureDocオブジェクトに対して設定が可能になった。
これはMarkdownドキュメントに対しては無効となる。

Gist

「各article要素を取得してループ→各要素に対して各figure要素を取得してループ→イベントリスナー追加」という単純な構造ではある。

ライトボックス用に用意されたimg要素のsrcを変更し、さらに最大幅と最大高をウィンドウサイズに合わせてから表示する、という処理が追加され、ちゃんと機能するものになった。

figure要素に入っていないものはサムネイルの構造を持っていないとみなす方式。

PureBuilder2を使ったリニューアル(1)

PureBuilder1のサイトがPureBuilder2で動作するように調整すると共に、PureBuilder2の機能を検証する作業を開始した。
現在はサイトの更新が滞っているので、それが完了すれば効率的に更新できるようになる。

ただ、従来は設定ファイルにロジックを組み込む形だったため、変更点は大きく、そのまま持ってくるというわけにはいかない。
そもそも、PureBuilder1が十分なツールでなく、ツールをサイトごとに書いて完成というものだったのが問題なのだが。

まず、PureDocドキュメントはそのまま持ち込むことができる。
従来の.rebuild_rulesファイルや、ReasonSetにおけるglobal.rcファイルに変えて、.purenuilderrc.rbを用意することとなる。

おおよそそれでいいのだが、既にrcファイルで組み込んでテンプレートで利用していたような情報は.purebuilderrc.rbファイルに組み込まなくてはいけない。
また、これによって従来環境変数で処理していた物を、DOC.pbenvを使うように整合性をとらなくてはいけない。

このデバッグ作業に伴って、Purebuild Allに.rebuild_all_timeを無視して全てを対象とする-cを追加した。

そして、問題になったのがプロフィール部分だ。

これまで、プロフィールは単独のRubyスクリプトだった。rebuild_rulesから呼ばれることを爽亭しているため、設定は環境変数を通じて受け取ってはいるが、ドキュメントの生成、テンプレートへの埋め込み、そしてファイルの出力までプロフィール自体で行っていた。

しかし、これをそのままにできるわけではない。
設計上、これをPureDocであるとみなした処理したほうが早い。
だが、問題として、設定はあくまでもドキュメントを生成したあとに使えるようになっているため、ドキュメント内で使うことができない。PureDocを拡張してPureBuilder向けスクリプトにすることがかなり難しいのだ。

DOC内の@bodyStringまたはArrayという扱いだったが、実際にStringだと参照時に@body.joinを呼ぶためエラーになってしまう。この点については、PureDocのほうを修正することとなった。

こうした拡張がいまいちしづらいことを考えると、修正が必要かもしれない。
また、既存のPurebuildスクリプトを用いるのではなく、DSLによってビルドを指示できる仕組みも追加すべきだろう。

暗号化ディスクをsystemdでがんばる

これまで単純にスクリプトで暗号化ディスクをマウントしていた。
systemdスクリプトにするのは簡単で、実際にSystemdで自動マウントしていた時期もある。

だが、今回は「ちゃんと」Systemdを使うことにした。

スクリプトはGitHubで公開している。

私の場合、btrfsのボリュームとして4つのデバイスを使い、その4つのデバイスはディスク全体をdm-crypt plainで暗号化したものだ。
つまり、おおまかにはcrpytsetupのあとmountする必要があり、かつcryptsetupは全ディスク分ループしなくてはいけない。

これ自体はスクリプトとして用意してあり、これまでそれを使っていた。
Systemd対応にするのも、単純にSystemd経由にするだけなら、以前のエントリの通り簡単なユニットで良い。

今回はスクリプトは、「後処理」に対応した。
もちろん、スクリプトに後処理を組み込むなら非常に簡単な話だ。
だが、それでは「後処理に失敗するとユニット全体が失敗」してしまう。
そこで、systemdのExecStartPostを使うことにした。
それに伴って、スクリプトは起動スクリプトらしくなったし、設定ファイルを使うようになったりしている。

elif [[ "$1" == post ]]
then
  ### StartPost
  if whence -f opendisk_after >&2
  then
	opendisk_after
  else
	exit 0
  fi

fi

これでだいぶ整った。これを呼ぶほうは

ExecStartPost=/usr/local/sbin/opencryptdisk.zsh post /etc/opencryptdisk/%I.conf

%Iについては後ほど説明。

しかしここでだいぶ躓いた。ExecStartの完了を待たずにExecStartPostしてしまうため、ここに処理が入っているとコケるのだ。

これは、Typeを省略しているとsimpleとして扱われる。これは、フォアグラウンドで走るデーモンのためのユニットタイプで、「実行と同時に起動完了・実行終了とみなす」というもの。
実際はスクリプトの実行が終わった時に起動完了・実行終了としてほしいため、

Type=oneshot

を追加した。

同じような理由でExecStopが入っているとこけていたため、スクリプトにはcloseも入っているが、ExecStopが外されている。
ExecStopでアンマウントするのであれば、

RemainAfterExit=yes

として、「スクリプト終了時に起動完了・実行は継続とみなす」にする必要がある。
ただ、マウントと暗号化デバイスは、多分終了時に自動的にうまくやってくれるため、必要ないと判断して外してある。

systemdユニットがname@.serviceの形になっていると、name@param.serviceとして起動することができ、@paramをユニット内で%Iとして使うことができる。
複数ボリュームのマウントを可能にするため、この機能を使用している。


Systemdについてだが、非常に高機能だが、一方で複雑でめんどくさい。

例えば、マウントもスクリプト内でやるのではなく、.mountユニットにして、そのAfterで復号化してから実行、という形もとれたが、明らかにmountを書くほうが早い。

やはりかなりの暗黒面だ。

PureBuilder2(3) Markdown for Blog

今やブログは主に Markdownで書かれている。

当初、blogtrというPuredoc用のスクリプトを書いたが、ブログ程度であれば圧倒的にMarkdownで事足りることが多いからだ。
実のところ、ACCSコンテンツなどでもMarkdownで十分なケースは多く、そのためにPureBuilder2はMarkdownをサポートする。

Markdownを変換し、ヘッダーを操作するblogmdはPureBuilder1.5で既に実装されたが、これはPandocを使ったスクリプトである。
PureBuilder2は全面的にRubyでいく予定であるため、MarkdownトランスレータにはKramdownを使用している。

これに合わせてKramdownを採用することにした。
機能的にはほとんど変わらないが、唯一の違いとして、手前に

* TOC
{:toc}

を入れるようにした。
これにより、KramdownはTOCを自動生成する。Pandocにはなかった便利な機能で、ちゃんとオフセットしたTOCを生成してくれる。

ちなみに、PureBuilderのMarkdown用ライブラリがKramdownをモンキーパッチングで拡張し、自動的にPureDocオブジェクトのメタ情報としてヘッダーを埋め込むので、XHTPureDocを使ってTOCを作ることもできなくはない。

まだpush予定はないため、コードを掲載する。

#!/usr/bin/ruby
# -*- mode: ruby; coding: UTF-8 -*-

require "purebuilder/purebuilder"
require "kramdown"
require "optparse"

opt = {}
op = OptionParser.new

op.on("-m", "--marshal") {|v| opt[:marshal] = true }
op.on("-M", "--without-meta") {|v| opt[:marshal] = false }
op.parse!(ARGV)


# String for Kramdown's TOC
TOC_PREFIX = "* TOC\n{:toc}\n\n"

# Get article file
sourcefile = ARGV.length == 1 ?  ARGV[0] : nil
sourcestr = ARGF.read

pbp = PureBuilder::Parser.new(sourcestr, sourcefile)
pbp.proc_header

html = Kramdown::Document.new(TOC_PREFIX + sourcestr.gsub(//m, "") ).to_html

if opt[:marshal]
  Marshal.dump({body: html, head: DOC.meta}, STDOUT)
else
  puts html
end

これだけ見るとシンプルだが、PureBuilderとの関係が深く、結構中まで突っ込んでみることになってしまった。
PureBuilderの設計があまりよくないのかもしれないとも思ったが、そもそもPureBuilderのMarkdownサポートは「MarkdownをPureDocに見せかける」ものなので、PureBuilderとPureDocの密結合はやむをえまい。

これで初めて動かすこととなったPureBuilderだが、これによってPureBuilder、さらにPureBuilder登場によって修正されたPureDocのバグが発見され、デバッグにかなりの手間を費やした。

基本的にはシンプルだが、オプションへの対応を加えたため、いくらか複雑化した。
オプションは、将来的にパイプしてAPI経由でのブログアップロードに対応するため、メタを含めたMarshalで渡すためのものだ。

Tweets保存のためのMikutterプラグイン

とりあえずコード

# -*- coding: utf-8 -*-
require 'json'

Plugin.create(:save_timeline) do
  
  logdir = "#{ENV['HOME']}/.mikutter/plugin/save_timeline/log"

  on_update do | service, messages |
    File.open("#{logdir}/#{BOOT_TIME.strftime("%y%m%d%H%M%S")}.#{service.user || "default"}", "a") do |f| 
      messages.each do |msg|
        f.puts JSON.dump msg.instance_variable_get(:@value) rescue puts $!
      end
    end

end

#  on_direct_messages do |service, dms|
#  end

end

プラグインの書き方を調べながら書いた。 色々インスペクションしたので、そこに時間がかかった。内容的には難しくない。

messagesArrayなのだけれど、その各要素はmsg.inspectするとHashに見えるが、実際はMessageクラスのオブジェクトだった。 Messageクラスはその内容をそのまま出力するメソッドがないようなので、msg.instance_variable_get(:@value)の形でデータを取得している。 もちろん、@valueに値が格納されていることを確認するまでが一番手間だった(全体で言えば、Messageクラスであることになかなか気づかなかった部分に時間がかかった)。

JSONライブラリは出力に際して1行にまとめてくれるため、単純に行出力していけば、行単位でパースして処理できるログファイルができあがる。

このあと、flockに対応させた。

ダイレクトメッセージも対応したかったが、on_direct_messagesの取り扱いがよくわからなかったので、そのままにした。

追記

GitHubにてコードは公開中。

Mikutterのプラグインページにも掲載させていただいた。

PureBuilder2 (2)

Kramdown拡張でPDocオブジェクト化

PureBuilder2はもともと思っていたよりもかなり大規模なものになっているが、MarkdownオブジェクトをPureDocと同様に扱えるようにする、というのが今回のテーマ。

例えばテンプレートで

DOC.body

のように書かれている場合がある。この場合は当然、HTMLへ変換したのであればHTML文字列が得られなくてはいけない。 また、

DOC.meta["title"]

のようにもアクセスできる。 それだけなら単にアクセッサを拡張してやればいい話なのだが、PureDocオブジェクトはTOCのためのループ機能が組み込まれている。 これにより章立てをループさせることができ、簡単に任意の形式でTOCを組める。 これはどうしてもパース時に情報を取らなくてはいけない。

もし、HTMLに出力するものである、というのであれば、単純に結果のHTMLをパースして取得する方法もある。 だが、KramdownライブラリはLaTeXとPDFをサポートする。PureDocもゆくゆくはLaTeX形式での出力をサポートする予定である。

であれば、やはりKramdownでのMarkdownパース時にTOCを作りたい。

基本的な方針としては、実際にPureDocオブジェクトを使用する。 これはパーサ/コンバータを含まないベースクラスで、本来は直接このクラスのインスタンスを生成することは想定していなかった。 だが、外側から使用するメソッドは一通り持っており、インターフェイスは揃っている。

DOC.bodyで返すべき@bodyDOC.body=を用いて入れ、DOC.metaに関してはPureDocクラスが持っている機能によってドキュメントから取り込むといったことが可能。 そのため、DOCPureDocインスタンスであり、Kramdownの結果はDOC.body=によって入れるだけだ。

だが、DOC.stock_ehaderを用いてヘッダを入力し、TOCを生成できるようにしなければいけない。 そこで、Kramdownに手を入れる必要があった。

ソースコードを追っていったが、結局Kramdown::Parser::Kramdown#new_block_elをオーバーライドするのが良いと分かった。 ヘッダを取得するパートはあるが、new_block_elメソッドはメソッド自体が短く、あくまでパース時に各エレメント対して呼ばれるものだ。何のために呼ばれているかを判定する必要もなく、引数を丸々渡すだけで良いため、overrideしやすかった。

require 'kramdown'

# Override Kramdown
class Kramdown::Parser::Kramdown

  alias _new_block_el_orig new_block_el
  

def new_block_el(*arg)

   	if arg[-1].kind_of?(Hash)
    
      case arg[0]
      
      # Is Header?
      when :header
        p arg[-1][:level]
        p arg[-1][:raw_text]
      end
      
    end
    
    _new_block_el_orig(*arg)

end end

p Kramdown::Document.new(ARGF.read).to_html

というテストコードを書き、実際に動作することを確認、when :header部分を

::DOC.stock_header(arg[-1][:level], arg[-1][:raw_text])

と書き換えた。

KramdownはPure Rubyで書かれているため、扱いやすいし、ソースコードを書くのも楽だ。 だが、できればサブクラス化するなど、もう少しスマートな方法でできればよかったな、と思う。クラスが細かく分割されて連携しているため、置き換えるのはかなり難しいと判断した。

Kramdownは非常に良いライブラリなのは間違いない。

forkの代わりに

RubyのKernel.forkをはじめとするfork機能(例えば、IO.popen-を渡すことを含む)はWindowsでは動作しない。 Perlerだった私としてはこれはかなり不満な点だ。Perlはコミュニティの努力により、forkがWindows上で動作する。これは、Windows版Perlではforkをエミュレートするためだ。

今回は、設定やドキュメントオブジェクトなどをセットアップした状態で、forkによって環境を独立させたいと考えていた。 これはグローバルなオブジェクトに変更を加えるためであり、また出力先の制御をSTDOUT.reopenによって行うことができるかということについて考えていたためだ。

RubyのforkとWindowsについて検索すると、「forkは邪悪だ、threadを使え」という内容があふれる。 だが、今回は並列化のために使いたいわけではないため、Threadは用を成さない。

また、大量のドキュメントを変換する際のオーバーヘッド低減という目的もある。

Unicorn(Webアプリケーションサーバー)がこのforkによるCOWを活用した設計となっている。Unicornはどうしているのかと調べてみたら、UnicornもMongrelもWindowsでは動作しないらしい。

というわけで、forkの利用は諦めて、グローバルな名前に対する変更をいなす方向とした。

グローバルな名前のオブジェクトが変更されるのは、ほとんど

DOC.is {
...
}

という書式で記述するためだ。 これはPureDocドキュメントを分かりやすく記述するためであり、実際にテンプレートもDOCオブジェクトを利用したデザインとなっている。 つまり、DOCはthe PureDoc objectであることを期待している。

この設計を維持するため、Delegateライブラリを使用することとした。 実体はDOCではなく、DOCはただのDelegatorというデザインだ。これはDOCに実体はなく、ただの代名詞となるわけだ。

DOC = SimpleDelegator.new(nil)

とすることにより、まずDOCという名前を用意しておく。 実際に新しいドキュメントを生成する場合は、

::DOC.__setobj__ @@config[:puredoc_class].new

のようにする。 これにより、DOCが意味するドキュメントを入れ替えることができ、DOCを変更しても、変更されるのはDOCではなく、移譲されているドキュメントであり、DOCをまた新しいドキュメントにすることもできる。