changeset 181:47dac58691ee

New templating system, small fixes
author sourcer
date Thu, 26 Jan 2012 01:15:57 +0400
parents d1676be8afcc
children adc7669172c4
files Lib/IMPL/Exception.pm Lib/IMPL/Object/Factory.pm Lib/IMPL/Web/View/TTControl.pm Lib/IMPL/Web/View/TTDocument.pm Lib/IMPL/Web/View/TTFactory.pm Lib/IMPL/Web/View/TTLoader.pm Lib/IMPL/clone.pm Lib/IMPL/lang.pm _doc/make.pl _test/Resources/large.out.xml _test/Test/SQL/Diff.pm _test/Test/Web/TT.pm conv.pl
diffstat 12 files changed, 656 insertions(+), 28 deletions(-) [+]
line wrap: on
line diff
--- a/Lib/IMPL/Exception.pm	Fri Dec 30 23:40:00 2011 +0300
+++ b/Lib/IMPL/Exception.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -96,6 +96,14 @@
 our @ISA = qw(IMPL::Exception);
 __PACKAGE__->PassThroughArgs;
 
+package IMPL::KeyNotFoundException;
+our @ISA = qw(IMPL::Exception);
+__PACKAGE__->PassThroughArgs;
+
+our %CTOR = (
+	'IMPL::Exception' => sub { "A specified element isn't found", $_[0] } 
+);
+
 package IMPL::NotImplementedException;
 our @ISA = qw(IMPL::Exception);
 __PACKAGE__->PassThroughArgs;
--- a/Lib/IMPL/Object/Factory.pm	Fri Dec 30 23:40:00 2011 +0300
+++ b/Lib/IMPL/Object/Factory.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -3,12 +3,12 @@
 
 use parent qw(IMPL::Object IMPL::Object::Serializable);
 
-use IMPL::Class::Property;
+use IMPL::lang qw(:declare :constants);
 
 BEGIN {
-	public property factory => prop_get | owner_set;
-	public property parameters => prop_get | owner_set;
-	public property method => prop_get | owner_set; 
+	public property factory => PROP_GET | PROP_OWNERSET;
+	public property parameters => PROP_GET | PROP_OWNERSET;
+	public property method => PROP_GET | PROP_OWNERSET; 
 }
 
 # custom factory, overrides default
@@ -33,7 +33,7 @@
 	my %args = @$data;
 	
 	if ($surrogate) {
-		$surrogate->callCTOR($args{factory},$args{parameters},$args{method});
+		$surrogate->self::CTOR($args{factory},$args{parameters},$args{method});
 		return $surrogate;
 	} else {
 		return $class->new($args{factory},$args{parameters},$args{method});
@@ -44,12 +44,19 @@
 	my $this = shift;
 	
 	if (my $method = $this->method) {
-		$this->factory->$method($this->parameters ? (_as_list($this->parameters),@_) : @_);	
+		$this->factory->$method($this->MergeParameters(@_));	
 	} else {
-		$this->factory->new($this->parameters ? (_as_list($this->parameters),@_) : @_);		
+		$this->factory->new($this->MergeParemeters(@_));		
 	}
 }
 
