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ブランチのみの対応だ。


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

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によってビルドを指示できる仕組みも追加すべきだろう。

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で渡すためのものだ。

PureBuilder2

PureBuilder2とは

PureBuilder2は現行のPureBuilderを置き換える新しいサイトビルドツールだ。 コマンド一発でウェブサイトの更新が可能になる。「動的生成の事前作業化」が可能となる。

PureBuilderからの主な変更点は次の通り

  • Rubyでの実装 (Windowsで動作可能に)
  • MarkdownとeRuby対応

変更点は少ないようにみえるが、PrueBuilderとは互換性はないし、コードも新規に起こす。

Markdownへの対応

Markdownへの対応はKramdownライブラリを使用した。 非常に使いやすく、問題はないように見えた。 何を何に出力するかは、指定クラスの入れ替えによるポリモーフィズムによる。

このラッパークラスはごく簡単だと思ったのだが、そうはいかなかった。

現状、PureDocはライブラリであり、ドキュメントがRubyコードである。 これを出力するためのユーティリティはZshスクリプトになっている。

PureBuilderはその多くをPureDocに依存している。 PureBuilderが直接に依存していなくても、テンプレート側はPureDocオブジェクトを触れることになっているし、現状テンプレートを呼ぶところまでがPureBuilderの仕事なので、当然テンプレートではドキュメントを出力するために、PureDocオブジェクトを必要とする。

だが、当然ながらKramdownオブジェクトはPureDocオブジェクトと互換性がない。 機能を維持するためには、単にKramdownを呼び出すラッパーではなく、Kramdownを内部で使うPureDoc互換クラスが必要となる。

予定とは比べ物にならないほど大変な作業だ。

PureDocのインターフェイス

加えて、今のところPureDoc Translatorが保持している機能については、PureBuilderから使用することができなくなる。 旧来のPureBuilderは、コマンドとしてPureDoc Translatorを呼んでいたため問題がなかったが、ライブラリとして使うとTranslatorは使えない。

PureDocにその機能があるにはかかわらずPureDocに組み込まれていないのは、PureDocの仕様によるものだ。

PureDocの

##-----
...
##-----

という形式でYAMLをヘッダーとして埋め込めるという仕様は、PureDocにはないが、便宜上の拡張としてTraslatorにあり、PureBuilderはそれを前提として使用する。

これをPureDocに組み込むのであれば、PureDocクラスにその機能をいれてしまえば良い。 要はこの仕様をPureDocに取り込むか、PureBuilderに取り込むかの話なのだが、おそらくはPureDocに取り込むのが妥当なところ。

一方同じようにこのヘッダを取り扱いながら、ヘッダにLast UpdateとSinceを書き込む機能があったりするが、これはあきらかにPureDocではなくPureBuilderに実装されるべき機能だ。

一応、いまのところ次の方針を考えている。

  • meta取り込みはPureDoc#read_meta(text)で行う。このヘッダはコメントになっているので、テキストを与える必要がある。これはPureDocライブラリが勝手に実行することはない
  • PureBuilderはpuredoc.metaによりsincelast-updateを確認し、ない場合は追記する

PureBuilder2のおおまかなモデル

purebuilder本体はRubyライブラリとなり、基本的には各ディレクトリの.rebuild_rulesまたは.up*ファイルが処理手順となる。

これらをまとめて呼ぶためのスクリプトが、purebuilder.rebuildallになる。

対象ファイルに対して

PureBuilder.build(file, outputdir, extname)

とすることでビルドできる形だ。

インタープリタ起動役は

rebuildallでrebuildスクリプトのインタープリタは拡張子によって判断するが、拡張子がない場合はperlを使う。

これは、perlはshebang行を解釈するためだ。この機能はsh/bash/zsh/rubyにないことを確認している。 Perlは「Shebangを解釈できないダメなシェルに代わって」起動するそうだが、どうやらLinuxが解釈するだけで、シェルに解釈を期待するのは厳しそうだ。

Markdownにまつわるもろもろ

Markdownで書くということ

Markdownはもうだいぶ普及している形式だと思う。

