Перейти к содержимому

Как сделать шаблонизатор на js

  • автор:

Простой шаблонизатор на чистом JS со связями

В последнее время все чаще требуется максимально быстрая разработка прототипов веб-приложений. Интерфейсы усложняются, требуется отображение огромного количества данных. И вот я озадачился созданием легкого и максимально простого html-шаблонизатора. Кому интересно, что получилось на ~50 строках JS-кода — под кат.

Задача минимум

Сделать односторонний (JS → HTML) шаблонизатор, связывающий JS данные с html отображением максимально просто. Решение должно быть быстрым, с минимальным порогом вхождения — тяп ляп и готово. Максимальная допиливаемость под конкретные требования — примитивность залог успеха.

Принцип работы

Со стороны html

Будем писать обычный html код, где для связи с js будут использоваться обычные data-атрибуты:
data-template — содержимое, которое будет отображаться внутри тега
data-namespace — данные, которые привязываются к JS

Со стороны JS

Данные должны записываться и обновляться прозрачно. Т.е. если у нас есть объект object со свойством data, то данный должны обновляться в html сразу после обычного:

object.data = 5; 

Без всяких вспомогательных методов типа:

object.setData = function(val) < this.data = val; document.getElementById("tpl").html = val; >object.setData(5); 

— это норм подход, но что будете делать, если с сервера приходит большая пачка json данных, которые надо распихать по разным объектам и отобразить изменения в интерфейсе? Писать и вызывать свой сеттер для каждого свойства/набора свойств — мне такая реализация надоела.

Решение простое. Для каждого свойства объекта мы можем задать дефолтные сеттеры/геттеры, срабатывающие как раз при изменении изменении значения обычным присваиванием. Называется это Object.defineProperty(. ) — пересказывать статью не буду, перейду сразу к идее.

Нам надо пробежаться по всем свойствам объекта, которые имеют связь с html. Способов это сделать два:

1) Пробежаться по dom, вытащив оттуда все возможные (не повторяющиеся) значения атрибута data-namespace в отдельный массив
2) Для каждого объекта в js вручную будем задавать массив, который говорит, какие данные (только какие а не с чем) будут использоваться в связях.

Так как код претендует на наглядность реализации и примитивность — выбираем второй вариант.
Чтобы не ходить вокруг да около, сразу приведу пример с пояснениями.

тут должно быть значение dataA из объекта со значением свойства this_namespace=test
тут должно быть значение dataB из объекта со значением свойства this_namespace=test
тут должно быть значение dataA из объекта со значением свойства this_namespace="test2"
var obj1 = < this_namespace: 'test', // имя которое будет использоваться для идентификации объекта this_data: ['dataA', 'dataB'] // имена которое будет использоваться для идентификации свойств объекта >; var obj2 = < this_namespace: 'test2', // имя которое будет использоваться для идентификации объекта this_data: ['dataA'] // имена которое будет использоваться для идентификации свойств объекта >

Как видим, изначально в объекте нет никаких данных, мы их получим позже — для начала объект надо подготовить к такому.

Magic

Вся магия заключается в том, чтобы внутри объекта пробежаться по массиву this_data и насоздавать в этом же объекте по 2 свойства на каждый элемент массива:

1) __dataA, __dataB… — тут будут храниться значения
2) dataA, dataB… — и тут будут храниться значения =)

Смысл такой манипуляции в том, что если мы зададим например дефолтный сеттер просто для свойства dataA, который будет записывать переданное значение сюда же — получится рекурсивненько. Чтобы этого избежать, создается отдельное свойство с префиксом «__» (можно любой, например fake_ или magic_ — просто я выбрал такой чтобы не засорять namespace объекта).

Получается, что когда мы напишем obj.dataA = 3, это значение запишется в свойство obj.__dataA, а дефолтный геттер при запросе obj.dataA будет отдавать значение obj.__dataA. То есть по факту, в массиве obj вообще нету свойств dataA и dataB в чистом виде — сеттеры и геттеры подменяют их на свойство с префиксом.

Как я и говорил, нужно обеспечить максимально низкий порог вхождения — чтобы начинающим было проще. А кому надо могут переписать как хотят. В связи с этим, весь метод придания объекту нужного вида будет реализован через прототип объекта.

Тут код неудобно читать, так что сразу смотрим сюда (jsfiddle) — там весь код с комментариями и запуск.

