lib/cucumber/js_support/js_language.rb



begin
  require 'v8'
rescue LoadError
  gem 'therubyracer', '~> 0.7.1'
  require 'v8'
end

require 'cucumber/js_support/js_snippets'

module Cucumber
  module JsSupport

    def self.argument_safe_string(string)
      arg_string = string.to_s.gsub(/[']/, '\\\\\'')
      arg_string.gsub("\n", '\n')
    end

    class JsWorld
      def initialize
        @world = V8::Context.new
      end

      def execute(js_function, args=[])
        js_function.call(*args)
      end

      def method_missing(method_name, *args)
        @world.send(method_name, *args)
      end
    end

    class JsStepDefinition
      def initialize(js_language, regexp, js_function)
        @js_language, @regexp, @js_function = js_language, regexp.to_s, js_function
      end

      def invoke(args)
        args = @js_language.execute_transforms(args)
        @js_language.current_world.execute(@js_function, args)
      end

      def regexp_source
        @regexp.inspect
      end

      def arguments_from(step_name)
        matches = eval_js "#{@regexp}.exec('#{step_name}')"
        if matches
          matches.to_a[1..-1].map do |match|
            JsArg.new(match)
          end
        end
      end

      def file_colon_line
        # Not possible yet to get file/line of js function with V8/therubyracer
        ""
      end
    end

    class JsHook
      def initialize(js_language, tag_expressions, js_function)
        @js_language, @tag_expressions, @js_function = js_language, tag_expressions, js_function
      end

      def tag_expressions
        @tag_expressions
      end

      def invoke(location, scenario)
        @js_language.current_world.execute(@js_function)
      end
    end

    class JsTransform
      def initialize(js_language, regexp, js_function)
        @js_language, @regexp, @js_function = js_language, regexp.to_s, js_function
      end

      def match(arg)
        arg = JsSupport.argument_safe_string(arg)
        matches = (eval_js "#{@regexp}.exec('#{arg}');").to_a
        matches.empty? ? nil : matches[1..-1]
      end

      def invoke(arg)
        @js_function.call([arg])
      end
    end

    class JsArg
      def initialize(arg)
        @arg = arg
      end

      def val
        @arg
      end

      def offset
      end
    end

    class JsLanguage
      include LanguageSupport::LanguageMethods
      include JsSnippets

      def initialize(runtime)
        @step_definitions = []
        @world = JsWorld.new
        @runtime = runtime

        @world["jsLanguage"] = self
        @world.load(File.dirname(__FILE__) + '/js_dsl.js')
      end

      def load_code_file(js_file)
        @world.load(js_file)
      end

      def world(js_files)
        js_files.each do |js_file|
          load_code_file("#{path_to_load_js_from}#{js_file}")
        end
      end

      def alias_adverbs(adverbs)
      end

      def begin_scenario(scenario)
        @language = scenario.language
      end

      def end_scenario
      end

      def step_matches(name_to_match, name_to_format)
        @step_definitions.map do |step_definition|
          if(arguments = step_definition.arguments_from(name_to_match))
            StepMatch.new(step_definition, name_to_match, name_to_format, arguments)
          else
            nil
          end
        end.compact
      end

      def add_step_definition(regexp, js_function)
        @step_definitions << JsStepDefinition.new(self, regexp, js_function)
      end

      #TODO: support multiline arguments when calling steps from within steps
      def execute_step_definition(name, multiline_argument = nil)
        @runtime.step_match(name).invoke(multiline_argument)
      end

      def register_js_hook(phase, tag_expressions, js_function)
        add_hook(phase, JsHook.new(self, tag_expressions, js_function))
      end

      def register_js_transform(regexp, js_function)
        add_transform(JsTransform.new(self, regexp, js_function))
      end

      def current_world
        @world
      end

      def steps(steps_text, file_colon_line)
        @runtime.invoke_steps(steps_text, @language, file_colon_line)
      end

      private
      def path_to_load_js_from
        paths = @runtime.features_paths
        if paths.empty?
          '' # Using rake
        else
          path = paths[0][/(^.*\/?features)/, 0]
          path ? "#{path}/../" : '../'
        end
      end

    end
  end
end