様々なマークアップ言語や記法がこれまで発達してきた。 それは例えばWikiであったり、plain2だったり、textileだったり、 場合によってはRDocだったり。

しかしながら、そのいずれもそれほど普及しなかった。 だが、Markdown記法はもはやスタンダードとも言うべき普及を見せている。

PureDocは当初、Markdownのような形式をとっていた。 実際に正規表現パーサだった時期もあるし、もう少し発展してZshの内部DSLだった時代もある。

この目的はHTMLと印刷用フォーマットの両方を生成するドキュメントメタフォーマットで、 かつHTMLと比べ簡潔に記述できるフォーマットを求めていた。

その要求を満たしてくれるのだ。 現在はPureDocはReasonSetに特化した多彩な機能を持つためMarkdownに乗り換えるということはしないが、 多くの場合Markdownで事足りるのも事実だ。

Markdown Editor

Markdownは普及している分、専用のEditorが多く存在する。 単に強調表示や入力支援があるだけでなく、リアルタイムで表示を確認できる。

主要な候補となるのは

  • Markdown#Editor(Windows)
  • Remarkable(Linux)
  • CuteMarkEd
  • Haroopad

の4つであるようだ。

Markdown#Editorに関しては表示領域がマッチしないことが多く、 いまひとつ使いにくい。この問題はCuteMarkEdでも生じる。

Haroopadはクロスプラットフォームで、最初はフォントに違和感があったが、 CSSによってフォントを含め見栄えを指定することができる。

Haroopadの弱点は、改行を反映してしまうことだろう。 だが、全体にはスタイリッシュで見やすく使いやすい。 ドキュメントの動的なリロード機能がないのと、Donateのバルーンがちょっとしつこいのは残念。 だが、Windowsではこれを使っている。 基本的に表示位置は狭い画面ではエディタ側の入力位置を上のほうにもってくると適切に表示してくれる。

Haroopadの欠点として、Fcitxで入力できなくなることが結構あるというのもある。 この対応として、入力のない、空のHaroopadを立ち上げておくと入力できるようだ。

RemarkableはUbuntu的なUIを持つ。 使いやすいといえば使いやすいが、D&Dによって開くことができないため、ファイル操作がちょっと面倒。

おもしろいのが、Remarkableはビューワがめいっぱい上までスクロールすると下にループする。下はしない。

またエクスポート機能もあり、CSSにも対応する。 今のところ最も安定しているということもあり、LinuxではRemarkableを使用している。

Remarkableで記述する場合は、エディタの記述部分を上のほうになるようにするか、めいいっぱい下にすると適切に表示される。 当然ながら、中途半端で適切に表示されない位置はどうしても生じる。

だが、なるべくならどのエディタも最も下に書いていくのが良いようだ。

Markdownで既存のテキストを引用する

HTMLを含め引用はpreされるべきではないかと思うのだが、そうなっていない。 プレーンなテキストを引用するには、次のようにすると良いようだ。

sed -i "s/\(.*\)/> \1\n> /" file

sedの出力は改行を伴うので最後には改行はいらない。 段落を分けてもらう必要があるため、空行を入れておく。 ちょっと複雑だ。

MarkdownをPureBuilderに取り込み

Markdownのほうが楽に、適切に書けるケースが多いようなので、MarkdownをPureBuilderの一部として取り込んでみた。

とりあえずblog用で、変換にはpandocを使う。 これはいずれPureBuilderの一部となる。

ちなみに、今回コードの埋め込みは次のようにした。

sed "s/\(.*\)/\t\1/" ~/local/devel/reasonset_builder02/scripts/md_processor.rb >| ~/tmp/out

主な動作としては、PureDoc同様のヘッダの取り扱いと、 pandocからbodyだけを切り出すことである。

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

require 'yaml'

module YEK
  class ReasonBuild