Что еще

  1. Не кроссбраузерно, (ie11+) из за необходимости использования let вместо var — чтобы особо не заморачиваться с замыканиями. Но опять же — можно переписать.
  2. Вложенные объекты из коробки работать не будут — можно дописать. Я привел лишь максимально сокращенный код, который можно подстроить под себя.
  3. Следуя концепции того, что в html не должно быть логики, я не стал добавлять возможность использования в шаблонах каких-то даже примитивных операций (например dataA + dataB). Тупо вывод данных. Логика должна быть на js. Хотя когда я только начал это делать, такая возможность была и там пришлось по-быстрому использовать eval.
  4. В коде нет никаких проверок вообще, это вы делаете сами по необходимости, просто в моем конкретном случае, для которого писался этот метод, все проверки идут через другую прослойку.
  5. Как утешительный приз: в начале статьи я говорил о том что «воооот, нам приходит куча json дааааных, что же мне с ними дееелать. » — тут все просто: парсим JSON, распихиваем его в нужные объекты через Object.assign — это будет работать.
  6. В своих проектах я именую все переменные и свойства с префиксом — типом данных. Что-то вроде arr_list = [1,2,3]; int_count = 2; и т.д. Мне это позволяет «без регистрации и смс» настроить дефолтные геттеры отдавать значение, соответствующее типу данных, и если вдруг у нас int_count = 1.4, нам вернется результат 1 и на лету проверять все значения. Это дисциплинирует и реально помогает.
  7. Для каждой ситуации можно предусмотреть разные set и get, в зависимости от того что хочется получить в итоге — оч удобно.

Шаблоны — JS: Express

Если проанализировать http запросы к типичному сайту, то можно заметить, что большая часть этих запросов направлена на получение контента, а не его модификацию. Другими словами, основная работа обработчиков состоит в том, чтобы сформировать правильный html и отправить его клиенту (браузеру). Единственный способ для генерации html , с которым мы знакомы, это ручной сбор строчки, содержащей разметку, и отправка посредством метода send .

