Jacal の日々 by rna

2006-02-08

[] 新メンバー?

5日に参加申請された方、よろしければ自己紹介をお願いします。rna◎cyber・email・ne・jp (記号は適当に読み替え)まで。

[] Perl の DOM (XML::DOM)

Perl 標準の DOM は DOM level 1。名前空間なし。別にいいやと思ってたけどやっぱり面倒。

[] HTML::Parser と文字化け(その2)

文字参照をパーサがユニコード文字に展開すると文字化けの原因になるっぽい。utf8 フラグ関係? まだよくわかりません。

jacal プロセッサでは大抵は展開せずに生のテキストを右から左へ流しているだけですが、属性値については展開済みの値を比較するし、param 要素の値は utf-8 に変換してテンプレート置換系に渡さないといけないので、ここで問題がでるかも。

[] 近況

作業が遅れっぱなしですみません。先月末からサラリーマンみたいな生活をしています。毎朝、毎晩満員電車。人身事故でダイヤの乱れが云々もそろそろ慣れてきました。東京は怖いところです。

あらかじめ先方に断ってあるので jacal の作業のために休みはとれるのですが、今週は色々制約があって無理です。

トラックバック - http://jacal.g.hatena.ne.jp/rna/20060208

2006-01-23

[] 三周目

式言語は pbyacc に頑張ってもらえばどうにでもなりそうなので、コンポネントクラスの実装を始めました。XML は DOM パーサでパースする(標準的に使える pull parser がないので)。

HTML::PullParser の xml mode を使う手も考えたけど、エンコーディングとか、整形式制約違反の扱いが怪しいので、まともな XML パーサを使います。

[] HTML::Parser と文字化け

別件で作ったスクリプトで、utf-8 の入力が部分的に文字化けするトラブルが。。。

HTML::Parser 自体のバグだと嫌なので後で調査します。

トラックバック - http://jacal.g.hatena.ne.jp/rna/20060123

2006-01-11

[] perl-byacc (pbyacc) その2: 簡単な式言語

${foo} でパラメータ foo を展開するような単純な式言語処理系を pbyacc で作る。

exp.y:

%token NAME ANYOTHER DELIM_S DELIM_E EOF
%%

start : EOF		   { exit; } 
	| template EOF	   { print $1; exit; }
	;
template : literal
	| exp 
	| template literal { $$ = $1 . $2; }
	| template exp	   { $$ = $1 . $2; }
	;
literal : NAME		   { $$ = $1; }
	| DELIM_E	   { $$ = $1; }
	| ANYOTHER	   { $$ = $1; }
	;
exp : DELIM_S NAME DELIM_E { $$ = $p->{params}->{$2}; }
      /* アクション中ではパーサオブジェクト自身(self)を
	 $p で参照できる。*/
	;
%%
sub yylex {
    my ($p) = @_;
    # yyparse でパーサ自身を引数に渡すと、
    # $p にパーサが渡される。

    my $s = $p->{stream};
    my $c;
    my $val;
    while (($c = $s->getc) =~ /[_.0-9a-zA-Z]/) {
	$val .= $c;
    }
    if ($val ne '') {
	$s->ungetc;
	return ($NAME, $val);
    }
    if ($c eq '') {
	return $EOF;
    } elsif ($c eq '$') {
	if (($c = $s->getc) eq '{') {
	    return $DELIM_S; 
	} else {
	    $s->ungetc;
	    return ($ANYOTHER, '$');
	}
    } elsif ($c eq '}') {
	return $DELIM_E;
    } else {
	return ($ANYOTHER, $c);
    }
}

sub yyerror {
    my ($msg, $p) = @_;
    my $s = $p->{stream};
    die "$msg at " . $s->name . " line " 
	. $s->lineno . ".\n";
}

アクションや yylex() ではパーサオブジェクトがパラメータやストリームを保持することを仮定している。pbyacc -P ExpParser exp.y で ExpParser.pm を作ったら、これのサブクラスを定義する。

ExpParserImpl.pm:

package ExpParserImpl;
use strict;
use base 'ExpParser';

sub new {
    my $proto = shift;
    my $class = ref($proto) || $proto;
    my $self = $class->SUPER::new();
    $self->{params} = shift;
    $self->{stream} = shift;
    $self->{yylex} = \&ExpParser::yylex;
    $self->{yyerror} = \&ExpParser::yyerror;
    # $self->{yydebug} = \&ExpParser::yydebug;
    bless $self, $class;
    return $self;
}
1;

params と stream が新規に定義したインスタンス変数。yylex, yyerror, yydebug は pbyacc が出力するパーサで定義されているもの。

このパーサは以下のようにして使う。

exp.pl:

use ExpParserImpl;
use Fstream;
use strict;

my $s = Fstream->new(\*STDIN, 'STDIN');
my $params = {
	foo => "value-for-foo",
	bar => "value-for-bar",
};
my $p = ExpParserImpl->new($params, $s);
$p->yyparse($p);

実行例(太字は出力):

