Написать шаблонизатор и "компилятор", который переводит описанный шаблон в "php" и "javascript" код.
Операторы:
<var[foo]> преобразуется в $input['foo']
<var[foo->bar]> преобразуется в $input['foo']['bar']
<var[:foo]> преобразуется в $foo
<const[authed]>
<print <var[foo]>>
<if (<var[foo]> gt 1 || <var[foo]> == "bar") && <var[foo]> == <var[bar]>>
...
<else>
...
</if>
<switch <var[foo]>>
<case "bar">
...
</case>
<case "baz">
...
</case>
</switch>
<each <var[foo]> as <var[:k]> <var[:v]>>
...
</each>
<include name="/tmp/tpl.txt" input="<var[foo]>">
<file name="/output_file_name.txt">
Логические выражения:
Методы:
<print <var[foo].methodname().nextmethodname()>>
Регулярными выражениями тут явно не обойтись. Для разбора данных конструкций необходим лексер и грамматика.
В качестве первого был выбран flex, а для генерации кода и описания грамматики - bison. Можно было все это описать и на php (тем более наработки уже есть), но я все таки рискнул сделать иной выбор.
Почему не flexc++ и bisonc++? Я, скажем так, не очень
хорошо знаю c++ (но использовать его в проекте все равно буду, так
как мне нужен хотя бы тип "string"
).
./template
-l язык, код которого необходимо построить (php|js)
-p префикс переменной (в описании var это $input)
-s резульатат работы будет импользован как подшаблон или нет (1|0)
-sp раздел с шаблонами (см. описание include)
-dp раздел в который необходимо записать результат (см. описание file)
< путь до файла
Например:
./template -p "\$foo" -l "js" -sp "./" -dp "./" < tests/template.txt
Исходник:
<if <var[foo->bar]> != <var[:innerVar]>>
Условие пройдено: <print <var[foo->bar]>>.
</if>
php:
<?php if($root['foo']['bar'] != $localVar) { ?> Условие пройдено:<?php print $root['foo']['bar']; ?><?php } ?>
js (создается callback):
var f = function($root) { return '' + ( $root['foo']['bar'] != $localVar ? (' Условие пройдено:' + $root['foo']['bar'] + '') : '' ) + ''; }
%{
#include <iostream>
#include <string>
#include <vector>
#include "template.tab.h"
using namespace std;
string stringBuffer;
string attributeNameBuffer;
vector<int> previousStates;
//#define YY_DECL extern "C" int yylex();
extern "C" int yylex();
char* string_to_char(string str);
int pop_last_state() {
int state = previousStates[previousStates.size() - 1];
previousStates.pop_back();
return state;
}
void push_last_state(int state) {
previousStates.push_back(state);
}
%}
%s SCRIPT
%s TEXT
%s VAR
%s STRING_DQ
%s STRING_SQ
%s ATTRIBUTES
%s ATTRIBUTE_VALUE
%s METHODS
%s PARAMETERS
WS [ \t\r]+
NUMBER [0-9.,]+
%%
<INITIAL,TEXT>{
{WS}*\<file{WS}* { BEGIN(ATTRIBUTES); return T_OUTPUT_FILE_NAME_OPEN; }
{WS}*\<include{WS}* { BEGIN(ATTRIBUTES); return T_INCLUDE_OPEN; }
{WS}*\<if{WS}* { BEGIN(SCRIPT); return T_IF_OPEN; }
{WS}*\<else> { return T_IF_ELSE; }
{WS}*\<\/if> { return T_IF_CLOSE; }
{WS}*\<print{WS}* { BEGIN(SCRIPT); return T_PRINT_OPEN; }
{WS}*\<each{WS}* { BEGIN(SCRIPT); return T_EACH_OPEN; }
{WS}*\<\/each> { return T_EACH_CLOSE; }
{WS}*\<switch{WS}* { BEGIN(SCRIPT); return T_SWITCH_OPEN; }
{WS}*\<\/switch>{WS}* { return T_SWITCH_CLOSE; }
{WS}*\<case{WS}* { BEGIN(SCRIPT); return T_CASE_OPEN; }
{WS}*\<\/case>{WS}* { return T_CASE_CLOSE; }
\n ;
{WS}+ { yylval.sval = string_to_char(" "); return T_TEXT; }
. { yylval.sval = strdup(yytext); return T_TEXT; }
<<EOF>> { return T_END; }
}
<SCRIPT>{
\<var\[ { push_last_state(SCRIPT); BEGIN(VAR); return T_VAR_OPEN; }
\<const\[ { push_last_state(SCRIPT); BEGIN(VAR); return T_CONST_VAR_OPEN; }
{NUMBER} { yylval.sval = strdup(yytext); return T_NUMBER; }
\" { push_last_state(SCRIPT); BEGIN(STRING_DQ); stringBuffer = "\""; }
' { push_last_state(SCRIPT); BEGIN(STRING_SQ); stringBuffer = "'"; }
\( { return T_OPEN_RBRACKET; }
\) { return T_CLOSE_RBRACKET; }
&& { return T_AND; }
\|\| { return T_OR; }
(==|eq) { return T_EQ; }
!= { return T_NOT_EQ; }
gt { return T_GT; }
ge { return T_GE; }
lt { return T_LT; }
le { return T_LE; }
as { return T_AS; }
> { BEGIN(TEXT); return T_TAG_CLOSE; }
{WS}* ;
}
<VAR>{
([:_a-zA-Z0-9]+(->)*)+ { yylval.sval = strdup(yytext); return T_TEXT; }
\] ;
\. { BEGIN(METHODS); }
> { BEGIN(pop_last_state()); return T_VAR_CLOSE; }
. ;
}
<METHODS>{
[_a-z]+ { yylval.sval = strdup(yytext); return T_METHOD; }
\. ;
\( { BEGIN(PARAMETERS); }
> { BEGIN(pop_last_state()); return T_VAR_CLOSE; }
}
<PARAMETERS>{
\" { push_last_state(PARAMETERS); BEGIN(STRING_DQ); stringBuffer = "\""; }
' { push_last_state(PARAMETERS); BEGIN(STRING_SQ); stringBuffer = "'"; }
\) { BEGIN(METHODS); }
, ;
}
<STRING_DQ>{
\\\" { stringBuffer += yytext; }
\" { BEGIN(pop_last_state()); yylval.sval = string_to_char(stringBuffer + yytext); return T_STRING; }
. { stringBuffer += yytext; }
}
<STRING_SQ>{
\\' { stringBuffer += yytext; }
' { BEGIN(pop_last_state()); yylval.sval = string_to_char(stringBuffer + yytext); return T_STRING; }
. { stringBuffer += yytext; }
}
<ATTRIBUTES>{
[a-z]+ { attributeNameBuffer += yytext; }
= { BEGIN(ATTRIBUTE_VALUE); yylval.sval = string_to_char(attributeNameBuffer); attributeNameBuffer=""; return T_ATTR_NAME; }
> { BEGIN(TEXT); return T_TAG_CLOSE; }
{WS}* ;
}
<ATTRIBUTE_VALUE>{
\" { push_last_state(ATTRIBUTE_VALUE); BEGIN(STRING_DQ); stringBuffer = "\""; }
' { push_last_state(ATTRIBUTE_VALUE); BEGIN(STRING_SQ); stringBuffer = "'"; }
\<var\[ { push_last_state(ATTRIBUTE_VALUE); BEGIN(VAR); return T_VAR_OPEN; }
\<const\[ { push_last_state(SCRIPT); BEGIN(VAR); return T_CONST_VAR_OPEN; }
> { BEGIN(TEXT); return T_TAG_CLOSE; }
{WS}* { BEGIN(ATTRIBUTES); }
}
%%
%{
#include <iostream>
#include <cstring>
#include <stdio.h>
#include <string>
#include <vector>
#include <map>
#include <fstream>
// aptitude install libboost-all-dev
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
// используем std::
using namespace std;
// корневой раздел для обработанных шаблонов
string targetPath = "";
// корневой раздел для исходников шаблонов
string sourcePath = "";
// файл для сохранения шаблона
string output = "";
// путь до шаблонизатора
string templater;
// имя "входной" переменной (см. описание шаблонизатора)
string varprefix = "$input";
// язык, код которого нужно сгенерировать
string language = "php";
// является ли шаблон подшаблоном
bool subtemplate = false;
// вхождения в E_MAIN
vector<vector<string> > statesMain;
// вхождения в E_CASES
vector<vector<string> > statesCase;
// список методов, которые были накастаны на "что-то"
vector<vector<string> > methodsList;
// список входных параметров метода
vector<string> methodParemetersList;
// список атрибутов оператора
map<string,string> attributes;
// список методов
map<string, map<string,string> > definedMethods = {
{
"php",
{
{"xmlescape", "htmlspecialchars"},
{"verbal_rus", "digits_verbal_rus"}
}
},
{
"js",
{
{"xmlescape", "Tools.FuncList.XmlEscape"},
{"verbal_rus", "Tools.FuncList.VerbalRus"}
}
}
};
// список предопределенных констант
map<string, map<string,string> > definedConstants = {
{
"php",
{
{"page", "$_SERVER['PHP_SELF']"},
{"authed", "user_is_authed()"}
}
},
{
"js",
{
{"page", "window.location.pathname"},
{"authed", "Tools.User.Id"}
}
}
};
extern "C" int yylex();
extern "C" int yyparse();
// сообщение об ошибке
void yyerror(const char *s);
/*** работа со стороками ***/
// перевести строку в char*
char* string_to_char(string str) {
char *cstr = new char[str.length() + 1];
strcpy(cstr, str.c_str());
return cstr;
}
// убираем все кавычки из строки
string string_from_attribute(string str) {
str = str.substr(1, str.size() - 2);
boost::replace_all(str, "\\", "");
return str;
}
/*** работа с "векторами-сторок" и "векторами-векторов-строк" ***/
// добавить вектор в slist
void slist_push(vector<vector<string> > & slist) {
vector<string> p;
slist.push_back(p);
}
// добавить в последний вектор slist строку
void slist_append_to_last(vector<vector<string> > & slist, string str) {
slist[slist.size() - 1].push_back(str);
}
// объединить вектор строк в одну строку через разделитель
string slist_implode(vector<string> p, string delimiter) {
string text;
for(int i=0; i<p.size(); i++) {
if(i!=0) {
text += delimiter;
}
text += p[i];
}
return text;
}
// вытащить вектор из slist и первратить его в строку
string slist_pop_last(vector<vector<string> > & slist) {
if(!slist.size()) {
return "";
}
vector<string> p = slist[slist.size() - 1];
slist.pop_back();
return slist_implode(p, "");
}
/*** выполнение системных команд ***/
// выполнить системную команду и получить результат ее работы
string execcmd(string cmd) {
string result = "";
char buffer[128];
FILE* pipe = popen(string_to_char(cmd), "r");
if (!pipe) {
return "";
}
while(!feof(pipe)) {
if(fgets(buffer, 128, pipe) != NULL)
result += buffer;
}
pclose(pipe);
return result;
}
/*** построение шаблона ***/
// собрать имя переменной в правильный вид
string build_var_name(string str) {
if(str.substr(0, 1) == ":") {
return "$" + str.substr(1, str.size() - 1);
}
boost::replace_all(str, "->", "']['");
return varprefix + "['" + str + "']";
}
// собрать имя константы в правильный вид
string build_const_name(string str) {
if(definedConstants[language].count(str)) {
return definedConstants[language][str];
}
return "\"\"";
}
// собрать конструкцию "print"
string build_print(string str) {
if(language == "php") {
return "<?php print " + str + "; ?>";
} else {
return "' + " + str + " + '";
}
}
// собрать конструкцию "if"
string build_if(string expr, string actions) {
if(language == "php") {
return "<?php if(" + expr + ") { ?>" + actions + "<?php } ?>";
} else {
return "' + ( " + expr + " ? ('" + actions + "') : '' ) + '";
}
}
// собрать конструкцию "if-else"
string build_if_else(string expr, string actions, string elseActions) {
if(language == "php") {
return "<?php if(" + expr + ") { ?>" + actions + "<?php } else { ?>" + elseActions + " } ?>";
} else {
return "' + ( " + expr + " ? ('" + actions + "') : ('" + elseActions + "') ) + '";
}
}
// собрать конструкцию "foreach"
string build_foreach(string var, string k, string v, string actions) {
if(language == "php") {
return "<?php foreach(" + var + " as " + k + " => " + v + ") { ?>" + actions + "<?php } ?>";
} else {
return "' + (function() { var html = ''; $.each(" + var + ", function(" + k + ", " + v + ") { html += '" + actions + "'}); return html; })() + '";
}
}
// собрать конструкцию "case"
string build_case(string cond, string actions) {
if(language == "php") {
return " case (" + cond + "): ?>" + actions + "<?php break; ";
} else {
return " case (" + cond + "): return ('" + actions + "'); break; ";
}
}
// собрать конструкцию "switch"
string build_switch(string var, string cases) {
if(language == "php") {
return "<?php switch(" + var + ") { " + cases + " } ?>";
} else {
return "' + (fucntion() { switch(" + var + ") { " + cases + " } }()) + '";
}
}
// накастать методы на var
string build_apply_methods(string var) {
for(int i=0; i<methodsList.size(); i++) {
vector<string> p = methodsList[i];
if(definedMethods[language].count(p[0])) {
for(int j=0; j<p.size(); j++) {
if(j == 0) {
var = definedMethods[language][p[j]] + "(" + var;
} else {
var += "," + p[j];
}
}
var += ")";
}
}
methodsList.clear();
return var;
}
// распарсить подшаблон
string build_subtemplate() {
string cmd = templater;
string var = varprefix;
if(attributes.count("name") == 0) {
return "";
}
if(attributes.count("input") != 0) {
var = attributes["input"];
}
cmd += " -p \"\\" + var + "\"";
cmd += " -l \"" + language + "\"";
cmd += " -sp \"" + targetPath + "\"";
cmd += " -tp \"" + sourcePath + "\"";
cmd += " -s 1";
cmd += " < " + sourcePath + "/" + string_from_attribute(attributes["name"]);
return execcmd(cmd);
}
// финальные действия над шаблоном
string build_template(string text) {
if(language == "js" && !subtemplate) {
return "var f = function(" + varprefix + ") { return '" + text + "'; } ";
}
return text;
}
// установить файл для вывода
void template_set_output_file_name() {
if(!subtemplate && attributes.count("name") != 0) {
output = attributes["name"];
}
}
// сохраняем шаблон
void template_store(string text) {
string path = targetPath;
if(output != "") {
std::vector<std::string> pathParts;
// заменяем все кавычки
output = string_from_attribute(output);
boost::split(pathParts, output, boost::is_any_of("/"));
for(int i=0; i<pathParts.size(); i++) {
if(pathParts[i] != "") {
path += "/" + pathParts[i];
if(i == (pathParts.size() - 1)) {
std::ofstream file;
file.open(path);
file << text;
file.close();
} else {
boost::filesystem::path dir(path.c_str());
if(!boost::filesystem::create_directory(dir)) {
// do error stuff
}
}
}
}
} else {
cout << text;
}
}
%}
// переопределение yylval
%union {
char *sval;
}
// опредления токенов
%token <sval> T_TEXT
%token <sval> T_NUMBER
%token <sval> T_STRING
%token <sval> T_METHOD
%token <sval> T_ATTR
%token <sval> T_ATTR_NAME
%token T_IF_OPEN T_IF_ELSE T_IF_CLOSE
%token T_EACH_OPEN T_AS T_EACH_CLOSE
%token T_SWITCH_OPEN T_SWITCH_CLOSE T_CASE_OPEN T_CASE_CLOSE
%token T_OUTPUT_FILE_NAME_OPEN;
%token T_INCLUDE_OPEN
%token T_PRINT_OPEN
%token T_VAR_OPEN T_CONST_VAR_OPEN T_VAR_CLOSE
%token T_TAG_CLOSE
%token T_END
%token T_EQ T_NOT_EQ T_GT T_GE T_LT T_LE
%token T_AND T_OR
%token T_OPEN_RBRACKET T_CLOSE_RBRACKET
// задаем левую ассоциативность для операций (...) http://www.opennet.ru/docs/RUS/bison_yacc/bison_yacc-prog.html.gz
%left T_EQ T_NOT_EQ T_GT T_GE T_LT T_LE T_AND T_OR
// определения типов нетерминальных символов
%type <sval> E_EXPR
%type <sval> E_VAR
%%
E_MAIN:
/* empty */ {
slist_push(statesMain);
}
| E_MAIN E_SCRIPT
| E_MAIN T_END {
template_store(build_template(slist_pop_last(statesMain)));
return 0; // all done
}
E_SCRIPT:
E_IF
| E_IF_ELSE
| E_INCLUDE
| E_PRINT
| E_EACH
| E_SWITCH
| E_OUTPUT_FILE
| T_TEXT {
slist_append_to_last(statesMain, string($1));
}
E_OUTPUT_FILE:
T_OUTPUT_FILE_NAME_OPEN E_ATTRIBUTES T_TAG_CLOSE {
template_set_output_file_name();
}
E_SWITCH:
T_SWITCH_OPEN E_VAR T_TAG_CLOSE E_CASES T_SWITCH_CLOSE {
slist_append_to_last(statesMain, build_switch(string($2), slist_pop_last(statesCase)));
}
E_CASES:
/* empty */ {
slist_push(statesCase);
}
| E_CASES T_CASE_OPEN E_VAR T_TAG_CLOSE E_MAIN T_CASE_CLOSE {
slist_append_to_last(statesCase, build_case(string($3), slist_pop_last(statesMain)));
}
E_EACH:
T_EACH_OPEN E_VAR T_AS E_VAR E_VAR T_TAG_CLOSE E_MAIN T_EACH_CLOSE {
slist_append_to_last(statesMain, build_foreach(string($2), string($4), string($5), slist_pop_last(statesMain)));
}
E_PRINT:
T_PRINT_OPEN E_VAR T_TAG_CLOSE {
slist_append_to_last(statesMain, build_print(string($2)));
}
E_INCLUDE:
T_INCLUDE_OPEN E_ATTRIBUTES T_TAG_CLOSE {
slist_append_to_last(statesMain, build_subtemplate());
}
E_ATTRIBUTES:
/* empty */
| E_ATTRIBUTES T_ATTR_NAME E_VAR {
attributes.insert(make_pair(string($2), string($3)));
}
E_IF_ELSE :
T_IF_OPEN E_EXPR T_TAG_CLOSE E_MAIN T_IF_ELSE E_MAIN T_IF_CLOSE {
string stateMain2 = slist_pop_last(statesMain);
string stateMain1 = slist_pop_last(statesMain);
slist_append_to_last(statesMain, build_if_else(string($2), stateMain1, stateMain2));
}
E_IF:
T_IF_OPEN E_EXPR T_TAG_CLOSE E_MAIN T_IF_CLOSE {
slist_append_to_last(statesMain, build_if(string($2), slist_pop_last(statesMain)));
}
E_EXPR:
E_VAR
| T_OPEN_RBRACKET E_EXPR T_CLOSE_RBRACKET {
$$ = string_to_char(" ( " + string($2) + " ) ");
}
| E_EXPR T_EQ E_EXPR {
$$ = string_to_char(string($1) + " == " + string($3));
}
| E_EXPR T_GE E_EXPR {
$$ = string_to_char(string($1) + " >= " + string($3));
}
| E_EXPR T_LE E_EXPR {
$$ = string_to_char(string($1) + " <= " + string($3));
}
| E_EXPR T_GT E_EXPR {
$$ = string_to_char(string($1) + " > " + string($3));
}
| E_EXPR T_LT E_EXPR {
$$ = string_to_char(string($1) + " < " + string($3));
}
| E_EXPR T_AND E_EXPR {
$$ = string_to_char(string($1) + " && " + string($3));
}
| E_EXPR T_OR E_EXPR {
$$ = string_to_char(string($1) + " || " + string($3));
}
| E_EXPR T_NOT_EQ E_EXPR {
$$ = string_to_char(string($1) + " != " + string($3));
}
E_VAR:
T_VAR_OPEN T_TEXT E_METHODS T_VAR_CLOSE {
$$ = string_to_char(build_apply_methods(build_var_name(string($2))));
}
| T_CONST_VAR_OPEN T_TEXT E_METHODS T_VAR_CLOSE {
$$ = string_to_char(build_apply_methods(build_const_name(string($2))));
}
| T_STRING
| T_NUMBER
E_METHODS:
/* empty */
| E_METHODS T_METHOD E_PARAMETERS {
vector<string> p;
p.push_back(string($2));
if(methodParemetersList.size() > 0) {
p.push_back(slist_implode(methodParemetersList, ","));
}
methodsList.push_back(p);
}
E_PARAMETERS:
| E_PARAMETERS T_STRING {
methodParemetersList.push_back(string($2));
}
%%
main( int argc, char* argv[] )
{
//yydebug=1;
int i;
string option = "";
templater = string(argv[0]);
for(i=1; i<argc; ++i) {
if( i % 2 == 0) {
if(option == "-p") {
varprefix = argv[i];
} else if(option == "-l") {
language = argv[i];
} else if(option == "-s") {
subtemplate = (bool)argv[i];
} else if(option == "-sp") {
sourcePath = argv[i];
} else if(option == "-dp") {
targetPath = argv[i];
}
} else {
option = argv[i];
}
}
yyparse();
}
void yyerror(const char *str) {
fprintf(stderr, "ошибка: %s\n", str);
}
bison -d -v -t template.y
flex template.l
g++ -std=c++11 template.tab.c lex.yy.c -o template -lfl -lboost_filesystem -lboost_system
Вот собственно и все
. После тестирования опишу чуть более подробно
о потенциальных проблемах и путях их решения.