class Slimi::Parser

Public Class Methods

new(_options = {}) click to toggle source
Calls superclass method
# 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

call(source) click to toggle source
# 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

expect_line_ending() click to toggle source

@raise

# File lib/slimi/parser.rb, line 475
def expect_line_ending
  parse_line_ending || @scanner.eos? || syntax_error!(Errors::LineEndingNotFoundError)
end
expecting_indentation?() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 470
def expecting_indentation?
  @stacks.length > @indents.length
end
indent_from_last_match() click to toggle source

@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() click to toggle source

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() click to toggle source

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
parse_block() click to toggle source
# 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
parse_broken_lines() click to toggle source

@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
parse_code_block() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 414
def parse_code_block
  parse_code_block_inner && expect_line_ending
end
parse_code_block_inner() click to toggle source

@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
parse_doctype() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 455
def parse_doctype
  parse_doctype_inner && expect_line_ending
end
parse_doctype_inner() click to toggle source

@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() click to toggle source

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
parse_html_comment() click to toggle source

@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
parse_html_conditional_comment() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 346
def parse_html_conditional_comment
  parse_html_conditional_comment_inner && expect_line_ending
end
parse_html_conditional_comment_inner() click to toggle source

@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
parse_indent() click to toggle source
# 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
parse_inline_html() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 393
def parse_inline_html
  parse_inline_html_inner && expect_line_ending
end
parse_inline_html_inner() click to toggle source

@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
parse_interpolate_line() click to toggle source

@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
parse_line_ending() click to toggle source

@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
parse_output_block() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 432
def parse_output_block
  parse_output_block_inner && expect_line_ending
end
parse_output_block_inner() click to toggle source

@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(quote) click to toggle source

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(attribute_delimiter_closing) click to toggle source

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
parse_slim_comment_block() click to toggle source

@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
parse_tag() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 122
def parse_tag
  parse_tag_inner && expect_line_ending
end
parse_tag_attribute_shortcuts() click to toggle source

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
parse_tag_inner() click to toggle source

@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() click to toggle source

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
parse_text_block() click to toggle source

@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
parse_verbatim_text_block() click to toggle source

@return [Boolean]

# File lib/slimi/parser.rb, line 376
def parse_verbatim_text_block
  parse_verbatim_text_block_inner && expect_line_ending
end
parse_verbatim_text_block_inner() click to toggle source

@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
peek_indent() click to toggle source

@return [Integer] Indent level.

# File lib/slimi/parser.rb, line 480
def peek_indent
  @scanner.match?(/[ \t]*/)
  indent_from_last_match
end
syntax_error!(syntax_error_class) click to toggle source

@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
with_position(&block) click to toggle source

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