#=NAME
#
#ReasonBuild MD Processor - PureBuilder script for Markdown file.
#=SYNOPSIS
#
#  md_processor.rb [ _file_ ] [ -- _pandocoptions_ ... ]
#
#=DESCRIPTION
#
#MD Processor reads ARGF and process with pandoc.
#
#If +-s+ any _file_ given, MD Processor understands header with same style as PureDoc,
#modify timestamp, and write out to given _file_.
#
#If pandoc options given, MD Processor invoke pandoc with these options.
#Otherwise, MD Processor invoke pandoc 
#
# pandoc -t -s -p
#
    class MDProceessor
      
      def proc_header(file=nil)
        @file_content ||= ARGF.read
        # Any file given?
        if file && @file_content =~ /^##--.*$/ && $' =~ /^##--.*$/

          begin
            yax = $`.each_line.map {|i| i.sub(/^# /, "") }.join
            header_meta = YAML.load(yax) || Hash.new
    
            # Is Header missing last-update or since?
            if  ( ! header_meta.key?("last-update") ) || ( ! header_meta.key?("since") )
                
              now = Time.now
            
              modsince, modupdate = nil, nil

              # Set to since
              if ! header_meta.key?("since") 
                modsince = true
                header_meta["since"] = now
              end
            
              # Set to last-update.
              if ! header_meta.key?("last-update") 
                modupdate = true
              end
            
              # OK, Header is ready.
              # Open the file!
              File.open(file.first, "r+") do |f|
                content = f.gets(nil)
            
                if content.sub!(/^##--.*?^##--.*?$/m) {
                  el = $&.each_line.to_a # Get header texts.
                
                  el.insert(1, "# since : #{now.strftime '%Y-%m-%d %H:%M:%S %:z'}\n") if modsince # Add since if since was not exist.
                
	          # Update last update time if last-upadte is not set or last-update is older than mtime.
                  el.insert(1, "# last-update : #{File.mtime(file.first).strftime '%Y-%m-%d %H:%M:%S %:z'}\n") if modupdate# Add last update timestamp 
              
                  el.join
                }

                  # Write to file if updated.
                  f.seek(0)
	          f.truncate(0)
	          f.write( content )
                end
              end # Close file.
            end # Missing header
            
          rescue # YAML or IO Rescue.
            STDOUT.puts $!
          end
          
        end # file given.
      end #proc_header
      
      # Invoke pandoc, format, and out.
      def pandoc(options)
        
        outstr = nil
        
        # filter pandoc.
        IO.popen(( ["pandoc"] + options ), "w+") do |io|
          io.write @file_content
          io.close_write
          
          outstr = io.gets(nil)
        end
                                                  
        # subscribe content
        flag = false
        outstr = outstr.each_line.select do |line|
            
          if line =~ /^<\/body>$/
            flag = false
          end

          if line =~ /^<body>$/
            flag = true
            next false
          end
            
          flag
        end.join
        
        return outstr
        
      end
      
      
      def initialize
          
        pandoc_opt = nil
        
        # Get pandoc options from argv.
        if sep = ARGV.index("--")
          pandoc_opt = ARGV[(sep + 1) .. -1]
          ARGV.pop
        else
          pandoc_opt = [ "-t", "html", "-s", "-p" ]
        end
        
        proc_header( ( ARGV.empty? ? nil : ARGV.dup ) )
        
        doc = pandoc(pandoc_opt)
        print doc

      end
   
    end #MDProceessor
    
  end
end

YEK::ReasonBuild::MDProceessor.new

PureBuilderでzsh functionを導入

Zshのautoloading functionは「functionを書いたファイルを大量に.する代わりにロードする手段を与える」ものだと思えばいい。基本的には1 file 1 functionであるようだ。

$fpathに含まれるディレクトリにあるファイル名がautoloadで指定する値であり、またfunction名にもなる。

これまでrcファイルに直接書いていたUpdate機能だが、Zsh functionとして提供することにした。ファンクションインストーラが勝手に専用ディレクトリ~/.yek/lib/purebuild_functionsにインストールするので($funcdirで直接指定することもできる)、それを$fpathに追加することでロードできるようになる。あとはautoload -Uzして使って欲しい。

このfunctionはかなり丁寧に情報を出力する。「Last Updateよりも新しい」の判定は、素直にfind(1)touch(1)を使っている。また、最終更新についてはstat(1)を使って表示している。

今回はfunctionを使うようになったのがメインだが、つられてpure builder及びpuredocの更新もあった。

また、purebuilderはGitHubで公開したので参考にしてほしい。