# Reusable parts of Treetop (treetop.rubyforge.org/) grammar for package # definitions in v1 format.

# Some aspects of this grammar are significantly dumber than they could be # because: # # * We want to treat statements as identically as possible to their # command-line equivalents. # * Treetop parse errors are pretty inscrutable at times and we can make # error messages clearer by validating a lot of the terminals ourselves. # # Just say “NO!” to PEG parsers.

require 'treetop'

module Fig

module Grammar
  # Consumers of this need to mix in Fig::Grammar::Base and
  # Fig::Grammar::Version and have a "rule config_statement".
  grammar V1Base
    # Shim between "package" and "package_statement" rules to allow the
    # compiled v0 and v1 grammars to have the same interface.
    rule package_statement_with_ws
      package_statement:package_statement ws_or_comment+ {
        def to_package_statement(build_state)
          return package_statement.to_package_statement(build_state)
        end
      }
    end

    rule package_statement
      archive / resource / retrieve / config
    end

    rule archive
      statement_start:'archive'
      ws_or_comment+
      location:quoted_or_bare_string
      {
        def to_package_statement(build_state)
          return build_state.new_asset_statement(
            Statement::Archive, statement_start, location
          )
        end
      }
    end

    rule resource
      statement_start:'resource'
      ws_or_comment+
      location:quoted_or_bare_string
      {
        def to_package_statement(build_state)
          return build_state.new_asset_statement(
            Statement::Resource, statement_start, location
          )
        end
      }
    end

    rule retrieve
      statement_start:'retrieve'
      ws_or_comment+
      variable:environment_variable_name '->' path:quoted_or_bare_string
      {
        def to_package_statement(build_state)
          return build_state.new_retrieve_statement(
            statement_start, variable, path
          )
        end
      }
    end

    rule config
      statement_start:'config'
      ws_or_comment+
      config_name
      ws_or_comment+
      statements:config_statement_with_ws*
      'end'
      {
        def to_package_statement(build_state)
          return build_state.new_configuration_statement(
            statement_start, config_name, statements
          )
        end
      }
    end

    # Shim between "config" and "config_statement" rules to allow the
    # compiled v0 and v1 grammars to have the same interface.
    rule config_statement_with_ws
      config_statement:config_statement ws_or_comment+ {
        def to_config_statement(build_state)
          return config_statement.to_config_statement(build_state)
        end
      }
    end

    # Need rule config_statement

    rule include
      statement_start:'include' ws_or_comment+ descriptor_string {
        def to_config_statement(build_state)
          return build_state.new_include_statement(
            statement_start, descriptor_string
          )
        end
      }
    end

    rule override
      statement_start:'override' ws_or_comment+ descriptor_string {
        def to_config_statement(build_state)
          return build_state.new_override_statement(
            statement_start, descriptor_string
          )
        end
      }
    end

    rule set
      statement_start:'set' ws_or_comment+ environment_variable_name_value {
        def to_config_statement(build_state)
          return build_state.new_environment_variable_statement(
            Statement::Set, statement_start, environment_variable_name_value
          )
        end
      }
    end

    rule path
      statement_start:('add' / 'append' / 'path')
      ws_or_comment+
      environment_variable_name_value
      {
        def to_config_statement(build_state)
          return build_state.new_environment_variable_statement(
            Statement::Path, statement_start, environment_variable_name_value
          )
        end
      }
    end

    rule command
      statement_start:'command'
      ws_or_comment+
      command_line
      ws_or_comment+
      'end'
      {
        def to_config_statement(build_state)
          return build_state.new_v1_command_statement(
            statement_start, gather_command_argument_nodes(command_line)
          )
        end

        def gather_command_argument_nodes(node, arguments = [])
          if node.respond_to? 'quoted_or_bare_string?'
            arguments << node
            return arguments
          end

          return arguments if not node.elements

          node.elements.each do
            |element|
            gather_command_argument_nodes(element, arguments)
          end

          return arguments
        end
      }
    end

    rule command_line
      quoted_or_bare_string
      ! { |sequence| sequence[-1].text_value == 'end' }
      (
        ws_or_comment+
        quoted_or_bare_string
        ! { |sequence| sequence[-1].text_value == 'end' }
      )*
    end

    # Terminals

    rule descriptor_string
      [^\s#]+
    end

    rule config_name
      [a-zA-Z0-9_.-]+
    end

    rule quoted_or_bare_string
      # In order to deal with the hierarchy of nodes that the command_line
      # rule above generates, we tag each of the expressions here so that
      # they can be found in the syntax tree.
      [^\s#\\"]* '"' ( [^"\\] / '\\' . )* '"'
      { def quoted_or_bare_string?() return true end }

      /

      [^\s#\\']* "'" ( [^'\\] / '\\' . )* "'"
      { def quoted_or_bare_string?() return true end }

      /

      [^\s#]+
      { def quoted_or_bare_string?() return true end }
    end

    rule environment_variable_name
      [a-zA-Z0-9_]+
    end

    rule environment_variable_name_value
      # This is like quoted_or_bare_string, but allows for the unquoted
      # variable name followed by equals sign prior to the quotation marks
      # coming in.
      #
      # Should result in somewhat reasonable handling by Treetop when
      # encountering mis-quoted constructs.
      [^\s#\\'"]* '"' ( [^"\\] / '\\' . )* '"' /
      [^\s#\\'"]* "'" ( [^'\\] / '\\' . )* "'" /
      [^\s#]+
    end
  end
end

end