Статьи

Code Safari: начало работы в HAML

С недавним выпуском HAML 3.1 я решил углубиться в его глубины, чтобы выяснить, что заставляет его работать. Какие звери прячутся в недрах шаблонной системы?

HAML — это язык шаблонов, который позволяет вам писать HTML, используя краткий синтаксис:

%article %h1 My great article %p Here is the text of my article 

Который компилируется в:

 <article> <h1>My great article</h1> <p>Here is the text of my article</p> </article> 

Это допускает некоторые изящные вещи, такие как встроенные блоки Ruby, которые закрыты значительным пробелом. Нет сомнений, что в рукаве есть интересные трюки.

Поехали на сафари.

Время сафари

Как всегда, начните с получения кода:

 git clone git://github.com/nex3/haml 

Я рекомендую вам прочитать его вместе с этой статьей.

Есть два места, которые я всегда начинаю при исследовании библиотеки: README и главное требование. К сожалению, в большинстве библиотек нет руководства по погружению в код в README, но это не помешает. Для HAML мы находим очень хорошую пользовательскую документацию, но ничто не указывает нам правильное направление. Это нормально, так как нас приветствует очень хороший комментарий в lib/haml.rb который вызывает у меня улыбку:

 # lib/haml.rb # The module that contains everything Haml-related: # # * {Haml::Engine} is the class used to render Haml within Ruby code. # * {Haml::Helpers} contains Ruby helpers available within Haml templates. # * {Haml::Template} interfaces with web frameworks (Rails in particular). # * {Haml::Error} is raised when Haml encounters an error. # * {Haml::HTML} handles conversion of HTML to Haml. 

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

Похоже, Haml::Engine станет денежным билетом, и, открыв lib/haml/engine.rb нас приветствует еще один комментарий, в котором выплачивается джекпот.

 # This is the frontend for using Haml programmatically. # It can be directly used by the user by creating a # new instance and calling {#render} to render the template. # For example: # # template = File.read('templates/really_cool_template.haml') # haml_engine = Haml::Engine.new(template) # output = haml_engine.render # puts output 

Давайте поиграем дома с irb и подтвердим, что предложенный синтаксис действительно работает. Lauch irb из каталога HAML. -I флаг, который добавляет каталог к ​​пути загрузки.

 $ irb -Ilib irb> require 'haml' irb> Haml::Engine.new("%b hello").render => "<b>hello</b>" 

Найдите «def initialize» в lib/haml/engine.rb чтобы найти нашу точку входа. Здесь много строк, уловка эффективного чтения при попытке понять суть библиотеки заключается в том, чтобы быстро пропустить код, который не важен для понимания сути программы. Обычно это означает пропуск пропусков и поиск вызовов методов. Я также часто работаю снизу вверх, начиная с возвращаемого значения. Обычно методы структурированы setup-action-return, и на данный момент нас интересуют последние два. Большая часть #initialize — это настройка переменных, но ближе к концу вы найдете очень интересную строку:

 # lib/haml/engine.rb:124 compile(parse) 

Наше первое понимание! Казалось бы, HAML отделяет разбор документа от компиляции до HTML. Это стандартная техника, разделяющая две очень разные проблемы.

анализ

Синтаксический анализ — это процесс получения представления (в данном случае нашего шаблона HAML) и подготовки его для вывода в другое представление (HTML). Вы можете найти код синтаксического анализа в lib/haml/parser.rb , либо lib/haml/parser.rb поиск по проекту «def parse», либо заметив включение Parser в верхней части Haml::Engine . Начиная с нижней части метода, мы видим, что он возвращает переменную экземпляра @root . Это удобно — поскольку Parser включен как модуль в класс Engine , мы должны иметь возможность легко проверить эту переменную экземпляра. Мы можем использовать метод instance_eval для оценки кода в контексте любого объекта, предоставляя нам доступ даже к закрытым методам и переменным экземпляра. Это действительно плохая идея для производственного кода, но это отличный инструмент для исследования.

 irb> input = "%article ... sample from above ..." irb> Haml::Engine.new(input).instance_eval { @root } => (root nil (tag {:name=>"article", :value=>nil} (tag {:name=>"h1", :value=>"My great article"}) (tag {:name=>"p", :value=>nil} (plain {:text=>"Here is the text of"}) (plain {:text=>"my article"}))) (haml_comment {:text=>""})) irb> Haml::Engine.new(input).instance_eval { @root }.class => Haml::Parser::ParseNode irb> Haml::Engine.new(input).instance_eval { @root }.children.map(&amp;:class) => [Haml::Parser::ParseNode, Haml::Parser::ParseNode] # (I edited out some extra values from the hashes for clarity.) 

