class Bade::Parser

Class to parse input string into AST::Document

Constants

ATTR_NAME_RE_STRING
CLASS_TAG_RE
CODE_ATTR_RE
ID_TAG_RE
NAME_RE_STRING
RUBY_ALL_DELIMITERS
RUBY_DELIMITERS_REVERSE
RUBY_END_DELIMITERS
RUBY_END_DELIMITERS_RE
RUBY_NOT_NESTABLE_DELIMITERS
RUBY_QUOTES
RUBY_START_DELIMITERS
RUBY_START_DELIMITERS_RE
TAG_RE
WORD_RE

Attributes

dependency_paths[R]

@return [Array<String>]

file_path[R]

@return [String]

Public Class Methods

new(tabsize: 4, file_path: nil) click to toggle source

@param [Fixnum] tabsize @param [String] file_path

# File lib/bade/parser.rb, line 56
def initialize(tabsize: 4, file_path: nil)
  @line = ''

  @tabsize = tabsize
  @file_path = file_path

  @tab_re = /\G((?: {#{tabsize}})*) {0,#{tabsize - 1}}\t/
  @tab = '\1' + ' ' * tabsize

  reset
end

Public Instance Methods

append_node(type, indent: @indents.length, add: false, value: nil) click to toggle source

Append element to stacks and result tree

@param [Symbol] type

# File lib/bade/parser.rb, line 104
def append_node(type, indent: @indents.length, add: false, value: nil)
  # add necessary stack items to match required indent
  @stacks << @stacks.last.dup while indent >= @stacks.length

  parent = @stacks[indent].last
  node = AST::NodeRegistrator.create(type, @lineno)
  parent.children << node

  node.value = value unless value.nil?

  @stacks[indent] << node if add

  node
end
fixed_trailing_colon(value) click to toggle source

@param value [String]

# File lib/bade/parser.rb, line 139
def fixed_trailing_colon(value)
  if value.is_a?(String) && value.end_with?(':')
    value = value.remove_last
    @line.prepend(':')
  end

  value
end
get_indent(line) click to toggle source

Calculate indent for line

@param [String] line

@return [Int] indent size

# File lib/bade/parser.rb, line 96
def get_indent(line)
  line.get_indent(@tabsize)
end
next_line() click to toggle source
# File lib/bade/parser/parser_lines.rb, line 53
def next_line
  if @lines.empty?
    @orig_line = @line = nil

    last_newlines = remove_last_newlines
    @root.children += last_newlines

    nil
  else
    @orig_line = @lines.shift
    @lineno += 1
    @line = @orig_line.dup
  end
end
parse(str) click to toggle source

@param [String, Array<String>] str @return [Bade::AST::Document] root node

# File lib/bade/parser.rb, line 71
def parse(str)
  @document = AST::Document.new(file_path: file_path)
  @root = @document.root

  @dependency_paths = []

  if str.is_a?(Array)
    reset(str, [[@root]])
  else
    reset(str.split(/\r?\n/, -1), [[@root]]) # -1 is for not suppressing empty lines
  end

  parse_line while next_line

  reset

  @document
end
parse_import() click to toggle source
# File lib/bade/parser.rb, line 127
def parse_import
  # TODO: change this to something better
  # rubocop:disable Security/Eval
  path = eval(@line)
  # rubocop:enable Security/Eval
  append_node(:import, value: path)

  @dependency_paths << path unless @dependency_paths.include?(path)
end
parse_line() click to toggle source
# File lib/bade/parser/parser_lines.rb, line 68
def parse_line
  if @line.strip.empty?
    append_node(:newline) unless @lines.empty?
    return
  end

  indent = get_indent(@line)

  # left strip
  @line.remove_indent!(indent, @tabsize)

  # If there's more stacks than indents, it means that the previous
  # line is expecting this line to be indented.
  expecting_indentation = @stacks.length > @indents.length

  if indent > @indents.last
    @indents << indent
  else
    # This line was *not* indented more than the line before,
    # so we'll just forget about the stack that the previous line pushed.
    if expecting_indentation
      last_newlines = remove_last_newlines

      @stacks.pop

      new_node = @stacks.last.last
      new_node.children += last_newlines
    end

    # This line was deindented.
    # Now we're have to go through the all the indents and figure out
    # how many levels we've deindented.
    while indent < @indents.last
      last_newlines = remove_last_newlines

      @indents.pop
      @stacks.pop

      new_node = @stacks.last.last
      new_node.children += last_newlines
    end

    # Remove old stacks we don't need
    while !@stacks[indent].nil? && indent < @stacks[indent].length - 1
      last_newlines = remove_last_newlines

      @stacks[indent].pop

      new_node = @stacks.last.last
      new_node.children += last_newlines
    end

    # This line's indentation happens lie "between" two other line's
    # indentation:
    #
    #   hello
    #       world
    #     this      # <- This should not be possible!
    syntax_error('Malformed indentation') if indent != @indents.last
  end

  parse_line_indicators
end
parse_line_indicators(add_newline: true) click to toggle source
# File lib/bade/parser/parser_lines.rb, line 132
def parse_line_indicators(add_newline: true)
  case @line
  when LineIndicatorRegexps::IMPORT
    @line = $'
    parse_import

  when LineIndicatorRegexps::MIXIN_DECL
    # Mixin declaration
    @line = $'
    parse_mixin_declaration($1)

  when LineIndicatorRegexps::MIXIN_CALL
    # Mixin call
    @line = $'
    parse_mixin_call($1)

  when LineIndicatorRegexps::BLOCK_DECLARATION
    @line = $'
    if @stacks.last.last.type == :mixin_call
      node = append_node(:mixin_block, add: true)
      node.name = $1
    else
      # keyword block used outside of mixin call
      parse_tag($&)
    end

  when LineIndicatorRegexps::HTML_COMMENT
    # HTML comment
    append_node(:html_comment, add: true)
    parse_text_block $', @indents.last + @tabsize

  when LineIndicatorRegexps::NORMAL_COMMENT
    # Comment
    append_node(:comment, add: true)
    parse_text_block $', @indents.last + @tabsize

  when LineIndicatorRegexps::TEXT_BLOCK_START
    # Found a text block.
    parse_text_block $', @indents.last + @tabsize

  when LineIndicatorRegexps::INLINE_HTML
    # Inline html
    parse_text

  when LineIndicatorRegexps::CODE_BLOCK
    # Found a code block.
    append_node(:code, value: $'.strip)

  when LineIndicatorRegexps::OUTPUT_BLOCK
    # Found an output block.
    # We expect the line to be broken or the next line to be indented.
    @line = $'
    output_node = append_node(:output)
    output_node.conditional = $1.length == 1
    output_node.escaped = $2.length == 1
    output_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_NEW_LINE)

  when LineIndicatorRegexps::DOCTYPE
    # Found doctype declaration
    append_node(:doctype, value: $'.strip)

  when TAG_RE
    # Found a HTML tag.
    @line = $' if $1
    parse_tag($&)

  when LineIndicatorRegexps::TAG_CLASS_START_BLOCK
    # Found class name -> implicit div
    parse_tag 'div'

  when LineIndicatorRegexps::TAG_ID_START_BLOCK
    # Found id name -> implicit div
    parse_tag 'div'

  else
    syntax_error 'Unknown line indicator'
  end

  append_node(:newline) if add_newline && !@lines.empty?
end
parse_mixin_call(mixin_name) click to toggle source
# File lib/bade/parser/parser_mixin.rb, line 23
def parse_mixin_call(mixin_name)
  mixin_name = fixed_trailing_colon(mixin_name)

  mixin_node = append_node(:mixin_call, add: true)
  mixin_node.name = mixin_name

  parse_mixin_call_params

  case @line
  when MixinRegexps::TEXT_START
    @line = $'
    parse_text

  when MixinRegexps::BLOCK_EXPANSION
    # Block expansion
    @line = $'
    parse_line_indicators(add_newline: false)

  when MixinRegexps::OUTPUT_CODE
    # Handle output code
    parse_line_indicators(add_newline: false)

  when ''
    # nothing

  else
    syntax_error "Unknown symbol after mixin calling, line = `#{@line}'"
  end
end
parse_mixin_call_params() click to toggle source
# File lib/bade/parser/parser_mixin.rb, line 53
def parse_mixin_call_params
  # between tag name and attribute must not be space
  # and skip when is nothing other
  return unless @line.start_with?('(')

  # remove starting bracket
  @line.remove_first!

  loop do
    case @line
    when MixinRegexps::PARAMS_KEY_PARAM_NAME
      @line = $'
      attr_node = append_node(:mixin_key_param)
      attr_node.name = fixed_trailing_colon($1)
      attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG, allow_multiline: true)

    when MixinRegexps::PARAMS_ARGS_DELIMITER
      # args delimiter
      @line = $'
      next

    when MixinRegexps::PARAMS_END_SPACES
      # spaces and/or end of line
      next_line
      next

    when MixinRegexps::PARAMS_END
      # Find ending delimiter
      @line = $'
      break

    else
      attr_node = append_node(:mixin_param)
      attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG, allow_multiline: true)
    end
  end
end
parse_mixin_declaration(mixin_name) click to toggle source
# File lib/bade/parser/parser_mixin.rb, line 91
def parse_mixin_declaration(mixin_name)
  mixin_node = append_node(:mixin_decl, add: true)
  mixin_node.name = mixin_name

  parse_mixin_declaration_params
end
parse_mixin_declaration_params() click to toggle source
# File lib/bade/parser/parser_mixin.rb, line 98
def parse_mixin_declaration_params
  # between tag name and attribute must not be space
  # and skip when is nothing other
  return unless @line.start_with?('(')

  # remove starting bracket
  @line.remove_first!

  loop do
    case @line
    when MixinRegexps::PARAMS_KEY_PARAM_NAME
      # Value ruby code
      @line = $'
      attr_node = append_node(:mixin_key_param)
      attr_node.name = fixed_trailing_colon($1)
      attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)

    when MixinRegexps::PARAMS_PARAM_NAME
      @line = $'
      append_node(:mixin_param, value: $1)

    when MixinRegexps::PARAMS_BLOCK_NAME
      @line = $'
      append_node(:mixin_block_param, value: $1)

    when MixinRegexps::PARAMS_ARGS_DELIMITER
      # args delimiter
      @line = $'
      next

    when MixinRegexps::PARAMS_END
      # Find ending delimiter
      @line = $'
      break

    else
      syntax_error('wrong mixin attribute syntax')
    end
  end
end
parse_ruby_code(outer_delimiters, allow_multiline: false) click to toggle source

Parse ruby code, ended with outer delimiters

@param [String, Regexp] outer_delimiters

@return [Void] parsed ruby code

# File lib/bade/parser/parser_ruby_code.rb, line 19
def parse_ruby_code(outer_delimiters, allow_multiline: false)
  code = String.new
  end_re = if outer_delimiters.is_a?(Regexp)
             outer_delimiters
           else
             /\A\s*[#{Regexp.escape outer_delimiters.to_s}]/
           end

  delimiters = []
  string_start_quote_char = nil

  loop do
    break if !allow_multiline && @line.empty?
    break if allow_multiline && @line.empty? && (@lines && @lines.empty?)
    break if delimiters.empty? && @line =~ end_re

    if @line.empty? && allow_multiline && !(@lines && @lines.empty?)
      next_line
      code << "\n"
    end

    char = @line[0]

    # backslash escaped delimiter
    if char == '\\' && RUBY_ALL_DELIMITERS.include?(@line[1])
      code << @line.slice!(0, 2)
      next
    end

    case char
    when RUBY_START_DELIMITERS_RE
      if RUBY_NOT_NESTABLE_DELIMITERS.include?(char) && delimiters.last == char
        # end char of not nestable delimiter
        delimiters.pop
        string_start_quote_char = nil
      else
        # diving into nestable delimiters
        delimiters << char if string_start_quote_char.nil?

        # mark start char of the not nestable delimiters, for example strings
        if RUBY_NOT_NESTABLE_DELIMITERS.include?(char) && string_start_quote_char.nil?
          string_start_quote_char = char
        end
      end

    when RUBY_END_DELIMITERS_RE
      # rising
      delimiters.pop if char == RUBY_DELIMITERS_REVERSE[delimiters.last]
    end

    code << @line.slice!(0)
  end

  syntax_error('Unexpected end of ruby code') unless delimiters.empty?

  code.strip
end
parse_tag(tag) click to toggle source

@param [String] tag tag name

# File lib/bade/parser/parser_tag.rb, line 19
def parse_tag(tag)
  tag = fixed_trailing_colon(tag)

  if tag.is_a?(AST::Node)
    tag_node = tag
  else
    tag_node = append_node(:tag, add: true)
    tag_node.name = tag
  end

  parse_tag_attributes

  case @line
  when TagRegexps::BLOCK_EXPANSION
    # Block expansion
    @line = $'
    parse_line_indicators(add_newline: false)

  when TagRegexps::OUTPUT_CODE
    # Handle output code
    parse_line_indicators(add_newline: false)

  when CLASS_TAG_RE
    # Class name
    @line = $'

    attr_node = append_node(:tag_attr)
    attr_node.name = 'class'
    attr_node.value = fixed_trailing_colon($1).single_quote

    parse_tag tag_node

  when ID_TAG_RE
    # Id name
    @line = $'

    attr_node = append_node(:tag_attr)
    attr_node.name = 'id'
    attr_node.value = fixed_trailing_colon($1).single_quote

    parse_tag tag_node

  when TagRegexps::TEXT_START
    # Text content
    @line = $'
    parse_text

  when ''
    # nothing

  else
    syntax_error "Unknown symbol after tag definition #{@line}"
  end
end
parse_tag_attributes() click to toggle source
# File lib/bade/parser/parser_tag.rb, line 74
def parse_tag_attributes
  # Check to see if there is a delimiter right after the tag name

  # between tag name and attribute must not be space
  # and skip when is nothing other
  return unless @line.start_with?('(')

  # remove starting bracket
  @line.remove_first!

  loop do
    case @line
    when CODE_ATTR_RE
      # Value ruby code
      @line = $'
      attr_node = append_node(:tag_attr)
      attr_node.name = $1
      attr_node.value = parse_ruby_code(ParseRubyCodeRegexps::END_PARAMS_ARG)

    when TagRegexps::PARAMS_ARGS_DELIMITER
      # args delimiter
      @line = $'
      next

    when TagRegexps::PARAMS_END
      # Find ending delimiter
      @line = $'
      break

    else
      # Found something where an attribute should be
      @line.lstrip!
      syntax_error('Expected attribute') unless @line.empty?

      # Attributes span multiple lines
      append_node(:newline)
      syntax_error('Expected closing tag attributes delimiter `)`') if @lines.empty?
      next_line
    end
  end
end
parse_text() click to toggle source
# File lib/bade/parser/parser_text.rb, line 13
def parse_text
  new_index = @line.index(TextRegexps::INTERPOLATION_START)

  # the interpolation sequence is not in text, mark whole text as static
  if new_index.nil?
    append_node(:static_text, value: @line)
    return
  end

  unparsed_part = String.new

  while (new_index = @line.index(TextRegexps::INTERPOLATION_START))
    if $1.nil?
      static_part = unparsed_part + @line.remove_first!(new_index)
      append_node(:static_text, value: static_part)

      @line.remove_first!(2) # #{ or &{

      dynamic_part = parse_ruby_code(TextRegexps::INTERPOLATION_END)
      node = append_node(:output, value: dynamic_part)
      node.escaped = $2 == '&'

      @line.remove_first! # ending }

      unparsed_part = String.new
    else
      unparsed_part << @line.remove_first!(new_index)
      @line.remove_first! # symbol \
      unparsed_part << @line.remove_first!(2) # #{ or &{
    end
  end

  # add the rest of line
  append_node(:static_text, value: unparsed_part + @line) unless @line.empty?
end
parse_text_block(first_line, text_indent = nil) click to toggle source
# File lib/bade/parser/parser_text.rb, line 49
def parse_text_block(first_line, text_indent = nil)
  if !first_line || first_line.empty?
    text_indent = nil
  else
    @line = first_line
    parse_text
  end

  until @lines.empty?
    if @lines.first.blank?
      next_line
      append_node(:newline)
    else
      indent = get_indent(@lines.first)
      break if indent <= @indents.last

      next_line

      @line.remove_indent!(text_indent || indent, @tabsize)

      parse_text

      # The indentation of first line of the text block
      # determines the text base indentation.
      text_indent ||= indent
    end
  end
end
remove_last_newlines() click to toggle source

@return [Array<AST::Node>]

# File lib/bade/parser.rb, line 121
def remove_last_newlines
  last_node = @stacks.last.last
  last_newlines_count = last_node.children.rcount_matching { |n| n.type == :newline }
  last_node.children.pop(last_newlines_count)
end
reset(lines = nil, stacks = nil) click to toggle source
# File lib/bade/parser/parser_lines.rb, line 24
def reset(lines = nil, stacks = nil)
  # Since you can indent however you like in Slim, we need to keep a list
  # of how deeply indented you are. For instance, in a template like this:
  #
  #   doctype       # 0 spaces
  #   html          # 0 spaces
  #    head         # 1 space
  #       title     # 4 spaces
  #
  # indents will then contain [0, 1, 4] (when it's processing the last line.)
  #
  # We uses this information to figure out how many steps we must "jump"
  # out when we see an de-indented line.
  @indents = [0]

  # Whenever we want to output something, we'll *always* output it to the
  # last stack in this array. So when there's a line that expects
  # indentation, we simply push a new stack onto this array. When it
  # processes the next line, the content will then be outputted into that
  # stack.
  @stacks = stacks

  @lineno = 0
  @lines = lines

  # @return [String]
  @line = @orig_line = nil
end
syntax_error(message) click to toggle source

Raise specific error

@param [String] message

# File lib/bade/parser.rb, line 154
def syntax_error(message)
  column = @orig_line && @line ? @orig_line.size - @line.size : 0
  raise SyntaxError.new(message, file_path, @orig_line, @lineno, column)
end