+sub MergeParameters {
+	my $this = shift;
+	
+	$this->parameters ? (_as_list($this->parameters),@_) : @_;
+}
+
+
 sub _as_list {
 	ref $_[0] ?
 		(ref $_[0] eq 'HASH' ?
@@ -169,6 +176,13 @@
 развернуты в список и переданы оператору C< new > фабрике из свойства C< factory >, за ними будут
 следовать параметры непосредственно текущей фабрики.
 
+=item C<MergeParameters(@params)>
+
+Метод смешивающий фиксированные параметры с параметрами переданными методу C<new(@params)>. По умолчанию
+добавляет пареметры фабрики в конец к фиксированным параметрам. Для изменения этого поведения требуется
+переопределить данный метод. Также этот метод можно переопределить для передачи параметров, значения
+которых вычисляются.
+
 =item C<new(@params)>
 
 Создает новый объект, используя свйство C<factory> как фабрику и передавая туда параметры
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/IMPL/Web/View/TTControl.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -0,0 +1,88 @@
+package IMPL::Web::View::TTControl;
+use strict;
+
+use IMPL::DOM::Property qw(_dom);
+use IMPL::lang qw(:declare :constants);
+
+use Template::Context();
+
+use parent qw(
+	IMPL::DOM::Node
+);
+
+my $nextId = 1;
+
+
+BEGIN {
+	public _dom property id => PROP_ALL;
+	
+	public property context => PROP_GET | PROP_OWNERSET;
+	public property template => PROP_ALL;
+}
+
+
+sub CTOR {
+	my ($this,$name,$template,$context,$refProps) = @_;
+	
+	$this->template( $template ) or die new IMPL::ArgumentException("A template is required");
+	$this->context( $context ) or die new IMPL::ArgumentException("A context is required");
+	
+	if ( my $ctor = $template->blocks->{CTOR} ) {
+		$context->process($ctor, { this => $this } );
+	}
+	
+}
+
+our %CTOR = (
+	'IMPL::DOM::Node' => sub {
+		nodeName => $_[0],
+		%{ $_[3] || {} }
+	}
+);
+
+sub Render {
+	my ($this) = @_;
+	
+	if(my $body = $this->template->blocks->{RENDER} ) {
+		return $this->context->process( $body, { this => $this } );
+	} else {
+		return "";
+	}
+	
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+C<IMPL::Web::View::TTControl>
+
+=head1 SYNPOSIS
+
+=head1 DESCRIPTION
+
+=head2 BLOCKS
+
+При загрузке шаблона, создается фабрика, с собственным контекстом в которой выполняется шаблон элемента управления
+
+=head3 INIT
+
+Данный блок шаблона управления выполняется один раз при создании первого экземпляра элемента управления
+
+=head3 CTOR
+
+данный блок выполняется каждый раз при создании нового экземпляра элемента управления, при этом переменная C<this>
+указывает на эземпляр элемента упарвления
+
+=head3 RENDER
+
+ 
+
+
+C<lang ru>
+
+=cut
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/IMPL/Web/View/TTDocument.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -0,0 +1,178 @@
+package IMPL::Web::View::TTDocument;
+use strict;
+
+use IMPL::lang qw(:declare :constants);
+use IMPL::DOM::Property qw(_dom);
+use IMPL::Web::View::TTFactory();
+use IMPL::Web::View::TTControl();
+
+
+use parent qw(
+	IMPL::DOM::Document
+	IMPL::Web::View::TTControl
+);
+
+BEGIN {
+	public _dom property layout => PROP_ALL;
+	public property opts => PROP_GET | PROP_OWNERSET;
+	public property loader => PROP_GET | PROP_OWNERSET;
+	public property controls => PROP_GET | PROP_OWNERSET;
+}
+
+sub CTOR {
+	my ($this,$template,$refOpts,%args) = @_;
+	
+	$this->controls({});
+	$this->loader($args{loader}) if $args{loader};
+	
+	$this->layout( $template->layout ) unless $this->layout;
+	
+	$this->opts($refOpts);
+}
+
+our %CTOR = (
+	'IMPL::Web::View::TTControl' => sub {
+		'document',
+		$_[0], # template
+		new Template::Context($_[1])  # context
+	},
+	'IMPL::DOM::Document' => sub {
+		nodeName => 'document'
+	}
+);
+
+sub require {
+	my ($this, $control) = @_;
+	
+	if (! $this->controls->{$control}) {
+		
+		(my $path = $control) =~ tr/\./\//;
+		if ( my $template = $this->loader->template($path) ) {
+			my $opts = ${$this->opts};
+			$opts->{STASH} = $this->context->stash->clone();
+			
+			my $ctx = new Template::Context($opts);
+			
+			my $factory = new IMPL::Web::View::TTFactory(
+				typeof IMPL::Web::View::TTControl,
+				$template,
+				$ctx,
+				$opts
+			);
+			
+			my @parts = split(/\.+/,$control);
+			
+			$this->context->stash->set([map { $_, 0 } @parts], $factory);
+			
+		} else {
+			die new IMPL::KeyNotFoundException($control);
+		}
+		
+	}
+}
+
+sub Render {
+	my ($this,$param) = @_;
+	
+	my $output = $this->context->process($this->template, {this => $this} );
+	
+	if ($this->layout) {
+		$output = $this->context->include($this->layout,{ content => $output });
+	}
+	
+	return $output;
+}
+
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+C<IMPL::Web::View::TTDocument> - документ для построения HTML страницы на основе шаблонов TT.
+
+=head1 SYNOPSIS
+
+=begin code
+
+use IMPL::Web::View::TTDocument();
+
+my $doc = new IMPL::Wbe::View::TTDocument($template,$ttOptions);
+
+return $doc->Render();
+
+=end code
+
+Однако, более предпочтительный способ использовать C<IMPL::Web::View::TTLoader>.
+
+=head1 DESCRIPTION
+
+Документ для представления данных. Документы представляют собой иерархически организованные данные,
+элементами данного документа являются данные для отображения, такие как
+
+=over
+
+=item * Объекты из БД
+
+=item * Навигационные цепочки
+
+=item * Меню и т.п. 
+
+=back
+
+Скприт шаблона формирует структуру документа, затем сформированная структура форматируется в готовый документ.
+Процесс преобразования объектной модели в готовый документ может быть выполнена как вручную, так и при помощи
+вспомогательного шаблона - обертки. Если у шаблона документа указан C<wrapper> в метаданных, то он будет
+использован как шаблон для форматирования объектной модели, вывод самого шаблона будет проигнорирован. Если
+обертка не задана, то результатом будет вывод самого скрипта шаблона.
+
+Каждый документ имеет свое собственное пространство имен, которое может быть вложенным в некоторое внешнее,
+указанное при создании документа.
+
+=head2 Загрузка элемента управления
+
+=over
+
+=item 1 C<document.require(my.org.input)>
+
+=item 1 Загружает шаблон C<my/org/input.tt>
+
+=item 1 Создает фабрику элементов управления с собственным контекстом, вложенным в контекст документа.
+
+=item 1 Выполняет шаблон в пространстве имен фабрики 
+
+=item 1 Регистритует фабрику в контексте документа по пути C<my.org.input>
+
+=back
+
+=head2 Создание элемента управления
+
+=over
+
+=item 1 C<< my.org.input.new('login') >>
+
+=item 1 Если это первый элемент управления, то выполняетя статический конструктор в контексте фабрики
+
+=item 1 Создается новый дочерний контекст к контексту фабрики
+
+=item 1 Создается экземпляр элемента управления
+
+=item 1 Выполняется блок конструктора в контексте элемента управления, параметр C<this> имеет значение
+нового экземпляра элемента управления  
+
+=back
+
+=head1 MEMBERS
+
+=over
+
+=item C<CTOR($template, %options)>
+
+Создает экземпляр документа с указанным шаблоном и параметрами, параметры 
+
+=back
+
+=cut
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/IMPL/Web/View/TTFactory.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -0,0 +1,181 @@
+package IMPL::Web::View::TTFactory;
+use strict;
+
+use Template::Context();
+
+use IMPL::lang qw(:hash :declare :constants);
+use IMPL::Exception();
+
+
+use parent qw(IMPL::Object::Factory);
+
+BEGIN {
+	public property template => PROP_ALL;
+	public property context => PROP_ALL;
+	public property opts => PROP_ALL;
+	public property nodeProperties => PROP_ALL;
+	public property instances => PROP_GET | PROP_OWNERSET;
+}
+
+__PACKAGE__->PassThroughArgs;
+
+sub CTOR {
+	my ($this,$factory,$template,$context,$options,$nodeProps) = @_;
+	
+	die IMPL::ArgumentException("A template is required") unless $template;
+	
+	$options ||= {};
+	
+	$this->template($template);
+	$this->context($context || new Template::Context($options));
+	$this->opts($options);
+	$this->nodeProperties($nodeProps || {});
+	$this->instances(0);
+	
+	# init factory context
+	$this->context->process($this->template);
+}
+
+our %CTOR = (
+	'IMPL::Object::Factory' => sub {
+		$_[0]
+	}
+);
+
+sub MergeParameters {
+	my ($this,$name,$refProps) = @_;
+	
+	my $opts = { $this->opts };
+	$opts->{STASH} = $opts->{STASH}->clone() if $opts->{STASH};
+	
+	my $ctx = new Template::Context($opts);
+	
+	return ($name, $this->template, $ctx, hashMerge($this->nodeProperties,$refProps));
+}
+
+sub CreateObject {
+	my $this = shift;
+	
+	my $count = $this->instances;
+	
+	unless($count) {
+		# нужно выполнить именно блок INIT шаблона при создании первого экземпляра
+		if (my $init = $this->template->blocks->{INIT}) {
+			$this->context->process($init);
+		}
+	}
+	
+	$count++;
+	$this->instances($count);
+	
+	return $this->SUPER::CreateObject(@_);
+}
+
+sub save {
+	die new IMPL::NotImplementedException("This class doesn't support serialization");
+}
+
+sub restore {
+	die new IMPL::NotImplementedException("This class doesn't support serialization");
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+C<IMPL::Web::View::TTFactory> - фабрика элементов управления
+
+=head1 SYNOPSIS
+
+=begin code
+
+my $factory = new IMPL::Web::View::TTFactory(
+	typeof IMPL::Web::View::TTControl,
+	$doc,
+	$context,
+	{
+		TRIM => 1
+	},
+	{
+		myprop => 'my value'
+	},
+);
+
+my $input1 = $factory->new('login', { class => "required" } );
+
+my $propval = $input->nodeProperty('myprop'); # 'my value'
+
+=end code
+
+=begin text
+
+[%
+	this.appendChild(
+		my.org.input.new('login', class = this.errors('login') ? "invalid" : "" )
+	);
+%]
+
+=end text
+
+=head1 DESCRIPTION
+
+C< Inherits L<IMPL::Object::Factory> >
+
+=head1 MEMBERS
+
+=over
+
+=item C<[get,set]template>
+
+Документ C<Template::Document> который описывает элемент управления. См. C<IMPL::Web::View::TTControl>.
+
+=item C<[get,set]context>
+
+Контекст фабрики элементов управления, в этом контексте выполняет шаблон элемента управления при загрузке.
+Далее в этом контексте будет выполнен блок инициализации при создании первого элемента управления.
+
+=item C<[get,set]opts>
+
+Параметры контекста элемента управления (ссылка на хеш). Каждый элемент управления при создании получает свой контекст,
+который создает с данными параметрами и хранилищем переменных, дочерним к контексту фабрики.
+
+=item C<[get,set]nodeProperties>
+
+Ссылка на хеш со значениями свойств по умолчанию для создаваемого элемента управления.
+
+=item C<[get]instances>
+
+Количество созданных элементов управления данной фабрикой
+
+=item C<[override]MergeParameters($name,$nodeProps)>
+
+Превращает значения переданные методу C<new> фабрики в параметры для создания элемента управления.
+
+=over
+
+=item C<$name>
+
+Имя создаваемого узла (C<nodeName>).
+
+=item C<$nodeProps>
+
+Ссылка на шех со значениями свойств узла. Данные значения будут совмещены со значениями из свойства C<nodeProperties>
+
+=back
+
+=item C<[override]CreateObject(@params)>
+
+Создает экземпляр элемента управления стандартным образом. Учитывает количество экземпляров и если это первый,
+то производит дополнительную инициализацию контекста выполнив блок шаблона C<INIT>.
+
+=item C<new($name,$nodeProps)>
+
+Создает элемент управления с указанным именем и набором свойств. 
+
+=back
+
+=cut
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Lib/IMPL/Web/View/TTLoader.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -0,0 +1,136 @@
+package IMPL::Web::View::TTLoader;
+use strict;
+
+use IMPL::lang qw(:declare :constants);
+
+use Template::Provider();
+use Template::Context();
+use Template::Constants qw(:status);
+
+use IMPL::Web::View::TTDocument();
+
+use parent qw(
+	IMPL::Object
+);
+
+BEGIN {
+	public property options => PROP_ALL;
+	public property provider => PROP_GET | PROP_OWNERSET;
+	public property context => PROP_GET | PROP_OWNERSET;
+	public property ext => PROP_ALL;
+	
+	public property isInitialized => PROP_GET | PROP_OWNERSET;
+	public property initializer => PROP_GET | PROP_OWNERSET;
+}
+
+sub CTOR {
+	my ($this,$refOpts,%args) = @_;
+	
+	$this->ext(delete $args{etx});
+	$this->initializer(delete $args{initializer});
+	
+	$this->options($refOpts);
+	
+	$refOpts->{LOAD_TEMPLATES} = $this->provider(new Template::Provider($refOpts));
+	
+	$this->context(new Template::Context($refOpts));
+}
+
+sub document {
+	my ($this,$name) = @_;
+	
+	my $tt = $this->template($name);
+		
+	$this->_init();
+	
+	my $opts = { $this->options };
+	
+	$opts->{STASH} = $this->context->stash->clone();
+	$opts->{LOAD_TEMPLATES} = $this->provider;
+	
+	return new IMPL::Web::View::TTDocument( $tt, $opts, loader => $this );
+}
+
+sub template {
+	my ($this,$name) = @_;
+	
+	$name =~ s/^\s+|\s+$//g;
+	
+	die new IMPL::ArgumentException("A valid template name is required") unless length $name;
+	
+	$name = $this->_appendExt($name);
+	
+	my ($tt,$error) = $this->provider->fetch($name);
+	
+	if ($error == STATUS_DECLINED) {
+		die new IMPL::KeyNotFoundException($name);
+	} elsif ($error == STATUS_ERROR) {
+		die new IMPL::Exception("Failed to load a template", $name, $tt);
+	}
+	
+	return $tt;
+}
+
+sub _appendExt {
+	my ($this,$name) = @_;
+	
+	
+	if (length $this->ext and substr( $name, -length($this->ext) ) eq $this->ext) {
+		return $name;
+	} else {
+		return $name . $this->ext;
+	}
+}
+
+sub _init {
+	my ($this) = @_;
+	
+	if (!$this->isInitialized) {
+		$this->isInitialized(1);
+		
+		if ($this->initializer) {
+			eval {
+				$this->context->process($this->initializer);
+			}
+		}
+	}
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+C<IMPL::Web::View::TTLoader> - предоставляет глобальный контекст для загрузки шаблонов
+
+=head1 SYNOPSIS
+
+=begin code
+
+use IMPL::Web::View::TTLoader();
+
+my $loader = new IMPL::Web::View::TTLoader(
+	{
+		INCLUDE_PATH => [
+			'/my/app/tt',
+			'/my/app/tt/lib'
+		]
+	},
+	ext => '.tt',
+	initializer => 'shared/global'
+		
+);
+
+my $doc = $loader->document('index');
+
+my $html = $doc->Render();
+
+=end code
+
+=head1 DESCRIPTION
+
+=cut
+
--- a/Lib/IMPL/clone.pm	Fri Dec 30 23:40:00 2011 +0300
+++ b/Lib/IMPL/clone.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -49,7 +49,7 @@
 		
 		return $_[0] unless ref $_[0];
 		
-		return $_[1]->{refaddr($_[0])} || ($handlers{reftype($_[0])} || sub { die "Unknown reftype " . reftype($_[0])} )->(@_);
+		return $_[1]->{refaddr($_[0])} || (UNIVERSAL::can($_[0],'_clone') || $handlers{reftype($_[0])} || sub { die "Unknown reftype " . reftype($_[0])} )->(@_);
 	}
 	
 }
--- a/Lib/IMPL/lang.pm	Fri Dec 30 23:40:00 2011 +0300
+++ b/Lib/IMPL/lang.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -38,7 +38,6 @@
 		  &property
 		  &static
 		  &property
-		  &ctor
 		  )
 	],
 	compare => [
@@ -55,6 +54,7 @@
 		  &hashDiff
 		  &hashCompare
 		  &hashParse
+		  &hashSave
 		  )
 	]
 );
