Написать шаблонизатор и "компилятор", который переводит описанный шаблон в "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
Вот собственно и все . После тестирования опишу чуть более подробно о потенциальных проблемах и путях их решения.