class Volt::SandlebarsParser

Parses html and bindings based on ejohn.org/files/htmlparser.js

takes the html and a handler object that will have the following methods called as each is seen: comment, text, binding, start_tag, end_tag

This is not a full html parser, but should cover most common cases.

Constants

ATTRIBUTES
BLOCK

Types of elements

CLOSE_SELF
EMPTY
END_TAG
FILL_IN_ATTRIBUTES
INLINE
SPECIAL
START_TAG

regex matchers

Public Class Methods

new(html, handler, file_path = nil) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 36
def initialize(html, handler, file_path = nil)
  @html      = StringScanner.new(html)
  @handler   = handler
  @file_path = file_path

  @stack = []

  parse
end
truth_hash(array) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 15
def self.truth_hash(array)
  hash = {}
  array.each { |v| hash[v] = true }

  hash
end

Public Instance Methods

end_tag(tag, tag_name) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 189
def end_tag(tag, tag_name)
  # If no tag name is provided, close all the way up
  new_size = 0

  if tag
    # Find the closest tag that closes.
    (@stack.size - 1).downto(0) do |index|
      if @stack[index] == tag_name
        new_size = index
        break
      end
    end
  end

  if new_size >= 0
    if @handler.respond_to?(:end_tag)
      (@stack.size - 1).downto(new_size) do |index|
        @handler.end_tag(@stack[index])
      end
    end

    @stack = @stack[0...new_size]
  end
end
last() click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 46
def last
  @stack.last
end
parse() click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 50
def parse
  loop do
    if last && SPECIAL[last]
      # In a script or style tag, just look for the first end
      close_tag = "</#{last}>"
      body      = @html.scan_until(/#{close_tag}/)
      special_tag(close_tag, body)
    elsif @html.scan(/\<\!--/)
      # start comment
      comment = @html.scan_until(/--\>/)
      comment = comment[0..-4]

      @handler.comment(comment) if @handler.respond_to?(:comment)
    elsif (tag = @html.scan(START_TAG))
      tag_name = @html[1]
      rest     = @html[2]
      unary    = @html[3]

      start_tag(tag, tag_name, rest, unary)
    elsif @html.scan(END_TAG)
      tag_name = @html[1]

      end_tag(tag_name, tag_name)
    elsif (escaped = @html.scan(/\{\{\{(.*?)\}\}\}([^\}]|$)/))
      # Anything between {{{ and }}} is escaped and not processed (treaded as text)
      if escaped[-1] != '}'
        # Move back if we matched a new non } for close, skip if we hit the end
        @html.pos = @html.pos - 1
      end

      text(@html[1])
    elsif (binding = @html.scan(/\{\{/))
      # We are in text mode and matched the start of a binding
      start_binding
    elsif (text = @html.scan(/\{/))
      # A single { outside of a binding
      text(text)
    elsif (text = @html.scan(/(?:[^\<\{]+)/))
      # matched text up until the next html tag
      text(text)
    else
      # Nothing left
      break
    end
  end

  end_tag(nil, nil)
end
raise_parse_error(error) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 135
def raise_parse_error(error)
  line_number = @html.pre_match.count("\n") + 1

  error_str = error + " on line: #{line_number}"
  error_str += " of #{@file_path}" if @file_path

  fail HTMLParseError, error_str
end
special_tag(close_tag, body) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 214
def special_tag(close_tag, body)
  body = body[0..((-1 * close_tag.size) - 1)]

  body = body.gsub(/\<\!--(.*?)--\>/, '\\1').gsub(/\<\!\[CDATA\[(.*?)\]\]\>/, '\\1')

  text(body)

  end_tag(last, last)
end
start_binding() click to toggle source

Findings the end of a binding

# File lib/volt/server/html_parser/sandlebars_parser.rb, line 104
def start_binding
  binding    = ''
  open_count = 1

  # scan until we reach a {{ or }}
  loop do
    binding << @html.scan_until(/(\{\{|\}\}|\n|\Z)/)

    match = @html[1]
    if match == '}}'
      # close
      open_count -= 1
      break if open_count == 0
    elsif match == '{{'
      # open more
      open_count += 1
    elsif match == "\n" || @html.eos?
      # Starting new tag, should be closed before this
      # or end of doc before closed binding
      raise_parse_error("unclosed binding: {#{binding.strip}")
    else
      #:nocov:
      fail 'should not reach here'
      #:nocov:
    end
  end

  binding = binding[0..-3]
  @handler.binding(binding) if @handler.respond_to?(:binding)
end
start_tag(tag, tag_name, rest, unary) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 144
def start_tag(tag, tag_name, rest, unary)
  section_tag = tag_name[0] == ':' && tag_name[1] =~ /[A-Z]/

  if section_tag
    tag_name = tag_name.underscore
  else
    tag_name = tag_name.downcase
  end

  # handle doctype so we get it output exactly the same way
  if tag_name == '!doctype'
    @handler.text(tag) if @handler.respond_to?(:start_tag)
    return
  end

  # Some tags close themselves when a new one of themselves is reached.
  # ex, a tr will close the previous tr
  end_tag(nil, tag_name) if CLOSE_SELF[tag_name] && last == tag_name

  unary = EMPTY[tag_name] || !unary.blank?

  # Section tag's are also unary
  @stack.push(tag_name) unless unary || section_tag

  if @handler.respond_to?(:start_tag)
    attributes = {}

    # Take the rest string and extract the attributes, filling in any
    # "fill in" attribute values if not provided.
    rest.scan(ATTRIBUTES).each do |match|
      name = match[0]

      value = match[1] || match[2] || match[3] || FILL_IN_ATTRIBUTES[name] || ''

      attributes[name] = value
    end

    if section_tag
      @handler.start_section(tag_name, attributes, unary)
    else
      @handler.start_tag(tag_name, attributes, unary)
    end
  end
end
text(text) click to toggle source
# File lib/volt/server/html_parser/sandlebars_parser.rb, line 99
def text(text)
  @handler.text(text) if @handler.respond_to?(:text)
end