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