class Bytewise::Parser

Constants

ALLOW_ESCAPE_FOR
ESCAPE_CHAR
FILTER_DIVIDER
OPENING_BRACKETS
RAW_BRACKETS
VARIABLE_REGEX

Attributes

lexer[R]

Public Class Methods

parse(input, options = {}) click to toggle source
# File lib/brace_markup/parser.rb, line 14
def self.parse(input, options = {})
  self.new.parse(input, options)
end

Public Instance Methods

apply_transaction() click to toggle source

Removes the last stored lexer (apply transaction)

# File lib/brace_markup/parser.rb, line 52
def apply_transaction
  @lexers.pop
  @lexer
end
begin_transaction() click to toggle source

Copys the lexer for later recover

# File lib/brace_markup/parser.rb, line 35
def begin_transaction
  # Store a copy of the real lexer
  @lexers.push(@lexer.clone)
end
parse(input, options = {}) click to toggle source

Starts the parsing of the input

@param { Hash } options @return { Bytewyse::Ast::Body }

# File lib/brace_markup/parser.rb, line 24
def parse(input, options = {})
  @lexer = Lexer.new input
  # Holds a 'safe copy' of the lexers when starting a transaction
  @lexers = []

  parse_body
end
parse_arguments() click to toggle source

Parses the arguments of a tag (also the parts of a hash)

# File lib/brace_markup/parser.rb, line 299
def parse_arguments
  args = {}

  loop do
    name = parse_tag_name

    break if name == ''

    if @lexer.peek[:chunk] == ':'
      @lexer.next

      value = parse_value
      value = value == '' ? nil : value
    else
      # Invalid
      raise Exception.new('Invalid parameters')
    end

    args[name.to_sym] = value

    if @lexer.peek[:chunk] == ','
      @lexer.next
    else
      break
    end
  end

  args
end
parse_array() click to toggle source

Parses an array

# File lib/brace_markup/parser.rb, line 412
def parse_array
  raise Exception.new('An array begins with a \'[\'') unless @lexer.next == '['

  arr = []

  loop do
    arr.push(parse_value)

    if @lexer.peek[:chunk] == ','
      @lexer.next
    else
      break
    end
  end

  unless @lexer.next == ']'
    # Todo: use some sort of an #error method
    raise Exception.new('Couldn\'t find a closing brace for the array')
  end

  arr
end
parse_body(allow_close_tag = false) click to toggle source

Parses the body with subnodes

@param { Boolean } allow_close_tag

# File lib/brace_markup/parser.rb, line 62
def parse_body(allow_close_tag = false)
  body = Bytewise::Ast::Body.new
  chunk = ''

  loop do
    if @lexer.peek(length: 2, skip_whitespace: false)[:chunk] == OPENING_BRACKETS
      begin
        body << parse_brackets
      rescue CloseTagException => e
        # Catches closing tags and stops body parsing
        unless allow_close_tag
          raise Exception.new("Wasn't expecting a closing tag #{@lexer.line_and_col}")
        end

        body
        break
      rescue TagSectionException => e
        # Catches and reraises it
        e.body = body
        raise e
      end
    else
      if @lexer.peek(skip_whitespace: false)[:chunk].nil?
        if allow_close_tag
          raise ::BraceMarkup::UnexpectedEndException.new("Unexpected End. Was waiting for a closing tag.")
        else
          break
        end
      else
        body << @lexer.next(skip_whitespace: false).to_s
      end
    end
  end

  body
end
parse_bracket_value() click to toggle source
# File lib/brace_markup/parser.rb, line 352
def parse_bracket_value
  # Find out the type of the block
  tag_type = @lexer.peek(offset: 2)[:chunk]

  if tag_type =~ VARIABLE_REGEX
    # Hash or Reference
    begin_transaction

    @lexer.next
    # Parse the name to see if it is a key
    parse_tag_name

    if @lexer.peek[:chunk] == ':'
      rollback_transaction
      parse_hash
    else
      # Its not a hash, It's a reference
      rollback_transaction
      @lexer.next
      parse_reference context: :in_tag
    end
  else
    # Is a tag
    tag_type = tag_type.to_sym

    parse_tag_value type: tag_type
  end
end
parse_brackets() click to toggle source

Tries to parse matching brackets e.g {{name}} or {{@func param: 4}}

Note: The closing brackets are parsed by parse_tag and parse_reference for the reason that a reference can begin with three braces.

# File lib/brace_markup/parser.rb, line 126
def parse_brackets
  raw = false

  if @lexer.peek(length: 3)[:chunk] == RAW_BRACKETS
    # This is a raw variable
    raw = true
    @lexer.next
  end

  if @lexer.peek(length: 2)[:chunk] == OPENING_BRACKETS
    @lexer.next length: 2

    # Find out the type of the block
    tag_type = @lexer.peek[:chunk]

    if tag_type =~ VARIABLE_REGEX
      parse_reference context: (raw ? :raw : :default)
    elsif tag_type == '('
      # Expression
      expr = parse_expression
      @lexer.expect '}}', raise_error: true

      expr
    else
      # Is a tag
      tag_type = tag_type.to_sym
      @lexer.next

      parse_tag type: tag_type
    end
  else
    # Todo: Error
  end
