Страницы

Поиск по вопросам

среда, 27 ноября 2019 г.

Вложенность конструкций (рекурсия). Regexp (регулярные выражения)

#php #регулярные_выражения #mvc


При разработке своего "легковесного" шаблонизатора, ал-я CMF MODx, столкнулся с проблемой
вложенности конструкций.

То, что скармливаем (упрощённая для восприятия конструкция):

/* html-код */
[[SNIPPET_1
    :if
        &is=`var`
        &then=`
               /* html-код */
               [[$CHUNK_1
                :if
                    &is=`var`
                    &then=`[[SNIPPET_2
                            :filter
                                &name=`var`
                           ]]`
               ]]`
        &else=`[[$CHUNK_2:upper]]`
    :filter
        &name=`var`
]]
/* html-код */
[[~LINK_1:abs]]


:if, :filter, :upper... – фильтры (модификаторы), а &is, &name … - переменные фильтров.

Переменные фильтров (&is=`var`), как можно догадаться, должны содержать всё что угодно:
от простой строки до html-кода приправленного переменными шаблона (сниппетами, чанками
и т.д.)

Проблема заключается в том, как закрыть в данном случае [[SNIPPET_1]], при наличии
в нём других переменных шаблона.
Стоить заметить, что [[SNIPPET_1]] имеет два применённых к нему фильтра: :if и :filter.
Это тоже необходимо учитывать.

Было бы чудесно распарсить данную конструкцию, как есть (т.е. учитывать и перевод
строки – удобство восприятия)

Собственно, regexp pattern, который применён в проекте:

preg_replace_callback(
    '/\[{2}([\$\*\@\%\~]?|\+{1,2})([\w-\.]+)\s*((?:\:[\w]+\s*(?:\s*\&[\w]*\=`(?:.[^\n]*)`)*\s*)*)\s*\]{2}/iu',
    function ($call) { },
    $subject
)



Выделяет отдельно название переменной шаблона (SNIPPET_1, CHUNK_1, SNIPPET_2 …),
его тип ("" - сниппет, "$" - чанк, "~" - ссылка …) и фильтры с их содержимым (:if&is=`var`&then=`[[$CHUNK_1]]`
:filter&name=`var`).

В данном случае, [^\n] является заглушкой, т.е. содержимое переменной фильтра пишется
в одну строку без переходов на следующую, чтобы определяет конец переменной фильтра,
а именно: 

&then=`[[$CHUNK_1:if&is=`var`&then=`[[SNIPPET_2:filter&name=`var`]]`]]` 


Согласитесь, не очень читабельно получается.
Далее парсится в массив конструкция фильтров. Определяется имя фильтра (if, filter…)
 и переменные каждого фильтра.
Regexp pattern: 

preg_match_all('/\:([\w]*)((?:\s*\&[\w]*\=`(?:.[^\n]*|)`\s*)*)/iu', $call[3], $found);

И в заключении, цикличный пробег по каждому из фильтров и выполнение функции (соответствует
названию фильтра).
Для примера приведу функцию фильтра :if:

preg_match_all('/\&([\w]*)\=\`((?:.[^\&]*)?(?(?=:).*?\`\]{2}(?:.[^&]*)?|(?:.[^\&\:])?))\`/iu',
$subject, $found);



Коллизии в текущем функционале шаблонизатора:


Повторюсь, содержимое переменной
фильтра пишется в одну строку без
переходов на следующую;
Ошибок не замечено, только при
двумерной вложенности. Лечится,
созданием дополнительного (нового)
чанка с размещением в нём
необходимой конструкции.




Резюмируя: Уважаемые Гуру регулярных выражений, поделитесь опытом, как закрыть конструкцию
при наличии в ней вложенности аналогичных конструкций.

UPDATE:

@ReinRaus Благодарю за ответ. Несмотря на то, что направление куда копать мне подсказал
@VladD (http://php.net/manual/ru/regexp.reference.recursive.php), Вы расписали возможные
подводные камни связанные с данной конструкцией.


Вы правы, есть проблема, т.к. внутри значений атрибутов имеется символ `

Впрочем, если заменить в шаблоне такого рода одинарные кавычки на что-нибудь, что
более похоже на ограничение, к примеру, &is={{…}}, то всё шикарно.   Вот пример:

'/\[{2}([\$\*\@\%\~]?|\+{1,2})([\w-\.]+)((?:\s*\:[\w]+\s*(?:\s*\&[\w]*\=\s*\{{2}\s*(?:[^\{\}]++|(?R))*\}{2})*)*)(?:[^\[\]]++|(?R))*\]{2}/iu'


Выделяется название переменной шаблона ([[имя]]), её тип ([[$...]] – чанк …), а также
перечень фильтров с их содержимым. (:if… :filter…), и так для каждой переменной шаблона.

Не получилось подобрать regexp pattern для замены одинарных кавычек `…` на {{…}}
с учётом \s , потому правку шаблонов придётся производить ручками. Конечно, символ
` сморится гораздо предпочтительнее. Если у Вас есть решение, то буду рад ознакомиться.
Вторая проблема заключается во втором паттерне (внутри callback функции), который
парсит непосредственно фильтры (для каждой переменной шаблона (сниппета, чанка) их
может быть несколько). 

:if
    &is={{var}}
    &then={{
           /* html-код */
           [[$CHUNK_1
            :if
                &is={{var}}
                &then={{[[SNIPPET_2
                        :filter
                            &name={{var}}
                       ]]}}
           ]]}}
    &else={{[[$CHUNK_2:upper]]}}
:filter
    &name={{var}}


Проблема заключается в выделении отдельно взятого фильтра независимо от наличия вложенных
аналогичных конструкций. 

С учётом приведённого выше паттерна, фильтры хранятся в $call[3].
Можно пойти на хитрость и все конструкции {{…}} с их содержимым заменить на что-то иное.

'/\{{2}(?:[^\{\}]++|(?R))*\}{2}/iu'


Далее благополучно распарсить c исключением [^\:]. Ведь конструкция фильтров приобретёт
более простой вид.

:if
  &is={{var_1}}
  &then={{var_2}}
:filter
 &name={{var_3}}


Есть ли возможность обойтись без замены?

    


Ответы

Ответ 1



UPDATE Стер старое сообщение ввиду противоречия обновления ему. Убираем все ограничения ранее наложенные и избавляемся от необходимости замен. Выражением $RE0 сначала выделяете все сниппеты. Потом определяете есть ли у сниппета фильтры, и если есть, то парсите их выражением $RE1. Значения аттрибутов получаете при помощи $RE2 $RE0=<<< REGEX_SNIPPET (?P\\[\\[ # открывающие скобки и именованная группа для рекурсии (?: # скобки для альтернативы (?: # что будет считаться внутренностями сниппета \\\\. | # экранированное что угодно [^\\[\\]] | # не кавычка, или \\[(?!\\[) | # кавычка за которой нет другой кавычки \\](?!\\]) )++ | # или все это выражение снова (?P>RegExpSnippet) )*+ # конец альтернативы, \\]\\]) # закрывающие скобки сниппета, конец именованной группы # ACHTUNG для всех кто решил поизучать это выражение и возможно составлять их в таком же стиле: # всегда в свободной записи делайте один лишний перевод строки в конце выражения # не повторяйте моей ошибки и полчаса убитых на ее поиск REGEX_SNIPPET; $RE2=<<< REGEX_ATTR (?: # что будет внутри аттрибута, этим куском можно выделять значение аттрибута \\\\. | # экранированное что-то [^`\\[] | # не апостроф и не скобка (чтобы не дергать постоянно рекурсию) $RE0 | # или вложенный сниппет \\[ # скобка )++ REGEX_ATTR; $RE1=<<< REGEX_FILTER \\s* # пробельные символы :\\w+ # двоеточие и лат.слово \\s* (?: # для нескольких аттрибутов &\\w+\\s*=\\s*` # амперсанд, слово, равно, апостроф $RE2 `\\s* # апостроф как конец атрибута )+ REGEX_FILTER; preg_match_all("/$RE1/xs", $text, $arr); Хочу выразить свою благодарность ТС: пока работал над его вопросом левелапнулся в своем знании регулярных выражения, и теперь гораздо лучше понимаю как поступает движок регексов в тех или иных ситуациях :)

Ответ 2



Резюме: рекурсивные грамматики нельзя надёжно распарсить регулярными выражениями. Регулярные выражения, к сожалению, не дают возможности парсить рекурсивный код (то есть код со вложенными как угодно глубоко конструкциями). Выразительной силы регулярных выражений не хватает на то, чтобы выразить рекурсивную зависимость. Для вашей грамматики придётся либо писать руками recursive descent parser, либо (гораздо лучше!) выучите lex/yacc, и напишите настоящий "взрослый" парсер. (Скучное объяснение) Дело в том, что множество языков, которые можно распарсить регулярными выражениями -- это как раз множество регулярных языков. Ваш же язык описывается как минимум контекстно-свободной грамматикой, которая не является регулярной. Соответственно она и не может обрабатываться регулярными выражениями. Добавка на смежную тему: https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags (почитайте верхний ответ, это просто произведение искусства!) Апдейт Современные языки включают модифицированную версию регулярных выражений, которая справляется с рекурсивными структурами. Тем не менее, такой парсинг при помощи регулярных выражений известен своей неадекватной сложностью. Кроме того, я не вполне понимаю формальный синтаксис языка: может ли HTML внутри &then содержать, например, или просто [[? (Надеюсь, что нет.)

Ответ 3



Нужен синтаксический парсер: https://stackoverflow.com/questions/2093228/lex-and-yacc-in-php#2093228 https://github.com/jakubkulhan/pacc

Комментариев нет:

Отправить комментарий