$ perl exp.pl
abc ${foo} def
${bar}
(ここで Control-D を入力)
abc value-for-foo def
value-for-bar
$

[] perl-byacc (pbyacc) その3: ユニコード文字を使う

ユニコード文字(perl の内部では utf-8)を扱えるパーサを生成する方法。

以下のサンプルでは入力された utf-8 の文字列を一文字ずつ空けて出力する。EOF がわりに「完」を使う。

実行例(太字は出力):

$ perl utf8test.pl
むかしむかし
あるところに
おじいさんが
いました。
完
む か し む か し
あ る と こ ろ に
お じ い さ ん が
い ま し た 。
$

utf-8 の一文字が一文字として読み込まれなくてはならない。まず、yacc ソースは以下のようにする。

utf8test.y:

%{
use utf8;
%}
%token UNICODE EOF
%%
start : EOF  { exit; }
	| story EOF { print $1; exit; }
	;
story : line
	| story line { $$ = $1 . $2; }
	;
line : '\n'
	| chars '\n' { $$ = $1 . "\n"; }
	;
chars: UNICODE
	| chars UNICODE { $$ = $1 . " " . $2; }
	;
%%
sub yylex {
    my ($s) = @_;
    my $c = $s->getc;
    if ($c eq '完') {
	return $EOF;
    } elsif ($c eq "\n") {
	return ord($c);
    } else {
	return ($UNICODE, $c);
    }
}

sub yyerror {}

太字の部分の use utf8; の宣言は生成された Perl モジュールに反映される。pbyacc で UTF8TestParser.pm を生成し、以下のように使う。

utf8test.pl:

use UTF8TestParser;
use Fstream;
use strict;

binmode STDIN, ":utf8";
binmode STDOUT, ":utf8";

my $s = Fstream->new(\*STDIN, 'STDIN');
my $p = UTF8TestParser->new(\&UTF8TestParser::yylex,
			    \&UTF8TestParser::yyerror);
$p->yyparse($s);

binmode でパーサへの入力とパーサからの出力を utf8 モードにする。こうすると utf8 の一文字がパーサ内でも一文字として扱われる。

ただし、utf8 の文字の値をそのままシンボルとして扱ってはいけない。

start : '完'  { exit; }
        | story '完' { print $1; exit; }
        ;

こういうルールを作って yylex() で ord('完') を返しても正しく動作しない。yydebug() で「完」を入力した時の様子を見ると、

yydebug: state 0, reading 23436 (illegal-symbol)
yydebug: error recovery discarding state 0

このように内部でエラーになる。

トラックバック - http://jacal.g.hatena.ne.jp/rna/20060111

2006-01-04

[] perl-byacc (pbyacc)

式言語のパーサを生成するのに Perl コードを吐く yacc を探したところ、Berkeley YACC の改造版があった。

perl-byacc のインストール

perl-byacc は CPAN にもあるが、少しパッチの当たってる Debian GNU/Linux 用のパッケージがある。それのソースを拝借。

perl-byacc_2.0.orig.tar.gz を展開して、perl-byacc_2.0-6.diff.gz をパッチで当てる。このパッチには Perl 5 対応パッチ(.pm ファイルを吐くようになる)が含まれる。test/Makefile にパッチが当たらないが気にしなくてよい(はず。Perl 4 用のテストなので)。

$ tar xvfz perl-byacc_2.0.orig.tar.gz
$ cd perl-byacc_2.0.orig
$ zcat perl-byacc_2.0-6.diff.gz | patch
$ make

make install すると /usr/bin/pbyacc と /usr/lib/libby.a 、あと man page がインストールされるけど、perl 用としては libby.a はいらない(はず)。pbyacc をパスの通った所に pbyacc をコピーするだけでも OK。

make install ではインストールされないが Fstream.pm も必要に応じてコピーして使う。Fstream.pm は getc と ungetc ができるストリームを提供する。pbyacc が生成するパーサ自体は Fstream.pm のインターフェースに依存しないので似たようなものを自分で作ってもいい。

使用例

サンプルとして calc.y と gen.y が付いてくる(make するとサンプルのパーサも生成される)が、もっと短い例を。

minicalc.y:

%token	NUM
%left	'-' '+'
%%
start:  /*empty*/ | start input;
input:  '\n' 
	| exp '\n'	{ print $1 . "\n"; }
	;
exp: 	NUM             { $$ = $1; }
	| exp '+' exp	{ $$ = $1 + $3; }
	| exp '-' exp	{ $$ = $1 - $3; }
	;
%%
sub yylex {
    my ($s) = @_;
    my ($c, $val);

    while (($c = $s->getc) =~ /[ \t\r]/) {}
    if ($c eq '') {
	return 0;
    } elsif ($c =~ /\d/) {
        $val = $c;
        while (($c = $s->getc) =~ /\d/) {
            $val .= $c;
        }
        $s->ungetc;
        return ($NUM, $val);
    } else {
        return ord($c);
    }
}
sub yyerror {
    my ($msg, $s) = @_;
    die "$msg at " . $s->name . " line " . $s->lineno . ".\n";
}