end
parse_comment_body() click to toggle source
# File lib/brace_markup/parser.rb, line 99
def parse_comment_body
  buf = ''

  loop do
    if @lexer.expect '{{/!}}', skip_whitespace: false
      break
    end

    curr = @lexer.next skip_whitespace: false

    if curr.nil?
      raise ::BraceMarkup::UnexpectedEndException.new("Unexpected End. Was waiting for a closing tag.")
    end

    buf += curr
  end

  buf
end
parse_const() click to toggle source

Parses a constant (nil and numbers)

# File lib/brace_markup/parser.rb, line 470
def parse_const
  # Test for nil
  if @lexer.expect 'nil'
    # Returning own "nil" class, in order to be able to test if
    # the return value of this method is nil.
    ::Bytewise::Ast::Chars::Nil.new
  elsif @lexer.expect 'true'
    true
  elsif @lexer.expect 'false'
    false
  else
    parse_numerical
  end
end
parse_end_tag() click to toggle source

Parses the closing tag

# File lib/brace_markup/parser.rb, line 257
def parse_end_tag
  if @lexer.peek(length: 2)[:chunk] == '}}'
    @lexer.next length: 2

    # Gets caught by Parser#parse_body
    raise CloseTagException.new
  else
    raise UnexpectedCharactersException.new '}}', @lexer
  end
end
parse_expression() click to toggle source

Parses an expression Expressions allow you to do actions like adding, subtracting, logical AND, OR, etc.

# File lib/brace_markup/parser.rb, line 520
def parse_expression
  @lexer.expect '(', raise_error: true

  parts = []

  loop do
    value = parse_value

    if value.nil?
      raise Exception.new('Invalid expression (no value found)')
    end

    parts << value

    operator = parse_operator
    break if operator.nil?

    parts << operator
  end

  @lexer.expect ')', raise_error: true

  ::Bytewise::Ast::Expression.new(parts)
end
parse_hash() click to toggle source

Parses a hash

# File lib/brace_markup/parser.rb, line 398
def parse_hash
  hash = {}

  @lexer.expect '{', raise_error: true do
    hash = parse_arguments
    @lexer.expect '}', raise_error: true
  end

  hash
end
parse_numerical() click to toggle source
# File lib/brace_markup/parser.rb, line 485
def parse_numerical
  i = 0
  buf = ''
  decimal = false

  @lexer.next_nowhitespace

  loop do
    curr = @lexer.peek skip_whitespace: false

    if curr[:chunk] == '-' && i != 0
      raise UnexpectedCharactersException.new '-', @lexer
    end

    if curr[:chunk] == '.'
      if decimal
        raise UnexpectedCharactersException.new '.', @lexer
      else
        decimal = true
      end
    end

    break unless curr[:chunk] =~ /^[\-0-9\.]$/

    buf += @lexer.next(skip_whitespace: false)
    i += 1
  end

  buf.empty? ? nil : (decimal ? buf.to_f : buf.to_i)
end
parse_operator() click to toggle source
# File lib/brace_markup/parser.rb, line 545
def parse_operator
  # @phpdevs
  @lexer.expect '!==' do
    return ::Bytewise::Ast::Operators::Equals.new
  end

  # @phpdevs
  @lexer.expect '===' do
    return ::Bytewise::Ast::Operators::Equals.new
  end

  # Equals
  @lexer.expect '==' do
    return ::Bytewise::Ast::Operators::Equals.new
  end

  @lexer.expect '!=' do
    return ::Bytewise::Ast::Operators::Equals.new negated: true
  end

  # AND
  @lexer.expect '&&' do
    return ::Bytewise::Ast::Operators::LogicalAnd.new
  end

  # OR
  @lexer.expect '||' do
    return ::Bytewise::Ast::Operators::LogicalOr.new
  end

  # Addition, Concatenation, etc.
  @lexer.expect '+' do
    return ::Bytewise::Ast::Operators::Add.new
  end

  # Addition, Concatenation, etc.
  @lexer.expect '>' do
    return ::Bytewise::Ast::Operators::GreaterThan.new
  end

  # Addition, Concatenation, etc.
  @lexer.expect '<' do
    return ::Bytewise::Ast::Operators::SmallerThan.new
  end

  @lexer.expect '-' do
    return ::Bytewise::Ast::Operators::Minus.new
  end

  @lexer.expect '*' do
    return ::Bytewise::Ast::Operators::Star.new
  end

  @lexer.expect '/' do
    return ::Bytewise::Ast::Operators::Slash.new
  end

  @lexer.expect '%' do
    return ::Bytewise::Ast::Operators::Modulo.new
  end

  nil
