├── .gitignore ├── MIT-LICENSE ├── README.rdoc ├── Rakefile ├── TODO ├── init.rb ├── lib ├── js_annotation_extractor.rb ├── js_fu_generator.rb ├── js_fu_matchers.rb └── js_statistics.rb ├── spec ├── jcon_integration_spec.rb ├── js_fu_generator_spec.rb ├── js_fu_matchers_spec.rb ├── read_json_arglist_spec.rb └── spec_helper.rb └── tasks ├── js_annotations.rake └── js_statistics.rake /.gitignore: -------------------------------------------------------------------------------- 1 | rdoc 2 | .\#* 3 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 Oliver Steele 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = JavaScript Fu Rails Plugin 2 | 3 | This plugin adds helpers, rake tasks, and rspec matchers for JavaScript 4 | development. 5 | 6 | == Installation 7 | 8 | git clone git://github.com/osteele/javascript_fu.git vendor/plugins/javascript_fu 9 | 10 | 11 | == Extended Rake Tasks 12 | 13 | The existing +notes+ and +statistics+ tasks are extended to compass 14 | the public/javascript directory: 15 | rake statistics 16 | rake notes 17 | 18 | 19 | == New View Helper 20 | 21 | The new +onload+ generator method generates code that executes the content 22 | of the block upon the completion of page load. 23 | page.onload do 24 | page.call alert', 'page loaded!' 25 | end 26 | generates 27 | Event.observe("window", "load", function() { alert("page loaded!"); }); 28 | or 29 | $(document).ready(function() { alert("page loaded!"); }); 30 | 31 | 32 | == New RSpec Matcher 33 | 34 | Use these in your specs to verify that a view is calling a JavaScript 35 | function: 36 | response.should call_js('fn') 37 | response.should call_js('fn(true)') 38 | response.should call_js('gApp.setup') 39 | 40 | If there is a body, the arguments to the call are parsed (as JSON) and 41 | passed to it: 42 | # response includes 43 | response.should call_js(fn') do |args| 44 | args.should == ['string', 2] 45 | end 46 | 47 | If the "JCON"[http://jcon.rubyforge.org/] gem is installed, you can 48 | use this to test arguments values against ECMAScript 4.0 types: 49 | 50 | ''.should call_js('fn') do |args| 51 | args[0].should conform_to_js('string') 52 | args[1].should conform_to_js('{x:int, y:int}') 53 | args[2].should conform_to_js('boolean') 54 | # or: 55 | args.should conform_to_js('[string, {x:int, y:int}, boolean]') 56 | end 57 | 58 | 59 | = License 60 | 61 | Copyright 2008 by {Oliver Steele}[http://workingwithrails.com/person/12359-oliver-steele]. All rights reserved. Released under the MIT License. 62 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake/rdoctask' 2 | require 'spec/rake/spectask' 3 | 4 | desc 'Default: run specs.' 5 | task :default => :spec 6 | 7 | desc 'Generate documentation for the plugin.' 8 | Rake::RDocTask.new(:rdoc) do |rdoc| 9 | rdoc.rdoc_dir = 'rdoc' 10 | rdoc.title = 'JavaScript Fu' 11 | rdoc.options << '--line-numbers' << '--inline-source' << 12 | '--main' << 'README.rdoc' 13 | rdoc.rdoc_files.include ['lib', 'README.rdoc', 'TODO', 'MIT-LICENSE'] 14 | end 15 | 16 | desc "Run all specs" 17 | Spec::Rake::SpecTask.new do |t| 18 | t.spec_files = FileList['spec/*_spec.rb'] 19 | if ENV['RCOV'] 20 | t.rcov = true 21 | t.rcov_dir = '../doc/output/coverage' 22 | t.rcov_opts = ['--exclude', 'spec\/spec'] 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | = JavaScript Fu To Do 2 | 3 | Next: 4 | - view helper 5 | - ignore framework files in public/javascript 6 | 7 | 8 | == RJS View Helper 9 | Use RJS inside a view: 10 | javascript_tag do |page| 11 | page.insert_html :bottom, 'list', "
  • #{@item.name}
  • " 12 | page << "alert('JavaScript with Prototype.');" 13 | page.call 'alert', 'My message!' 14 | end 15 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'js_fu_generator' 2 | -------------------------------------------------------------------------------- /lib/js_annotation_extractor.rb: -------------------------------------------------------------------------------- 1 | require 'source_annotation_extractor' 2 | 3 | # Monkey-patch to look in public/javascripts and in js source files 4 | class SourceAnnotationExtractor #:nodoc: 5 | def find_in_with_js(dir) 6 | results = find_in_without_js(dir) 7 | 8 | Dir.glob("#{dir}/*") do |item| 9 | case 10 | when File.basename(item)[0] == ?. 11 | when File.directory?(item) 12 | when item =~ /\.js$/ 13 | results.update(extract_annotations_from(item, /(?:\/\/|\/\*)\s*(#{tag})(?:.*:|:)?\s*(.*?)(?:\*\/)?$/)) 14 | end 15 | end 16 | 17 | results 18 | end 19 | alias_method_chain :find_in, :js 20 | 21 | def default_dirs 22 | %w[app lib test] 23 | end unless method_defined?(:default_dirs) 24 | 25 | def default_dirs_with_js 26 | default_dirs_without_js + %w[public/javascripts] 27 | end 28 | alias_method_chain :default_dirs, :js 29 | 30 | def find_with_default_dirs(dirs=default_dirs) 31 | find_without_default_dirs(dirs) 32 | end 33 | alias_method_chain :find, :default_dirs 34 | end 35 | -------------------------------------------------------------------------------- /lib/js_fu_generator.rb: -------------------------------------------------------------------------------- 1 | module ActionView #:nodoc: 2 | module Helpers #:nodoc: 3 | module PrototypeHelper #:nodoc: 4 | class JavaScriptGenerator #:nodoc: 5 | module GeneratorMethods 6 | # Executes the content of the block upon the completion of 7 | # page load. This uses Prototype's 8 | # Event.observe(window, 'load'), or 9 | # $(document).ready() if jrails is loaded. 10 | # 11 | # Example: 12 | # 13 | # # Generates: 14 | # # Event.observe("window", "load", function() { fn(); }); 15 | # # or: 16 | # # $(document).ready(function() { fn(); }); 17 | # page.onload do 18 | # page.call 'fn' 19 | # end 20 | def onload 21 | if jquery? 22 | self << "$(document).ready(function() {\n" 23 | else 24 | self << "Event.observe(\"window\", \"load\", function() {\n" 25 | end 26 | yield 27 | self << "\n});" 28 | end 29 | 30 | private 31 | def jquery? 32 | js = JavaScriptGenerator.new(nil) do |page| 33 | page.insert_html('top', 'id') 34 | end.to_s 35 | js =~ /^\$\(/ 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/js_fu_matchers.rb: -------------------------------------------------------------------------------- 1 | module JavascriptFu 2 | module Matchers 3 | class CallJS #:nodoc: 4 | def initialize(pattern, spec_scope) 5 | @expected = pattern 6 | @spec_scope = spec_scope 7 | @pattern = case 8 | when pattern.kind_of?(Regexp) 9 | pattern 10 | when pattern =~ /^[$_\w\d\.]+\(/ 11 | # already has an lparen: search verbatim 12 | pattern = /\b#{pattern}/ 13 | when pattern =~ /^new\b/ 14 | # 'new': the arglist is optional 15 | pattern = /\b#{pattern}\b\s*\((.*)?/ 16 | else 17 | # else the arglist is required 18 | pattern = /\b#{pattern}\s*\((.*)/ 19 | end 20 | end 21 | 22 | def matches?(response_or_text, &block) 23 | actual = response_or_text 24 | # Adapted from AssertSelect#matches? in rspec_on_rails: 25 | if ActionController::TestResponse === response_or_text and 26 | response_or_text.headers.key?('Content-Type') and 27 | response_or_text.headers['Content-Type'].to_sym == :xml 28 | actual = HTML::Document.new(response_or_text.body, false, true).root 29 | elsif String === response_or_text 30 | actual = HTML::Document.new(response_or_text).root 31 | end 32 | begin 33 | @spec_scope.assert_select(actual, 'script', @pattern) do |tags| 34 | if block 35 | raise "no arguments detected" unless tags[0].to_s =~ @pattern 36 | args = JavascriptFu.read_json_arglist($1) 37 | block.call(args) 38 | end 39 | end 40 | rescue ::Test::Unit::AssertionFailedError => @error 41 | end 42 | 43 | @error.nil? 44 | end 45 | 46 | def failure_message; "should #{description}, but did not"; end 47 | def negative_failure_message; "should not #{description}, but did"; end 48 | 49 | def description 50 | "call js(#{@expected.inspect})" 51 | end 52 | end 53 | 54 | # Passes if the response has a script element that contains a call 55 | # to the specified function. The argument can be a Regexp or a 56 | # String. If it is a Regexp or a String that contains 57 | # '(', the matcher simply looks for a matching tag 58 | # (using +have_tag+). Otherwise, the matcher further verifies that 59 | # the string is followed by '(', to indicate a 60 | # function call. 61 | # 62 | # Examples: 63 | # response.should call_js('fn') 64 | # response.should call_js('fn(1)') 65 | # response.should call_js('obj.fn') 66 | # response.should call_js('obj.fn(1)') 67 | # response.should call_js('new C') 68 | # response.should call_js('new C(1)') 69 | # 70 | # If block is supplied, the arguments to the function are decoded 71 | # as JSON and passed to the block: 72 | # # response includes 73 | # response.should call_js(fn') do |args| 74 | # args.should == ['string', 2] 75 | # end 76 | # 77 | # Since in this case the whole argument list is parsed as one JSON list, 78 | # it can't include any comments or non-literal expressions. 79 | def call_js(pattern, &block) 80 | return CallJS.new(pattern, self, &block) 81 | end 82 | end 83 | 84 | # Parse until the first unmatched ")]}" or end of string 85 | def self.read_json_arglist(string) 86 | require 'activesupport' 87 | scanner, level = StringScanner.new(string), 0 88 | while scanner.scan_until(/(['"\/(){}\[\]])/) 89 | token = scanner[1] 90 | break if token =~ /[)}\]]/ and level == 0 91 | case token 92 | when /[({\[]/ 93 | level += 1 94 | when /[)}\]]/ 95 | level -= 1 96 | when /'/ 97 | scanner.scan(/.*?'/) 98 | when /"/ 99 | scanner.scan(/.*?"/) 100 | else 101 | raise "unimplemented" 102 | end 103 | end 104 | string = string[0...scanner.pos-1] if scanner.pos > 0 105 | begin 106 | ActiveSupport::JSON.decode('[' + string + ']') 107 | rescue ActiveSupport::JSON::ParseError => error 108 | raise ActiveSupport::JSON::ParseError.new("#{string.inspect}") 109 | end 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/js_statistics.rb: -------------------------------------------------------------------------------- 1 | require 'code_statistics' 2 | 3 | class CodeStatistics #:nodoc: 4 | def calculate_directory_statistics_with_js(directory, pattern = /.*\.rb$/) 5 | pattern = /.*\.js$|(#{pattern})/ 6 | calculate_directory_statistics_without_js(directory, pattern) 7 | end 8 | alias_method_chain :calculate_directory_statistics, :js 9 | end 10 | -------------------------------------------------------------------------------- /spec/jcon_integration_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + "/../lib/js_fu_matchers") 3 | 4 | require 'jcon' 5 | 6 | describe :call_js do 7 | include JavascriptFu::Matchers 8 | include JCON::Matchers 9 | 10 | it "should integrate with the jcon gem" do 11 | ''.should call_js('fn') do |args| 12 | args[0].should conform_to_js('string') 13 | args[1].should conform_to_js('{x:int, y:int}') 14 | args[2].should conform_to_js('boolean') 15 | args.should conform_to_js('[string, {x:int, y:int}, boolean]') 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/js_fu_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + "/../lib/js_fu_generator") 3 | 4 | describe :JavaScriptGenerator, :onload do 5 | include ::ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods 6 | 7 | def text_from_generator 8 | ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(nil) do |page| 9 | yield page 10 | end.to_s 11 | end 12 | 13 | it "should wrap the block in Event.observe or $(document).ready" do 14 | text = text_from_generator do |page| 15 | page.onload do 16 | page.call 'alert', 'loaded!' 17 | end 18 | end 19 | text.should =~ /(\bEvent.observe\("window", "load", |\$\(document\)\.ready\()function\(\) \{\s*alert\("loaded!"\);\s*\}\);/m 20 | end 21 | 22 | it "should preserve the order of statements" do 23 | text = text_from_generator do |page| 24 | page.call 'f', 1 25 | page.onload do page.call 'f', 2 end 26 | page.call 'f', 3 27 | end 28 | text.should =~ /f\(1\).*f\(2\).*f\(3\)/m 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/js_fu_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.dirname(__FILE__) + '/spec_helper') 2 | require File.expand_path(File.dirname(__FILE__) + "/../lib/js_fu_matchers") 3 | 4 | describe :call_js do 5 | include JavascriptFu::Matchers 6 | 7 | it 'should match script content using a regexp' do 8 | string = '' 9 | string.should call_js(/fo*/) 10 | lambda { string.should call_js(/bar/) }.should raise_error("should call js(/bar/), but did not") 11 | lambda { string.should_not call_js(/foo/) }.should raise_error("should not call js(/foo/), but did") 12 | end 13 | 14 | describe "with a function name" do 15 | it 'should match a function call' do 16 | string = '' 17 | string.should call_js('fname') 18 | end 19 | 20 | it 'should match a method call' do 21 | string = '' 22 | string.should call_js('fname') 23 | string.should call_js('obj.fname') 24 | end 25 | 26 | it 'should require parentheses' do 27 | string = '' 28 | string.should_not call_js('fname') 29 | end 30 | 31 | it 'should not match part of a name' do 32 | string = '' 33 | string.should_not call_js('fnam') 34 | string.should_not call_js('name') 35 | end 36 | 37 | it 'should not match outside of a script tag' do 38 | string = '

    fname()

    ' 39 | string.should_not call_js('fname') 40 | end 41 | end 42 | 43 | describe "with a constructor" do 44 | it 'should match a constructor with arguments' do 45 | string = '' 46 | string.should call_js('new klass') 47 | end 48 | 49 | it 'should match a constructor without arguments' do 50 | string = '' 51 | string.should call_js('new klass') 52 | end 53 | 54 | it 'should not match part of a name' do 55 | string = '' 56 | string.should_not call_js('new klas') 57 | end 58 | end 59 | 60 | describe "argument list parsing" do 61 | it "should match the arguments of a function call" do 62 | string = '' 63 | string.should call_js('fname') do |args| 64 | args.should == ['string', 2] 65 | end 66 | end 67 | 68 | it "should match the arguments of a method call" do 69 | string = '' 70 | string.should call_js('obj.fname') do |args| 71 | args.should == ['string', 2] 72 | end 73 | end 74 | 75 | it "should ignore following material" do 76 | string = '' 77 | string.should call_js('fname') do |args| 78 | args.should == ['string', 2] 79 | end 80 | end 81 | 82 | it "should parse nested objects" do 83 | string = '' 84 | string.should call_js('fname') do |args| 85 | args.should == ['string', [1,2], {'a' => 3}] 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/read_json_arglist_spec.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), '/../lib/js_fu_matchers') 2 | 3 | describe JavascriptFu do 4 | describe :read_json_arglist do 5 | it "should read some values" do 6 | JavascriptFu.read_json_arglist('1,2').should == [1, 2] 7 | end 8 | 9 | it "should ignore trailing characters" do 10 | JavascriptFu.read_json_arglist('1,2)]]').should == [1, 2] 11 | end 12 | 13 | it "should enclose nested braces" do 14 | JavascriptFu.read_json_arglist('1,{a:2,b:3},4)]]').should == [1, {'a' => 2, 'b' => 3}, 4] 15 | end 16 | 17 | it "should enclose nested brackets" do 18 | JavascriptFu.read_json_arglist('1,[2,3],4)]]').should == [1, [2, 3], 4] 19 | end 20 | 21 | it "should enclose multiple nesting levels" do 22 | JavascriptFu.read_json_arglist('1,[{a:[2],b:{c:3}},[4]],5)]]').should == [1, [{'a' => [2], 'b' => {'c' => 3}}, [4]], 5] 23 | end 24 | 25 | it "should scan strings" do 26 | JavascriptFu.read_json_arglist("'str')").should == ['str'] 27 | JavascriptFu.read_json_arglist('"str")').should == ['str'] 28 | JavascriptFu.read_json_arglist("'str','ing')").should == ['str', 'ing'] 29 | end 30 | 31 | it "should scan strings with quoted quotes" do 32 | JavascriptFu.read_json_arglist('"str\\"ing")').should == ['str"ing'] 33 | end 34 | 35 | it "should scan simple regular expressions" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | raise "this spec require rspec_on_rails, and can only be run within an application that includes it" unless File.expand_path(File.dirname(__FILE__)) =~ %r|/vendor/plugins/| and File.directory?(File.dirname(__FILE__) + '/../../rspec_on_rails') 2 | 3 | require File.dirname(__FILE__) + '/../../rspec_on_rails/spec/spec_helper.rb' 4 | -------------------------------------------------------------------------------- /tasks/js_annotations.rake: -------------------------------------------------------------------------------- 1 | task :notes => "javascript:notessetup" 2 | 3 | namespace :javascript do 4 | task :notessetup do 5 | require "#{File.dirname(__FILE__)}/../lib/js_annotation_extractor" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /tasks/js_statistics.rake: -------------------------------------------------------------------------------- 1 | task :stats => "javascript:statsetup" 2 | 3 | namespace :javascript do 4 | task :statsetup do 5 | require "#{File.dirname(__FILE__)}/../lib/js_statistics" 6 | STATS_DIRECTORIES += [['JavaScript', "#{RAILS_ROOT}/public/javascripts"]].select { |_, dir| File.directory?(dir) } 7 | end 8 | end 9 | --------------------------------------------------------------------------------