На тему шаблонизаторов статей написано великое множество, в том числе и здесь, на хабре.
Раньше мне казалось, что сделать что-нибудь своё — «на коленке» — будет очень сложно.
Но, случилось так, что прислали мне тестовое задание.
Напиши, мол, JavaScript шаблонизатор, вот по такому сценарию, тогда придёшь на собеседование.
Требование, конечно, было чрезмерным, и поначалу я решил просто игнорить.
Но из спортивного интереса решил попробовать.
Оказалось, что не всё так сложно.
Собственно, если интересно, то под катом некоторые заметки и выводы по процессу создания.
Для тех, кому только глянуть: the result, the cat.
Дано:
Исходный шаблон — это JS String(), а данные это JS Object().
Блоки вида {% name %} body {% / %} , возможна неограниченная вложенность.
Если значение name является списком, то выводятся все элементы, иначе если не undefined, выводится один элемент.
Подстановки вида: {{ name }} .
В блоках и подстановках возможно использование точек в качестве имени, например {{.}} или {%.%} , где точка будет текущим элементом объекта верхнего уровня.
Есть ещё комментарии — это {# any comment w\wo multiline #} .
Для самих значений возможны фильтры, задаются через двоеточие: {{ .:trim:capitalize… }} .
Работать оно должно как:
Доказать:
+1 к самооценке.
UPD 2: Сразу скажу, чтобы «чётко понять» что там и зачем, нужно начать это делать, желательно вместе с debugger'ом.
UPD 3: Оно разобрано «на пальцах». Там есть ещё где «оптимизнуть». Но будет гораздо менее наглядно.
Приступим.
Т.к. исходный шаблон — это строка, то можно пользоваться преимуществами регулярок.
Для начала можно убрать комментарии, чтобы не отсвечивали:
Hint: [^] означает любой символ, * — сколько угодно раз.
Теперь можно подумать над тем, как будем парсить «чистый» результат.
Так как блоки возможны вложенные, предлагаю хранить всё в виде дерева.
На каждом уровне дерева будет JS Array (), элементы которого могут содержать аналогичную структуру.
Чтобы создать этот массив нужно отделить мух от котлет.
Для этого я воспользовался String.split() и String.match().
Ещё нам понадобится глубокий поиск по строковому val имени внутри объекта obj.
И сразу тут скажу СПАСИБО, subzey за greedy quantificator fix .
UPD 1: спасибо lynx1983 за Issue #2.
Итак, разделим строку на части parts и элементы matches:
Для разбора полётов нам понадобятся два массива.
В одном мы будем хранить блоки, в другом будет текущий элемент из цикла по спичкам.
Тут blocks — итоговый массив с выделенными блоками, а curnt — массив с текущей вложенностью.
На каждом шаге цикла мы определяем, что сейчас в str, начало блока или завершение.
Если начало блока, т.е. str !== '/' , то создаём новый элемент и push его в массив.
И ещё push его в curnt, т.к. нам необходимо понимать на каком мы уровне.
Попутно заносим в блок сами строки.
Соответственно, если у нас пустой curnt, то мы на нулевом уровне дерева.
Если curnt не пустой, то нужно заносить в nested элемент последнего curnt.
Соотвественно, каждый элемент массива это, минимум:
Т.к. у нас может быть ситуация, когда после блока есть что-нибудь ещё, то здесь af3e.str и должно быть строкой, идущей сразу после {% / %} текущего блока. Все необходимые ссылки мы проставим в момент завершения блока, так наглядней.
В этот же момент мы удаляем последний элемент элемент curnt.
Теперь мы можем собрать одномерный массив, в котором будут все нужные подстроки с их текущими obj.
Для этого нужно «разобрать» получившийся blocks, учитывая что могут быть списки.
Понадобится немного рекурсии, но в целом это будет уже не так сложно.
Далее мы поэлементно распарсим получившийся stars и, собрав результат в строку, получим итоговый результат:
Приведённый код немного меньше финального результата.
Так, например, я не показал что делать с текущим элементом, если он задан ка точка.
Так же я не привёл обработку фильтров.
Кроме того в итоговом варианте, я «от себя» добавил в обработку ситуаций, когда «текущий элемент» или «значение для» являются функциями.
Но моей целью было показать саму концепцию…
А результат, как уже было сказано в начале статьи, можно найти здесь.
Итоговый пример тут.
Надеюсь, кому-нибудь пригодится.
Спасибо за внимание!
:)
Раньше мне казалось, что сделать что-нибудь своё — «на коленке» — будет очень сложно.
Но, случилось так, что прислали мне тестовое задание.
Напиши, мол, JavaScript шаблонизатор, вот по такому сценарию, тогда придёшь на собеседование.
Требование, конечно, было чрезмерным, и поначалу я решил просто игнорить.
Но из спортивного интереса решил попробовать.
Оказалось, что не всё так сложно.
Собственно, если интересно, то под катом некоторые заметки и выводы по процессу создания.
Для тех, кому только глянуть: the result, the cat.
Дано:
Исходный шаблон — это JS String(), а данные это JS Object().
Блоки вида {% name %} body {% / %} , возможна неограниченная вложенность.
Если значение name является списком, то выводятся все элементы, иначе если не undefined, выводится один элемент.
Подстановки вида: {{ name }} .
В блоках и подстановках возможно использование точек в качестве имени, например {{.}} или {%.%} , где точка будет текущим элементом объекта верхнего уровня.
Есть ещё комментарии — это {# any comment w\wo multiline #} .
Для самих значений возможны фильтры, задаются через двоеточие: {{ .:trim:capitalize… }} .
Работать оно должно как:
var str = render (tpl, obj);
Доказать:
+1 к самооценке.
UPD 2: Сразу скажу, чтобы «чётко понять» что там и зачем, нужно начать это делать, желательно вместе с debugger'ом.
UPD 3: Оно разобрано «на пальцах». Там есть ещё где «оптимизнуть». Но будет гораздо менее наглядно.
Приступим.
Т.к. исходный шаблон — это строка, то можно пользоваться преимуществами регулярок.
Для начала можно убрать комментарии, чтобы не отсвечивали:
// to cut the comments tpl = tpl.replace ( /\{#[^]*?#\}/g, '' );
Hint: [^] означает любой символ, * — сколько угодно раз.
Теперь можно подумать над тем, как будем парсить «чистый» результат.
Так как блоки возможны вложенные, предлагаю хранить всё в виде дерева.
На каждом уровне дерева будет JS Array (), элементы которого могут содержать аналогичную структуру.
Чтобы создать этот массив нужно отделить мух от котлет.
Для этого я воспользовался String.split() и String.match().
Ещё нам понадобится глубокий поиск по строковому val имени внутри объекта obj.
Применённый вариант getObjDeep:
var deeps = function (obj, val) { var hs = val.split('.'); var len = hs.length; var deep; var num = 0; for (var i = 0; i < len; i++) { var el = hs[i]; if (deep) { if (deep[el]) { deep = deep[el]; num++; } } else { if (obj[el]) { deep = obj[el]; num++; } } } if (num == len) { return deep; } else { return undefined; } };
И сразу тут скажу СПАСИБО, subzey за greedy quantificator fix .
UPD 1: спасибо lynx1983 за Issue #2.
Итак, разделим строку на части parts и элементы matches:
// регулярка для парсинга: // цифробуквы, точка, подчеркивание, // двоеточие, слеш и минус, сколько угодно раз var ptn = /\{\%\s*[a-zA-Z0-9._/:-]+?\s*\%\}/g; // строковые куски var parts = tpl.split (ptn); // сами спички var matches = tpl.match (ptn);
Для разбора полётов нам понадобятся два массива.
В одном мы будем хранить блоки, в другом будет текущий элемент из цикла по спичкам.
// все блоки var blocks = []; // вложенности var curnt = []; if( matches ){ // т.к. м.б. null var len = matches.length; for ( var i = 0; i < len; i++ ) { // выкидываем {% и %}, и попутно делаем trim var str = matches[i].replace (/^\{\%\s*|\s*\%\}$/g, ''); if (str === '/') { // finalise block // ... } else { // make block // ... } // ...
Тут blocks — итоговый массив с выделенными блоками, а curnt — массив с текущей вложенностью.
На каждом шаге цикла мы определяем, что сейчас в str, начало блока или завершение.
Если начало блока, т.е. str !== '/' , то создаём новый элемент и push его в массив.
И ещё push его в curnt, т.к. нам необходимо понимать на каком мы уровне.
Попутно заносим в блок сами строки.
Соответственно, если у нас пустой curnt, то мы на нулевом уровне дерева.
Если curnt не пустой, то нужно заносить в nested элемент последнего curnt.
// длина текущей вложенности var cln = curnt.length; if (cln == 0) { // т.к. это верхний уровень, то просто в него и кладём текущий элемент blocks.push ( struct ); // пишем текущую вложенность, она же нулевая curnt.push ( struct ); } else { // нужно положить в nested текущего вложенного блока curnt[cln - 1].nest.push ( struct ); // теперь взять этот "последний" элемент и добавить его в curnt var last = curnt[cln - 1].nest.length - 1; curnt.push ( curnt[cln - 1].nest [ last ] ); }
Соотвественно, каждый элемент массива это, минимум:
var struct = { // текущий obj для блока cnt: deeps( obj, str ), // вложенные блоки nest: [], // строка перед всеми вложенными блоками be4e: parts[ i + 1 ], // str -- строка, идущая после завершения данного // cnt -- блок-родитель, парсить строку будем в его рамках af3e: { cnt: null, str: '' } };
Т.к. у нас может быть ситуация, когда после блока есть что-нибудь ещё, то здесь af3e.str и должно быть строкой, идущей сразу после {% / %} текущего блока. Все необходимые ссылки мы проставим в момент завершения блока, так наглядней.
В этот же момент мы удаляем последний элемент элемент curnt.
if (str === '/') { // предыдущий элемент curnt // является родителем // завершившегося сейчас блока curnt [cln - 1].af3e = { cnt: ( curnt [ cln - 2 ] ? curnt [ cln - 2 ].cnt : obj ), str: parts[ i + 1 ] }; curnt.pop();
Теперь мы можем собрать одномерный массив, в котором будут все нужные подстроки с их текущими obj.
Для этого нужно «разобрать» получившийся blocks, учитывая что могут быть списки.
Понадобится немного рекурсии, но в целом это будет уже не так сложно.
// массив строк для парсинга элементарных частей блоков var stars = [ [ parts[0], obj ] ]; parseBlocks( blocks, stars );
Примерный вид parseBlocks()
var parseBlocks = function ( blocks, stars ) { var len = blocks.length; for (var i = 0; i < len; i++) { var block = blocks [i]; // если определён текущий obj для блока if (block.cnt) { var current = block.cnt; // найдём списки switch ( Object.prototype.toString.call( current ) ) { // если у нас массив case '[object Array]': var len1 = current.length; for ( var k = 0; k < len1; k++ ) { // кладём в stars текущий элемент массива и его строку stars.push ( [ block.be4e, current[k] ] ); // парсим вложенные блоки parseBlocks( block.nest, stars ); } break; // если у нас объект case '[object Object]': for (var k in current) { if (current.hasOwnProperty(k)) { // кладём в stars текущий элемент объекта и его строку stars.push ( [ block.be4e, current[k] ] ); // парсим вложенные блоки parseBlocks( block.nest, stars ); } } break; // у нас не массив и не объект, просто выведем его default: stars.push ( [ block.be4e, current ] ); parseBlocks( block.nest, stars ); } // кладём в stars то, что было после текущего блока stars.push ( [ block.af3e.str, block.af3e.cnt ] ); } } };
Далее мы поэлементно распарсим получившийся stars и, собрав результат в строку, получим итоговый результат:
var pstr = []; var len = stars.length; for ( var i = 0; i < len; i++ ) { pstr.push( parseStar ( stars[i][0], stars[i][1] ) ); } // Результат: return pstr.join ('');
Примерный вид parseStar()
var parseStar = function ( part, current ) { var str = ''; // убираем лишнее var ptn = /\{\{\s*.+?\s*\}\}/g; var parts = part.split (ptn); var matches = part.match (ptn); // начинаем собирать строку str += parts[0]; if (matches) { var len = matches.length; for (var i = 0; i < len; i++) { // текущий элемент со значением var match = matches [i]; // убираем лишнее и делаем trim var el = match.replace(/^\{\{\s*|\s*\}\}$/g, ''); var strel = ''; // находим элемент в текущем объекте var deep = deeps( current, el ); // если нашли, то добавляем его к строке deep && ( strel += deep ); str += strel; } if (len > 0) { str += parts[ len ]; } } return str; }
Приведённый код немного меньше финального результата.
Так, например, я не показал что делать с текущим элементом, если он задан ка точка.
Так же я не привёл обработку фильтров.
Кроме того в итоговом варианте, я «от себя» добавил в обработку ситуаций, когда «текущий элемент» или «значение для» являются функциями.
Но моей целью было показать саму концепцию…
А результат, как уже было сказано в начале статьи, можно найти здесь.
Итоговый пример тут.
Надеюсь, кому-нибудь пригодится.
Спасибо за внимание!
:)