app.get('/', (req, res) =>  res.send('
Hello World!
'); >);

Сказать, что этот способ плох, это ничего не сказать. Кроме того, что это крайне неудобно, существует масса других недостатков в таком подходе. Если заглянуть в историю развития web, то выяснится интересный факт: php появился как средство решения описанной выше задачи, а не как язык программирования.

Задачу по формированию разметки называют шаблонизацией, а конкретные библиотеки для шаблонизации называют шаблонизаторами. Общий принцип работы такой: описываются файлы с разметкой, а библиотека предоставляет функции для загрузки этих шаблонов в код. Во время загрузки происходят необходимые подстановки и шаблон заполняется конкретными данными.

Jinja Like

Классическим примером может служить шаблонизатор jinja из мира питона. Его популярность привела к тому, что в каждом языке есть множество шаблонизаторов, очень похожих и даже работающих так же, как jinja . Поэтому можно говорить о целом классе jinja-like шаблонизаторов.

 class="header"> pagename | title >>   class="small">authors  for author in authors %>  if loop.first %> class="first" endif %>>  author >>   endfor %>  

По сути jinja — это хоть и примитивный, но полноценный язык программирования, который вкрапливается в файл с разметкой и расширяет его во время обработки. Несмотря на очевидность этого решения, оно обладает рядом недостатков. Первое — это сложность редактирования такого рода шаблонов. Из-за перемешивания кода с версткой приходится скакать вверх-вниз чтобы добавить/удалить/изменить теги, и то же самое нужно делать с конструкциями самого языка. Этот недостаток может быть не очевиден тем, кто никогда не видел альтернативных решений, и как мы увидим позже, они есть. Второе: в подобных шаблонизаторах текст вне конструкций шаблонизатора, то есть та самая вёрстка, никак не анализируется. Это легко приводит к проблемам типа «незакрытый тег», или семантическому нарушению html , когда неправильно друг в друга вкладываются теги, используются несуществующие атрибуты и тому подобное. И третий немаловажный момент: оформление шаблонов не проверяется автоматическими инструментами, и поэтому стиль будет сильно зависеть от человека.

Pug (Haml Like)

Существует и совершенно другой подход к организации шаблонов. Когда я в первый раз увидел такое, то был немало удивлен. Кажется, что самостоятельно дойти до этого решения очень сложно. Чтобы не томить, сразу покажу пример:

Этот пример почти идентичен тому, что было выше с использованием jinja-like шаблонизатора. Обратите внимание насколько чище шаблон во втором примере и на то, что он почти в два раза короче.

История таких шаблонизаторов берет свое начало с haml , Ruby шаблонизатора, который в мире rails является решением номер один уже очень много лет. После этого оно было скопировано во многие языки, как и jinja . В js мире haml-like шаблонизатор был долгое время известен как jade , и лишь недавно его переименовали в pug .

Попробуем разобраться с основными принципами работы таких шаблонизаторов. Во-первых, это так же язык программирования, но в отличие от jinja-like шаблонизаторов, то что не является кодом, на самом деле не является версткой. Всё, что пишется в pug -шаблонах, будет обрабатываться парсером, другими словами, в haml-like шаблонизаторах вы не можете писать всё что угодно вне управляющих конструкций. Во-вторых, шаблон строится с помощью особого синтаксиса, который задаёт теги в виде имен, а вложенность определяется отступом на следующем уровне.

И, с одной стороны, у вас появляется новый язык и новый способ построения html . Что требует некоторого привыкания, но с другой, преимущества оказываются настолько сильными, что человек, распробовавший подобные шаблонизаторы, вряд ли добровольно вернется на jinja-like библиотеки. Ниже перечислены основные преимущества:

  • Шаблон чище, гораздо короче и уже
  • Отсутствует проблема незакрытых тегов (т.к. их просто нет)
  • Писать и модифицировать такие шаблоны гораздо проще
  • Стиль задаётся грамматикой (писать по разному практически невозможно)
  • Шаблоны валидируются, и соответствующая библиотека не даст делать совсем злые вещи
  • Вставляемые данные по умолчанию всегда экранируются (привет php!)

Интеграция pug с Express выглядит очень просто:

// npm install pug app.set('view engine', 'pug'); app.get('/', (req, res) =>  const data =  title: 'Hey', message: 'Hello there!' >; res.render('index', data); >) 

Всё сводится к установке зависимости и установке pug в качестве движка для рендеринга шаблонов. После этого, внутри обработчиков можно начинать использовать метод render . Первый параметр которого — это путь до шаблона, второй — набор параметров для подстановок внутри шаблона.

Это не единственный способ передачи параметров в шаблон. В большинстве случаев они передаются именно вторым параметром в render , но иногда возможны ситуации, в которых у нас есть сквозная функциональность, и было бы крайне неудобно прокидывать их в шаблон в каждом обработчике. Реализуется это через установку свойств в объект res.locals , а в шаблоне эти свойства становятся доступны как переменные. Эту особенность мы будем использовать позже, когда начнем работать с сессиями и аутентификацией. Помните, что злоупотреблять этим способом не стоит, явное лучше неявного. Стремитесь к тому, чтобы код был чистый (использовал чистые функции).

Наследование шаблонов

На практике сайт не всегда состоит из уникальных страниц. Обычно меняется только контентная часть, а вокруг одно и тоже. Часть, которая не меняется, принято называть макетом или лейаутом (layout). Это настолько распространенный кейс, что большинство шаблонизаторов поддерживают механизм для выделения лейаутов. В pug он называется наследованием шаблонов. Ниже приведён пример такого наследования.

 block scripts script(src='/jquery.js') body block content block foot #footer p some footer content //- page-a.pug extends layout.pug append scripts script(src='/pets.js') block content h1= title - const pets = ['cat', 'dog'] each petName in pets h2= petName 

В шаблоне, который мы используем для рендеринга нашей страницы, пишется специальная директива extends . . В неё передаётся имя окружающего шаблона, который чаще является макетом. В макете определяется блок (или блоки), в которые будет происходить подстановка кусков шаблона. Далее необходимо в шаблоне (не макете) определить такие же блоки и наполнить их контентом. Синтаксис задания блоков в обоих местах одинаковый, только в одном случае блок не содержит тела, а в другом содержит.

Включения

Так же бывает полезным механизм включения, позволяющий выделять из шаблонов общие части и переиспользовать их.

Чистота

На просторах интернета постоянно спорят о том, что может быть в шаблоне, а чего нет. Что является логикой вывода, а что нет. При этом есть ряд правил, которые объективно нарушать не стоит:

  1. Ни в коем случае шаблон не должен порождать побочных эффектов. В шаблоне нельзя писать в базу, изменять данные на диске и вообще любым способом пытаться влиять на окружающую среду. Шаблон это исключительно чтение.
  2. Шаблоны должны быть декларативны, никакого изменения состояний, другими словами, если вы начинаете внутри шаблона вводить переменные и изменять их, то, по сути, шаблон превращается в полноценный скрипт, который вы программируете. Не допускайте этого.
  3. Использовать логику, влияющую на вывод внутри шаблона — это нормально. Если у вас, с точки зрения ui , блок показывается по определённому условию, то вы не сможете этого избежать, единственное о чем нужно помнить, это создавать вовремя правильные абстракции (функции) для избежания дублирования, а так же для выделения бизнес-правил.

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов

Наши выпускники работают в компаниях:

Эволюция шаблонных систем для JavaScript

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Различных шаблонных систем – много.

Они постепенно эволюционировали и развивались.

В этой главе мы разберём, как шёл этот процесс, какие шаблонки «родились», какие бонусы нам даёт использование той или иной шаблонной системы.

Микрошаблоны

Микрошаблоны (англ. microtemplate) мы уже видели на примере _.template .

Это HTML со вставками переменных и произвольным JS.

Шаблонная система компилирует этот код в JavaScript-функцию с минимальными модификациями, и она уже, запустившись с данными, генерирует результат.

Достоинства и недостатки такого подхода:

Недостатки

  • Жёстко привязан к языку JavaScript.
  • При ошибке в шаблоне приходится лезть внутрь «страшной» функции

Достоинства

  • Простая и быстрая шаблонная система
  • Внутри JS-функции доступен полноценный браузерный отладчик, функция хоть и страшна, но понятна.

Код в шаблоне

Включение произвольного JS-кода в шаблон, в теории, позволяет делать в нём всё, что угодно. Но обратная сторона медали – шаблон вместо внятного HTML может стать адским нагромождением разделителей вперемешку с вычислениями. Что рекомендуется делать в шаблонах, а что нет?

Можно разделить код на два типа с точки зрения шаблонизации:

  • Бизнес-логика – код, формирующий данные, основной код приложения.
  • Презентационная логика – код, описывающий, как показываются данные.

Например, код, получающий данные с сервера для вывода в таблице – бизнес-логика, а код, форматирующий даты для вывода – презентационная логика.

В шаблонах допустима лишь презентационная логика.

Кросс-платформенность

Зачастую, нужно использовать один и тот же шаблон и в браузере и на сервере.

Например, серверный код генерирует HTML со списком сообщений, а JavaScript на клиенте добавляет к нему новые по мере появления.

…Но как использовать на сервере шаблон с JavaScript, если его основной язык – PHP, Ruby, Java?

Эту проблему можно обойти. На сервер, использующем PHP, Ruby, Java или какой-то другой язык, дополнительно ставится виртуальная машина V8 и настраивается интеграция с ней. Почти все платформы это умеют.

После этого становится возможным запускать JavaScript-шаблоны и передавать им данные в виде объектов, массивов и так далее.

Этот подход может показаться искусственным, но на самом деле он вполне жизнеспособен и используется в ряде крупных проектов.

Прекомпиляция

Эта шаблонка и большинство других систем, которые мы рассмотрим далее, допускают прекомпиляцию.

То есть, можно заранее, до выкладывания сайта на «боевой сервер», обработать шаблоны, создать из них JS-функции, объединить их в единый файл и далее, в «боевом окружении» использовать уже их.

Современные системы сборки (brunch, grunt с плагинами и другие) позволяют делать это удобно, а также хранить шаблоны в разных файлах, каждый – в нужной директории с JS-кодом для виджета.

Хелперы и фильтры

JavaScript-вставки не всегда просты и элегантны. Иногда, чтобы что-то сделать, нужно написать порядочно кода.

Для того, чтобы сделать шаблоны компактнее и проще, в них стали добавлять фильтры и хелперы.

    Хелпер (англ. helper) – вспомогательная функция, которая доступна в шаблонах и используется для решения часто возникающих задач. В _.template , чтобы объявить хелпер, можно просто сделать глобальную функцию. Но это слишком грубо, так не делают. Гораздо лучше – использовать объект _.templateSettings.imports , в котором можно указать, какие функции добавлять в шаблоны, или опцию imports для _.template . Пример хелпера – функция t(phrase) , которая переводит phrase на текущий язык:

_.templateSettings.imports.t = function(phrase) < // обычно функция перевода немного сложнее, но здесь это не важно if (phrase == "Hello") return "Привет"; >// в шаблоне используется хелпер t для перевода var compiled = _.template(" 
"); alert( compiled() ); //
Привет

Свой язык

Для того, чтобы сделать шаблон ещё короче, а также с целью «отвязать» их от JavaScript, ряд шаблонных систем предлагают свой язык.

Шаблон для меню в Handlerbars, к примеру, будет выглядеть так:

Как видно, вместо JavaScript-конструкций здесь используются хелперы. В примере выше > . – «блочный» хелпер: он показывает своё содержимое для каждого элемента items и является альтернативой forEach .

Есть и другие встроенные в шаблонизатор хелперы, можно легко делать свои.

Использование такого шаблона:

// текст шаблона должен быть в переменной tmpl var compiled = Handlebars.compile(tmpl); var result = compiled(< title: "Сладости", items: ["Торт", "Пирожное", "Пончик"] >);

Библиотека шаблонизации Handlebars «понимает» этот язык. Вызов Handlebars.compile принимает строку шаблона, разбивает по разделителям и, аналогично предыдущему виду шаблонов, делает JavaScript-функцию, которая затем по данным выдаёт строку-результат.

Запрет на встроенный JS

Если «свой язык шаблонизатора» очень прост, то библиотеку для его поддержки можно легко написать под PHP, Ruby, Java и других языках, которые тем самым научатся понимать такие шаблоны.

Если шаблонка действительна нацелена на кросс-платформенность, то явные JS-вызовы в ней запрещены. Всё делается через хелперы.

Если же нужна какая-то логика, то она либо выносится во внешний код, либо делается через новый хелпер – он отдельно пишется на JavaScript (для клиента) и для сервера (на его языке). Получается полная совместимость.

Это создаёт определённые сложности. Например, в Handlebars есть хелпер > . > , который выводит содержимое, если истинно условие cond . При этом вместо cond нельзя поставить, к примеру, a > b или вызов str.toUpperCase() , будет ошибка. Все вычисления должны быть сделаны на этапе передачи данных в шаблон.

Так сделано как раз для переносимости шаблонной системы на другие языки, но на практике не очень-то удобно.

Продвинутые кросс-платформенные шаблонизаторы, в частности, Closure Templates, обладают более мощным языком и умеют самостоятельно разбирать и компилировать многие выражения.

Шаблонизация компонент

До этого мы говорили о шаблонных системах «общего назначения». По большому счёту, это всего лишь механизмы для преобразования одной строки в другую. Но при описании шаблона для компоненты мы хотим сгенерировать не просто строку, а DOM-элемент, и не просто генерировать, а в дальнейшем – с ним работать.

Современные шаблонные системы «заточены» на это. Они умеют создавать по шаблону DOM-элементы и автоматически выполнять после этого разные полезные действия.

  • Можно сохранить важные подэлементы в свойства компоненты, чтобы было проще к ним обращаться из JavaScript.
  • Можно автоматически назначать обработчики из методов компонента.
  • Можно запомнить, какие данные относятся к каким элементам и в дальнейшем, при изменении данных автоматически обновлять DOM («привязка данных» – англ. data binding).

Одной из первых систем шаблонизации, которая поддерживает подобные возможности была Knockout.JS.

Попробуйте поменять значение в примере ниже и вы увидите двухстороннюю привязку данных в действии:

Пишем свой JavaScript шаблонизатор

На тему шаблонизаторов статей написано великое множество, в том числе и здесь, на хабре.
Раньше мне казалось, что сделать что-нибудь своё — «на коленке» — будет очень сложно.
Но, случилось так, что прислали мне тестовое задание.
Напиши, мол, JavaScript шаблонизатор, вот по такому сценарию, тогда придёшь на собеседование.
Требование, конечно, было чрезмерным, и поначалу я решил просто игнорить.
Но из спортивного интереса решил попробовать.
Оказалось, что не всё так сложно.

Собственно, если интересно, то под катом некоторые заметки и выводы по процессу создания.

Для тех, кому только глянуть: the result, the cat.

Исходный шаблон — это JS String(), а данные это JS Object().
Блоки вида body , возможна неограниченная вложенность.
Если значение name является списком, то выводятся все элементы, иначе если не undefined, выводится один элемент.
Подстановки вида: > .
В блоках и подстановках возможно использование точек в качестве имени, например > или , где точка будет текущим элементом объекта верхнего уровня.
Есть ещё комментарии — это .
Для самих значений возможны фильтры, задаются через двоеточие: > .

Работать оно должно как:

 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; >>; 

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 (/^\$/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 = /\\>/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; > 

Приведённый код немного меньше финального результата.
Так, например, я не показал что делать с текущим элементом, если он задан ка точка.
Так же я не привёл обработку фильтров.
Кроме того в итоговом варианте, я «от себя» добавил в обработку ситуаций, когда «текущий элемент» или «значение для» являются функциями.

Но моей целью было показать саму концепцию…

А результат, как уже было сказано в начале статьи, можно найти здесь.
Итоговый пример тут.

Надеюсь, кому-нибудь пригодится.
Спасибо за внимание!

  • javascript
  • javascript library
  • шаблонизатор
  • шаблонизаторы
  • шаблонизация
  • templates
  • template engine
  • templater
  • template parser

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *