Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
JS-синтаксис для BEMHTML

Синтаксис BEMHTML

Вступление

Мы уже достаточно давно обсуждаем вопрос изменения синтаксиса в BEMHTML. Основные причины такие:

  • "умные" редакторы подсвечивают BEMHTML-код как ошибки
  • незнакомый синтаксис и похожесть на json путает людей и не все пользуются возможностью сэкономить пару символов на кавычках
  • кастомный синтаксис требует компиляции даже для дев-режима, что в свою очередь:
    • не очень быстро (с ростом количества шаблонов просто парсинг уже занимает существенное время)
    • мешает дебажить, т.к. исполняется в итоге не совсем тот код, который пишется

Чтобы разом поубивать всех зайцев и решить эти проблемы мы придумали:

  • как можно было бы пожертвовать синтаксическим сахаром и возможностью не писать некоторые лишние скобочки и function() {} и сделать синтаксис BEMHTML полностью JS-ным
  • ещё немного отказавшись от красоты в синтаксисе сделать возможным исполнение в дев-режиме без компиляции (просто собирая всё в один файл и дописывая сверху и снизу служебные части)

Поскольку BEMHTML сильно базируется на XJST, то это одновременно означает, что и в XJST будет аналогичный JS-ный синтаксис.

Кроме того, эти же технологии давно хочется использовать для написания того, что щас называется priv.js. Поэтому компиляторную часть из BEMHTML (то что про синтаксический сахар сверху XJST, не базовые шаблоны) мы переименуем в проект github.com/bem/bem-xjst и будем использовать как базовую для обеих технологий (и BEMHTML, и priv.js).

Подробнее про предполагаемый синтаксис

XJST

Задача про реализацию: https://github.com/veged/xjst/issues/18.

template

Сейчас синтаксис определения шаблона такой:

template(subPredic1 === 1 && subPredic2 === 2) ex + press + ion
template(subPredic1 === 1 && subPredic2 === 2) {
    var res = 1
    // some code
    return res
}

Т.е. это какбы новое ключевое слово, по структуре похожее на while.

Можно иметь такой синтаксис:

template(subPredic1 === 1, subPredic2 === 2)(ex + press + ion)
template(subPredic1 === 1, subPredic2 === 2)(function() {
    var res = 1
    // some code
    return res
})

Т.е. теперь, template это функция, от произвольного количества аргументов (подпредикатов), которая возвращает функцию от одного аргумента (тело шаблона).

Тут есть важные нюансы, связанные с возможностью исполнять этот код без компиляции:

  • Все подпредикаты будут выполняться сразу при проверке шаблона. Это не совпадает с первородной семантикой XJST, когда мы гарантировали возможность писать предикаты типа таких: this.x && this.x.y. Но уже достаточно давно мы при компиляции делаем оптимизации, связанные с пересортировкой подпредикатов и поэтому если важно соблюдать порядок, это нужно оформлять как отдельный целый предикат (например, взять в скобки): (this.x && this.x.y). Кстати, в новом синтаксисе мы не наделяем && особым смыслом, а явно используем разные аргументы для разных подпредикатов, поэтому никаких хаков со скобками не надо (используется или , если это разные подпредикаты, или && если один).
  • Тело в виде выражения тоже будет выполняться сразу при проверке шаблона. Это немного ломает предыдущее поведение, когда можно было написать template(this.x === 2) apply(this.x = 1), при условии исполнения без компиляции apply позовётся даже если шаблон не подошёл. Но это в целом нормально работает для литеральных выражений, где не важно когда оно вызовется -- template(this.block === 'b1', this._mode === 'content')({ elem: 'e1' }) -- так можно писать. Если же тело нельзя вызывать сразу, то нужно обернуть всё в анонимную функцию: template(this.x === 2)(funtion() { apply(this.x = 1) }).
local

Напомню что такое local. Он нужен для того, чтобы как-то помодифицировать что-то, сделать тело и потом автоматически откатить модификации назад. Он используется как способ быстрой реализации создания нового контекста на основе старого. Альтернативные способы, прототипное наследование и extend, работают медленнее, чем если мы не создаём никаких новых объектов для новых контекстов, а пользуемся одним существующим объектом, туда-сюда меняя в нём нужные поля.

Сейчас синтаксис сейчас такой:

local(this.x = 1, this.a.b.c = 2) ex + press + ion
local(this.x = 1, this.a.b.c = 2) {
    // statements body
}

Т.е. это какбы новое ключевое слово, по структуре похожее на имеющийся template.

Можно иметь такой синтаксис:

local(this)({ x: 1, 'a.b.c': 2 })(ex + press + ion)
local(this)({ x: 1, 'a.b.c': 2 })(function() {
    // statements body
})
local(this)({ x: 1 }, { 'a.b.c': 2 }, { z: 3 })(ex + press + ion)

