Хочется сразу заметить, что название статьи никак не отражает её истинного содержания, однако является хорошим отражением того, чем мы будем тут сегодня заниматься. Чем же? Сегодня у нас воскресенье, поэтому мы можем себе позволить написать какой-нибудь код не ради практической полезности, а ради забавы (хотя не исключаю, что он таки нам сможет пригодиться в будущем). Ах да, ещё сегодня день Ruby.
Итак, вкратце о задаче: нам нужна максимально-детерминированная и структурированная в плане внешнего вида система, которая могла бы описать (X)HTML с возможностью саморасширяемости до шаблонизатора или иных других средств (по желанию автора). Приступим, наконец.
Всемогущий, о, CSS!
Смотрю на CSS и думаю: ну просто идеальная система, как для парсинга, так и для хранения каких-нибудь метаданных. Если хорошенько посмотреть на любой набор CSS-правил или открыть любой CSS-файл (главное, чтобы он не был пустым), то можно заметить лишь два состояния, присутствующие в грамматической составляющей CSS: лексемы-определители (названия определённых свойств) и блоки (вместе с их содержанием). Вот, посмотрите сами:
div.footer /* <--- Наша лексема-определитель */
{ /* <--- Начало блока */
color: #777;
font-size: 0.85em; /* <--- CSS-свойство */
/* ... */
} /* <--- Конец блока */
Всмотритесь: у этого кода всего два состояния: вне-блока и в-блоке. Всё получается просто замечательно! Тогда давайте представим себе, раз эта система является более-менее приемлемой для построения систем, что мы можем на её основе сделать кое-что другое, более интересное
Представили? Поехали!
Дескриптивный HTML
Первый вопрос самому себе: почему HTML назван дескриптивным? Ответ заключается в том, что подобный синтаксис HTML-содержимого более опрятно. Подобная система более логично отображает структуру документа в целом и отдельных его блоков в частности. Самому уже не терпится показать пример, но мы пойдём несколько другим путём и для начала посмотрим на обычный HTML.
<html>
<head>
<title>
Заголовок страницы
</title>
</head>
<body>
<h2>
Заголовок
</h2>
<div class="default" id="personal">
<p class="first">
Первый параграф с текстом
</p>
<p>
Второй параграф с текстом
</p>
</div>
</body>
</html>
Никогда не пренебрегайте DOCTYPE-декларациями
и xmlns-сущностями в документе.
Я опущу на этот раз различные DOCTYPE-декларации и детерминированные свойства xmlns, да простят меня Всемогущие Спецификации. Посмотрите на представленный HTML: большое количество метаинформации сгущает краски при чтении подобной разметки, поэтому я за то, чтобы хотя бы в сегодняшний шутливый воскресный вечер упростить её до подобного формата:
html
{
head
{
title
{
#text { Заголовок страницы }
}
}
body
{
h2
{
#text{ Заголовок }
}
div
{
#attributes
{
class: default;
id: personal;
}
p
{
#attributes
{
class: first;
}
#text { Первый параграф с текстом }
}
p
{
#text { Второй параграф с текстом }
}
}
}
}
Если вы подумаете о модульности и рясширяемости
своего проекта в первые часы его жизни, то
в дальнейшем вам будет намного легче.
Заметьте, как стало свободно дышать в этом лёгком документе! Заметьте, как TAB`ы сами создают определённую струткуру вложенности документа (чуть не сказал, эффект присутствия) и как явно среди всего этого выделяются метаданные документа, которые начинаются со знака # в лексемах. Пусть приходится писать метаданные отдельно, зато чувствуется непосредственная структуризация документа, его чёткость и… Лёгкость, как бы странно это не могло прозвучать.
И, что очень важно, следите за потенциалом: для расширения до шаблонизатора нужно вводить лишь новые метаблоки (#-блоки), и писать для них соответствующие обработчики.
Могучий Ruby
Для обработки подобных документов нам понадобится особый класс тега, который будет одновременно и типом данных и хранилищем данных:
module Lucie # Так называется наша система
#Класс тега
class LTag
#Процедура инициализации
def initialize name, type
#Родитель нашего тега
@parent = false
#Содержимое тега
@content = ""
#Массив атрибутов
@attributes = {}
#Массив десцедантов (нисходящих элементов)
@children = []
#Счётчик нисходящих элементов
@children_count = 0
#Тип элемента
# :default для обычного тега
# :attribute для определения атрибутов
# :text для текстового содержимого (CDATA?)
@type = type
#Название тега
@name = name
end
end
end
Вот такое вот получилось воплощение мысли. Теперь расширим его для работы с соседними тегами, добавим перед методом инициализации определители уровня доступа к свойствам класса:
#Свойства на чтение и запись
attr_accessor :parent, :name, :type, :content, :attributes
#Ай-ай-ай! Свойство только для чтения
attr_reader :children
Добавим функции проверки существования необходимого содержимого:
#Есть ли атрибуты у тега?
def has_attributes?
return (@attributes.length != 0)
end
#Наполнен ли тег содержимым?
def has_content?
return (@content != "")
end
Сделаем внешнюю функцию для добавления нового нисходящего элемента (потомка данного тега):
#Новый потомок
def add_child child
#Тип тега (узла)
case child.name
when "#attributes"
child.type = :attribute
when "#text"
child.type = :text
end
#Банальное увеличение числа элементов-потомков
@children_count = @children_count + 1
#Устанавливаем родителя нашего элемента (элементарно, Ватсон!)
child.parent = self
#Добавляем в коллекцию потомков
@children << child
#Возвращаем обработанное дитя
child
end
И ещё одну функцию обратного вызова для обработки содержимого тега при добавлении последнего:
#Добавление контента
def process_content content
if self.type == :text
#Обычный текст, и ничего лишнего!
self.parent.content = content
elsif self.type == :attribute
#Для атрибутов необходима специальная обработка
attributes = content.split ";";
attributes.each do |attr|
attr = attr.split ":"
self.parent.attributes[attr.shift.strip] = (attr.join ":").strip
end
end
end
Рассмотрим подробно разбор атрибутов (свойств). В общем виде их можно представить так:
имя_свойства1 [разделитель] содержимое свойства [терминатор]
имя_свойства2 [разделитель] содержимое_свойства [терминатор]
В нашем случае терминатором является не Арнольд Шварценеггер , а точка с запятой, а разделителем — двоеточие. Таким образом мы получили CSS-подобный синтаксис для набора свойств.
Идём дальше по лестнице вверх и приходим к тому. Что теперь нам необходим сам парсер-обработчик (хотя это было логично с самого начала, правда?), поэтому создаём его базис:
#Подключаем наш тип данных, предполагая, что он в дочерней директории types
require "types/ltag"
module Lucie #Заключаем его в тот же модуль
#Класс генерала Парсера
#Всмысле, главный класс обработчика
class LucieParser
#Инициализация базовых свойств
def initialize filepath
#Создаём корень нашего деревообразного документа
#Он не участвует в выводе, но вскоре его можно заменить DOCTYPE-декларацией
@root = Lucie::LTag.new "document", :document
#Читаем файл и записываем его содержимое в переменную
#Знаю, что плохо, но мы ведь просто занимаемся воскресными баловнями?
@file_contents = File.read filepath
#Вызываем функцию-обработчик документа
self.parse_file
#Выводим обычный, обработанный HTML на основе нашего дерева
self.show_tree @root
end
end
end
Вот! Уже и фонариком светить не надо: всё как на ладони, особенно то, что мы не написали две важные функции. Зашпатлюем этот пробел, начав с функции parse_file (ядро нашего парсера):
#Обработка документа
def parse_file
#Состояние парсера
# :section_literal, когда мы находимся в литерале
# :section_content, когда в содержимом мы находимся
parser_state = :section_literal
#Это чистый литерал?
#Чистым литералом является обрамлённая двойными кавычками строка
is_literal = false
#Буфер для имени блока (литерала)
section_literal = ""
#Буфер для содержимого блока
section_content = ""
#Текущий элемент; в начеле парсинга это корень документа
current_element = @root
#Предыдущий символ
previous_char = false
#Вперёд, мои кони!
@file_contents.each_byte do |char|
#Получаем текущий символ
char = char.chr
#Это предполагаемый литерал?
if char == "\"" and previous_char != ""
if is_literal == false
is_literal = true
else
is_literal = false
end
end
#Это блок?
if char == "{" and !is_literal
#Изменяем состояние обработки
parser_state = :section_content
#Создаём нового потомка
new_child = Lucie::LTag.new section_literal.strip, :default
current_element.add_child new_child
current_element = new_child
#Обнуляем содержимое блока и литерала
section_literal = ""
section_content = ""
#Пропускаем остальные операции
next
end
#Блок заканчивается?
if char == "}"
#Изменяем состояние обработки
parser_state = :section_literal
#Устанавливаем содержимое через функцию обратного вызова
current_element.process_content section_content.strip
#Текущим элементом должен стать родитель текущего элемента (нет, не рекурсия)
current_element = current_element.parent
#Обнуляем всё на свете
section_literal = ""
section_content = ""
#Пропускаем дальнейшие операции
next
end
#Получаем содержимое блока
if parser_state == :section_content
section_content += char
end
#Устанавливаем литерал
section_literal += char
#Запоминаем предыдущый сивмол
previous_char = char
end
end
Обработчик написан, но нам нужна ещё одна функция, а именно: функция для вывода дерева в качестве HTML-кода:
#Отображение дерева
#В качестве аргументтов: стартовый элемент и внутренная переменная level
#Вот теперь и можно сказать: рекурсия!
def show_tree element, level = 0
#Для каждого элемента-дитяти
element.children.each do |child|
#Если это необычный элемент, мы не останавливаемся на нём
if child.type != :default
next
end
#Отступ, для красивости
v_string = " " * level
print v_string + "<" + child.name
#Если у нас есть атрибуты, выводим их
if child.has_attributes?
child.attributes.each do |aname, aval|
print " " + aname + "="" +aval + """
end
end
#Закрываем тег
print ">"
puts
#Если у нас есть содержимое, выводим его
if child.has_content?
puts v_string + child.content
end
#Если у нас есть потомки, выводим каждого, заново вызывая эту же самую функцию
if child.children.length != 0
#Увеличиваем уровень отступа
level = level+1
self.show_tree child, level
#Уменьшаем обратно
level = level-1
end
#Выводим окончание тега
puts v_string + "</" + child.name + ">"
end
end
Наконец, создадим последнюю функцию для вывода дерева, которое будет отражать саму структуру документа:
#Выводим обычное дерево
def explain_tree element, level = 0
#Делаем для каждого потомка
element.children.each do |child|
#Выводим имя текущего элемента
v_string = "--" * level
puts v_string + child.name
#У нас есть потомки?
if child.children.length != 0
#Увеличиваем уровень
level = level+1
#Рекурсивно вызываем нашу функцию
self.explain_tree child, level
#Возвращаем уровень на место
level = level-1
end
end
end
Её вывод похож для нашего примера будет таким:
html
--head
----title
------#text
--body
----h2
------#text
----div
------#attributes
------p
--------#attributes
--------#text
------p
--------#text
Вот и всё программирование на сегодняшний день. Осталось подвести краткий итог.
Силы разума и бездумья
Всё в ваших руках: даже маленькие
радости могут оказаться полезными для вас.
Наши сегодняшние отягощения всё-таки не прошли даром! Теперь мы знаем, как устроен CSS, что такое дескриптивный HTML, как его создавать и как его обрабатывать. Конечно, это всего-лишь баловство, но оно является очень полезным для нашего собственного разума. Плюсом ко всему, если вам понравилась задумка статей «Забавы ради, пользы для», то можно её воплощать каждое воскресенье. Как вам? А мне остаётся вам пожелать валидных и семантичных документов.