class Enolib::Parser

Public Class Methods

new(context) click to toggle source
# File lib/enolib/parser.rb, line 5
def initialize(context)
  @context = context
  @depth = 0
  @index = 0
  @line = 0
  @unresolved_non_section_elements = {}
  @unresolved_sections = {}
end

Public Instance Methods

run() click to toggle source
# File lib/enolib/parser.rb, line 14
def run
  if @context.input.empty?
    @context.line_count = 1
    return
  end

  comments = nil
  last_continuable_element = nil
  last_non_section_element = nil
  last_section = @context.document

  while @index < @context.input.length
    match = Grammar::REGEX.match(@context.input, @index)

    unless match && match.begin(0) == @index
      instruction = parse_after_error
      raise Errors::Parsing.invalid_line(@context, instruction)
    end

    instruction = {
      index: @index,
      line: @line,
      ranges: {
        line: match.offset(0)
      }
    }

    multiline_field = false

    # TODO: In all implementations we could optimize the often occurring
    #       empty line case - we need not allocate the instruction object
    #       because it is discarded anyway
    if match[Grammar::EMPTY_LINE_INDEX]

      if comments
        @context.meta.concat(comments)
        comments = nil
      end

    elsif match[Grammar::ELEMENT_OPERATOR_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      instruction[:key] = match[Grammar::KEY_UNESCAPED_INDEX]

      if instruction[:key]
        instruction[:ranges][:element_operator] = match.offset(Grammar::ELEMENT_OPERATOR_INDEX)
        instruction[:ranges][:key] = match.offset(Grammar::KEY_UNESCAPED_INDEX)
      else
        instruction[:key] = match[Grammar::KEY_ESCAPED_INDEX]
        instruction[:ranges][:element_operator] = match.offset(Grammar::ELEMENT_OPERATOR_INDEX)
        instruction[:ranges][:escape_begin_operator] = match.offset(Grammar::KEY_ESCAPE_BEGIN_OPERATOR_INDEX)
        instruction[:ranges][:escape_end_operator] = match.offset(Grammar::KEY_ESCAPE_END_OPERATOR_INDEX)
        instruction[:ranges][:key] = match.offset(Grammar::KEY_ESCAPED_INDEX)
      end

      value = match[Grammar::FIELD_VALUE_INDEX]

      if value
        instruction[:ranges][:value] = match.offset(Grammar::FIELD_VALUE_INDEX)
        instruction[:type] = :field
        instruction[:value] = value
      else
        instruction[:type] = :field_or_fieldset_or_list
      end

      instruction[:parent] = last_section
      last_section[:elements].push(instruction)
      last_continuable_element = instruction
      last_non_section_element = instruction

    elsif match[Grammar::LIST_ITEM_OPERATOR_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      instruction[:ranges][:item_operator] = match.offset(Grammar::LIST_ITEM_OPERATOR_INDEX)
      instruction[:type] = :list_item

      value = match[Grammar::LIST_ITEM_VALUE_INDEX]

      if value
        instruction[:ranges][:value] = match.offset(Grammar::LIST_ITEM_VALUE_INDEX)
        instruction[:value] = value
      end

      if !last_non_section_element
        parse_after_error(instruction)
        raise Errors::Parsing.missing_list_for_list_item(@context, instruction)
      elsif last_non_section_element[:type] == :list
        last_non_section_element[:items].push(instruction)
      elsif last_non_section_element[:type] == :field_or_fieldset_or_list
        last_non_section_element[:items] = [instruction]
        last_non_section_element[:type] = :list
      else
        parse_after_error(instruction)
        raise Errors::Parsing.missing_list_for_list_item(@context, instruction)
      end

      instruction[:parent] = last_non_section_element
      last_continuable_element = instruction

    elsif match[Grammar::FIELDSET_ENTRY_OPERATOR_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      instruction[:type] = :fieldset_entry

      instruction[:key] = match[Grammar::KEY_UNESCAPED_INDEX]

      if instruction[:key]
        instruction[:ranges][:key] = match.offset(Grammar::KEY_UNESCAPED_INDEX)
        instruction[:ranges][:entry_operator] = match.offset(Grammar::FIELDSET_ENTRY_OPERATOR_INDEX)
      else
        instruction[:key] = match[Grammar::KEY_ESCAPED_INDEX]
        instruction[:ranges][:entry_operator] = match.offset(Grammar::FIELDSET_ENTRY_OPERATOR_INDEX)
        instruction[:ranges][:escape_begin_operator] = match.offset(Grammar::KEY_ESCAPE_BEGIN_OPERATOR_INDEX)
        instruction[:ranges][:escape_end_operator] = match.offset(Grammar::KEY_ESCAPE_END_OPERATOR_INDEX)
        instruction[:ranges][:key] = match.offset(Grammar::KEY_ESCAPED_INDEX)
      end

      value = match[Grammar::FIELDSET_ENTRY_VALUE_INDEX]

      if value
        instruction[:ranges][:value] = match.offset(Grammar::FIELDSET_ENTRY_VALUE_INDEX)
        instruction[:value] = value
      end

      if !last_non_section_element
        parse_after_error(instruction)
        raise Errors::Parsing.missing_fieldset_for_fieldset_entry(@context, instruction)
      elsif last_non_section_element[:type] == :fieldset
        last_non_section_element[:entries].push(instruction)
      elsif last_non_section_element[:type] == :field_or_fieldset_or_list
        last_non_section_element[:entries] = [instruction]
        last_non_section_element[:type] = :fieldset
      else
        parse_after_error(instruction)
        raise Errors::Parsing.missing_fieldset_for_fieldset_entry(@context, instruction)
      end

      instruction[:parent] = last_non_section_element
      last_continuable_element = instruction

    elsif match[Grammar::SPACED_LINE_CONTINUATION_OPERATOR_INDEX]

      instruction[:spaced] = true
      instruction[:ranges][:spaced_line_continuation_operator] = match.offset(Grammar::SPACED_LINE_CONTINUATION_OPERATOR_INDEX)
      instruction[:type] = :continuation

      value = match[Grammar::SPACED_LINE_CONTINUATION_VALUE_INDEX]

      if value
        instruction[:ranges][:value] = match.offset(Grammar::SPACED_LINE_CONTINUATION_VALUE_INDEX)
        instruction[:value] = value
      end

      unless last_continuable_element
        parse_after_error(instruction)
        raise Errors::Parsing.missing_element_for_continuation(@context, instruction)
      end

      if last_continuable_element.has_key?(:continuations)
        last_continuable_element[:continuations].push(instruction)
      else
        if last_continuable_element[:type] == :field_or_fieldset_or_list
          last_continuable_element[:type] = :field
        end

        last_continuable_element[:continuations] = [instruction]
      end

      if comments
        @context.meta.concat(comments)
        comments = nil
      end

    elsif match[Grammar::DIRECT_LINE_CONTINUATION_OPERATOR_INDEX]

      instruction[:ranges][:direct_line_continuation_operator] = match.offset(Grammar::DIRECT_LINE_CONTINUATION_OPERATOR_INDEX)
      instruction[:type] = :continuation

      value = match[Grammar::DIRECT_LINE_CONTINUATION_VALUE_INDEX]

      if value
        instruction[:ranges][:value] = match.offset(Grammar::DIRECT_LINE_CONTINUATION_VALUE_INDEX)
        instruction[:value] = value
      end

      unless last_continuable_element
        parse_after_error(instruction)
        raise Errors::Parsing.missing_element_for_continuation(@context, instruction)
      end

      if last_continuable_element.has_key?(:continuations)
        last_continuable_element[:continuations].push(instruction)
      else
        if last_continuable_element[:type] == :field_or_fieldset_or_list
          last_continuable_element[:type] = :field
        end

        last_continuable_element[:continuations] = [instruction]
      end

      if comments
        @context.meta.concat(comments)
        comments = nil
      end

    elsif match[Grammar::SECTION_OPERATOR_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      instruction[:elements] = []
      instruction[:ranges][:section_operator] = match.offset(Grammar::SECTION_OPERATOR_INDEX)
      instruction[:type] = :section

      instruction[:key] = match[Grammar::SECTION_KEY_UNESCAPED_INDEX]

      if instruction[:key]
        instruction[:ranges][:key] = match.offset(Grammar::SECTION_KEY_UNESCAPED_INDEX)
      else
        instruction[:key] = match[Grammar::SECTION_KEY_ESCAPED_INDEX]
        instruction[:ranges][:escape_begin_operator] = match.offset(Grammar::SECTION_KEY_ESCAPE_BEGIN_OPERATOR_INDEX)
        instruction[:ranges][:escape_end_operator] = match.offset(Grammar::SECTION_KEY_ESCAPE_END_OPERATOR_INDEX)
        instruction[:ranges][:key] = match.offset(Grammar::SECTION_KEY_ESCAPED_INDEX)
      end

      template = match[Grammar::SECTION_TEMPLATE_INDEX]

      if template
        instruction[:ranges][:template] = match.offset(Grammar::SECTION_TEMPLATE_INDEX)
        instruction[:template] = template

        copy_operator_offset = match.offset(Grammar::SECTION_COPY_OPERATOR_INDEX)

        if copy_operator_offset[1] - copy_operator_offset[0] == 2
          instruction[:deep_copy] = true
          instruction[:ranges][:deep_copy_operator] = copy_operator_offset
        else
          instruction[:ranges][:copy_operator] = copy_operator_offset
        end

        if @unresolved_sections.has_key?(template)
          @unresolved_sections[template][:targets].push(instruction)
        else
          @unresolved_sections[template] = { targets: [instruction] }
        end

        instruction[:copy] = @unresolved_sections[template]
      end

      new_depth = instruction[:ranges][:section_operator][1] - instruction[:ranges][:section_operator][0]

      if new_depth == @depth + 1
        instruction[:parent] = last_section
        @depth = new_depth
      elsif new_depth == @depth
        instruction[:parent] = last_section[:parent]
      elsif new_depth < @depth
        while new_depth < @depth
          last_section = last_section[:parent]
          @depth -= 1
        end

        instruction[:parent] = last_section[:parent]
      else
        parse_after_error(instruction)
        raise Errors::Parsing.section_hierarchy_layer_skip(@context, instruction, last_section)
      end

      instruction[:parent][:elements].push(instruction)

      if template
        parent = instruction[:parent]
        while parent[:type] != :document
          parent[:deep_resolve] = true
          parent = parent[:parent]
        end
      end

      last_section = instruction
      last_continuable_element = nil
      last_non_section_element = nil

    elsif match[Grammar::MULTILINE_FIELD_OPERATOR_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      operator = match[Grammar::MULTILINE_FIELD_OPERATOR_INDEX]
      key = match[Grammar::MULTILINE_FIELD_KEY_INDEX]

      instruction[:key] = key
      instruction[:parent] = last_section
      instruction[:ranges][:multiline_field_operator] = match.offset(Grammar::MULTILINE_FIELD_OPERATOR_INDEX)
      instruction[:ranges][:key] = match.offset(Grammar::MULTILINE_FIELD_KEY_INDEX)
      instruction[:type] = :multiline_field_begin

      @index = match.end(0)

      last_section[:elements].push(instruction)
      last_continuable_element = nil
      last_non_section_element = instruction

      terminator_regex = /\n[^\S\n]*(#{operator})(?!-)[^\S\n]*(#{Regexp.escape(key)})[^\S\n]*(?=\n|$)/
      terminator_match = terminator_regex.match(@context.input, @index)

      @index += 1  # move past current char (\n) into next line
      @line += 1

      unless terminator_match
        parse_after_error
        raise Errors::Parsing.unterminated_multiline_field(@context, instruction)
      end

      end_of_multiline_field_index = terminator_match.begin(0)

      if end_of_multiline_field_index != @index - 1
        instruction[:lines] = []

        loop do
          end_of_line_index = @context.input.index("\n", @index)

          if !end_of_line_index || end_of_line_index >= end_of_multiline_field_index
            last_non_section_element[:lines].push(
              line: @line,
              ranges: {
                line: [@index, end_of_multiline_field_index],
                value: [@index, end_of_multiline_field_index]
              },
              type: :multiline_field_value
            )

            @index = end_of_multiline_field_index + 1
            @line += 1

            break
          else
            last_non_section_element[:lines].push(
              line: @line,
              ranges: {
                line: [@index, end_of_line_index],
                value: [@index, end_of_line_index]
              },
              type: :multiline_field_value
            )

            @index = end_of_line_index + 1
            @line += 1
          end
        end
      end

      instruction = {
        length: terminator_match.end(0),
        line: @line,
        ranges: {
          key: terminator_match.offset(2),
          line: [@index, terminator_match.end(0)],
          multiline_field_operator: terminator_match.offset(1)
        },
        type: :multiline_field_end
      }

      last_non_section_element[:end] = instruction
      last_non_section_element = nil

      @index = terminator_match.end(0) + 1
      @line += 1

      multiline_field = true

    elsif match[Grammar::COMMENT_OPERATOR_INDEX]

      if comments
        comments.push(instruction)
      else
        comments = [instruction]
      end

      instruction[:ranges][:comment_operator] = match.offset(Grammar::COMMENT_OPERATOR_INDEX)
      instruction[:type] = :comment

      comment = match[Grammar::COMMENT_VALUE_INDEX]

      if comment
        instruction[:comment] = comment
        instruction[:ranges][:comment] = match.offset(Grammar::COMMENT_VALUE_INDEX)
      end

    elsif match[Grammar::COPY_OPERATOR_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      template = match[Grammar::TEMPLATE_INDEX]

      instruction[:ranges][:copy_operator] = match.offset(Grammar::COPY_OPERATOR_INDEX)
      instruction[:ranges][:template] = match.offset(Grammar::TEMPLATE_INDEX)
      instruction[:template] = template
      instruction[:type] = :field_or_fieldset_or_list

      instruction[:key] = match[Grammar::KEY_UNESCAPED_INDEX]

      if instruction[:key]
        instruction[:ranges][:key] = match.offset(Grammar::KEY_UNESCAPED_INDEX)
      else
        instruction[:key] = match[Grammar::KEY_ESCAPED_INDEX]
        instruction[:ranges][:escape_begin_operator] = match.offset(Grammar::KEY_ESCAPE_BEGIN_OPERATOR_INDEX)
        instruction[:ranges][:escape_end_operator] = match.offset(Grammar::KEY_ESCAPE_END_OPERATOR_INDEX)
        instruction[:ranges][:key] = match.offset(Grammar::KEY_ESCAPED_INDEX)
      end

      instruction[:parent] = last_section
      last_section[:elements].push(instruction)
      last_continuable_element = nil
      last_non_section_element = instruction

      if @unresolved_non_section_elements.has_key?(template)
        @unresolved_non_section_elements[template][:targets].push(instruction)
      else
        @unresolved_non_section_elements[template] = { targets: [instruction] }
      end

      instruction[:copy] = @unresolved_non_section_elements[template]
    elsif match[Grammar::KEY_UNESCAPED_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      instruction[:key] = match[Grammar::KEY_UNESCAPED_INDEX]
      instruction[:ranges][:key] = match.offset(Grammar::KEY_UNESCAPED_INDEX)
      instruction[:type] = :empty

      instruction[:parent] = last_section
      last_section[:elements].push(instruction)
      last_continuable_element = nil
      last_non_section_element = instruction

    elsif match[Grammar::KEY_ESCAPED_INDEX]

      if comments
        instruction[:comments] = comments
        comments = nil
      end

      instruction[:key] = match[Grammar::KEY_ESCAPED_INDEX]
      instruction[:ranges][:escape_begin_operator] = match.offset(Grammar::KEY_ESCAPE_BEGIN_OPERATOR_INDEX)
      instruction[:ranges][:escape_end_operator] = match.offset(Grammar::KEY_ESCAPE_END_OPERATOR_INDEX)
      instruction[:ranges][:key] = match.offset(Grammar::KEY_ESCAPED_INDEX)
      instruction[:type] = :empty

      instruction[:parent] = last_section
      last_section[:elements].push(instruction)
      last_continuable_element = nil
      last_non_section_element = instruction

    end

    unless multiline_field
      @index = match.end(0) + 1
      @line += 1
    end
  end

  @context.line_count = @context.input[-1] == "\n" ? @line + 1 : @line
  @context.meta.concat(comments) if comments

  resolve unless @unresolved_non_section_elements.empty? && @unresolved_sections.empty?
end

Private Instance Methods

consolidate_non_section_elements(element, template) click to toggle source
# File lib/enolib/parser.rb, line 505
def consolidate_non_section_elements(element, template)
  if template.has_key?(:comments) && !element.has_key?(:comments)
    element[:comments] = template[:comments]
  end

  case element[:type]
  when :field_or_fieldset_or_list
    case template[:type]
    when :multiline_field_begin
      element[:type] = :field
      mirror(element, template)
    when :field
      element[:type] = :field
      mirror(element, template)
    when :fieldset
      element[:type] = :fieldset
      mirror(element, template)
    when :list
      element[:type] = :list
      mirror(element, template)
    end
  when :fieldset
    case template[:type]
    when :fieldset
      element[:extend] = template
    when :field, :list, :multiline_field_begin
      raise Errors::Parsing.missing_fieldset_for_fieldset_entry(@context, element[:entries].first)
    end
  when :list
    case template[:type]
    when :list
      element[:extend] = template
    when :field, :fieldset, :multiline_field_begin
      raise Errors::Parsing.missing_list_for_list_item(@context, element[:items].first)
    end
  end
end
consolidate_sections(section, template, deep_merge) click to toggle source
# File lib/enolib/parser.rb, line 543
def consolidate_sections(section, template, deep_merge)
  if template.has_key?(:comments) && !section.has_key?(:comments)
    section[:comments] = template[:comments]
  end

  if section[:elements].empty?
    mirror(section, template)
  else
    # TODO: Handle possibility of two templates (one hardcoded in the document, one implicitly derived through deep merging)
    #       Possibly also elswhere (e.g. up there in the mirror branch?)
    section[:extend] = template

    return unless deep_merge

    merge_map = {}

    section[:elements].each do |section_element|
      merge_map[section_element[:key]] =
        if section_element[:type] != :section || merge_map.has_key?(section_element[:key])
          false # non-mergable (no section or multiple instructions with same key)
        else
          { section: section_element }
        end
    end

    template[:elements].each do |section_element|
      next unless merge_map.has_key?(section_element[:key])

      merger = merge_map[section_element[:key]]

      next unless merger

      if section_element[:type] != :section || merger.has_key?(:template)
        merge_map[section_element[:key]] = false # non-mergable (no section or multiple template instructions with same key)
      else
        merger[:template] = section_element
      end
    end

    merge_map.each_value do |merger|
      if merger && merger.has_key?(:template)
        consolidate_sections(merger[:section], merger[:template], true)
      end
    end
  end
end
index(section) click to toggle source
# File lib/enolib/parser.rb, line 598
def index(section)
  section[:elements].each do |element|
    if element[:type] == :section
      index(element)

      if @unresolved_sections &&
         @unresolved_sections.has_key?(element[:key]) &&
         element[:key] != element[:template]
        copy_data = @unresolved_sections[element[:key]]

        if copy_data.has_key?(:template)
          raise Errors::Parsing.two_or_more_templates_found(@context, copy_data[:targets].first, copy_data[:template], element)
        end

        copy_data[:template] = element
      end
    elsif @unresolved_non_section_elements &&
          @unresolved_non_section_elements.has_key?(element[:key]) &&
          element[:key] != element[:template]
      copy_data = @unresolved_non_section_elements[element[:key]]

      if copy_data.has_key?(:template)
        raise Errors::Parsing.two_or_more_templates_found(@context, copy_data[:targets].first, copy_data[:template], element)
      end

      copy_data[:template] = element
    end
  end
end
mirror(element, template) click to toggle source
# File lib/enolib/parser.rb, line 590
def mirror(element, template)
  if template.has_key?(:mirror)
    element[:mirror] = template[:mirror]
  else
    element[:mirror] = template
  end
end
parse_after_error(error_instruction = nil) click to toggle source
# File lib/enolib/parser.rb, line 699
def parse_after_error(error_instruction = nil)
  if error_instruction
    @context.meta.push(error_instruction)
    @index = error_instruction[:ranges][:line][RANGE_END]
    @line += 1
  end

  while @index < @context.input.length
    end_of_line_index = @context.input.index("\n", @index) || @context.input.length

    instruction = {
      line: @line,
      ranges: { line: [@index, end_of_line_index] },
      type: :unparsed
    }

    error_instruction ||= instruction

    @context.meta.push(instruction)
    @index = end_of_line_index + 1
    @line += 1
  end

  @context.line_count = @context.input[-1] == "\n" ? @line + 1 : @line

  error_instruction
end
resolve() click to toggle source
# File lib/enolib/parser.rb, line 628
def resolve
  index(@context.document)

  if @unresolved_non_section_elements
    @unresolved_non_section_elements.each_value do |copy|
      unless copy.has_key?(:template)
        raise Errors::Parsing.non_section_element_not_found(@context, copy[:targets].first)
      end

      copy[:targets].each do |target|
        resolve_non_section_element(target) if target.has_key?(:copy)
      end
    end
  end

  if @unresolved_sections
    @unresolved_sections.each_value do |copy|
      unless copy.has_key?(:template)
        raise Errors::Parsing.section_not_found(@context, copy[:targets].first)
      end

      copy[:targets].each do |target|
        resolve_section(target) if target.has_key?(:copy)
      end
    end
  end
end
resolve_non_section_element(element, previous_elements = []) click to toggle source
# File lib/enolib/parser.rb, line 656
def resolve_non_section_element(element, previous_elements = [])
  if previous_elements.include?(element)
    raise Errors::Parsing.cyclic_dependency(@context, element, previous_elements)
  end

  template = element[:copy][:template]

  if template.has_key?(:copy)
    resolve_non_section_element(template, [*previous_elements, element])
  end

  consolidate_non_section_elements(element, template)

  element.delete(:copy)
end
resolve_section(section, previous_sections = []) click to toggle source
# File lib/enolib/parser.rb, line 672
def resolve_section(section, previous_sections = [])
  if previous_sections.include?(section)
    raise Errors::Parsing.cyclic_dependency(@context, section, previous_sections)
  end

  if section.has_key?(:deep_resolve)
    section[:elements].each do |section_element|
      if section_element[:type] == :section &&
         (section_element.has_key?(:copy) || section_element.has_key?(:deep_resolve))
        resolve_section(section_element, [*previous_sections, section])
      end
    end
  end

  if section.has_key?(:copy)
    template = section[:copy][:template]

    if template.has_key?(:copy) || template.has_key?(:deep_resolve)
      resolve_section(template, [*previous_sections, section])
    end

    consolidate_sections(section, template, section.has_key?(:deep_copy))

    section.delete(:copy)
  end
end