Т.е. теперь local это фунция от одного аргумента (объекта в котором нужно совершить временные модификации), которая возвращает функцию от произвольного количества аргументов (объектов, описывающих сами модификации), которая возвращает (одинаково с template) функцию от одного аргумента (тело блока).

Тут самый главный нюанс, это невозможность сделать натуральную запись для присваиваний, чтобы это корректно работало без компиляции (теряется информация про то, что откатывать после завершения тела). Поэтому приходится терпеть запись чего-то что похоже на JS-выражение как строку ('a.b.c').

apply/applyNext

С apply и applyNext всё просто, это по сути local без тела, поэтому используется тот же синтаксис.

BEM-XJST

Это БЭМ-ориентированные хелперы над стандартным XJST-синтаксисом. Сюда входит:

  • синтаксический сахар для подпредикатов про БЭМ предметную область
  • возможность писать вложенные шаблоны (по хорошему, стоит это перенести в сам XJST)
  • синтаксический сахар для apply по какой-то mode (apply(this._mode = 'bla'))
  • ключевое слово applyCtx (синтаксический сахар для applyNext(this.ctx = { some: 'new' }))

Задача на реализацию живёт в bem-bl: https://github.com/bem/bem-bl/issues/333.

Подпредикаты про БЭМ

Сейчас шаблон состоит из перечисления подпредикатов через запятую и тела после двоеточия:

block link, tag, this.ctx.url: 'a'

Есть такие типы подпредикатов: кастомные, про матчинг на входной BEMJSON и про моду (генерация результата).

Прийдётся ввести ключевое слово для кастомных подпредикатов -- match -- оно ведёт себя так же (почти, об этом ниже) как и template в XJST:

match(this.block === 'link', this._mode === 'tag', this.ctx.url)('a')

А ещё можно написать так (цепочкой вызовов):

match(this.block === 'link').match(this._mode === 'tag').match(this.ctx.url)('a')

Теперь для кастомных подпредикатов про БЭМ абсолютно логично пишется:

block('link').tag().match(this.ctx.url)('a')
block('link').mod('pseudo', 'yes').tag().match(this.ctx.url)('a')
Кастомные подпредикаты про элементы

Для того, чтобы шаблоны на блоки не срабатывали при обработке элементов мы используем автоматической добавление кастомного подпредиката !this.elem к тем шаблонам, где нет подпредикатов про элементы. Кастомные подпредикаты про элементы возможно определить при использовании компиляции (заглянув внутрь выражения), поэтому без компиляции нам понадобится отдельный подпредикат elemMatch для записи кастомного подпредиката про элемент.

block('my-block')
    .elemMatch(function() { return this.elem === 'e1' || this.elem === 'e2'  })
        .tag()('span')
Вложенные шаблоны

Сейчас можно писать шаблоны так:

block link {
    tag: 'a'
    attrs: { href: this.ctx.url }
}

и это просто способ не повторять два раза под предикат на блок, в конечном счёте это два шаблона:

block link, tag: 'a'
block link, attrs: { href: this.ctx.url }

Так можно группировать любые подпредикаты, в том числе кастомные:

this.block === 'link' {
    this._mode === 'tag': 'a'
    this._mode === 'attrs': { href: this.ctx.url }
}

Предлагается иметь такой синтаксис:

block('link')(
    tag()('a'),
    attrs()({ href: this.ctx.url })
)

Попутно мы получаем более красивую возможность вкладывать на один уровень тело и подшаблоны. То что раньше записывалось через подпредикат true:

this.ctx.url {
    true: 'a'
    mods not-link yes: 'span'
}

можно будет написать:

match(this.ctx.url)(
    'a',
    mods('not-link', 'yes')('span')
)

Вложенность естественным образом может быть любой глубины:

block('link')(
    tag()('span'),
    match(this.ctx.url)(
        tag()('a'),
        attrs()({ href: this.ctx.url })
    )
)
apply по моде

Сейчас вместо apply(this._mode = 'bla') можно написать просто apply('bla').

Благодаря возможности apply в XJST иметь произвольное количество аргументов для модификации контекста, мы можем иметь такой синтаксис: apply(this)('bla').

applyCtx

Сейчас вместо applyNext(this.ctx = { some: 'new' }) можно написать просто applyCtx({ some: 'new' }).

По аналогии с предыдущим пунктом, можно иметь: applyCtx(this)({ some: 'new' }).

Тут есть небольшой нюанс, т.к. в applyCtx можно также использовать модификацию контекста: applyCtx(this.bla = 1, { some: ['new', 'ctx'] }).

Поскольку в новом синтаксисе для выражения модификации контекста используются объекты, прийдётся договориться, что первый параметр это всегда новый this.ctx: applyCtx(this)({ bla: 1 }, { some: ['new', 'ctx'] }).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.