# -----------------------------------------------------------------------------
# Tripletail::Template::Node - Templateノードオブジェクト
# -----------------------------------------------------------------------------
package Tripletail::Template::Node;
use strict;
use warnings;
use Tripletail;
#use Smart::Comments;
my @_SPLIT_CACHE;
1;
# テンプレートをパーツ毎に分割
#
#
# aaa<&FOO>bbb
#
#
#
#
# $this->{tmplvec} = []; # Template Vector
# ==> [0] = "\n aaa"
# [1] = ['tag', 'foo', \"tag:foo"]
# [2] = "bbb\n "
# [3] = ['mark', 'bar', \"node:bar"]
# [4] = "\n "
# [5] = ['copy', 'baz', \"node:baz"]
# [6] = "\n "
#
# $this->[tmpltags] = ['foo'];
#
# $this->{tmplback} = []; # tmplvec のコピー
#
# 挿入タグや, への挿入は、{タグ名 => 値} のハッシュへ値を設定する事で行う。
# リセット時にはそのハッシュの内容を空にすると同時に tmplvec をバックアップから書き戻す。
#
# $this->{valmap} = {}; # Value Map
# ==> [tag:foo] = "FOOに入れたテキスト"
# [node:bar] = "ノード bar を add した時の内容"
#
# flush 時にはテンプレートの先頭から少しずつ削って行く事になる為、
# tmplvec の内容は浅く変化する。(つまり配列は変化しても配列の要素までは
# 変化しない。)
sub _new {
my $class = shift;
my $parent = shift; # Tripletail::Template::Node または undef (rootの場合)
my $name = shift; # の名前。rootならundef
my $html = shift; # template html
my $allow_unexpanded_tags = shift; # allow_unexpanded_tags
my $this = bless {} => $class;
$this->_reset;
$this->{parent} = $parent;
$this->{name} = lc $name;
$this->{allow_unexpanded_tags} = $allow_unexpanded_tags || 'false';
if(defined $html) {
$this->_setTemplate($html);
}
$this;
}
sub _reset {
my $this = shift;
# 以下はルートにのみ存在する
$this->{is_xhtml} = undef;
# ソース冒頭参照
$this->{tmplvec} = [];
$this->{tmplback} = [];
$this->{valmap} = {};
# ノード -- {name => Tripletail::Template::Node}
$this->{node} = {};
# タグ属性
$this->{attr} = {};
# trim
$this->{trimed} = {
first => undef,
last => undef,
leadings => undef,
followings => undef,
leadings_join => undef,
followings_join => undef,
};
$this;
}
sub isRoot {
my $this = shift;
!defined($this->{parent});
}
sub isXHTML {
my $this = shift;
$this->isRoot ? $this->{is_xhtml} : $this->{parent}->isXHTML;
}
sub _setTemplate {
my $this = shift;
my $str = shift;
$this->_reset;
if($str =~ m/^\s*<\?xml/) {
$this->{is_xhtml} = 1;
} else {
$this->{is_xhtml} = undef;
}
# テンプレートに既にが入っていたらエラー。
if($str =~ m//) {
die __PACKAGE__."#setTemplate: we can't implant in a template by hand anymore. Use instead.".
" (テンプレートにタグを入れることは出来ません。を使用してください)\n";
}
# - をパースして、ノードを生成。
$str =~ s{(.*?)}{
my ($name, $template) = (lc $1, $2);
if($this->{node}{$name}) {
# 既に同じノードが存在していたらエラー。
die __PACKAGE__."#setTemplate: node [$name] is duplicated. (ノード[$name]が複数あります)\n";
}
$this->{node}{$name} = Tripletail::Template::Node->_new(
$this, $name, $template, $this->{allow_unexpanded_tags}
);
"";
}egs;
# 置換されなかったやがあったらエラー。
if($str =~ m{():.+?>)}) {
die __PACKAGE__."#setTemplate: $1 doesn't match to an another side. ($1のブロックの対応がとれていません)\n";
}
$this->_split($str,1);
$this;
}
# -----------------------------------------------------------------------------
# $node->trim().
# $node->trim(@where).
# @where ::= (
# '-first', '-last', '-leadings', '-followings',
# '-begin', '-end', '-inside', '-outside', '-line',
# ). (default:line)
#
#
[LEADINGS_JOIN]
# [LEADINGS][FIRST]
# ...
# [LAST][FOLLOWINGS]
# [FOLLOWINGS_JOIN]
#
sub trim
{
my $this = shift;
our $TRIM_KEYS ||= [qw(
first last leadings followings
begin end
inside outside
line join
)];
my $val = [];
my $opts = {};
if( !@_ )
{
$opts->{line} = [];
}else
{
foreach (@_)
{
if( /^-([a-z_]\w*)\z/ )
{
my $key = $1;
$val = [];
if( $key =~ s/_join// )
{
push(@$val, 'join');
}
$opts->{$key} = $val;
}else
{
push(@$val, $_);
}
}
}
foreach my $key (@$TRIM_KEYS)
{
if( my $val = $opts->{$key} )
{
my $sub = "_trim_${key}";
$this->$sub(@$val);
}
}
$this;
}
sub _trim_line
{
my $this = shift;
$this->_trim_first(@_);
$this->_trim_last(@_);
$this->_trim_leadings(@_);
$this->_trim_followings(@_);
$this;
}
sub _trim_join
{
my $this = shift;
$this->_trim_line('join', @_);
}
sub _trim_begin
{
my $this = shift;
$this->_trim_leadings(@_);
$this->_trim_first(@_);
}
sub _trim_end
{
my $this = shift;
$this->_trim_last(@_);
$this->_trim_followings(@_);
}
sub _trim_inside
{
my $this = shift;
$this->_trim_first(@_);
$this->_trim_last(@_);
}
sub _trim_outside
{
my $this = shift;
$this->_trim_leadings(@_);
$this->_trim_followings(@_);
}
sub _trim_first
{
my $this = shift;
my $join = grep { /^-?join\z/ } @_;
my $mode = $join ? 'join' : 'line';
if( $this->{trimed}{first}{$mode} )
{
return $this;
}
$this->{trimed}{first}{$mode} = 1;
if( $join )
{
$this->{trimed}{first}{line} = 1;
}
foreach my $vec ($this->{tmplvec}, $this->{tmplback})
{
foreach my $val (@$vec)
{
ref($val) and last;
if( $join )
{
$val =~ s/^\s+//;
}else
{
# 改行以外の空白.
$val =~ s/^(?:[^\S\r\n]+)//;
}
$val eq '' and next;
$val =~ s/^(?:\r?\n|\r)//;
last;
}
}
$this;
}
sub _trim_last
{
my $this = shift;
my $join = grep { /^-?join\z/ } @_;
my $mode = $join ? 'join' : 'line';
if( $this->{trimed}{'last'}{$mode} )
{
return $this;
}
$this->{trimed}{'last'}{$mode} = 1;
if( $join )
{
$this->{trimed}{'last'}{line} = 1;
}
foreach my $vec ($this->{tmplvec}, $this->{tmplback})
{
foreach my $val (reverse @$vec)
{
ref($val) and last;
if( $join )
{
$val =~ s/\s+\z//;
}else
{
# 改行以外の空白.
$val =~ s/(?:[^\S\r\n]+)\z//;
}
$val eq '' and next;
last;
}
}
$this;
}
sub _trim_leadings
{
my $this = shift;
my $join = grep { /^-?join\z/ } @_;
my $mode = $join ? 'join' : 'line';
my $par = $this->{parent};
my $name = $this->{name};
$par or return $this;
if( $par->{trimed}{leadings}{$name} )
{
return $this;
}
$par->{trimed}{leadings}{$name} = 1;
foreach my $vec ($par->{tmplvec}, $par->{tmplback})
{
my $found;
foreach my $i (0..$#$vec)
{
ref($vec->[$i]) or next;
$vec->[$i][0] eq 'mark' or next;
$vec->[$i][1] eq $name or next;
$found = $i;
last;
}
if( $found )
{
foreach my $i (reverse 0..$found-1)
{
ref($vec->[$i]) and last;
if( $join )
{
$vec->[$i] =~ s/\s+\z//;
}else
{
# 改行以外の空白.
$vec->[$i] =~ s/(?:[^\S\r\n]+)\z//;
}
$vec->[$i] eq '' and next;
last;
}
}
}
$this;
}
sub _trim_followings
{
my $this = shift;
my $join = grep { /^-?join\z/ } @_;
my $mode = $join ? 'join' : 'line';
my $par = $this->{parent};
my $name = $this->{name};
$par or return $this;
if( $par->{trimed}{followings}{$name} )
{
return $this;
}
$par->{trimed}{followings}{$name} = 1;
foreach my $vec ($par->{tmplvec}, $par->{tmplback})
{
my $found;
foreach my $i (0..$#$vec)
{
ref($vec->[$i]) or next;
$vec->[$i][0] eq 'mark' or next;
$vec->[$i][1] eq $name or next;
$found = $i;
last;
}
if( defined($found) )
{
foreach my $i ($found+1 .. $#$vec)
{
ref($vec->[$i]) and last;
if( $join )
{
$vec->[$i] =~ s/^\s+//;
}else
{
# 改行以外の空白.
$vec->[$i] =~ s/^(?:[^\S\r\n]+)//;
}
$vec->[$i] eq '' and next;
$vec->[$i] =~ s/^(?:\r?\n|\r)//;
last;
}
}
}
$this;
}
sub getHtml {
my $this = shift;
$this->_compose(save_marks => 1);
}
sub setHtml {
my $this = shift;
my $html = shift;
if(!defined($html)) {
die __PACKAGE__."#setHtml: arg[1] is not defined. (第1引数が指定されていません)\n";
} elsif(ref($html)) {
die __PACKAGE__."#setHtml: arg[1] is a reference. (第1引数がリファレンスです)\n";
}
$this->_split($html,1);
$this;
}
sub node {
my $this = shift;
my $name = shift;
if(!defined($name)) {
die __PACKAGE__."#node: arg[1] is not defined. (第1引数が指定されていません)\n";
} elsif(ref($name)) {
die __PACKAGE__."#node: arg[1] is a reference. (第1引数がリファレンスです)\n";
}
$name = lc($name);
my $node = $this->{node}{$name};
if(!$node) {
my $me = $this->isRoot ? "the root" : "node [$this->{name}]";
my $me_ja = $this->isRoot ? "ルートノード" : "ノード [$this->{name}]";
die __PACKAGE__."#node: $me does not have a child node [$name]. (${me_ja}は子ノード [$name] を持っていません)\n";
}
$node;
}
sub exists {
my $this = shift;
my $name = shift;
if(!defined($name)) {
die __PACKAGE__."#exists: arg[1] is not defined. (第1引数が指定されていません)\n";
} elsif(ref($name)) {
die __PACKAGE__."#exists: arg[1] is a reference. (第1引数がリファレンスです)\n";
}
$name = lc($name);
exists $this->{node}{$name};
}
sub setAttr {
my $this = shift;
my $param = do {
if(ref($_[0]) eq 'HASH') {
shift;
} elsif(!ref($_[0])) {
scalar { @_ };
} else {
die __PACKAGE__."#setAttr: arg[1] is neither a HASH Ref nor a scalar. [$_[0]] (第1引数がハッシュでもハッシュのリファレンスでもありません)\n";
}
};
foreach my $key (keys %$param) {
if($param->{$key} eq 'plain'
|| $param->{$key} eq 'raw'
|| $param->{$key} eq 'js'
|| $param->{$key} eq 'jsstring'
|| $param->{$key} eq 'br') {
$this->{attr}{lc($key)} = $param->{$key};
} else {
die __PACKAGE__."#setAttr: arg[1] is an invalid type. [$param->{$key}] (第1引数の指定に不正な展開方法が含まれます)\n";
}
}
$TL->getDebug->_templateLog(
node => $this,
type => 'setattr',
args => $param
);
$this;
}
sub expand {
my $this = shift;
my $param = do {
if(ref($_[0]) eq 'HASH') {
shift;
} elsif(!ref($_[0])) {
scalar { @_ };
} else {
die __PACKAGE__."#expand: arg[1] is neither a HASH Ref nor a scalar. [$_[0]] (第1引数がハッシュでもハッシュのリファレンスでもありません)\n";
}
};
$this->_expand($param, 0);
}
sub expandAny {
my $this = shift;
my $param = do {
if(ref($_[0]) eq 'HASH') {
shift;
} elsif(!ref($_[0])) {
scalar { @_ };
} else {
die __PACKAGE__."#expandAny: arg[1] is neither a HASH Ref nor a scalar. [$_[0]] (第1引数がハッシュでもハッシュのリファレンスでもありません)\n";
}
};
$this->_expand($param, 1);
}
sub add {
my $this = shift;
$this->expand(@_);
$this->_dieIfDirty('add');
if(!defined($this->{parent})) {
die __PACKAGE__."#add: internal error [I have no parents]. (内部エラー:親がいません)";
} elsif(!defined($this->{name})) {
die __PACKAGE__."#add: internal error [I have no name]. (内部エラー:名前がありません)";
}
$TL->getDebug->_templateLog(
node => $this,
type => 'add'
);
# 文字列化
my $composed = $this->_compose;
# 親の及びの前に自分自身を挿入する
$this->{parent}{valmap}{"node:$this->{name}"} .= $composed;
# 元のテンプレートに戻す
$this->{tmplvec} = [ @{$this->{tmplback}} ];
%{$this->{valmap}} = ();
$this;
}
sub toStr {
my $this = shift;
$this->_dieIfDirty('toStr');
$TL->getDebug->_templateLog(
node => $this,
type => 'toStr'
);
$this->_dieIfAnyUnexpandedTag('toStr');
$this->_compose;
}
sub getForm {
my $this = shift;
my $name = shift;
if(ref($name)) {
die __PACKAGE__."#getForm: arg[1] is a reference. (第1引数がリファレンスです)\n";
}
if(!defined($name)) {
$name = '';
}
my $filter = $TL->newHtmlFilter(
interest => ['input'],
track => [qw[form textarea select option]],
filter_text => 1,
);
my $source = $this->getHtml;
$this->_dieIfAnyNestedTag('getForm', $source);
$filter->set($source);
my $form = $TL->newForm;
### html: $this->getHtml
my $found;
while(my ($context, $elem) = $filter->next) {
### elem: $elem
if(my $f = $context->in('form')) {
my $curname = $f->attr('name');
$curname = defined($curname) ? $curname : '';
if($curname ne $name) {
# 関係無いフォーム
next;
} else {
$found = 1;
}
} else {
# form要素の中でない。
next;
}
if($elem->isElement) {
### name: $elem->name
if(lc($elem->name) eq 'input') {
my $name = $elem->attr('name');
my $type = lc $elem->attr('type');
my $value = do {
my $str = $elem->attr('value');
defined $str ? $str : '';
};
my $checked = do {
my $str = lc($elem->attr('checked'));
if($str && $str eq 'checked') {
$str;
} elsif($elem->end && $elem->end eq 'checked') {
$elem->end;
} else {
undef;
}
};
if(defined($name)) {
if(!defined $type
|| $type eq ''
|| $type eq 'text'
|| $type eq 'password'
|| $type eq 'hidden'
|| $type eq 'submit'
) {
$form->add(
$TL->unescapeTag($name) => $TL->unescapeTag($value)
);
} elsif($type eq 'radio' || $type eq 'checkbox') {
if($checked) {
$form->add(
$TL->unescapeTag($name) => $TL->unescapeTag($value)
);
} else {
if(!$form->exists($name)) {
$form->set($TL->unescapeTag($name) => []);
}
}
}
}
}
} elsif($elem->isText) {
if(my $textarea = $context->in('textarea')) {
if(defined(my $name = $textarea->attr('name'))) {
my $text = $elem->str;
$text =~ s/^(?:\r?\n|\r)//;
$form->add(
$TL->unescapeTag($name) => $TL->unescapeTag($text)
);
}
} elsif(my $option = $context->in('option')) {
my $select = $context->in('select');
if($select && defined(my $name = $select->attr('name'))) {
my $value = do {
my $str = $option->attr('value');
if(defined($str)) {
$str;
} else {
my $str = $elem->str;
$str =~ s/^\s*//;
$str =~ s/\s*$//;
$str;
}
};
my $selected = do {
my $str = lc $option->attr('selected');
if($str && $str eq 'selected') {
$str;
} elsif($option->end && $option->end eq 'selected') {
$option->end;
}
};
if($selected) {
$form->add(
$TL->unescapeTag($name) => $TL->unescapeTag($value)
);
}
}
}
}
}
if(!$found) {
die __PACKAGE__."#getForm: form [$name] does not exist. (form [$name] が存在しません)\n";
}
$form;
}
sub __popform
{
# 指定されたkeyの先頭の値を取り出し、それを消す。
my $form = shift;
my $key = shift;
my @array = $form->getValues($key);
if( !@array )
{
return '';
}
my $val = shift @array;
$form->remove($key => $val);
$val;
}
sub setForm {
my $this = shift;
my $form = shift;
my $name = shift;
if(!defined($form)) {
die __PACKAGE__."#setForm: arg[1] is not defined. (第1引数が指定されていません)\n";
} elsif(ref($form) eq 'HASH') {
$form = $TL->newForm($form);
} elsif(ref($form) ne 'Tripletail::Form') {
die __PACKAGE__."#setForm: arg[1] is not an instance of Tripletail::Form. [$form]. (第1引数がFormオブジェクトではありません)\n";
}
if(ref($name)) {
die __PACKAGE__."#setForm: arg[2] is a reference. (第2引数がリファレンスです)\n";
}
# $formは後で変更してしまうのでcloneして置く
$form = $form->clone;
if(!defined $name) {
$name = '';
}
$TL->getDebug->_templateLog(
node => $this,
type => 'setForm',
form => $form,
name => $name,
);
my $html = $this->getHtml;
my $has_textarea = $html=~/