Исходники
Статьи
Языки программирования
.NET Delphi Visual C++ Borland C++ Builder C/С++ и C# Базы Данных MySQL MSSQL Oracle PostgreSQL Interbase VisualFoxPro Веб-Мастеру PHP HTML Perl Java JavaScript Протоколы AJAX Технология Ajax Освоение Ajax Сети Беспроводные сети Локальные сети Сети хранения данных TCP/IP xDSL ATM Операционные системы Windows Linux Wap Книги и учебники
Скрипты
Магазин программиста
|
Генерация контента сайта с использованием Template Toolkit.ОПИСАНИЕЭтот пособие представляет собой введние в Template Toolkit и показывает несколько типичных способов использования этой библиотеки для генерации контента сайта. Пособие описывает генерацию статических страниц с использованием утилит tpage и ttree и динамических страниц с использованием CGI скриптов и обработчиков Apache/mod_perl. Знакомство, краткое описание и объяснение с использованием примеров различных возможностей Template Toolkit. Дополнительная информация приведена в pod-документации Template, Template::Manual и в подразделах, например perldoc Template # использование модуля Template.pm perldoc Template::Manual # содержание документации perldoc Template::Manual::Config # опции конфигурации Документация также поставляется в HTML формате (или точнее, в форме HTML шаблонов). Смотрите каталог 'docs' пакета для получения дополнительной информации о построении HTML документации. Если же вы читаете это руководство как часть HTML документации, значит вам не нужно беспокоится об этом. Вы можете присесть, расслабиться и наслаждаться чтением остальной части пособия... ВВЕДЕНИЕTemplate Toolkit - это набор модулей Perl, который в совокупности реализуют систему по обработке шаблонов. В данном контексте шаблон - это текстовый документ, содержащий специальные разметочные теги, называемые 'директивами'. Директива это инструкция процессору шаблонов выполнить некоторое действие и заменить директиву в документе на результат этого действия. Директивы используются для определения или вывода значения переменной, выполнения циклических операций с массивами (FOREACH), выполнения условных операций (IF/UNLESS/ELSE), включения и выполнения других шаблонов (INCLUDE) и т.д. Во всем остальном, документ это обычный текстовый файл и может иметь любое содержимое (например HTML, XML, RTF, LaTeX, и т.п.). Директивы включаются в документ внутри специальных разметочных тегов. По умолчанию, в качестве таких тегов используются [% и %], но они могут быть заменены на другие через опции конфигурации модуля. Пример HTML документа с дополнительными директивами Template Toolkit. [% INCLUDE header title = 'This is an HTML example' %] <h1>Some Interesting Links</h1> [% webpages = [ { url => 'http://foo.org', title => 'The Foo Organisation' } { url => 'http://bar.org', title => 'The Bar Organisation' } ] %] Links: <ul> [% FOREACH link = webpages %] <li><a href="[% link.url %]">[% link.title %]</a> [% END %] </ul> [% INCLUDE footer %] Этот пример показывает как директива INCLUDE используется для загрузки, обработки и включения в текущий документ отдельных шаблонов 'header' и 'footer'. Эти файлы могут выглядеть например так: header: <html> <head> <title>[% title %]</title> </head> <body bgcolor="#ffffff"> footer: <hr> <center> © Copyright 2000 Me, Myself, I </center> </body> </html> Также в примере показано использование директивы FOREACH для построения таблицы ссылок с помощью цикла по массиву 'webpages'. Мы определяем внутри шаблона массив, содержащий несколько ссылок на хэши, состоящих из двух элементов 'url' и 'title'. Директива FOREACH проходит по массиву, используя 'link' в качестве указателя на каждый элемент массива (ссылка на хэш). Затем директивы [% link.url %] и [% link.title %] получают соответсвующие значения хэша и вставляют их в документ. В следующих разделах показаны другие способы определения данных, которые используются в шаблонах. ГЕНЕРАЦИЯ СТАТИЧЕСКИХ СТРАНИЦСоздав шаблон, мы теперь можем обработать его и получить некоторый вывод. Наиболее быстрый и простой способ для этого использование утилиты tpage. Она поставляется как часть библиотеки Template Toolkit и должна быть установлена в каталог с исполняемыми файлами perl. Если вы сохранили свой шаблон как файл 'mypage.html', вам нужно выполнить команду: tpage mypage.html Эта команда запустит обработку шаблона, а вывод будет отправлен на STDOUT (т.е. пролетит по вашему экрану). Вы может перенаправить вывод в файл, но будьте осторожны, не указывайте то же имя файла, что и имя шаблона, иначе вы перезапишите его. Вы можете использовать для шаблонов и обычных файлов различные расширения. Например, для шаблонов используйте расширение '.atml' (видимо 'Another Template Markup Language'?) и привычное '.html' для выходных файлов (преполагается, что вы создаете HTML). Или, вы можете перенаправить вывод в другой каталог, например tpage mypage.atml > mypage.html tpage templates/mypage.html > html/mypage.html Утилита tpage довольно проста и предназначена для того, чтобы дать возможность просто обработать шаблон без необходимости писать perl-код. Более гибкая утилита ttree описанна ниже, но сейчас нам достаточно вывода сгенерированного командой из примера выше (для краткости пустые строки удалены): <html> <head> <title>This is an HTML example</title> </head> <body bgcolor="#ffffff"> <h1>Some Interesting Links</h1> Links: <ul> <li><a href="http://foo.org">The Foo Organisation</a> <li><a href="http://bar.org">The Bar Organisation</a> </ul> <hr> <center> © Copyright 2000 Me, Myself, I </center> </body> </html> Шаблоны header и footer включены (предполагается, что вы их создали и они находятся в текущем каталоге) и данные о ссылках преобразованы в список в HTML-файле. Утилита ttree, также поставляемая в составе библиотеки Template Toolkit, предоставляет более гибкий способ обработки шаблонов. При первом запуске утилиты, вам будет предложено создать конфигурационный файл, в большинстве случаев называемый '.ttreerc' в вашем домашнем каталоге. Ответьте 'y', чтобы создать файл. Документация по ttree описывает, как вы можете изменить расположение это файла, а также описывает синтаксис и назначение различных опций в этом файле. Конфигурационный файл, приведенный в качестве примера, содержит комментарии, которые также могут помочь. perldoc ttree ttree -h Коротко говоря, конфигурационный файл описывает каталоги, в которых хранятся шаблоны (src), и куда будут сохраняться соответсвующие файлы (dest), и некоторые другие каталоги (lib), которые могут содержать файлы шаблонов, которые вы планируете включать с помощью директивы INCLUDE в ваши исходные шаблоны. Также в конфигурационном файле можно указать опции парсера (такие как 'verbose' и 'recurse') и при помощи регулярных выражений ограничить список файлов, которые необходимо обрабатывать парсером (ignore, accept) или указать файлы, которые вместо обработки нужно скопировать (copy). Пример файла .ttreerc: $HOME/.ttreerc: verbose recurse # каталог, в котором хранятся другие конфигурационные файлы ttree cfg = ~/.ttree src = ~/websrc/src lib = ~/websrc/lib dest = ~/public_html/test ignore = \b(CVS|RCS)\b ignore = ^# Можно создать много других конфигурационных файлов и сохранить их в каталоге, указанном в опции 'cfg', как показано выше. Затем вы можете указать команде ttree опцию '-f filename', чтобы использовать файлы из этого каталога. Когда вы запускаете скрипт, он сравнивает все файлы в каталоге 'src' (включая файлы в подкаталогах, если установлена опция 'recurse') с файлами в каталоге 'dest'. Если выходного файла не существует или он имеет более раннее время модификации, чем соответсвующий исходный файл (шаблон), то исходный файл будет обработан парсером и вывод записан в выходной файл. Опция '-a' вынуждает скрипт обработать все файлы вне зависимости от времени модификации. Скрипт не обрабатывает файлы из каталога 'lib', но он включает каталог в переменную INCLUDE_PATH, передаваемую процессору шаблонов для того чтобы он мог определить местонахождение файлов, указываемых в директивах INCLUDE или PROCESS. Таким образом, каталог 'lib' - хорошее место для хранения элементов, используемых в шаблонах, таких как header, footer, и т.п., которые не являются документами в полном смысле этого слова. Также в конфигурационном файле можно указать различные опции Template Toolkit. Для получения подробной информации обратитесь к документации ttree и краткой справке ('ttree -h'). Например, $HOME/.ttreerc: pre_process = config interpolate post_chomp Опция 'pre_process' позволяет указать шаблон, который будет обработан перед каждым файлом. Неудивительно, что также есть опция 'post_process' для добавления шаблона, обрабатываемого после обработки файла. В приведенном выше фрагменте мы указываем, что шаблон 'config' будет использован в качестве префиксного шаблона. Мы можем создать этот файл в каталоге 'lib' и использовать его для определния некоторых общих переменных, включая определенные ранее ссылки на веб-страницы и которые мы можем захотеть использовать в других шаблонах. Также в этот файл мы можем включить голову и заголовок HTML-документа, или меню, которые затем будут включаться в начало любого шаблона, но пока мы будем использовать для этих целей отдельный файл 'header'. $lib/config: [% root = '~/abw' home = "$root/index.html" images = "$root/images" email = 'abw@wardley.org' graphics = 1 webpages = [ { url => 'http://foo.org', title => 'The Foo Organsiation' } { url => 'http://bar.org', title => 'The Bar Organsiation' } ] %] Подготовив шаблоны 'header' и 'footer' из приведенного ранее примера и поместив их в каталог 'lib', вы можете начинать создавать в вашем каталоге 'src' веб-страницы, подобные приведенной ниже, и обрабатывать их с помощью ttree. $src/newpage.html: [% INCLUDE header title = 'Another Template Toolkit Test Page' %] <a href="[% home %]">Home</a> <a href="mailto:[% email %]">Email</a> [% IF graphics %] <img src="[% images %]/logo.gif" align=right width=60 height=40> [% END %] [% INCLUDE footer %] Здесь мы показали, как использовать предопределенные переменные в качестве флагов для включения различных возможностей (например, 'graphics') и определять общие переменные, такие как адрес email, URLы домашней страницы, каталога с картинками и т.д. Такой подход позволяет определить эти перемнные один раз, и таким образом поддерживать их целостность на всех страницах и обеспечить возможность легко изменить их значения на новые. После запуска ttree, вы должны увидеть схожий с приведенным ниже вывод (предполагается, что флаг verbose установлен). ttree 1.14 (Template Toolkit version 1.02a) Source: /home/abw/websrc/src Destination: /home/abw/public_html/test Include Path: [ /home/abw/websrc/lib ] Ignore: [ \b(CVS|RCS)\b, ^# ] Copy: [ ] Accept: [ * ] + newpage.html '+' перед 'newpage.html' показывает что файл был обработан процессором, а вывод сохранен в каталоге назначения. Если вы снова запустите эту команду, вы увидите, что в этой строке вывода вместо '+' будет выведен '-' и будет приведена причина по которой файл не был обработан процессором. - newpage.html (not modified) Процессор обнаружил страницу 'newpage.html' в каталоге назначения с датой модификации большей, чем у исходного файла, и не стал тратить время на его повторную обработку. Для того, чтобы принудительно обработать все исходные файлы, используйте опцию '-a'. Дополнительно вы можете один или более исходных файлов в качестве аргументов командной строки для утилиты ttree: ttree newpage.html Полученный после обработки процессором файл будет выглядет следующим образом. $dest/newpage.html: <html> <head> <title>Another Template Toolkit Test Page</title> </head> <body bgcolor="#ffffff"> <a href="~/abw/index.html">Home</a> <a href="mailto:abw@wardley.org">Email</a> <img src="~/abw/images/logo.gif" align=right width=60 height=40> <hr> <center> © Copyright 2000 Me, Myself, I </center> </body> </html> Вы можете добавить сколько угодно документов в каталог 'src' и ttree обработает их аналогично. Таким образом можно полностью построить статическое содержимое сайта с помощью одной команды. Дополнительная выгода заключается в том, что вы можете быть уверены в целостности ссылок, едином стиле шапки и всех прочих компонентов, реализованных через общие шаблоны и переменные. ДИНАМИЧЕСКАЯ ГЕНЕРАЦИЯ СТРАНИЦ С ПОМОЩЬЮ CGI СКРИПТОВМодуль Template обеспечивает простой интерфейс к библиотеке Template Toolkit для использования в CGI-скриптах и обработчиках Apache/mod_perl. Просто включите в ваши скрипты модуль Template с помощью команды 'use', создайте экземпляр объекта с помощью метода new(), а затем вызывайте метод объекта process(), передавая в качестве параметра имя файла шаблона. Второй передаваемый параметр - ссылка на хэш с переменными, которые мы хотим сделать доступными в шаблоне: #!/usr/bin/perl -w use strict; use Template; my $file = 'src/greeting.html'; my $vars = { message => "Hello World\n" }; my $template = Template->new(); $template->process($file, $vars) || die "Template process failed: ", $template->error(), "\n"; Для того чтобы наши скрипты работали с шаблонами из предыдущих примеров, мы можем добавить некоторые опции конфигурации в вызове конструктора, чтобы сообщить парсеру о нашем окружении: my $template->new({ # где искать файлы шаблонов INCLUDE_PATH => '/home/abw/websrc/src:/home/abw/websrc/lib', # предварительно обработать lib/config, чтобы определить дополнительные переменные PRE_PROCESS => 'config', }); Заметьте, что здесь мы определяем в опции PRE_PROCESS файл 'config'. Это означает, что шаблоны, которые мы будем обрабатывать могут использовать те же глобальные переменные, которые мы определили раньше для использования в статических страницах. Нам не нужно повторно определять их в скрипте. Тем не менее, мы можем предоставить дополнительные данные и функциональность, специфичную для скрипта через хэш переменных, передаваемый методу process(). Элементы хэша могут содержать обычный текст или другие переменные, ссылки на массивы, другие хэши, функции или объекты. Template Toolkit автоматически применит нужную процедуру для доступа к данным различного типа, когда вы будете использовать переменные в шаблоне. Приведем более детальный пример для дальнейшего рассмотрения. Среди различных переменных шаблона, определенных в '$vars', мы создали ссылку на объект CGI и ссылку на функцию 'get_user_projects'. #!/usr/bin/perl -w use strict; use Template; use CGI; $| = 1; print "Content-type: text/html\n\n"; my $file = 'userinfo.html'; my $vars = { 'version' => 3.14, 'days' => [ qw( mon tue wed thu fri sat sun ) ], 'worklist' => \&get_user_projects, 'cgi' => CGI->new(), 'me' => { 'id' => 'abw', 'name' => 'Andy Wardley', }, }; sub get_user_projects { my $user = shift; my @projects = ... # заполняем данными return \@projects; } my $template = Template->new({ INCLUDE_PATH => '/home/abw/websrc/src:/home/abw/websrc/lib', PRE_PROCESS => 'config', }); $template->process($file, $vars) || die $template->error(); Ниже приведен пример шаблона, который мы можем использовать в скрипте. $src/userinfo.html: [% INCLUDE header title = 'Template Toolkit CGI Test' %] <a href="mailto:[% email %]">Email [% me.name %]</a> <p>This is version [% version %]</p> <h3>Projects</h3> <ul> [% FOREACH project = worklist(me.id) %] <li> <a href="[% project.url %]">[% project.name %]</a> [% END %] </ul> [% INCLUDE footer %] Этот пример показывает как можно разделить perl-реализацию (код) и представление (HTML), что не только делает проще поддержку изоляции, но и позволяет многократно использовать существующие шаблонные элементы, такие как шапки, подвалы и т.п. Используя шаблоны для организации вывода CGI скриптов, вы можете обеспечить такую же целостность, какой мы добились при построении статических страниц с помощью ttree и других средств. Более того, мы можем так модифицировать наш скрипт, что он будет обрабатывать любой файл из набора шаблонов, использующих определенную модель. CGI скрипт для поддержки базы данных пользователей может, например, обрабатывать один шаблон для формы заведения новых пользователей, ту же форму с некоторыми установленными переменными для обновления существующих записей о пользователях, и третий шаблон для вывода списка всех пользователей системы, и т.д. вы можете реализовать в perl-коде логику вашего приложения, а затем выбрать тот или иной шаблон для того, чтобы обеспечить необходимый для текущего состояния приложения вывод. ДИНАМИЧЕСКАЯ ГЕНЕРАЦИЯ СТРАНИЦ ЧЕРЕЗ ОБРАБОТЧИК APACHE/MOD_PERLВНИМАНИЕ: Вы можете скачать из сети CPAN модуль Apache::Template, который обеспечивает простой и легкий в использовании интерфейс между Apache/mod_perl и Template Toolkit. В момент написания этого документа был доступен первый релиз (0.01), который предлагает только самые базовые возможности, но в нем реализована большая часть, если не все, что описано ниже. Следует избегать необходимости писать свой собственный обработчик. Тем не менее, во многих случаях, вам может понадобиться написать свой собственный обработчик под ваши собственные нужды, и этот раздел покажет вам с чего начинать. Модуль Template можно использовать обычным способом из обработчика Apache/mod_perl. Ниже приведен типичный фрагмент файла конфигурации Apache httpd.conf: PerlModule CGI; PerlModule Template PerlModule MyOrg::Apache::User PerlSetVar websrc_root /home/abw/websrc <Location /user/bin> SetHandler perl-script PerlHandler MyOrg::Apache::User </Location> Этот фрагмент определяет URL '/user/bin', все запросы к которому будут передаваться методу handler() модуля MyOrg::Apache::User. Этот модуль может выглядеть приблизительно так: package MyOrg::Apache::User; use strict; use vars qw( $VERSION ); use Apache::Constants qw( :common ); use Template qw( :template ); use CGI; $VERSION = 1.59; sub handler { my $r = shift; my $websrc = $r->dir_config('websrc_root') or return fail($r, SERVER_ERROR, "'websrc_root' not specified"); my $template = Template->new({ INCLUDE_PATH => "$websrc/src/user:$websrc/lib", PRE_PROCESS => 'config', OUTPUT => $r, # прямой вывод в объект запроса Apache }); my $params = { uri => $r->uri, cgi => CGI->new, }; # используем path_info для определения шаблона, который необходимо обработать my $file = $r->path_info; $file =~ s[^/][]; $r->content_type('text/html'); $r->send_http_header; $template->process($file, $params) || return fail($r, SERVER_ERROR, $template->error()); return OK; } sub fail { my ($r, $status, $message) = @_; $r->log_reason($message, $r->filename); return $status; } Обработчик получает в качестве параметра запрос и использует его для определения значения 'websrc_root' из файла конфигурации. Это значение затем используется для определения INCLUDE_PATH при создании объекта Template. Далее из запроса получаем URI и создаем объект CGI. Оба объекта определяются как переменные шаблона. Само имя шаблона извлекается из PATH_INFO запроса. В нашем примере, оно будет частью URL, идущего после '/user/bin', то есть для '/user/bin/edit', шаблоном будет файл 'edit', расположенный в "$websrc/src/user" Далее выводим заголовки и обрабатываем шаблон. Весь вывод отправляется напрямую в метод print() объекта запроса Apache. ИСПОЛЬЗОВАНИЕ МОДУЛЕЙ-РАСШИРЕНИЙ (ПЛАГИНОВ)Как мы уже показали, при создании динамического контента с помощью CGI скриптов или обработчиков Apache/mod_perl можно связывать данные и функции из Perl с переменными шаблона. Template Toolkit также предоставляет интерфейс для создания модулей расширений (плагинов), который позволяет вам определить эти дополнительные данные и/или функциональность в отдельном модуле и затем загрузить его и использовать по назначению с помощью директивы USE. Основное преимущество такого подхода заключается в том, что вы можете использовать такое расширение в любых шаблонах, даже в тех, которые используются при построении статического контента с помощью tpage или ttree. Вам не нужно специально писать обертку на Perl только для того, чтобы загрузить модуль и сделать его доступным через хэш переменных шаблона. Давайте продемонстрируем этот принцип на примере плагина DBI, написанного Симоном Мэттьюсом (Simon Matthews <sam@knowledgepool.com>). Вы можете создать этот шаблон в вашем каталоге 'src', обработать его при помощи ttree и посмотреть результат. Разумеется, этот пример зависит от наличия подходящей базы данных SQL, но вы можете адаптировать его под ваши возможности, либо просто рассматривать как пример возможностей Template Toolkit. [% INCLUDE header title = 'User Info' %] [% USE DBI('dbi:mSQL:mydbname') %] <table border=0 width="100%"> <tr> <th>User ID</th> <th>Name</th> <th>Email</th> </tr> [% FOREACH user = DBI.query('SELECT * FROM user ORDER BY id') %] <tr> <td>[% user.id %]</td> <td>[% user.name %]</td> <td>[% user.email %]</td> </tr> [% END %] </table> [% INCLUDE footer %] Плагин - это обычный Perl модуль, расположенный в определенном месте и согласующийся с известными стандартами таким образом, чтобы Template Toolkit смог найти его и автоматически загрузить. Вы можете создать свой собственный плагин с помощью наследования модуля Template::Plugin. Ниже приведен пример, который определяет некоторые данные ('foo' и 'people') и также метод объекта ('bar'). За неимением лучшего мы назовем плагин 'FooBar' и создадим его в пакете 'MyOrg::Template::Plugin::FooBar'. Мы добавили 'MyOrg' к обычному имени пакета 'Template::Plugin::*', чтобы избежать конфликта имен с существующими плагинами. Заготовку модуля можно сделать с помощью Perl утилиты h2xs: h2xs -A -X -n MyOrg::Template::Plugin::FooBar Эта команда создаст структуру каталогов, отражающую имя пакета с набором файлов, составляющих ваш новый модуль. Вы можете отредактировать FooBar.pm, чтобы он выглядел примерно так: package MyOrg::Template::Plugin::FooBar; use Template::Plugin; use vars qw( $VERSION ); use base qw( Template::Plugin ); $VERSION = 1.23; sub new { my ($class, $context, @params) = @_; bless { _CONTEXT => $context, foo => 25, people => [ 'tom', 'dick', 'harry' ], }, $class; } sub bar { my ($self, @params) = @_; # ...что-то делает... return $some_value; } Как принято в Perl, конструктор плагина new() получает имя класса в качестве первого параметра. Следующий параметр - ссылка на экземпляр объекта Template::Context. Сейчас вам не нужно беспокоится об этом объекте. Это основной объект парсера Template Toolkit. Он предоставляет доступ к функциональности процессора и некоторым плагинам может понадобится взаимодествовать с ним. Нам это пока не нужно, но мы тем не менее сохраним эту ссылку в переменной объекта '_CONTEXT'. Начальное подчеркивание - это соглашение об именах переменных, которое указывает что эта переменная приватная и Template Toolkit не будет пытаться получить доступ к этой переменной. Другие определяемые переменные 'foo' и 'people' - это обычные данные, которые будут доступны из шаблонов, использующих этот плагин. Следом за ссылкой на объект Template::Context идут дополнительные параметры, которые указываются с директивой USE, такие как источник данных, 'dbi:mSQL:mydbname', который мы использовали в предыдущем примере с плагином DBI. Если для создания заготовки модуля вы использовали h2xs, то у вас уже есть файл Makefile.PL и вы можете выполнить знакомую процедуру по сборке и установке модуля. Не забудьте добавить тесты в test.pl! perl Makefile.PL make make test make install Если вы не стали или не смогли установить плагин в обычное место для хранения модулей Perl (например, если у вас недостаточно привилегий), вы можете установить переменную окружения PERL5LIB, чтобы указать другое место, где установлен модуль. Если вы используете ttree, вместо этого вы можете добавить следующую строку в ваш конфигурационный файл. Это будет равносильно добавлению '/path/to/modules' в массив @INC. $HOME/.ttreerc: perl5lib = /path/to/modules Еще одна дополнительная строка должна быть добавлена, чтобы указать парсеру новое имя пакетов, которое мы создали для наших плагинов: $HOME/.ttreerc: plugin_base = 'MyOrg::Template::Plugin' Если вы пишите скрипт, использующий модуль Template напрямую, эта переменная может быть передана как параметр конфигурации при создании объекта. use Template; my $template = Template->new({ PLUGIN_BASE => 'MyOrg::Template::Plugin' }); Теперь мы можем создать шаблон, использующий этот плагин: [% INCLUDE header title = 'FooBar Plugin Test' %] [% USE FooBar %] Some values available from this plugin: [% FooBar.foo %] [% FooBar.bar %] The users defined in the 'people' list: [% FOREACH uid = FooBar.people %] * [% uid %] [% END %] [% INCLUDE footer %] Члены 'foo', 'bar' и 'people' из плагина FooBar автоматически преобразуются в соответствующие переменные или вызовы методов лежащего в основе объекта. Используя этот подход, можно создать один модуль, содержащий всю функциональность приложения, который затем может быть использован по запросу в любом шаблоне. Простой интерфейс между директивами шаблона и объектами модуля позволяет создавать сложный, динамический контент из нескольких простых шаблонов, ничего не зная о лежащей в основе реализации. |
Форум Программиста
Новости Обзоры Магазин Программиста Каталог ссылок Поиск Добавить файл Обратная связь Рейтинги
|