@@ -118,20 +118,6 @@
 	$class->static_accessor( $name, $value );
 }
 
-sub ctor(&;$) {
-	my ( $code, %base ) = @_;
-	no strict 'refs';
-	my $class = caller;
-	
-	if ($code) {
-		*{"${class}::CTOR"} = $code;
-	}
-	
-	if (%base) {
-		%{"${class}::CTOR"} = %base;
-	}
-}
-
 sub equals {
 	if (defined $_[0]) {
 		return 0 if (not defined $_[1]);
@@ -223,7 +209,25 @@
 }
 
 sub hashSave {
+	my ($hash,$p,$d) = @_;
 	
+	return "" unless ref $hash eq 'HASH';
+	
+	$p ||= "\n";
+	$d ||= " = ";
+	
+	return
+		join(
+			$p,
+			map(
+				join(
+					$d,
+					$_,
+					$hash->{$_}
+				),
+				keys %$hash
+			)
+		);
 }
 
 1;
--- a/_doc/make.pl	Fri Dec 30 23:40:00 2011 +0300
+++ b/_doc/make.pl	Thu Jan 26 01:15:57 2012 +0400
@@ -29,7 +29,7 @@
     	mkdir $dir unless -d $dir;
     }
     
-    open my $hPod, "<:encoding(cp1251)", $fname or die "Failed to open $fname for input: $!";
+    open my $hPod, "<:encoding(utf-8)", $fname or die "Failed to open $fname for input: $!";
     open my $hOut, ">:encoding(utf-8)", $fnameOut or die "Failed to open $fnameOut for output: $!";
     
     my $parser = Pod::POM->new();