Метод parse создает дерево Haml::Parser::ParseNode , создавая абстрактное представление нашего документа. Другими словами, это представление не связано с тем, что наш ввод был строкой. Это отделяет синтаксис HAML от выходных данных, что приводит к лучшей архитектуре. Обратите внимание, что всегда есть один специальный корневой узел, к которому нужно присоединить остальную часть дерева.

Давайте углубимся в разбор немного больше. Сканируя метод parse , мы получаем следующую базовую структуру:

 while next_line process_indent # decrease nesting if needed process_line if block_opened? increase nesting end end close open tags 

Здесь есть две основные функции: обработка отступов и разбор строки. Я сосредоточусь на последнем здесь и оставлю чтение кода отступа в качестве упражнения для вас (см. Конец статьи). Еще раз, я возьму скелетное представление process_line :

 case first_char_of_line when '%'; push tag(text) when '.'; push div(text) # ... other cases else; push plain(text) end 

Методы tag , div и plain ParseNode и возвращают объекты ParseNode , а push добавляет узел к ParseNode текущего узла.

Делая наши собственные

Теперь у нас достаточно представления о том, как работает синтаксический анализ HAML, чтобы попытаться составить сценарий самостоятельно. Это помогает подтвердить, что мы правильно прочитали код, а также закрепить все полученные знания. Давайте создадим простой синтаксический анализатор, который сможет преобразовать наш образец документа сверху в дерево узлов, начиная с простого случая, игнорируя отступы.

 require 'test/unit' class HamlParserTest < Test::Unit::TestCase def test_one_line_plain tree = HamlParser.new("hello").parse assert_equal 1, tree.children.size assert_equal :plain, tree.children[0].type assert_equal 'hello', tree.children[0].data[:value] end def test_one_line_tag_with_value tree = HamlParser.new("%em hello").parse assert_equal 1, tree.children.size assert_equal :tag, tree.children[0].type assert_equal 'em', tree.children[0].data[:name] assert_equal 'hello', tree.children[0].data[:value] end end class HamlParser class Node < Struct.new(:type, :data) attr_accessor :children attr_accessor :parent # Used in next example def initialize(*args) super self.children = [] end end def initialize(string) @string = string end def parse @root = Node.new(:root, {}) @root.children = @string.lines.map do |line| parse_line(line) end @root end def parse_line(line) case line[0] when ?% name, value = line[1..-1].split(' ') Node.new(:tag, :name => name, :value => value) else Node.new(:plain, :value => line) end end end 

Test::Unit — это среда модульного тестирования, представленная в стандартной библиотеке Ruby. Если вы запустите этот файл, вы увидите, что он автоматически запускает указанные тесты. Это отличный способ быстро создать небольшой проект, подобный этому. Я сформировал код аналогично коду HAML, с методом parse_line который включает первый символ строки, и корневым узлом для хранения дерева.

Чтобы поддерживать отступ, нам нужно настроить синтаксический анализатор так, чтобы у него была концепция текущего узла, к которому добавляются дочерние элементы (вместо того, чтобы всегда добавлять к корню в соответствии с нашим первым примером), а также текущей глубины. Чтобы облегчить это, мы добавим parent метод доступа к узлам, чтобы мы могли проходить как вниз, так и вверх по дереву. Эта версия на самом деле немного проще, чем код HAML, но на данный момент она выполняет свою работу.

 require 'test/unit' class HamlParser < Test::Unit::TestCase def test_tag_with_nested_value tree = HamlParser.new("%em hello").parse assert<em>equal 1, tree.children.size assert</em>equal :tag, tree.children[0].type assert<em>equal 'em', tree.children[0].data[:name] assert</em>equal 'hello', tree.children[0].children[0].data[:value] end end 
 class HamlParser # Node and initialize as above def parse @root = Node.new(:root, {}) @parent = @root @depth = 0 @string.lines.each do |line| process_indent(line) push parse_line(line.strip) end @root end def process_indent(line) indent = line[/^s+/].to_s.length / 2 if indent > @depth @parent = @parent.children.last @depth = indent end end def push(node) @parent.children << node node.parent = @parent end def parse_line(line) # ... as above end end 

Это хорошее начало, и оно анализирует наш исходный пример кода, но есть еще много чего сделать:

  • Исправьте process_indent в нашем примере, чтобы он также корректно «отступал».
  • Трудно визуализировать наш вывод синтаксического анализатора, потому что стандартная реализация Ruby inspect не включает дочерние элементы узла. Переопределите Node#inspect чтобы обеспечить хороший вывод, как это делает HAML.
  • Парсер HAML фактически отслеживает две строки одновременно, а не одну, как это делает наш парсер. Прочитайте код HAML, чтобы найти примеры того, где это полезно.

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

Наслаждайтесь этой статьей и есть что сказать? RubySource в настоящее время ищет постоянных авторов Ruby для оплачиваемой работы, посетите страницу « Напишите нам» и свяжитесь с нами.