end
parse_reference(context: :default) click to toggle source

Parses a reference {{ user.name|to_lower }} or {{{ user.name }}} The context symbolizes the amount of braces required.

:default => }} :raw => }}} :in_tag => }

# File lib/brace_markup/parser.rb, line 169
def parse_reference(context: :default)
  name = parse_tag_name
  filter = nil

  if @lexer.expect '|'
    # Reference has a filter
    filter = parse_tag_name
  end

  case context
    when :raw
      @lexer.expect '}}}', raise_error: true
    when :in_tag
      @lexer.expect '}', raise_error: true
    else
      @lexer.expect '}}', raise_error: true
  end

  # Todo: Validate filter & name

  ::Bytewise::Ast::Reference.new name, filter, context
end
parse_string() click to toggle source

Parses a string with ' or “

Todo: Fix escaped characters

# File lib/brace_markup/parser.rb, line 441
def parse_string
  quotes = @lexer.next
  buffer = ''

  loop do
    curr = @lexer.peek skip_whitespace: false

    # Wait for ending quotes
    if @lexer.expect quotes, skip_whitespace: false, ignore_escaped: true
      break
    end

    # Raise error if document ends without an ending quote
    raise ::BraceMarkup::UnexpectedEndException.new("Unexpected End. Was waiting for matching quotes.") if curr[:chunk].nil?

    # Ignore escaping characters
    unless @lexer.escapes? offset: curr[:offset], escape: [quotes]
      buffer += curr[:chunk]
    end

    @lexer.next skip_whitespace: false
  end

  buffer
end
parse_tag(type:, context: :default) click to toggle source

Parses a tag with, or without body

# File lib/brace_markup/parser.rb, line 195
def parse_tag(type:, context: :default)
  name = parse_tag_name(true)
  body = nil

  if type == :/
    return parse_end_tag
  end

  arguments = parse_arguments

  if @lexer.expect '}}'
    # Section
    if type == :':'
      raise TagSectionException.new name: name, arguments: arguments
    end

    # Parse the body of the tag
    sections = []
    curr_section = {}
    next_section = {}

    curr_body = nil

    loop do
      begin
        if type == :!
          curr_body = parse_comment_body
        else
          curr_body = parse_body(true)
        end
        break
      rescue TagSectionException => e
        next_section[:name] = e.tag_name.to_sym
        next_section[:args]= e.arguments
        curr_body = e.body
      ensure
        if body.nil?
          body = curr_body
        else
          sections << Bytewise::Ast::Tags::Section.new(name: curr_section[:name], arguments: curr_section[:args], body: curr_body)
        end

        curr_section = next_section.clone
        next_section = {}
      end
    end


    # Create tag
    Bytewise::Ast::Tag.create(type, name: name, arguments: arguments, body: body, sections: sections)
  elsif @lexer.peek(length: 3)[:chunk] == '/}}'
    @lexer.next length: 3
    # Tag with no content
    Bytewise::Ast::Tag.create(type, name: name, arguments: arguments)
  else
    raise UnexpectedCharactersException.new '}}', @lexer
  end
end
parse_tag_name(allow_strings = false) click to toggle source

Parses a tag name (also used for hash keys)

# File lib/brace_markup/parser.rb, line 271
def parse_tag_name(allow_strings = false)
  @lexer.next_nowhitespace

  if allow_strings && (@lexer.expect('"', continue: false) || @lexer.expect("'", continue: false))
    parse_string
  else
    name = ''

    @lexer.peek_loop skip_whitespace: false do |char, offset|
      if char =~ VARIABLE_REGEX
        name += char
      else
        # Allow ? at the end
        if char == '?'
          name += char
          @lexer.next skip_whitespace: false
        end

        @lexer.next(length: offset - 1)
        return name
      end
    end
  end
end
parse_tag_value(type:) click to toggle source

Parses a tag as value

# File lib/brace_markup/parser.rb, line 384
def parse_tag_value(type:)
  @lexer.expect '{', raise_error: true

  name = parse_tag_name
  arguments = parse_arguments

  @lexer.expect '}', raise_error: true

  Bytewise::Ast::Tag.create(type, name: name, arguments: arguments, body: nil)
end
parse_value() click to toggle source

Parses a value in arguments or a hash

# File lib/brace_markup/parser.rb, line 332
def parse_value
  peek = @lexer.peek[:chunk]

  case peek
    when '"', "'"
      # String
      parse_string
    when '['
      # Array
      parse_array
    when '{'
      parse_bracket_value
    when '('
      parse_expression
    else
      # Nil, Numbers
      parse_const
  end
end
rollback_transaction() click to toggle source

Reverts the last transaction

# File lib/brace_markup/parser.rb, line 43
def rollback_transaction
  unless @lexers.empty?
    @lexer = @lexers.pop
  end
end