require 'parser/current' require 'ruby2js' module Ruby2JS module Filter module AngularRB include SEXP def self.s(type, *args) Parser::AST::Node.new type, args end Angular = s(:const, nil, :Angular) # convert simple assignments, simple method calls, and simple method # definitions into a hash when possible; return false otherwise def self.hash(pairs) if pairs.length == 1 and pairs.first.type == :begin pairs = pairs.first.children end s(:hash, *pairs.map {|pair| if pair.type == :send and pair.children[0] == nil s(:pair, s(:sym, pair.children[1]), pair.children[2]) elsif pair.type == :lvasgn s(:pair, s(:sym, pair.children[0]), pair.children[1]) elsif pair.type == :def s(:pair, s(:sym, pair.children[0]), s(:block, s(:send, nil, :proc), *pair.children[1..-1])) else return false end }) end def initialize(*args) @ngApp = nil @ngContext = nil @ngAppUses = [] @ngClassUses = [] @ngClassOmit = [] super end # input: # module Angular::AppName # use :Dependency # ... # end # # output: # AppName = angular.module("AppName", ["Dependency"]) # ... def on_module(node) module_name = node.children[0] parent_name = module_name.children[0] return super unless parent_name == Angular @ngApp = s(:lvar, module_name.children[1]) @ngChildren = node.children[1..-1] while @ngChildren.length == 1 and @ngChildren.first and @ngChildren.first.type == :begin @ngChildren = @ngChildren.first.children.dup end @ngAppUses = [] block = process_all(node.children[1..-1]) # convert use calls into dependencies depends = @ngAppUses.map {|sym| s(:sym, sym)} + extract_uses(block) depends = depends.map {|node| node.children.first.to_s}.uniq. map {|sym| s(:str, sym)} ngApp, @ngApp, @ngChildren = @ngApp, nil, nil name = module_name.children[1].to_s # construct app app = s(:send, s(:lvar, :angular), :module, s(:str, name), s(:array, *depends.uniq)) # return a single chained statement when there is only one call block.compact! if block.length == 0 return app elsif block.length == 1 call = block.first.children.first if block.first.type == :send and call == ngApp return block.first.updated nil, [app, *block.first.children[1..-1]] elsif block.first.type == :block and call.children.first == ngApp call = call.updated nil, [app, *call.children[1..-1]] return block.first.updated nil, [call, *block.first.children[1..-1]] end end # replace module with a constant assign followed by the module # contents all wrapped in an anonymous function s(:send, s(:block, s(:send, nil, :lambda), s(:args), s(:begin, s(:casgn, nil, name, app), *block)), :[]) end # input: # class name {...} # # output: # app.factory(uses) do # ... # end def on_class(node) return super unless @ngApp and @ngChildren.include? node name = node.children.first if name.children.first == nil @ngClassUses, @ngClassOmit = [], [] block = [node.children.last] uses = extract_uses(block) node = s(:class, name, node.children[1], s(:begin, *process_all(block))) @ngClassUses -= @ngClassOmit + [name.children.last] args = @ngClassUses.map {|sym| s(:arg, sym)} + uses args = args.map {|node| node.children.first.to_sym}.uniq. map {|sym| s(:arg, sym)} @ngClassUses, @ngClassOmit = [], [] s(:block, s(:send, @ngApp, :factory, s(:sym, name.children.last)), s(:args, *args), s(:begin, node, s(:return, s(:const, nil, name.children.last)))) else super end end # input: # filter :name { ... } # controller :name { ... } # factory :name { ... } # directive :name { ... } def on_block(node) ngApp = @ngApp call = node.children.first target = call.children.first if target and target.type == :const and target.children.first == Angular @ngApp = s(:send, s(:lvar, :angular), :module, s(:str, target.children.last.to_s)) else return super if target return super unless @ngApp end begin case call.children[1] when :controller ng_controller(node, :controller) when :factory ng_factory(node) when :filter ng_filter(node) when :directive hash = AngularRB.hash(node.children[2..-1]) if hash node = node.updated nil, [*node.children[0..1], s(:return, hash)] end ng_controller(node, :directive) when :watch ng_watch(node) else super end ensure @ngApp = ngApp end end # input: # controller :name do # ... # end # # output: # AppName.controller("name", do |uses| # ... # end def ng_controller(node, scope) ngContext, @ngContext = @ngContext, scope @ngClassUses, @ngClassOmit = [], [] target = node.children.first target = target.updated(nil, [@ngApp, *target.children[1..-1]]) block = process_all(node.children[2..-1]) # convert use calls into args @ngClassUses -= @ngClassOmit args = node.children[1].children args += @ngClassUses.map {|sym| s(:arg, sym)} + extract_uses(block) args = args.map {|node| node.children.first.to_sym}.uniq. map {|sym| s(:arg, sym)} node.updated :block, [target, s(:args, *args), s(:begin, *block)] ensure @ngClassUses, @ngClassOmit = [], [] @ngContext = ngContext end # input: # filter :name do |input| # ... # end # # output: # AppName.filter :name do # return lambda {|input| return ... } # end EXPRESSION = [ :and, :array, :attr, :const, :cvar, :defined?, :dstr, :dsym, :false, :float, :gvar, :hash, :int, :ivar, :lvar, :nil, :not, :or, :regexp, :self, :send, :str, :sym, :true, :undefined?, :xstr ] def ng_filter(node) @ngClassUses, @ngClassOmit = [], [] call = node.children.first # insert return args = process_all(node.children[1].children) block = process_all(node.children[2..-1]) uses = (@ngClassUses - @ngClassOmit).uniq.map {|sym| s(:arg, sym)} tail = [block.pop || s(:nil)] while tail.length == 1 and tail.first.type == :begin tail = tail.first.children.dup end tail.push s(:return, tail.pop) if EXPRESSION.include? tail.last.type block.push (tail.length == 1 ? tail.first : s(:begin, *tail)) # construct a function returning a function inner = s(:block, s(:send, nil, :lambda), s(:args, *args), *block) outer = s(:send, @ngApp, :filter, *call.children[2..-1]) node.updated nil, [outer, s(:args, *uses), s(:return, inner)] end # input: # factory :name do |uses| # ... # end # # output: # AppName.factory :name, [uses, lambda {|uses| ...}] def ng_factory(node) call = node.children.first call = call.updated(nil, [@ngApp, *call.children[1..-1]]) # insert return block = process_all(node.children[2..-1]) tail = [block.pop || s(:nil)] while tail.length == 1 and tail.first.type == :begin tail = tail.first.children.dup end tail.push s(:return, tail.pop) unless tail.last.type == :return block.push (tail.length == 1 ? tail.first : s(:begin, *tail)) # extract dependencies @ngClassUses.delete call.children[2].children[0] args = process_all(node.children[1].children) args += @ngClassUses.map {|sym| s(:arg, sym)} + extract_uses(block) args = args.map {|node| node.children.first.to_sym}.uniq. map {|sym| s(:arg, sym)} # construct a function function = s(:block, s(:send, nil, :lambda), s(:args, *args), *block) array = args.map {|arg| s(:str, arg.children.first.to_s)} s(:send, *call.children, s(:array, *array, function)) end # input: # Constant = ... # # output: # AppName.factory :name, [uses, lambda {|uses| ...}] def on_casgn(node) return super if node.children[0] @ngClassOmit << node.children[1] return super unless @ngApp and @ngChildren.include? node ng_factory s(:block, s(:send, nil, :factory, s(:sym, node.children[1])), s(:args), process(node.children[2])) end # convert ivar referencess in controllers to $scope def on_ivar(node) if @ngContext == :controller process s(:attr, s(:gvar, :$scope), node.children.first.to_s[1..-1]) else super end end # input: # watch 'expression' do |oldvalue, newvalue| # ... # end # # output: # $scope.$watch 'expression' do |oldvalue, newvalue| # ... # end def ng_watch(node) call = node.children.first if @ngContext == :controller and call.children.first == nil call = s(:send, s(:gvar, :$scope), :$watch, *call.children[2..-1]) node = node.updated nil, [call, *node.children[1..-1]] end return process node end # convert ivar assignments in controllers to $scope def on_ivasgn(node) if @ngContext == :controller if node.children.length == 1 process s(:attr, s(:gvar, :$scope), "#{node.children.first.to_s[1..-1]}") else process s(:send, s(:gvar, :$scope), "#{node.children.first.to_s[1..-1]}=", node.children.last) end else super end end # convert instance method definitions in controllers to $scope def on_def(node) if @ngContext == :controller process s(:defs, s(:gvar, :$scope), *node.children) else super end end def on_gvar(node) if @ngClassUses @ngClassUses << node.children.first end super end BUILTINS = [ :Array, :Boolean, :Date, :Error, :Function, :Infinity, :JSON, :Math, :NaN, :Number, :Object, :RegExp, :String ] def on_const(node) if @ngClassUses and not node.children.first unless BUILTINS.include? node.children.last @ngClassUses << node.children.last end end super end def extract_uses(block) # find the block while block.length == 1 and block.first and block.first.type == :begin block.push *block.shift.children end # find use class method calls uses = block.find_all do |node| node and node.type == :send and node.children[0..1] == [nil, :use] end # convert use calls into dependencies depends = [] uses.each do |use| use.children[2..-1].each do |node| depends << node if [:str, :sym].include? node.type end block.delete use end depends end end DEFAULTS.push AngularRB end end