require 'hamlet/forked_slim_parser' # @api private module Hamlet class Parser < ForkedSlim::Parser if RUBY_VERSION > '1.9' CLASS_ID_REGEX = /\A\s*(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/ else CLASS_ID_REGEX = /\A\s*(#|\.)(\w[\w:-]*)/ end # Compile string to Temple expression # # @param [String] str Slim code # @return [Array] Temple expression representing the code]] def call(str) # Set string encoding if option is set if options[:encoding] && str.respond_to?(:encoding) old = str.encoding str = str.dup if str.frozen? str.force_encoding(options[:encoding]) # Fall back to old encoding if new encoding is invalid str.force_encoding(old_enc) unless str.valid_encoding? end result = [:multi] reset(str.split($/), [result]) while @lines.first && @lines.first =~ /\A\s*\Z/ @stacks.last << [:newline] next_line end if @lines.first and @lines.first =~ /\A<doctype\s+([^>]*)/i if !$'.empty? and $'[0] !~ /\s*#/ fail("did not expect content after doctype") end @stacks.last << [:html, :doctype, $1] next_line end parse_line while next_line reset result end private def parse_line if @line =~ /\A\s*\Z/ @stacks.last << [:newline] return end indent = get_indent(@line) # Remove the indentation @line.lstrip! indent +=1 if @line[0] == '>' # If there's more stacks than indents, it means that the previous # line is expecting this line to be indented. expecting_indentation = @stacks.size > @indents.size if indent > @indents.last # This line was actually indented, so we'll have to check if it was # supposed to be indented or not. unless expecting_indentation syntax_error!('Unexpected indentation') end @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. @stacks.pop if expecting_indentation # 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 @indents.pop @stacks.pop 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 def parse_line_indicators case @line[0] when '-' # code block. block = [:multi] @line.slice!(0) @stacks.last << [:slim, :control, parse_broken_line, block] @stacks << block when '=' # output block. @needs_space = true @line =~ /\A=(=?)('?)/ @line = $' block = [:multi] @stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block] @stacks.last << [:static, ' '] unless $2.empty? @stacks << block when '<' if @needs_space && !(@line[0] == '>') @stacks.last << [:slim, :interpolate, "\n" ] @stacks.last << [:newline] end @needs_space = false case @line when /\A<(\w+):\s*\Z/ # Embedded template. It is treated as block. @needs_space = false block = [:multi] @stacks.last << [:newline] << [:slim, :embedded, $1, block] @stacks << block parse_text_block(nil, :from_embedded) return # Don't append newline, this has already been done before when /\A<([#\.]|\w[:\w-]*)/ # HTML tag. @needs_space = false parse_tag($1) when /\A<!--( ?)(.*)\Z/ # HTML comment @needs_space = false block = [:multi] @stacks.last << [:html, :comment, block] @stacks << block @stacks.last << [:slim, :interpolate, $2] unless $2.empty? parse_text_block($2.empty? ? nil : @indents.last + $1.size + 2) end else if @line[0] == '#' and @line[1] != '{' @needs_space = false if @line =~ %r!\A#\[\s*(.*?)\s*\]\s*\Z! # HTML conditional comment block = [:multi] @stacks.last << [:slim, :condcomment, $1, block] @stacks << block else # otherwise the entire line is commented - ignore end else if @needs_space and not @line[0] == '>' @stacks.last << [:slim, :interpolate, "\n" ] @stacks.last << [:newline] end @needs_space = true push_text end end @stacks.last << [:newline] end def push_text if @line[0] == '>' @line.slice!(0) end if @line =~ /(\A|[^\\])#([^{]|\Z)/ @line = $` + $1 end @stacks.last << [:slim, :interpolate, @line] end # This is fundamentally broken # Can keep this for multi-lie html comment perhaps # But don't lookahead on text otherwise def parse_text_block(text_indent = nil, from = nil) empty_lines = 0 first_line = true embedded = nil case from when :from_tag first_line = true when :from_embedded embedded = true end close_bracket = false until @lines.empty? if @lines.first =~ /\A\s*>?\s*\Z/ next_line @stacks.last << [:newline] empty_lines += 1 if text_indent else indent = get_indent(@lines.first) break if indent <= @indents.last if @lines.first =~ /\A\s*>/ indent += 1 #$1.size if $1 close_bracket = true else close_bracket = false end if empty_lines > 0 @stacks.last << [:slim, :interpolate, "\n" * empty_lines] empty_lines = 0 end next_line # The text block lines must be at least indented # as deep as the first line. if text_indent && indent < text_indent # special case for a leading '>' being back 1 char unless first_line && close_bracket && (text_indent - indent == 1) @line.lstrip! syntax_error!('Unexpected text indentation') end end @line.slice!(0, text_indent || indent) @line = $' if @line =~ /\A>/ # a code comment if @line =~ /(\A|[^\\])#([^{]|\Z)/ @line = $` + $1 end @stacks.last << [:newline] if !first_line && !embedded @stacks.last << [:slim, :interpolate, (text_indent ? "\n" : '') + @line] << [:newline] # The indentation of first line of the text block # determines the text base indentation. text_indent ||= indent first_line = false end end end def parse_tag(tag) @line.slice!(0,1) # get rid of leading '<' if tag == '#' || tag == '.' tag = options[:default_tag] else @line.slice!(0, tag.size) end tag = [:html, :tag, tag, parse_attributes] @stacks.last << tag case @line when /\A=(=?)('?)/ # Handle output code @needs_space = true block = [:multi] @line = $' content = [:slim, :output, $1 != '=', parse_broken_line, block] tag << content @stacks.last << [:static, ' '] unless $2.empty? @stacks << block when /\A\s*\Z/ # Empty content content = [:multi] tag << content @stacks << content when %r!\A/>! # Do nothing for closing tag else # Text content @needs_space = true content = [:multi, [:slim, :interpolate, @line]] tag << content @stacks << content end end def parse_attributes attributes = [:html, :attrs] # Find any literal class/id attributes while @line =~ CLASS_ID_REGEX # The class/id attribute is :static instead of :slim :text, # because we don't want text interpolation in .class or #id shortcut attributes << [:html, :attr, ATTR_SHORTCUT[$1], [:static, $2]] @line = $' end # Check to see if there is a delimiter right after the tag name delimiter = '>' orig_line = @orig_line lineno = @lineno while true # Parse attributes while @line =~ /#{ATTR_NAME_REGEX}\s*(=\s*)?/ name = $1 @line = $' if !$2 attributes << [:slim, :attr, name, false, 'true'] elsif @line =~ /\A["']/ # Value is quoted (static) @line = $' attributes << [:html, :attr, name, [:slim, :interpolate, parse_quoted_attribute($&)]] elsif @line =~ /\A[^ >]+/ @line = $' attributes << [:html, :attr, name, [:slim, :interpolate, $&]] end end @line.lstrip! # Find ending delimiter if @line =~ /\A(>|\Z)/ @line = $' break elsif @line =~ %r!\A/>! # Do nothing for closing tag # don't eat the line either, we check for it again if not $'.empty? and $' !~ /\s*#/ syntax_error!("Did not expect any content after self closing tag", :orig_line => orig_line, :lineno => lineno, :column => orig_line.size) end break end syntax_error!('Expected attribute') unless @line.empty? # Attributes span multiple lines @stacks.last << [:newline] next_line || syntax_error!("Expected closing delimiter #{delimiter}", :orig_line => orig_line, :lineno => lineno, :column => orig_line.size) end attributes end end end