Админ

среда, 21 января 2026 г.

Простой парсер HTML-шаблонов (аналог Twig) на Free Pascal / Lazarus

|

Иногда возникает такая задача: нужно отобразить отформатированный текст или какие-то данные, для которых стандартные визуальные компоненты Delphi / Lazarus оказываются не очень хорошо приспособлены, зато было бы удобно использовать HTML. Для отображения самого HTML компоненты есть, но данные в него нужно сначала как-то вставить — не собирать же гипертекст из отдельных кусочков строк при помощи функций вроде Format() и StringReplace()? Для этой цели человечество давно изобрело шаблоны, и я здесь покажу один пример того, как это можно воплотить.

Сначала дам ссылку на репозиторий GitHub, а потом расскажу подробнее: https://github.com/MichaelDemidov/LazSimpleTemplateParser.

В принципе, этой ссылкой можно было бы и ограничиться: там очень подробный файл readme (на английском) с разъяснениями всех аспектов, описанием методов класса и примерами. Поэтому здесь я сделаю немного более общий обзор.

Важные предварительные замечания

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

Класс разработан для Lazarus, для его использования в Delphi придётся переписать часть кода.

Общие сведения

Это не готовый компонент для Lazarus, а отдельный модуль, в котором определён класс SimpleTemplateParser, выполняющий всю работу. Часть кода с мелкими вспомогательными классами вынесена в два файла с расширением inc, чтобы сохранить структуру основного модуля простой и ясной. Кроме того, в репозитории есть незамысловатый демо-проект, который просто загружает содержимое текстового файла и построчно вставляет его в HTML, преобразуя одни строки в заголовки и подзаголовки, другие в текст, в зависимости от их позиции в исходном файле, см. ниже.

Как правило, для отображения HTML я пользуюсь компонентами TurboPower IPro из стандартной поставки Lazarus (они же задействованы в демо-проекте). Они, конечно, не очень удобны, странновато организованы и безнадёжно проигрывают любому браузеру по части поддержки HTML и CSS, зато не тянут за приложением полноценные браузерные библиотеки объёмом в сотни мегабайт, а для отображения простого HTML с разными шрифтами и ссылками их хватает.

Источником вдохновения при разработке синтаксиса шаблонов стал Twig, откуда перекочевали конструкции {{data}}{%if ...%} и т. п. Ещё один момент касается работы с базами данных: поскольку это моя основная сфера деятельности, и класс предназначался для использования в таких приложениях, в синтаксисе шаблонов определены специальные конструкции для вставки данных из БД. Это, конечно, добавляет некоторые зависимости — возможно, ненужные в конкретном проекте. Стоило бы разделить класс на некий общий и его специализированный потомок для работы с БД.

Элементы синтаксиса шаблонов

Для вставки строк в шаблон служат следующие конструкции:

  1. Вставка значения переменной: {{VAR_NAME}}. Имя переменной регистро-зависимое, то есть строчные и заглавные буквы различаются. Значения переменных только строковые, они берутся либо из массива TSimpleTemplateParser.Variables, либо из параметра AVarList метода CreateContent(), либо являются строками входного текстового файла (см. ниже пункт про циклы).
  2. Вставка данных из базы: {{DataSet.FieldName}} или {{DataSet.FieldName??Coalesce string}}. В шаблон подставляется значение указанного поля. Если значение поля null, и присутствует часть после «??», то вместо значения выводится указанная строка Coalesce string. Как правило, вставка значений полей используется внутри цикла по записям, опять же см. ниже.

Для проверки значений используется условный оператор {%if ...%}:

  1. Проверка существования переменной: {%if VAR_NAME%}...{%else%}...{%endif%} или {%if VAR_NAME%}...{%endif%}. Происходит следующее: проверяется, есть ли переменная с таким именем в списке Variables или параметре AVarList процедуры CreateContent(). Если переменная определена, то обрабатывается часть шаблона между {%if…} и {%else%} (или {%endif%}, если ветки {%else%} нет), в противном случае обрабатывается всё, что лежит между {%else%} и {%endif%} (если эта часть присутствует). Важно: парсер не проверяет значение переменной, только факт её существования! Будет ли там пустая строка или что-то ещё, роли не играет.
  2. Проверка текстового файла: {%if Text%}...{%else%}...{%endif%} или {%if Text%}...{%endif%}. Проверяет, упомянут ли текстовый файл в списке TextFiles. Если он там есть, парсер обрабатывает часть шаблона между {%if…} и {%else%} (или {%endif%}), в противном случае между {%else%} и {%endif%} (если она присутствует). Опять же, содержимое и физическое существование файла на диске не проверяются!
  3. Проверка наличия записей в таблице базы данных: {%if Dataset%}...{%else%}...{%endif%} или {%if Dataset%}...{%endif%}. Проверяет набор данных, который ранее был добавлен методом AddDataset(). Здесь уже проверяется именно то, есть ли хотя бы одна строка в наборе, и далее парсится соответствующая часть выражения.
  4. Проверка поля базы данных: {%if Dataset.Field%}...{%else%}...{%endif%} или {%if Dataset.Field%}...{%endif%}. Если поле не пусто (not null), то обрабатывается часть перед {%else%} или {%endif%}. Важно: проверяется только null или не null, значение самого поля роли не играет! Даже если это логическое поле со значением false, всё равно {%if ...%} не будет переходить на ветку {%else%}! (Да, это, возможно, кажется не очень логичным. Возможно, стоило назвать оператор ifdef или как-то ещё.)

Наконец, есть оператор цикла {%for ...%}:

  1. Цикл по текстовому файлу: {%for Text%}...{{Text}}...{%endfor%}. Перебирает поочерёдно все строки текстового файла (здесь Text — это строка с именем в массиве TextFiles) и заменяет переменную {{Text}} на очередную строку. Пустые строки игнорируются. Если переменная {{Text}} внутри цикла встречается несколько раз, то для каждого следующего вхождения берётся новая строка, то есть таким образом можно обрабатывать файл по две, три и т. д. строки. Собственно, в демо-проекте именно так и сделано.
  2. Цикл по набору данных в базе: {%for Dataset%}...{%endfor%}. Аналогичным образом перебирает записи в базе. Для вставки значений полей используется {{DataSet.FieldName}} из пункта 2, для проверки на null — {%if Dataset.Field%} из пункта 6. Комбинация {%if Dataset%} и {%for Dataset%} позволяет, например, вывести какое-то сообщение, если таблица пуста.

Заметим, что условного оператора для проверки строк в текстовом файле по аналогии с пунктом 6 не существует, потому что пустые строки игнорируются. Возможно, это тоже не очень логично, но обработка текстовых файлов была добавлена именно для демо-проекта, в реальных задачах она не использовалась.

Собственно, это всё, что умеет парсер. В принципе, можно расширять его функции, добавляя новые ключевые слова и классы выражений, — там это делается достаточно просто.

Я планирую чуть позднее выложить тут пример более интересного приложения, разработанного с использованием этого класса, а то демо-пример из репозитория действительно скучноват.

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

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

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

К началу