Забавы ради, пользы для

15 июня 2008 года, 15:41

Хочется сразу заметить, что название статьи никак не отражает её истинного содержания, однако является хорошим отражением того, чем мы будем тут сегодня заниматься. Чем же? Сегодня у нас воскресенье, поэтому мы можем себе позволить написать какой-нибудь код не ради практической полезности, а ради забавы (хотя не исключаю, что он таки нам сможет пригодиться в будущем). Ах да, ещё сегодня день 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, как его создавать и как его обрабатывать. Конечно, это всего-лишь баловство, но оно является очень полезным для нашего собственного разума. Плюсом ко всему, если вам понравилась задумка статей «Забавы ради, пользы для», то можно её воплощать каждое воскресенье. Как вам? А мне остаётся вам пожелать валидных и семантичных документов.

Мнения (10)

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

  • Curly Brace

    15 июня 2008 г.17:18

    Да! :) Мне нравятся твои изыски, Дин.

    Если исходить из утверждения что «Сон разума рождает чудовищ», то ты — бесплоден! :)))

    З.Ы. Проверка типографа:

    ОЯЕБУ!!!

  • гриззли

    16 июня 2008 г.00:47

    >если вам понравилась задумка статей «Забавы ради, пользы для»

    Да, интересно. Хоть, конечно, та часть, что из руби, и оказалась гораздо менее понятна :) но зато «теперь мы знаем, как устроен CSS, что такое дескриптивный HTML» ;)

  • Дин автор

    16 июня 2008 г.00:56

    Я не специально, правда! ;-) По крайней мере, обработчик написать уже сможем.

    Что касается Ruby, думаешь не стоило на нём писать пример? Я просто на PHP не хочу этого делать, а Ruby показался для этой цели мне вполне подходящим языком.

    Хотя, я к любому языку даю обильные комментарии, типа того «что делает та или иная строка кода».

  • Sannis

    18 июня 2008 г.14:20

    Давно хотел спросить: а какие способы шаблонизации традиционно применяются в Ruby(oR)? Используются ли активные шаблоны, или ввиду быстроты системы довольствуются пассивными?

  • Дин автор

    18 июня 2008 г.18:51

    В RubyOnRails может быть и используются какие-то шаблонизаторы (может быть, даже сторонние), но я о них ничего не знаю, оттого, что работаю с обычным, «plain», как я говорю, Ruby.

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

  • Sannis

    18 июня 2008 г.23:40

    С Ruby я не знаком, так что не могу поспорить :) Но, скажем, в PHP на скорости генерации и, следовательно, на загрузке сервера, выбор типа шаблонов(активные/пассивные) влияет достаточно ощутимо, если сервер не свой. Какова ситуация с Ruby(мои поверхностные знания говорят, что он компилируемый)? Спрашиваю я это в том ключе, что меня всем устраивает на данный момент PHP, но с собственной удобной и быстрой реализацией этой части сайта у меня пока что возникают сложности. Вот и решил немного облегчить себе жизнь, возьмя в арсенал Ruby.

  • Дин автор

    18 июня 2008 г.23:52

    Ruby такой же интерпретируемый (не будем сегодня считать YAML, который будет в следующей версии языка интегрирован в его ядро и позволит компилировать Ruby-код в байт-код, что, в теории, увеличит производительность кода), как и PHP.

    С точки зрения шаблонизации — я не сравнивал скорость работы разных шаблонизаторов, но логика мне подсказывает, что зависимость, конечно же, будет такая же. Разные шаблоны реализуются совершенно по-разному и можно сказать с абсолютной уверенностью о том, что их скорость работы будет различаться в различных формализованных внешних условиях среды (конфигурация сервера и иные вопросы).

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

    Хотя, если сравнить с RubyOnRails, думаю, то PHP будет чуточку быстрее (за это я не люблю этих железнодорожников-диспетчеров).

  • Sannis

    20 июня 2008 г.03:52

    Спасибо за ответ. Жаль, отпала одна причина изучать :) Но книжкой всё-таки обзавёлся, надеюсь никто на учёбе не будет против, если я буду серверные скрипты на нём писать.

  • Максим

    31 июля 2008 г.22:35

    Дин, но почему ты не используешь эту технику в Веб Зайне?

  • Дин автор

    31 июля 2008 г.22:41

    Это же всего-лишь забавы, @Максим. :-)

    Тем паче, если бы я их стал использовать в блоге, то я бы делал это на стороне сервера, и никто бы ничего внешне не заметил. :-)

Я тоже знаю!

Для обращения к человеку используйте символ @, после которого следует имя того, к кому обращаетесь (пробелы заменяются на знак подчёркивания). Если вам интересно, можете подписаться на комментарии по RSS или по эл. почте. Ведите себя достойно, вы же не роботы, правда?

Вы можете использовать следующие XHTML-элементы в разметке комментария: strong, em, span[class=crossline], a[href=uri], code[type=язык], blockquote, ul и ol. В качестве языка кода может быть указан, например, javascript или css.