class Slimi::Parser
Public Class Methods
# File lib/slimi/parser.rb, line 26 def initialize(_options = {}) super @file_path = options[:file] || '(__TEMPLATE__)' factory = Factory.new( attribute_delimiters: options[:attr_list_delims] || {}, default_tag: options[:default_tag] || 'div', ruby_attribute_delimiters: options[:code_attr_delims] || {}, shortcut: options[:shortcut] || {} ) @attribute_delimiters = factory.attribute_delimiters @attribute_shortcuts = factory.attribute_shortcuts @tag_shortcuts = factory.tag_shortcuts @attribute_shortcut_regexp = factory.attribute_shortcut_regexp @attribute_delimiter_regexp = factory.attribute_delimiter_regexp @quoted_attribute_regexp = factory.quoted_attribute_regexp @tag_name_regexp = factory.tag_name_regexp @attribute_name_regexp = factory.attribute_name_regexp @ruby_attribute_regexp = factory.ruby_attribute_regexp @ruby_attribute_delimiter_regexp = factory.ruby_attribute_delimiter_regexp @ruby_attribute_delimiters = factory.ruby_attribute_delimiters @embedded_template_regexp = factory.embedded_template_regexp end
Public Instance Methods
# File lib/slimi/parser.rb, line 49 def call(source) @stacks = [[:multi]] @indents = [] @scanner = ::StringScanner.new(source) parse_block until @scanner.eos? @stacks[0] end
Private Instance Methods
@raise
# File lib/slimi/parser.rb, line 475 def expect_line_ending parse_line_ending || @scanner.eos? || syntax_error!(Errors::LineEndingNotFoundError) end
@return [Boolean]
# File lib/slimi/parser.rb, line 470 def expecting_indentation? @stacks.length > @indents.length end
@return [Integer]
# File lib/slimi/parser.rb, line 486 def indent_from_last_match @scanner.matched.chars.map do |char| case char when "\t" 4 when ' ' 1 else 0 end end.sum(0) end
Parse attributes part.
e.g. input type="text" value="a" autofocus ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `- attributes part
@return [Array] S-expression of attributes.
# File lib/slimi/parser.rb, line 250 def parse_attributes attributes = %i[html attrs] attributes += parse_tag_attribute_shortcuts if @scanner.scan(@attribute_delimiter_regexp) attribute_delimiter_opening = @scanner[1] attribute_delimiter_closing = @attribute_delimiters[attribute_delimiter_opening] attribute_delimiter_closing_regexp = ::Regexp.escape(attribute_delimiter_closing) boolean_attribute_regexp = /#{@attribute_name_regexp}(?=(?:[ \t]|#{attribute_delimiter_closing_regexp}|$))/ attribute_delimiter_closing_part_regexp = /[ \t]*#{attribute_delimiter_closing_regexp}/ end # TODO: Support splat attributes. loop do if @scanner.skip(@quoted_attribute_regexp) attribute_name = @scanner[1] escape = @scanner[2].empty? quote = @scanner[3] attributes << [:html, :attr, attribute_name, [:escape, escape, parse_quoted_attribute_value(quote)]] elsif @scanner.skip(@ruby_attribute_regexp) attribute_name = @scanner[1] escape = @scanner[2].empty? charpos = @scanner.charpos attribute_value = parse_ruby_attribute_value(attribute_delimiter_closing) syntax_error!(Errors::InvalidEmptyAttributeError) if attribute_value.empty? attributes << [:html, :attr, attribute_name, [:slimi, :position, charpos, charpos + attribute_value.length, [:slimi, :attrvalue, escape, attribute_value]]] elsif !attribute_delimiter_closing_part_regexp break elsif @scanner.skip(boolean_attribute_regexp) attributes << [:html, :attr, @scanner[1], [:multi]] elsif @scanner.skip(attribute_delimiter_closing_part_regexp) # rubocop:disable Lint/DuplicateBranch break else @scanner.skip(/[ \t]+/) expect_line_ending syntax_error!(Errors::AttributeClosingDelimiterNotFoundError) if @scanner.eos? end end attributes end
Parse blank line. @return [Boolean] True if it could parse a blank line.
# File lib/slimi/parser.rb, line 79 def parse_blank_line if @scanner.skip(/[ \t]*(?=\R|$)/) parse_line_ending true else false end end
# File lib/slimi/parser.rb, line 59 def parse_block return if parse_blank_line parse_indent parse_html_comment || parse_html_conditional_comment || parse_slim_comment_block || parse_verbatim_text_block || parse_inline_html || parse_code_block || parse_output_block || parse_embedded_template || parse_doctype || parse_tag || syntax_error!(Errors::UnknownLineIndicatorError) end
@note Broken line means line-breaked lines, separated by trailing “,” or “". @return [String]
# File lib/slimi/parser.rb, line 547 def parse_broken_lines result = +'' result << @scanner.scan(/.*/) while result.end_with?(',') || result.end_with?('\\') syntax_error!(Errors::UnexpectedEosError) unless @scanner.scan(/\r?\n/) result << "\n" result << @scanner.scan(/.*/) end result end
@return [Boolean]
# File lib/slimi/parser.rb, line 414 def parse_code_block parse_code_block_inner && expect_line_ending end
@return [Boolean]
# File lib/slimi/parser.rb, line 419 def parse_code_block_inner if @scanner.skip(/-/) block = [:multi] @scanner.skip(/[ \t]+/) @stacks.last << with_position { [:slimi, :control, parse_broken_lines, block] } @stacks << block true else false end end
@return [Boolean]
# File lib/slimi/parser.rb, line 455 def parse_doctype parse_doctype_inner && expect_line_ending end
@return [Boolean]
# File lib/slimi/parser.rb, line 460 def parse_doctype_inner if @scanner.skip(/doctype[ \t]*/) @stacks.last << [:html, :doctype, @scanner.scan(/.*/).rstrip] true else false end end
Parse embedded template lines.
e.g. ruby: a = b + c
# File lib/slimi/parser.rb, line 113 def parse_embedded_template return unless @scanner.skip(@embedded_template_regexp) embedded_template_engine_name = @scanner[1] attributes = parse_attributes @stacks.last << [:slimi, :embedded, embedded_template_engine_name, parse_text_block, attributes] end
@return [Boolean]
# File lib/slimi/parser.rb, line 334 def parse_html_comment if @scanner.skip(%r{/![ \t]*}) text_block = parse_text_block text = [:slimi, :text, :verbatim, text_block] @stacks.last << [:html, :comment, text] true else false end end
@return [Boolean]
# File lib/slimi/parser.rb, line 346 def parse_html_conditional_comment parse_html_conditional_comment_inner && expect_line_ending end
@return [Boolean]
# File lib/slimi/parser.rb, line 351 def parse_html_conditional_comment_inner if @scanner.skip(%r{/\[\s*(.*?)\s*\][ \t]*}) block = [:multi] @stacks.last << [:html, :condcomment, @scanner[1], block] @stacks << block true else false end end
# File lib/slimi/parser.rb, line 88 def parse_indent @scanner.skip(/[ \t]*/) indent = indent_from_last_match @indents << indent if @indents.empty? if indent > @indents.last syntax_error!(Errors::UnexpectedIndentationError) unless expecting_indentation? @indents << indent else @stacks.pop if expecting_indentation? while indent < @indents.last && @indents.length > 1 @indents.pop @stacks.pop end syntax_error!(Errors::MalformedIndentationError) if indent != @indents.last end end
@return [Boolean]
# File lib/slimi/parser.rb, line 393 def parse_inline_html parse_inline_html_inner && expect_line_ending end
@return [Boolean]
# File lib/slimi/parser.rb, line 398 def parse_inline_html_inner if @scanner.match?(/<.*/) begin_ = @scanner.charpos value = @scanner.matched @scanner.pos += @scanner.matched_size end_ = @scanner.charpos block = [:multi] @stacks.last << [:multi, [:slimi, :interpolate, begin_, end_, value], block] @stacks << block true else false end end
@return [Array, nil] S-expression if found.
# File lib/slimi/parser.rb, line 535 def parse_interpolate_line return unless @scanner.match?(/.+/) begin_ = @scanner.charpos value = @scanner.matched @scanner.pos += @scanner.matched_size end_ = @scanner.charpos [:slimi, :interpolate, begin_, end_, value] end
@return [Boolean]
# File lib/slimi/parser.rb, line 500 def parse_line_ending if @scanner.skip(/\r?\n/) @stacks.last << [:newline] true else false end end
@return [Boolean]
# File lib/slimi/parser.rb, line 432 def parse_output_block parse_output_block_inner && expect_line_ending end
@return [Boolean]
# File lib/slimi/parser.rb, line 437 def parse_output_block_inner if @scanner.skip(/=(=?)([<>']*)/) escape = @scanner[1].empty? white_space_marker = @scanner[2] with_trailing_white_space = white_space_marker.include?('<') || white_space_marker.include?("'") with_leading_white_space = white_space_marker.include?('>') block = [:multi] @stacks.last << [:static, ' '] if with_trailing_white_space @scanner.skip(/[ \t]+/) @stacks.last << with_position { [:slimi, :output, escape, parse_broken_lines, block] } @stacks.last << [:static, ' '] if with_leading_white_space @stacks << block else false end end
Parse quoted attribute value part.
e.g. input type="text" ^^^^^^ `- quoted attribute value
@note Skip closing quote in {}. @param [String] quote ‘“’”‘ or `’“‘`. @return [Array] S-expression.
# File lib/slimi/parser.rb, line 217 def parse_quoted_attribute_value(quote) begin_ = @scanner.charpos end_ = nil value = +'' count = 0 loop do if @scanner.match?(/#{quote}/) if count.zero? end_ = @scanner.charpos @scanner.pos += @scanner.matched_size break else @scanner.pos += @scanner.matched_size value << @scanner.matched end elsif @scanner.skip(/\{/) count += 1 value << @scanner.matched elsif @scanner.skip(/\}/) count -= 1 value << @scanner.matched else value << @scanner.scan(/[^{}#{quote}]*/) end end [:slimi, :interpolate, begin_, end_, value] end
Parse Ruby attribute value part.
e.g. div class=foo ^^^ `- Ruby attribute value
@param [String] attribute_delimiter_closing @return [String]
# File lib/slimi/parser.rb, line 299 def parse_ruby_attribute_value(attribute_delimiter_closing) ending_regexp = /\s/ ending_regexp = ::Regexp.union(ending_regexp, attribute_delimiter_closing) if attribute_delimiter_closing count = 0 attribute_value = +'' opening_delimiter = nil closing_delimiter = nil loop do break if count.zero? && @scanner.match?(ending_regexp) if @scanner.skip(/([,\\])\r?\n/) attribute_value << @scanner[1] << "\n" else if count.positive? if opening_delimiter && @scanner.match?(/#{::Regexp.escape(opening_delimiter)}/) count += 1 elsif closing_delimiter && @scanner.match?(/#{::Regexp.escape(closing_delimiter)}/) count -= 1 end elsif @scanner.match?(@ruby_attribute_delimiter_regexp) count = 1 opening_delimiter = @scanner.matched closing_delimiter = @ruby_attribute_delimiters[opening_delimiter] end if (character = @scanner.scan(/./)) attribute_value << character end end end syntax_error!(Errors::RubyAttributeClosingDelimiterNotFoundError) if count != 0 attribute_value end
@return [Boolean]
# File lib/slimi/parser.rb, line 363 def parse_slim_comment_block if @scanner.skip(%r{/.*}) while !@scanner.eos? && (@scanner.match?(/[ \t]*$/) || peek_indent > @indents.last) @scanner.skip(/.*/) parse_line_ending end true else false end end
@return [Boolean]
# File lib/slimi/parser.rb, line 122 def parse_tag parse_tag_inner && expect_line_ending end
Parse attribute shortcuts part.
e.g. div#foo.bar ^^^^^^^^ `- attribute shortcuts
@return [Array] Found attribute s-expressions.
# File lib/slimi/parser.rb, line 197 def parse_tag_attribute_shortcuts result = [] while @scanner.skip(@attribute_shortcut_regexp) marker = @scanner[1] attribute_value = @scanner[2] attribute_names = @attribute_shortcuts[marker] attribute_names.map do |attribute_name| result << [:html, :attr, attribute_name.to_s, [:static, attribute_value]] end end result end
@return [Boolean]
# File lib/slimi/parser.rb, line 127 def parse_tag_inner tag_name = parse_tag_name if tag_name attributes = parse_attributes white_space_marker = @scanner.scan(/[<>']*/) with_trailing_white_space = white_space_marker.include?('<') || white_space_marker.include?("'") with_leading_white_space = white_space_marker.include?('>') tag = [:html, :tag, tag_name, attributes] @stacks.last << [:static, ' '] if with_leading_white_space @stacks.last << tag @stacks.last << [:static, ' '] if with_trailing_white_space if @scanner.skip(/[ \t]*$/) content = [:multi] tag << content @stacks << content elsif @scanner.skip(/[ \t]*=(=?)([<>'])*/) escape = @scanner[1].empty? white_space_marker = @scanner[2] with_trailing_white_space2 = !with_trailing_white_space && white_space_marker && (white_space_marker.include?('<') || white_space_marker.include?("'")) with_leading_white_space2 = !with_leading_white_space && white_space_marker && white_space_marker.include?('>') block = [:multi] @stacks.last.insert(-2, [:static, ' ']) if with_leading_white_space2 @scanner.skip(/[ \t]+/) tag << with_position { [:slimi, :output, escape, parse_broken_lines, block] } @stacks.last << [:static, ' '] if with_trailing_white_space2 @stacks << block elsif @scanner.skip(%r{[ \t]*/[ \t]*}) syntax_error!(Errors::UnexpectedTextAfterClosedTagError) unless @scanner.match?(/\r?\n/) else @scanner.skip(/[ \t]+/) tag << [:slimi, :text, :inline, parse_text_block] end true else false end end
Parse tag name part.
e.g. div.foo ^^^ `- tag name e.g. .foo ^ `- tag name shortcut (not consume input in this case) e.g. ?.foo ^ `- tag name shortcut if `?` is registered as only-tag shortcut (consume input in this case)
@return [String, nil] Tag name if found.
# File lib/slimi/parser.rb, line 179 def parse_tag_name return unless @scanner.match?(@tag_name_regexp) if @scanner[1] @scanner.pos += @scanner.matched_size @scanner.matched else marker = @scanner.matched @scanner.pos += @scanner.matched_size unless @attribute_shortcuts[marker] @tag_shortcuts[marker] end end
@todo Append new_line for each empty line.
# File lib/slimi/parser.rb, line 510 def parse_text_block result = [:multi] interpolate = parse_interpolate_line result << interpolate if interpolate until @scanner.eos? if @scanner.skip(/\r?\n[ \t]*(?=\r?\n)/) result << [:newline] next end @scanner.match?(/\r?\n[ \t]*/) indent = indent_from_last_match break if indent <= @indents.last @scanner.pos += @scanner.matched_size result << [:newline] result << parse_interpolate_line end result end
@return [Boolean]
# File lib/slimi/parser.rb, line 376 def parse_verbatim_text_block parse_verbatim_text_block_inner && expect_line_ending end
@return [Boolean]
# File lib/slimi/parser.rb, line 381 def parse_verbatim_text_block_inner if @scanner.skip(/([|']) ?/) with_trailing_white_space = @scanner[1] == "'" @stacks.last << [:slimi, :text, :verbatim, parse_text_block] @stacks.last << [:static, ' '] if with_trailing_white_space true else false end end
@return [Integer] Indent level.
# File lib/slimi/parser.rb, line 480 def peek_indent @scanner.match?(/[ \t]*/) indent_from_last_match end
@param [Class] syntax_error_class A child class of Slimi::Errors::SlimSyntaxError
. @raise [Slimi::Errors::SlimSyntaxError]
# File lib/slimi/parser.rb, line 569 def syntax_error!(syntax_error_class) range = Range.new(index: @scanner.charpos, source: @scanner.string) raise syntax_error_class.new( column: range.column, file_path: @file_path, line: range.line, line_number: range.line_number ) end
Wrap the result s-expression of given block with slimi-position s-expression.
# File lib/slimi/parser.rb, line 560 def with_position(&block) begin_ = @scanner.charpos inner = block.call end_ = @scanner.charpos [:slimi, :position, begin_, end_, inner] end