字句解析はベタに書いている。ここを生成する lex に相当する Perl モジュール Parse::YYLex もあるが今回は使わなかった。yylex, yyerror の引数 $s は、ここでは Fstream オブジェクトを想定している。

上の minicalc.y から pbyacc で MiniCalcParser.pm を生成するには以下のようにする。

$ pbyacc -P MiniCalcParser minicalc.y

以下は MiniCalcParser を使った計算機。要 Fstream.pm。

minicalc.pl:

use MiniCalcParser;
use Fstream;
use strict;
my $s = Fstream->new(\*STDIN, 'STDIN');
my $p = MiniCalcParser->new(\&MiniCalcParser::yylex, 
			    \&MiniCalcParser::yyerror);
$p->yyparse($s);

パーサのコンストラクタの引数にはサブルーチンのリファレンスを渡す。minicalc.y に書いた yylex, yyerror を渡すが、これらは MiniCalcParser のサブルーチンになっているのでこのようになる。オプションで3番目の引数にデバッグルーチンも渡せる。デフォルトの実装 MIniCalcParser::yydebug が生成されているのでそれを渡せばよい。

実行例(太字が出力):

$ perl minicalc.pl
1 + 1
2
1 + 2 + 3
6
1 + 2 - 3
0
トラックバック - http://jacal.g.hatena.ne.jp/rna/20060104

2005-12-27

[] エンコーディングの扱い

  • コンポネント変換処理は utf-8 で行う。
  • 入力ファイルからのパラメータは一度 utf-8 に変換する。
  • コンポネントクラスのリソースは utf-8 に変換する。
  • 変換結果は入力ファイルのエンコーディングに戻す。
  • ただしコンポネント変換の再帰処理は utf-8 のまま行う。

変換表の揺れによる文字化けが起こりうるが仕方がない。

コンポネントクラスと入力ファイルのエンコーディングが一致している限り文字化けしないことを保証するような方法もありうるが、面倒なのでやらない。

入力ファイル全体を utf-8 に変換するともっと楽になるが、それだとユーザ(入力ファイルを書く人)が文書全体について文字化けに注意しなくてはならなくなるので負担が大きい。その点上のやり方ならユーザは jacal 要素に渡すパラメータだけ気を付ければいい。

[] 再帰の深さ

snapshot-051225 では単純に jacal 要素のネストを数えていたので、fallback 内の jacal 要素は深さ 2 になってしまっていた。変換結果を得る毎に深さが 1 増えるように数えるようにする。

[] utf-16* の扱い

一度 utf-8 に変換した文字列をパースして、最後に戻すようにした。utf-16 の場合ただ戻すと body の先頭にBOMが付いてしまうので、BOMを見てLEかBEか判定してBOMなしのエンコーディングに変換して戻す。

ちなみに utf-32 は未サポート。

[] jacal-0_0_2

二周目できました。

新機能

  • JACALコンポネントを再帰的に変換できるようにした(再帰の深さは20段まで)。
  • 入力ファイルのエンコーディングを指定できるようにした(-e オプション)。
  • より適切なエラー/警告メッセージを出すようにした。
  • より適切な位置に出力ファイルを書き込むようにした。

Usage:

jacal.pl [options] files

> jacal.pl foo.jacal.html

foo.jacal.html を変換して foo.html に出力。

> jacal.pl -c foo.jacal.html

foo.jacal.html を変換して標準出力に出力。

> jacal.pl - 

標準入力を変換して標準出力に出力。

> jacal.pl -e shift_jis foo.jacal.html

シフトJIS で記述された入力ファイル foo.jacal.html を変換して foo.html に出力。foo.htmlシフトJIS のファイルになる。

# シェルが bash の場合
> export JACAL_DOC_BASE=./build
> jacal.pl foo/bar.jacal.html

foo/bar.jacal.html を変換して ./build/foo/bar.html に出力。

ディレクトリ build, foo はなければ作成される。

TODO

  • コンポネントクラスのパース
  • テンプレート内の式言語の評価
  • デバッグ用オプション(-v)
  • id パラメータの扱い(明示的に指定して上書きできるようにする?)

KENZKENZ2006/01/01 02:38こっそりと生暖かく見守っている者です。せっかくはてなグループを使っているので、TODO管理には「あしか」を使ってみては:
http://jacal.g.hatena.ne.jp/task/

rnarna2006/01/01 06:35コメントありがとうございます。あしか知りませんでした! ちょっと試してみます。でも僕ははてダラ使いなので使い続けるかどうか…(檜山さんがご希望なら使うけど)

m-hiyamam-hiyama2006/01/03 14:17使い方がわからない。別に希望じゃないけど、使い方わかるまで(つまり試しで)やろう。

KENZKENZ2006/01/03 19:41簡単な説明はここにあります↓。でも、どう運用するのがいいのかは分かりにくいですね。タスクグループを作るって言うのが特に…
http://hatena.g.hatena.ne.jp/hatenagroup/20051222/1135244949

トラックバック - http://jacal.g.hatena.ne.jp/rna/20051227