--- a/_test/Test/SQL/Diff.pm	Fri Dec 30 23:40:00 2011 +0300
+++ b/_test/Test/SQL/Diff.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -38,7 +38,7 @@
 	$users->SetPrimaryKey('id');
 	$users->AddConstraint( unique => { name => 'unique_login', columns => ['login'] } );
 	
-	warn Dumper(IMPL::SQL::Schema::Traits::Diff->Diff($schemaSrc,$schemaDst));
+	#warn Dumper(IMPL::SQL::Schema::Traits::Diff->Diff($schemaSrc,$schemaDst));
 	
 	$schemaSrc->Dispose;
 	$schemaDst->Dispose;
--- a/_test/Test/Web/TT.pm	Fri Dec 30 23:40:00 2011 +0300
+++ b/_test/Test/Web/TT.pm	Thu Jan 26 01:15:57 2012 +0400
@@ -1,7 +1,7 @@
 package Test::Web::TT;
 use strict;
 use warnings;
-use encoding 'cp1251';
+use encoding 'utf8';
 
 use parent qw(IMPL::Test::Unit);
 use IMPL::Test qw(test failed);
@@ -21,11 +21,11 @@
     
     failed "Failed to create document" unless $document;
     
-    $document->LoadFile('Resources/simple.tt','cp1251');
+    $document->LoadFile('Resources/simple.tt','utf8');
     
     my $out = $document->Render;
     
-    open my $hFile,'<:encoding(cp1251)',"Resources/simple.txt" or die "Failed to open etalon file: $!";
+    open my $hFile,'<:encoding(utf8)',"Resources/simple.txt" or die "Failed to open etalon file: $!";
     local $/;
     my $eta = <$hFile>;
     
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conv.pl	Thu Jan 26 01:15:57 2012 +0400
@@ -0,0 +1,19 @@
+#!/usr/bin/perl
+use strict;
+use File::Find;
+
+find(sub {
+    open my $hfile, "<:encoding(cp1251)", $_  or warn "failed: $!" and return;
+    
+    my @data = <$hfile>;
+    
+    close $hfile;
+    undef($hfile);
+    
+    chomp foreach @data;
+    
+    open $hfile, ">:encoding(utf8)", $_ or warn "failed: $!" and return;
+    
+    print $hfile "$_\n" foreach @data;
+    
+}, qw(Lib _test _doc));
\ No newline at end of file