lib/opal/context.rb



module Opal
  class Context
    # Options are mainly just passed onto the builder/parser.
    def initialize(options = {})
      @options      = options
      @root_dir     = options[:dir] || Dir.getwd
      @builder      = Opal::Builder.new
      @loaded_paths = false

      setup_v8
    end

    ##
    # Require the given id as if it was required in the context. This simply
    # passes the require through to the underlying context.

    def require_file(path)
      setup_v8
      @v8.eval "opal.run(function() {opal.require('#{path}');});", path
      finish
    end

    # Set ARGV for the context.
    # @param [Array<String>] args
    def argv=(args)
      puts "setting argv to #{args.inspect}"
      @v8.eval "opal.runtime.cs(opal.runtime.Object, 'ARGV', #{args.inspect});"
    end

    # Start normal js repl
    def start_repl
      require 'readline'
      setup_v8

      loop do
        # on SIGINT lets just return from the loop..
        trap("SIGINT") { finish; return }
        line = Readline.readline '>> ', true

        # if we type exit, then we need to close down context
        if line == "exit"
          break
        end

        puts "=> #{eval line, '(opal)'}"
      end

      finish
    end

    def eval(content, file = "(opal)", line = "")
      js = @builder.parse content, @options
      code =  "opal.run(function() { var result = (#{js})(opal.runtime, "
      code += "opal.runtime.top, '(opal)'); if (result == null || !result"
      code += ".m$inspect) { return '(Object does not support #inspect)'; }"
      code += "else { return result.m$inspect() } });"

      @v8.eval code, file
    end

    # Finishes the context, i.e. tidy everything up. This will cause
    # the opal runtime to do it's at_exit() calls (if applicable) and
    # then the v8 context will de removed. It can be reset by calling
    # #setup_v8
    def finish
      return unless @v8
      @v8.eval "opal.runtime.do_at_exit()", "(opal)"

      @v8 = nil
    end

    # Setup the context. This basically loads opal.js into our context, and
    # replace the loader etc with our custom loader for a Ruby environment. The
    # default "browser" loader cannot access files from disk.
    def setup_v8
      return if @v8

      begin
        require 'v8'
      rescue LoadError => e
        abort "therubyracer is required for running javascript. Install it with `gem install therubyracer`"
      end

      @v8 = V8::Context.new
      @v8['console'] = Console.new

      @v8.eval File.read(OPAL_JS_PATH), "(opal)"
      opal = @v8['opal']
      opal['fs'] = FileSystem.new self

      # FIXME: we cant use a ruby array as a js array :(
      opal['loader'] = Loader.new self, @v8.eval("[]")
    end

    ##
    # Console class is used to mimic the console object in web browsers
    # to allow simple debugging to the stdout.

    class Console
      def log(*str)
        puts str.join("\n")
        nil
      end
    end

    ##
    # FileSystem is used to interact with the file system from the ruby
    # version of opal. The methods on this class replace the default ones
    # made available in the web browser.

    class FileSystem

      def initialize(context)
        @context = context
      end

      def cwd
        Dir.getwd
      end

      def glob(*arr)
        Dir.glob arr
      end

      def exist_p(path)
        File.exist? path
      end

      def expand_path(filename, dir_string = nil)
        File.expand_path filename, dir_string
      end

      def dirname(file)
        File.dirname file
      end

      def join(*parts)
        File.join *parts
      end
    end

    # Loader for v8 context
    class Loader

      attr_reader :paths

      def initialize(context, paths)
        @context = context
        @paths = paths
      end

      def resolve_lib(id)
        resolved = find_lib id
        raise "Cannot find lib `#{id}'" unless resolved

        resolved
      end

      def find_lib(id)
        @paths.each do |path|
          candidate = File.join path, "#{id}.rb"
          return candidate if File.exists? candidate

          candidate = File.join path, id
          return candidate if File.exists? candidate
        end

        return File.expand_path id if File.exists? id
        return File.expand_path(id + '.rb') if File.exists?(id + '.rb')

        nil
      end

      def ruby_file_contents(filename)
        Opal::Parser.new.parse File.read(filename)
      end

      def wrap(content, filename)
        code = "(function($rb, self, __FILE__) { #{content} });"
        @context.eval code, filename
        # code
      end
    end

  end
end