├── dist └── .gitignore ├── pkg └── .gitignore ├── .gitignore ├── spec ├── script │ ├── autotest │ └── rstakeout ├── base.js ├── json.html ├── results.html ├── lib │ ├── JSSpec.css │ ├── diff_match_patch.js │ └── JSSpec.js ├── gsa.html └── fixtures │ └── results.js ├── src ├── gsa-prototype.js.erb ├── json.js ├── results.js └── gsa.js ├── lib ├── protodoc.rb └── builder.js ├── LICENSE ├── README ├── Rakefile └── xsl └── json.xsl /dist/.gitignore: -------------------------------------------------------------------------------- 1 | [^.]* 2 | -------------------------------------------------------------------------------- /pkg/.gitignore: -------------------------------------------------------------------------------- 1 | [^.]* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | demo.html -------------------------------------------------------------------------------- /spec/script/autotest: -------------------------------------------------------------------------------- 1 | spec/script/rstakeout "rake spec" spec/*.html src/*.js -------------------------------------------------------------------------------- /spec/base.js: -------------------------------------------------------------------------------- 1 | //Prototype and Scriptaculous' Builder are required 2 | if(typeof Prototype == 'undefined') 3 | throw("prototype.js version > 1.6 is required"); 4 | if(typeof Builder == 'undefined') 5 | throw("script.aculo.us' builder.js library is required"); 6 | 7 | var Gsa = {}; -------------------------------------------------------------------------------- /src/gsa-prototype.js.erb: -------------------------------------------------------------------------------- 1 | <%= include '../lib/prototype.js' if ENV['WITH_PROTOTYPE'] || ENV['WITH_BUILDER'] %><%= include '../lib/builder.js' if ENV['WITH_BUILDER'] %>/* gsa-prototype, version <%= GSA_VERSION %> 2 | * (c) 2008 Jesse Newland 3 | * jnewland@gmail.com 4 | * 5 | * gsa-prototype is freely distributable under the terms of an MIT-style license. 6 | *--------------------------------------------------------------------------*/ 7 | 8 | //Prototype and Scriptaculous' Builder are required 9 | if(typeof Prototype == 'undefined') 10 | throw("prototype.js version > 1.6 is required"); 11 | if(typeof Builder == 'undefined') 12 | throw("script.aculo.us' builder.js library is required"); 13 | 14 | var Gsa = {}; 15 | 16 | <%= include 'gsa.js', 'results.js', 'json.js' %> -------------------------------------------------------------------------------- /lib/protodoc.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | 3 | class String 4 | def lines 5 | split $/ 6 | end 7 | 8 | def strip_whitespace_at_line_ends 9 | lines.map {|line| line.gsub(/\s+$/, '')} * $/ 10 | end 11 | end 12 | 13 | module Protodoc 14 | module Environment 15 | def include(*filenames) 16 | filenames.map {|filename| Preprocessor.new(filename).to_s}.join("\n") 17 | end 18 | end 19 | 20 | class Preprocessor 21 | include Environment 22 | 23 | def initialize(filename) 24 | @filename = File.expand_path(filename) 25 | @template = ERB.new(IO.read(@filename), nil, '%') 26 | end 27 | 28 | def to_s 29 | @template.result(binding).strip_whitespace_at_line_ends 30 | end 31 | end 32 | end 33 | 34 | if __FILE__ == $0 35 | print Protodoc::Preprocessor.new(ARGV.first) 36 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008 Jesse Newland 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | gsa-prototype 2 | ============= 3 | 4 | Prototype/Javascript wrapper for the Google Search Appliance Search Protocol. Fancy cross-domain JSON support included. 5 | 6 | Install 7 | ======= 8 | 9 | gsa-prototype requires a custom XSL be installed on your Google Search Appliance 10 | 11 | * Login to the GSA Admin Console 12 | * Click 'Serving' on the sidebar 13 | * Create a new frontend named 'json' 14 | * Click 'Edit' beside the newly created frontend 15 | * Click 'Edit underlying XSLT code' 16 | * Select 'Import Stylesheet' 17 | * Import the template at xsl/json.xsl 18 | * Done! 19 | 20 | Usage 21 | ===== 22 | 23 | >>> var gsa = new Gsa('foo.com') 24 | >>> gsa.search('jesse newland') 25 | true 26 | >>> gsa.results.first().get('title') 27 | "LexBlog IT Director talks about today's platform upgrade : Real ..." 28 | >>> gsa.results.first().get('url') 29 | "http://kevin.lexblog.com/2007/07/articles/cool-stuff/lexblog-it-director-talks-about-todays-platform-upgrade/" 30 | 31 | See inline documentation in gsa.js for more details. -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/packagetask' 3 | 4 | desc 'Specs!' 5 | task :test => :spec 6 | 7 | GSA_ROOT = File.expand_path(File.dirname(__FILE__)) 8 | GSA_SRC_DIR = File.join(GSA_ROOT, 'src') 9 | GSA_DIST_DIR = File.join(GSA_ROOT, 'dist') 10 | GSA_PKG_DIR = File.join(GSA_ROOT, 'pkg') 11 | GSA_VERSION = '0.2.0' 12 | 13 | desc 'Build a combined JS file for distibution' 14 | task :dist do 15 | $:.unshift File.join(GSA_ROOT, 'lib') 16 | require 'protodoc' 17 | 18 | Dir.chdir(GSA_SRC_DIR) do 19 | File.open(File.join(GSA_DIST_DIR, 'gsa-prototype.js'), 'w+') do |dist| 20 | dist << Protodoc::Preprocessor.new('gsa-prototype.js.erb') 21 | end 22 | end 23 | end 24 | 25 | Rake::PackageTask.new('gsa-prototype', GSA_VERSION) do |package| 26 | package.need_tar_gz = true 27 | package.package_dir = GSA_PKG_DIR 28 | package.package_files.include( 29 | '[A-Z]*', 30 | 'dist/gsa-prototype.js', 31 | 'lib/**', 32 | 'src/**', 33 | 'spec/**', 34 | 'xsl/**' 35 | ) 36 | end 37 | 38 | task :clean_package_source do 39 | rm_rf File.join(GSA_PKG_DIR, "gsa-prototype-#{VERSION}") 40 | end 41 | 42 | task :spec do 43 | files = ENV['STAKEOUT'] rescue 'spec/*.html' 44 | files = FileList[files] 45 | 46 | files.each do |file| 47 | if file =~ /\/([^\/]+)\.js$/ 48 | file = "spec/#{$1}.html" 49 | end 50 | unless File.exists?(file) 51 | puts "Notice: Test file does not exist: #{file}" 52 | next 53 | end 54 | `open #{file} -a Safari -g` 55 | end 56 | end 57 | 58 | #TODO create a task to build with builder.js prepended 59 | #TODO create a task to build with prototype.js and builder.js prepended 60 | #TODO create a task to compress the JS file we build -------------------------------------------------------------------------------- /spec/json.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSSpec results 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 43 | 44 |

A

B

45 | -------------------------------------------------------------------------------- /spec/script/rstakeout: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | ## 4 | # Originally by Mike Clark. 5 | # 6 | # From http://www.pragmaticautomation.com/cgi-bin/pragauto.cgi/Monitor/StakingOutFileChanges.rdoc 7 | # 8 | # Runs a user-defined command when files are modified. 9 | # 10 | # Like autotest, but more customizable. This is useful when you want to do 11 | # something other than run tests. For example, generate a PDF book, run 12 | # a single test, or run a legacy Test::Unit suite in an app that also 13 | # has an rSpec suite. 14 | # 15 | # Can use Ruby's Dir[] to get file glob. Quote your args to take advantage of this. 16 | # 17 | # rstakeout 'rake test:recent' **/*.rb 18 | # => Only watches Ruby files one directory down (no quotes) 19 | # 20 | # rstakeout 'rake test:recent' '**/*.rb' 21 | # => Watches all Ruby files in all directories and subdirectories 22 | # 23 | # Modified (with permission) by Geoffrey Grosenbach to call growlnotify for 24 | # rspec and Test::Unit output. 25 | # 26 | # See the PeepCode screencast on rSpec or other blog articles for instructions on 27 | # setting up growlnotify. 28 | 29 | command = ARGV.shift 30 | files = {} 31 | 32 | ARGV.each do |arg| 33 | Dir[arg].each { |file| 34 | files[file] = File.mtime(file) 35 | } 36 | end 37 | 38 | puts "Watching #{files.keys.join(', ')}\n\nFiles: #{files.keys.length}" 39 | 40 | trap('INT') do 41 | puts "\nQuitting..." 42 | exit 43 | end 44 | 45 | 46 | loop do 47 | 48 | sleep 1 49 | 50 | changed_file, last_changed = files.find { |file, last_changed| 51 | File.mtime(file) > last_changed 52 | } 53 | 54 | if changed_file 55 | files[changed_file] = File.mtime(changed_file) 56 | puts "=> #{changed_file} changed, running env STAKEOUT=#{changed_file} #{command}" 57 | results = `env STAKEOUT=#{changed_file} #{command}` 58 | puts results 59 | 60 | 61 | puts "=> done" 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /src/json.js: -------------------------------------------------------------------------------- 1 | var Json = { 2 | activeRequestCount: 0, 3 | currentRequest: false 4 | }; 5 | 6 | Json.Responders = { 7 | responders: [], 8 | 9 | _each: function(iterator) { 10 | this.responders._each(iterator); 11 | }, 12 | 13 | register: function(responder) { 14 | if (!this.include(responder)) 15 | this.responders.push(responder); 16 | }, 17 | 18 | unregister: function(responder) { 19 | this.responders = this.responders.without(responder); 20 | }, 21 | 22 | dispatch: function(callback, request, json) { 23 | this.each(function(responder) { 24 | if (Object.isFunction(responder[callback])) { 25 | try { 26 | responder[callback].apply(responder, [request, json]); 27 | } catch (e) { } 28 | } 29 | }); 30 | } 31 | }; 32 | 33 | Object.extend(Json.Responders, Enumerable); 34 | 35 | Json.Responders.register({ 36 | onCreate: function() { 37 | Json.activeRequestCount++; 38 | }, 39 | onComplete: function(request,json) { 40 | Json.activeRequestCount--; 41 | request.response = json; 42 | } 43 | }); 44 | 45 | //Json.callback(JSON) is used to store the JSON 46 | Object.extend(Json, { 47 | callback: function(json) { 48 | try { 49 | Json.Responders.dispatch('onComplete', Json.currentRequest, json); 50 | (Json.currentRequest.options.get('onComplete') || Prototype.emptyFunction)(Json.currentRequest, json); 51 | } catch (e) { 52 | Json.currentRequest.dispatchException(e); 53 | } 54 | } 55 | }); 56 | 57 | Json.Request = Class.create({ 58 | initialize: function(url, options) { 59 | this.options = $H({ 60 | //um, nothing yet 61 | }).update($H(options)); 62 | Json.currentRequest = this; 63 | this.request(url); 64 | }, 65 | 66 | request: function(url) { 67 | try { 68 | (this.options.get('onCreate') || Prototype.emptyFunction)(this); 69 | Json.Responders.dispatch('onCreate', this); 70 | var head = $$('head')[0]; 71 | var script = document.createElement('script'); 72 | script.type = 'text/javascript'; 73 | script.src = url+'&callback=Json.callback'+ '&cachebuster=' + (new Date() * 1); 74 | script.id = 'json_request'; 75 | head.appendChild(script); 76 | } 77 | catch (e) { 78 | this.dispatchException(e); 79 | } 80 | }, 81 | 82 | dispatchException: function(exception) { 83 | (this.options.get('onException') || Prototype.emptyFunction)(this, exception); 84 | Json.Responders.dispatch('onException', this, exception); 85 | } 86 | }); -------------------------------------------------------------------------------- /spec/results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSSpec results 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 52 | 53 |

A

B

54 | -------------------------------------------------------------------------------- /src/results.js: -------------------------------------------------------------------------------- 1 | Gsa.Results = Class.create({ 2 | mappings: $H({ 3 | CRAWLDATE: 'crawlDate', 4 | FS: 'details', 5 | LANG: 'lang', 6 | RK: 'rank', 7 | S: 'snippet', 8 | T: 'title', 9 | UD: 'url', 10 | MT: 'meta', 11 | UE: 'encoded_url' 12 | }), 13 | 14 | initialize: function(json) { 15 | this.json = json; 16 | this._object = $H().toObject(); 17 | this.parseJSON(); 18 | }, 19 | 20 | parseJSON: function () { 21 | this.set('query', this.json.Q); 22 | this.set('time', this.json.TM); 23 | if (!Object.isUndefined(this.json.RES)) { 24 | this.set('start', this.json.RES.SN); 25 | this.set('end', this.json.RES.EN); 26 | this.set('total', this.json.RES.M); 27 | this.set('has_next', !!this.json.RES.NB && !!this.json.RES.NB.NU); 28 | this.set('has_previous', !!this.json.RES.NB && !!this.json.RES.NB.PU); 29 | this.set('results', $A(this.json.RES.R).map(function (value){ 30 | return this.parseResult(value); 31 | }.bind(this))); 32 | } else { 33 | this.set('results', $A()); 34 | this.set('start', 0); 35 | this.set('end', 0); 36 | this.set('total', 0); 37 | this.set('has_next', false); 38 | this.set('has_previous', false); 39 | } 40 | }, 41 | 42 | parseResult: function (r) { 43 | r = $H(r); 44 | this.mappings.each(function (pair) { 45 | var key = pair.key, value = pair.value; 46 | if (Object.isString(r.get(key)) || Object.isNumber(r.get(key))) { 47 | r.set(value, new String(r.unset(key)).strip()); 48 | } else if (key == 'FS' || key == 'MT'){ 49 | var array = $A(r.unset(key)); 50 | var hash = $H(); 51 | array.each(function(item) { 52 | item = $H(item); 53 | var key = item.keys()[0]; 54 | var value = item.values()[0]; 55 | if (Object.isUndefined(hash.get(key))) { 56 | hash.update(item); 57 | } else { 58 | if (Object.isString(hash.get(key))) { 59 | hash.set(key,$A([hash.get(key)])); 60 | } 61 | if (Object.isArray(hash.get(key))) { 62 | hash.get(key).push(value) 63 | //FIXME: totally inefficient 64 | var array = hash.get(key); 65 | array.toString = function() { return this.join(', ') } 66 | hash.set(key,array); 67 | } 68 | } 69 | }); 70 | r.set(value, hash); 71 | } else if (!Object.isUndefined(r.get(key))){ 72 | r.set(value, $H(r.unset(key))); 73 | } 74 | }.bind(r)); 75 | 76 | //cache_url 77 | r.unset('U'); 78 | if (!Object.isUndefined(r.get('HAS')) && !Object.isUndefined(r.get('HAS').C) && !Object.isUndefined(r.get('HAS').C.SZ)) { 79 | r.set('cache_url', 'search?q=cache:'+r.get('HAS').C.CID+':'+r.get('encoded_url')) 80 | } 81 | r.unset('HAS'); 82 | 83 | //toTemplateReplacements 84 | r.toTemplateReplacements = function () { 85 | hash = r.clone(); 86 | hash.each(function (pair) { 87 | var key = pair.key, value = pair.value; 88 | if (!Object.isString(value) && !Object.isNumber(value)) { 89 | hash.set(key,value._object); 90 | } 91 | }); 92 | return hash._object; 93 | }.bind(r); 94 | 95 | return r; 96 | }, 97 | 98 | _each: function(iterator) { 99 | for (var i = 0, length = this.get('results').length; i < length; i++) 100 | iterator(this.get('results')[i]); 101 | }, 102 | 103 | first: function() { 104 | return this.get('results')[0]; 105 | }, 106 | 107 | last: function() { 108 | return this.get('results')[this.get('results').size() - 1]; 109 | }, 110 | 111 | set: function(key, value) { 112 | return this._object[key] = value; 113 | }, 114 | 115 | get: function(key) { 116 | return this._object[key]; 117 | }, 118 | 119 | unset: function(key) { 120 | var value = this._object[key]; 121 | delete this._object[key]; 122 | return value; 123 | }, 124 | 125 | toObject: function() { 126 | return Object.clone(this._object); 127 | }, 128 | 129 | size: function() { 130 | return this.get('results').length; 131 | }, 132 | 133 | toTemplateReplacements: function() { 134 | return this._object; 135 | } 136 | }); 137 | 138 | Object.extend(Gsa.Results.prototype, Enumerable); -------------------------------------------------------------------------------- /spec/lib/JSSpec.css: -------------------------------------------------------------------------------- 1 | @CHARSET "UTF-8"; 2 | 3 | /* -------------------- 4 | * @Layout 5 | */ 6 | 7 | html { 8 | overflow: hidden; 9 | } 10 | 11 | body, #jsspec_container { 12 | overflow: hidden; 13 | padding: 0; 14 | margin: 0; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | 19 | #title { 20 | padding: 0; 21 | margin: 0; 22 | position: absolute; 23 | top: 0px; 24 | left: 0px; 25 | width: 100%; 26 | height: 40px; 27 | overflow: hidden; 28 | } 29 | 30 | #list { 31 | padding: 0; 32 | margin: 0; 33 | position: absolute; 34 | top: 40px; 35 | left: 0px; 36 | bottom: 0px; 37 | overflow: auto; 38 | width: 250px; 39 | _height:expression(document.body.clientHeight-40); 40 | } 41 | 42 | #log { 43 | padding: 0; 44 | margin: 0; 45 | position: absolute; 46 | top: 40px; 47 | left: 250px; 48 | right: 0px; 49 | bottom: 0px; 50 | overflow: auto; 51 | _height:expression(document.body.clientHeight-40); 52 | _width:expression(document.body.clientWidth-250); 53 | } 54 | 55 | 56 | 57 | /* -------------------- 58 | * @Decorations and colors 59 | */ 60 | * { 61 | padding: 0; 62 | margin: 0; 63 | font-family: "Lucida Grande", Helvetica, sans-serif; 64 | } 65 | 66 | li { 67 | list-style: none; 68 | } 69 | 70 | /* hiding subtitles */ 71 | h2 { 72 | display: none; 73 | } 74 | 75 | /* title section */ 76 | div#title { 77 | padding: 0em 0.5em; 78 | } 79 | 80 | div#title h1 { 81 | font-size: 1.5em; 82 | float: left; 83 | } 84 | 85 | div#title ul li { 86 | float: left; 87 | padding: 0.5em 0em 0.5em 0.75em; 88 | } 89 | 90 | div#title p { 91 | float:right; 92 | margin-right:1em; 93 | font-size: 0.75em; 94 | } 95 | 96 | /* spec container */ 97 | ul.specs { 98 | margin: 0.5em; 99 | } 100 | ul.specs li { 101 | margin-bottom: 0.1em; 102 | } 103 | 104 | /* spec title */ 105 | ul.specs li h3 { 106 | font-weight: bold; 107 | font-size: 0.75em; 108 | padding: 0.2em 1em; 109 | cursor: pointer; 110 | _cursor: hand; 111 | } 112 | 113 | /* example container */ 114 | ul.examples li { 115 | border-style: solid; 116 | border-width: 0px 0px 1px 5px; 117 | margin: 0.2em 0em 0.2em 1em; 118 | } 119 | 120 | /* example title */ 121 | ul.examples li h4 { 122 | font-weight: normal; 123 | font-size: 0.75em; 124 | margin-left: 1em; 125 | } 126 | 127 | /* example explaination */ 128 | ul.examples li div { 129 | padding: 1em 2em; 130 | font-size: 0.75em; 131 | } 132 | 133 | /* styles for ongoing, success, failure, error */ 134 | div.success, div.success a { 135 | color: #FFFFFF; 136 | background-color: #65C400; 137 | } 138 | 139 | ul.specs li.success h3, ul.specs li.success h3 a { 140 | color: #FFFFFF; 141 | background-color: #65C400; 142 | } 143 | 144 | ul.examples li.success, ul.examples li.success a { 145 | color: #3D7700; 146 | background-color: #DBFFB4; 147 | border-color: #65C400; 148 | } 149 | 150 | div.exception, div.exception a { 151 | color: #FFFFFF; 152 | background-color: #C20000; 153 | } 154 | 155 | ul.specs li.exception h3, ul.specs li.exception h3 a { 156 | color: #FFFFFF; 157 | background-color: #C20000; 158 | } 159 | 160 | ul.examples li.exception, ul.examples li.exception a { 161 | color: #C20000; 162 | background-color: #FFFBD3; 163 | border-color: #C20000; 164 | } 165 | 166 | div.ongoing, div.ongoing a { 167 | color: #000000; 168 | background-color: #FFFF80; 169 | } 170 | 171 | ul.specs li.ongoing h3, ul.specs li.ongoing h3 a { 172 | color: #000000; 173 | background-color: #FFFF80; 174 | } 175 | 176 | ul.examples li.ongoing, ul.examples li.ongoing a { 177 | color: #000000; 178 | background-color: #FFFF80; 179 | border-color: #DDDD00; 180 | } 181 | 182 | 183 | 184 | /* -------------------- 185 | * values 186 | */ 187 | .number_value, .string_value, .regexp_value, .boolean_value, .dom_value { 188 | font-family: monospace; 189 | color: blue; 190 | } 191 | .object_value, .array_value { 192 | line-height: 2em; 193 | padding: 0.1em 0.2em; 194 | margin: 0.1em 0; 195 | } 196 | .date_value { 197 | font-family: monospace; 198 | color: olive; 199 | } 200 | .undefined_value, .null_value { 201 | font-style: italic; 202 | color: blue; 203 | } 204 | .dom_attr_name { 205 | } 206 | .dom_attr_value { 207 | color: red; 208 | } 209 | .dom_path { 210 | font-size: 0.75em; 211 | color: gray; 212 | } 213 | strong { 214 | font-weight: normal; 215 | background-color: #FFC6C6; 216 | } -------------------------------------------------------------------------------- /lib/builder.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005-2007 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us) 2 | // 3 | // script.aculo.us is freely distributable under the terms of an MIT-style license. 4 | // For details, see the script.aculo.us web site: http://script.aculo.us/ 5 | 6 | var Builder = { 7 | NODEMAP: { 8 | AREA: 'map', 9 | CAPTION: 'table', 10 | COL: 'table', 11 | COLGROUP: 'table', 12 | LEGEND: 'fieldset', 13 | OPTGROUP: 'select', 14 | OPTION: 'select', 15 | PARAM: 'object', 16 | TBODY: 'table', 17 | TD: 'table', 18 | TFOOT: 'table', 19 | TH: 'table', 20 | THEAD: 'table', 21 | TR: 'table' 22 | }, 23 | // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken, 24 | // due to a Firefox bug 25 | node: function(elementName) { 26 | elementName = elementName.toUpperCase(); 27 | 28 | // try innerHTML approach 29 | var parentTag = this.NODEMAP[elementName] || 'div'; 30 | var parentElement = document.createElement(parentTag); 31 | try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 32 | parentElement.innerHTML = "<" + elementName + ">"; 33 | } catch(e) {} 34 | var element = parentElement.firstChild || null; 35 | 36 | // see if browser added wrapping tags 37 | if(element && (element.tagName.toUpperCase() != elementName)) 38 | element = element.getElementsByTagName(elementName)[0]; 39 | 40 | // fallback to createElement approach 41 | if(!element) element = document.createElement(elementName); 42 | 43 | // abort if nothing could be created 44 | if(!element) return; 45 | 46 | // attributes (or text) 47 | if(arguments[1]) 48 | if(this._isStringOrNumber(arguments[1]) || 49 | (arguments[1] instanceof Array) || 50 | arguments[1].tagName) { 51 | this._children(element, arguments[1]); 52 | } else { 53 | var attrs = this._attributes(arguments[1]); 54 | if(attrs.length) { 55 | try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707 56 | parentElement.innerHTML = "<" +elementName + " " + 57 | attrs + ">"; 58 | } catch(e) {} 59 | element = parentElement.firstChild || null; 60 | // workaround firefox 1.0.X bug 61 | if(!element) { 62 | element = document.createElement(elementName); 63 | for(attr in arguments[1]) 64 | element[attr == 'class' ? 'className' : attr] = arguments[1][attr]; 65 | } 66 | if(element.tagName.toUpperCase() != elementName) 67 | element = parentElement.getElementsByTagName(elementName)[0]; 68 | } 69 | } 70 | 71 | // text, or array of children 72 | if(arguments[2]) 73 | this._children(element, arguments[2]); 74 | 75 | return element; 76 | }, 77 | _text: function(text) { 78 | return document.createTextNode(text); 79 | }, 80 | 81 | ATTR_MAP: { 82 | 'className': 'class', 83 | 'htmlFor': 'for' 84 | }, 85 | 86 | _attributes: function(attributes) { 87 | var attrs = []; 88 | for(attribute in attributes) 89 | attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) + 90 | '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'"') + '"'); 91 | return attrs.join(" "); 92 | }, 93 | _children: function(element, children) { 94 | if(children.tagName) { 95 | element.appendChild(children); 96 | return; 97 | } 98 | if(typeof children=='object') { // array can hold nodes and text 99 | children.flatten().each( function(e) { 100 | if(typeof e=='object') 101 | element.appendChild(e) 102 | else 103 | if(Builder._isStringOrNumber(e)) 104 | element.appendChild(Builder._text(e)); 105 | }); 106 | } else 107 | if(Builder._isStringOrNumber(children)) 108 | element.appendChild(Builder._text(children)); 109 | }, 110 | _isStringOrNumber: function(param) { 111 | return(typeof param=='string' || typeof param=='number'); 112 | }, 113 | build: function(html) { 114 | var element = this.node('div'); 115 | $(element).update(html.strip()); 116 | return element.down(); 117 | }, 118 | dump: function(scope) { 119 | if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope 120 | 121 | var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " + 122 | "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " + 123 | "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+ 124 | "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+ 125 | "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+ 126 | "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/); 127 | 128 | tags.each( function(tag){ 129 | scope[tag] = function() { 130 | return Builder.node.apply(Builder, [tag].concat($A(arguments))); 131 | } 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /xsl/json.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | (); 23 | 24 | 25 | 26 | { 27 | "CT": "", 28 | "CUSTOM": "", 29 | "PARAM": {}, 30 | "Q": "", 31 | "RES": , 32 | "TM": 33 | } 34 | 35 | 36 | "": { 37 | "value": "", 38 | "original_value": "" 39 | }, 40 | 41 | 42 | { 43 | "SN": "", 44 | "EN": "", 45 | "FI": true, 46 | "XT": true, 47 | "M": "", 48 | "NB": , 49 | "R": [] 50 | } 51 | 52 | { 53 | "PU": "", 54 | "NU": "" 55 | } 56 | 57 | { 58 | "CRAWLDATE": "", 59 | "FS": [], 60 | "HAS": {}, 61 | "HN": {"U": ""}, 62 | "LANG": "", 63 | "MT": [], 64 | "RK": , 65 | "S": "", 66 | "T": "", 67 | "U": "", 68 | "UD": "", 69 | "UE": "" 70 | }, 71 | 72 | 73 | { "": "" }, 74 | 75 | 76 | 77 | { "": "" }, 78 | 79 | 80 | 81 | "L": true, 82 | "C": {} 83 | 84 | 85 | 86 | "SZ": "", 87 | "CID": "" 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /spec/gsa.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JSSpec results 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 183 | 184 |

A

B

185 | -------------------------------------------------------------------------------- /src/gsa.js: -------------------------------------------------------------------------------- 1 | Gsa = Class.create({ 2 | /** 3 | * Initializes a new Gsa object. Takes a required 'domain' argument, and an options hash. Default options are as follows: 4 | * 5 | * output: 'xml_no_dtd', 6 | * proxystylesheet: 'json', 7 | * client: 'json', 8 | * site: 'default_collection', 9 | * start: 0, 10 | * protocol: 'http://', 11 | * port: 80, 12 | * scroll_to: $$('body')[0], 13 | * observe_form: true 14 | * 15 | * Note: a Gsa object must be initialized after the DOM has loaded. For example: 16 | * 17 | * var gsa; 18 | * document.observe('dom:loaded', function () { 19 | * gsa = new Gsa('foo.com', {}); 20 | * }; 21 | * 22 | * Callbacks: 23 | * 24 | * The following callbacks are supported via passing a function to the appropriate key in the options hash: 25 | * 26 | * beforeSearch, onSearch, onComplete 27 | * 28 | * Additional options: 29 | * 30 | * indicator: a dom element, expected to be hidden by default, that will be made visable upon start of a search 31 | * and hidden again at the search's end. 32 | * form: a Form to serialize and trigger a search upon submit. only works if observe_form == true and results is set 33 | * results: dom element in which results will be placed if observe_form == true and results is set 34 | * 35 | * When using the form obvserving functionality of GSA, the follow template options are avaialble. Their defaults are listed. 36 | * 37 | * summary_template: "
Results #{start} - #{end} of about #{total} for #{query}. (#{time} seconds)
"; 38 | * result_template: "

#{title}

#{snippet}...

"; 39 | * pagination_template: "
#{previous_link}#{page_links}#{next_link}
"; 40 | * previous_link_template: "<#{tag} #{link} class='#{klass}' id='page_previous'>« Previous"; 41 | * next_link_template: "<#{tag} #{link} class='#{klass}' id='page_next'>Next »"; 42 | * page_link_template: "<#{tag} #{link} class='#{klass}' id='page_#{page}'>#{page}"; 43 | */ 44 | initialize: function(domain, options) { 45 | 46 | //required options 47 | if (domain == null) 48 | throw("'domain' argument required"); 49 | 50 | //default parameters for the request 51 | this.options = $H({ 52 | output: 'xml_no_dtd', 53 | proxystylesheet: 'json', 54 | client: 'json', 55 | site: 'default_collection', 56 | start: 0, 57 | protocol: 'http://', 58 | port: 80, 59 | scroll_to: $$('body')[0], 60 | observe_form: true 61 | }).update(options); 62 | 63 | //set some properties based on the options 64 | this.domain = domain; 65 | this.protocol = this.options.unset('protocol'); 66 | this.port = this.options.unset('port'); 67 | this.current_page = false; 68 | 69 | //indicator 70 | this.indicator = this.options.unset('indicator'); 71 | 72 | //scroll element 73 | this.scroll_to = this.options.unset('scroll_to'); 74 | 75 | //form stuffs 76 | this.form_element = $(this.options.unset('form')); 77 | this.results_element = $(this.options.unset('results')); 78 | this.observe_form = this.options.unset('observe_form'); 79 | 80 | //check for templates 81 | this.summary_template = this.options.unset('summary_template') || "
Results #{start} - #{end} of about #{total} for #{query}. (#{time} seconds)
"; 82 | this.result_template = this.options.unset('result_template') || "

#{title}

#{snippet}...

"; 83 | this.pagination_template = this.options.unset('pagination_template') || "
#{previous_link}#{page_links}#{next_link}
"; 84 | this.previous_link_template = this.options.unset('previous_link_template') || "<#{tag} #{link} class='#{klass}' id='page_previous'>« Previous"; 85 | this.next_link_template = this.options.unset('next_link_template') || "<#{tag} #{link} class='#{klass}' id='page_next'>Next »"; 86 | this.page_link_template = this.options.unset('page_link_template') || "<#{tag} #{link} class='#{klass}' id='page_#{page}'>#{page}"; 87 | 88 | //if the options hash has 'form' and 'results' keys and observing hasn't been explicitly disabled, go nuts 89 | if (this.observe_form && !Object.isUndefined(this.form_element) && !Object.isUndefined(this.results_element)) { 90 | this.form_element.observe('submit', this.observeFormFunction.bind(this)); 91 | } 92 | }, 93 | 94 | /** 95 | * Triggers a request to the GSA and fires the onSearch callback 96 | * and shows the indicator 97 | */ 98 | _request: function(url) { 99 | this.request = new Json.Request(url, { onComplete: this._response.bind(this) }); 100 | (this.options.get('onSearch') || Prototype.emptyFunction)(this); 101 | (this.searchOptions.get('onSearch') || Prototype.emptyFunction)(this); 102 | if(this.indicator) Element.show(this.indicator); 103 | return this.request; 104 | }, 105 | 106 | /** 107 | * Triggered in a callback when a call to _request completed. This also fires 108 | * the onComplete callback and hides the indicator 109 | */ 110 | _response: function () { 111 | this.results = new Gsa.Results(this.request.response); 112 | (this.options.get('onComplete') || Prototype.emptyFunction)(this); 113 | (this.searchOptions.get('onComplete') || Prototype.emptyFunction)(this); 114 | if(this.indicator) Element.hide(this.indicator); 115 | return this.results; 116 | }, 117 | 118 | /** 119 | * Takes a required query, and then an options hash. The give options hash is merged with the options 120 | * given at initialization. The beforeSearch callback is triggered before _request() is called: 121 | * 122 | * Note on Callbacks: 123 | * 124 | * Callbacks set at initiazation are run before callbacks defined on a call to search(). 125 | */ 126 | search: function (q, options) { 127 | if (q == null || q.blank()) 128 | return false; 129 | this.searchOptions = $H(); 130 | this.searchOptions.update(this.options); 131 | this.searchOptions.set('q', q); 132 | this.searchOptions.update(options); 133 | (this.options.get('beforeSearch') || Prototype.emptyFunction)(this); 134 | if (!this.options.get('beforeSearch')) 135 | (this.searchOptions.get('beforeSearch') || Prototype.emptyFunction)(this); 136 | this.current_page = 1; 137 | this._request(this.buildUri()); 138 | return true; 139 | }, 140 | 141 | /** 142 | * Perform a search using previously set options, requesting a specific page. 143 | * Must be called after a call to search() 144 | */ 145 | page: function(page) { 146 | if (!Object.isUndefined(this.searchOptions)) { 147 | var num = 10; 148 | if (!Object.isUndefined(this.searchOptions.get('num'))) 149 | num = this.searchOptions.get('num'); 150 | this.searchOptions.set('start', ((page-1)*num)); 151 | this.searchOptions.update({start: this.searchOptions.get('start')}); 152 | this.current_page = page; 153 | this._request(this.buildUri()); 154 | return true; 155 | } else { 156 | return false; 157 | } 158 | }, 159 | 160 | /** 161 | * Request the next page of results 162 | * Must be called after a call to search() 163 | */ 164 | next: function () { 165 | if (Object.isNumber(this.current_page)) { 166 | return this.page(this.current_page+1); 167 | } else { 168 | return false; 169 | } 170 | }, 171 | 172 | /** 173 | * Request the next page of results 174 | * Must be called after a call to search() and a subsequent call to next() or page(n) 175 | */ 176 | previous: function () { 177 | if (Object.isNumber(this.current_page) && this.current_page > 1) { 178 | return this.page(this.current_page-1); 179 | } else { 180 | return false; 181 | } 182 | }, 183 | 184 | /** 185 | * Parse the given options hash to format the sort, getfields, requiredfields, 186 | * and partialfields options into a format the GSA understands 187 | */ 188 | parseOptions: function (options) { 189 | var options = $H(options); 190 | //sort 191 | if (!Object.isUndefined(options.get('sort'))) { 192 | var sort = options.get('sort'); 193 | if (Object.isString(sort)) { 194 | options.set('sort',('date:' + sort.replace('date:',''))); 195 | } else if (Object.isHash($H(sort))) { 196 | try { 197 | sort = $H(sort); 198 | var mode = (sort.get('mode') == 'date') ? 'S' : 'L'; 199 | var direction = (sort.get('direction') == 'ascending') ? 'A' : 'D'; 200 | options.set('sort','date:'+direction+':'+mode+':d1'); 201 | } catch (e) { 202 | options.set('sort','date:D:L:d1'); 203 | } 204 | } 205 | } 206 | //getfields 207 | if (!Object.isUndefined(options.get('getfields'))) { 208 | var getfields = options.get('getfields'); 209 | if (!Object.isString(getfields) && Object.isArray(getfields)) { 210 | options.set('getfields', getfields.join('.')); 211 | } 212 | } 213 | //requiredfields 214 | if (!Object.isUndefined(options.get('requiredfields'))) { 215 | var requiredfields = $H(options.get('requiredfields')); 216 | if (Object.isHash(requiredfields)) { 217 | options.set('requiredfields', this.toFieldValues(requiredfields.get('fields'), requiredfields.get('mode'))); 218 | } 219 | } 220 | //partialfields 221 | if (!Object.isUndefined(options.get('partialfields'))) { 222 | var partialfields = $H(options.get('partialfields')); 223 | if (Object.isHash(partialfields)) { 224 | options.set('partialfields', this.toFieldValues(partialfields.get('fields'), partialfields.get('mode'))); 225 | } 226 | } 227 | return options; 228 | }, 229 | 230 | /** 231 | * Construct the URI that will return the requested search results 232 | */ 233 | buildUri: function () { 234 | var uriOptions = this.searchOptions.clone(); 235 | uriOptions.unset('beforeSearch'); 236 | uriOptions.unset('onSearch'); 237 | uriOptions.unset('onComplete'); 238 | if (uriOptions.get('start') == 0) 239 | uriOptions.unset('start'); 240 | var uriString = this.protocol + this.domain + '/search?' + this.parseOptions(uriOptions).toQueryString(); 241 | return uriString; 242 | }, 243 | 244 | buildPaginationHTML: function () { 245 | var html = { 246 | previous_link: '', 247 | page_links: '', 248 | next_link: '' 249 | } 250 | if (this.results.get('has_previous')) { 251 | html.previous_link = new String(this.previous_link_template).interpolate({tag: 'a', link: "href='#previous'"}); 252 | } else { 253 | html.previous_link = new String(this.previous_link_template).interpolate({tag: 'span', link: '', klass: "disabled"}); 254 | } 255 | if (this.results.get('has_next')) { 256 | html.next_link = new String(this.next_link_template).interpolate({tag: 'a', link: "href='#next'"}); 257 | } else { 258 | html.next_link = new String(this.next_link_template).interpolate({tag: 'span', link: '', klass: "disabled"}); 259 | } 260 | 261 | return Builder.build(new String(this.pagination_template).interpolate(html)); 262 | }, 263 | 264 | /** 265 | * Parse a hash of fields and a mode "OR" or "AND" into a string the GSA understands. 266 | * For use with the requiredfields and partialfields results filtering 267 | */ 268 | toFieldValues: function (hash, mode) { 269 | hash = $H(hash); 270 | var joinstring; 271 | if (mode == 'OR') { 272 | joinstring = '|'; 273 | } else { 274 | joinstring = '.'; 275 | } 276 | return hash.map(function (pair){ 277 | var key = pair.key, value = pair.value; 278 | if (Object.isArray(value)) { 279 | return value.map(function (a) { 280 | return key + ':' + a; 281 | }).join(joinstring); 282 | } else { 283 | return key + ':' + value; 284 | } 285 | }).join(joinstring); 286 | }, 287 | 288 | /** 289 | * Function that's used when obvserve_form is true and form and results options are set. 290 | * On submit of the form, it's elements are serialized and passed to the GSA. 291 | * A 'q' element is required. 292 | */ 293 | observeFormFunction: function(event){ 294 | var form = Event.element(event); 295 | var hash = $H(form.serialize(true)); 296 | this.search(hash.unset('q'), hash.update({ onComplete: this.observeFormOnComplete })); 297 | Event.stop(event); 298 | }, 299 | 300 | /** 301 | * onComplete callback that's used when obvserve_form is true and form and results options are set. 302 | * This interprets the results, builds dom elements from the given templates, and inserts them into the page. 303 | * Pagination elements are also inserted that automgically wired. 304 | */ 305 | observeFormOnComplete: function(gsa) { 306 | Element.update(gsa.results_element); 307 | Element.insert(gsa.results_element, Builder.build(new String(gsa.summary_template).interpolate(gsa.results))); 308 | gsa.results.each(function (result, index) { 309 | Element.insert(gsa.results_element, Builder.build(new String(gsa.result_template).interpolate(result))); 310 | }); 311 | Element.insert(gsa.results_element, gsa.buildPaginationHTML()); 312 | $$('a.page_link').each(function(link) { 313 | link.observe('click', function(event) { 314 | gsa.scroll_to.scrollTo(); 315 | var page_link = Event.element(event); 316 | gsa.page(page_link.innerHTML); 317 | Event.stop(event); 318 | }) 319 | }); 320 | $$('a#page_next').each(function(link) { 321 | link.observe('click', function(event) { 322 | gsa.scroll_to.scrollTo(); 323 | gsa.next(); 324 | Event.stop(event); 325 | }) 326 | }); 327 | $$('a#page_previous').each(function(link) { 328 | link.observe('click', function(event) { 329 | gsa.scroll_to.scrollTo(); 330 | gsa.previous(); 331 | Event.stop(event); 332 | }) 333 | }); 334 | gsa.scroll_to.scrollTo(); 335 | } 336 | }); 337 | -------------------------------------------------------------------------------- /spec/fixtures/results.js: -------------------------------------------------------------------------------- 1 | var results = { 2 | jnewland: {"PARAM":{"entqr":{"value":"0","original_value":"0"},"access":{"value":"p","original_value":"p"},"getfields":{"value":"*","original_value":"*"},"sort":{"value":"date:D:L:d1","original_value":"date%3AD%3AL%3Ad1"},"output":{"value":"xml_no_dtd","original_value":"xml_no_dtd"},"cachebuster":{"value":"1201621537043","original_value":"1201621537043"},"ie":{"value":"UTF-8","original_value":"UTF-8"},"client":{"value":"json_new","original_value":"json_new"},"q":{"value":"jesse newland","original_value":"jesse+newland"},"requiredfields":{"value":"page_id:individual","original_value":"page_id%3Aindividual"},"callback":{"value":"Json.callback","original_value":"Json.callback"},"ud":{"value":"1","original_value":"1"},"site":{"value":"default_collection","original_value":"default_collection"},"oe":{"value":"UTF-8","original_value":"UTF-8"},"proxystylesheet":{"value":"json_new","original_value":"json_new"},"ip":{"value":"76.240.16.230","original_value":"76.240.16.230"},"google_search":{"value":"","original_value":""}},"Q":"jesse newland","RES":{"SN":"1","EN":"10","FI":true,"M":"17","NB":{"NU":"/search?q=jesse+newland&site=default_collection&hl=en&lr=&ie=UTF-8&oe=UTF-8&output=xml_no_dtd&client=json_new&access=p&sort=date:D:L:d1&getfields=*&requiredfields=page_id:individual&start=10&sa=N"},"R":[{"FS":[{"date":"2007-07-13"}],"HAS":{"L":true,"C":{"SZ":"67k","CID":"k52-oliCmR0J"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"LexBlog IT Director talks about today's platform upgrade"},{"description":"On Wednesday, Colin posted about a major upgrade to LexBlog's blogging platform. Today I spoke with Jesse Newland, LexBlog's IT Director, about the sp"},{"date":"2007-07-13"},{"page_id":"individual"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Rob La Gatta"},{"author_email":"rob@lexblog.com"},{"entry_id":"101595"}],"RK":0,"S":"Today I spoke with Jesse Newland, LexBlog's IT Director, about the sp. ... Jesse
Newland: Speed, reliability, [and] scalability. Speed. ... ","T":"LexBlog IT Director talks about today's platform upgrade : Real ...","U":"http://kevin.lexblog.com/2007/07/articles/cool-stuff/lexblog-it-director-talks-about-todays-platform-upgrade/","UD":"http://kevin.lexblog.com/2007/07/articles/cool-stuff/lexblog-it-director-talks-about-todays-platform-upgrade/","UE":"http://kevin.lexblog.com/2007/07/articles/cool-stuff/lexblog-it-director-talks-about-todays-platform-upgrade/"},{"FS":[{"date":"2006-02-15"}],"HAS":{"L":true,"C":{"SZ":"66k","CID":"XYitYbnme1MJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Trackbacks are comments"},{"description":"Is 'trackbacks' a needed term in the world of blogging? LexBlog's IT Director, Jesse Newland, thinks not. Trackbacks are comments is his a"},{"date":"2006-02-15"},{"page_id":"individual"},{"category":"Blog Basics"},{"category_id":"27978"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"100366"}],"RK":0,"S":"LexBlog's IT Director, Jesse Newland, thinks not. Trackbacks are comments
is his a. ... LexBlog's IT Director, Jesse Newland, thinks not. ... ","T":"Trackbacks are comments : Real Lawyers Have Blogs","U":"http://kevin.lexblog.com/2006/02/articles/blog-basics/trackbacks-are-comments/","UD":"http://kevin.lexblog.com/2006/02/articles/blog-basics/trackbacks-are-comments/","UE":"http://kevin.lexblog.com/2006/02/articles/blog-basics/trackbacks-are-comments/"},{"FS":[{"date":"2006-01-06"}],"HAS":{"L":true,"C":{"SZ":"62k","CID":"XHIV156ku50J"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Xservs are out of the box and up at LexBlog"},{"description":"LexBlog's two shiny Apple Xservs were booted up for the first time this afternoon. Jesse Newland, LexBlog's IT Director, has been configuring,"},{"date":"2006-01-06"},{"page_id":"individual"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"100227"}],"RK":0,"S":"LexBlog's two shiny Apple Xservs were booted up for the first time this afternoon.
Jesse Newland, LexBlog's IT Director, has been configuring,. ... ","T":"Xservs are out of the box and up at LexBlog : Real Lawyers Have ...","U":"http://kevin.lexblog.com/2006/01/articles/cool-stuff/xservs-are-out-of-the-box-and-up-at-lexblog/","UD":"http://kevin.lexblog.com/2006/01/articles/cool-stuff/xservs-are-out-of-the-box-and-up-at-lexblog/","UE":"http://kevin.lexblog.com/2006/01/articles/cool-stuff/xservs-are-out-of-the-box-and-up-at-lexblog/"},{"FS":[{"date":"2007-10-06"}],"HAS":{"L":true,"C":{"SZ":"63k","CID":"PsIFHHkMCXgJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Legal News - LexBlogosphere: 10/6/07"},{"description":"We're back, after a brief outage this morning (which was quickly taken care of by LexBlog's IT Director Jesse Newland). It's Saturday, and the crop of"},{"date":"2007-10-06"},{"page_id":"individual"},{"category":"Legal News - LexBlogosphere"},{"category_id":"27997"},{"author":"Rob La Gatta"},{"author_email":"rob@lexblog.com"},{"entry_id":"105447"}],"RK":0,"S":"We're back, after a brief outage this morning (which was quickly taken care of by
LexBlog's IT Director Jesse Newland). It's Saturday, and the crop of. ... ","T":"Legal News - LexBlogosphere: 10/6/07 : Real Lawyers Have Blogs","U":"http://kevin.lexblog.com/2007/10/articles/legal-news-lexblogosphere/legal-news-lexblogosphere-10607/","UD":"http://kevin.lexblog.com/2007/10/articles/legal-news-lexblogosphere/legal-news-lexblogosphere-10607/","UE":"http://kevin.lexblog.com/2007/10/articles/legal-news-lexblogosphere/legal-news-lexblogosphere-10607/"},{"FS":[{"date":"2007-04-25"}],"HAS":{"L":true,"C":{"SZ":"62k","CID":"_dfgrlVT5TgJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"We're gathering in Seattle"},{"description":"LexBlog's IT Director, Jesse Newland, will be in from Atlanta for Thursday's Amazon Web Services conference. Amazon and Madrona Venture Group are co-h"},{"date":"2007-04-25"},{"page_id":"individual"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"101405"}],"RK":0,"S":"LexBlog's IT Director, Jesse Newland, will be in from Atlanta for Thursday's Amazon
Web Services conference. Amazon and Madrona Venture Group are co-h. ... ","T":"We're gathering in Seattle : Real Lawyers Have Blogs","U":"http://kevin.lexblog.com/2007/04/articles/cool-stuff/were-gathering-in-seattle/","UD":"http://kevin.lexblog.com/2007/04/articles/cool-stuff/were-gathering-in-seattle/","UE":"http://kevin.lexblog.com/2007/04/articles/cool-stuff/were-gathering-in-seattle/"},{"FS":[{"date":"2007-04-17"}],"HAS":{"L":true,"C":{"SZ":"65k","CID":"dtsZaccfncoJ"}},"HN":{"U":"kevin.lexblog.com/2007/04/articles"},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Full-text RSS feeds preferable to excerpt feeds"},{"description":"Dennis Kennedy has a nice post about the advantages of offering a full-text RSS feed, as opposed to a RSS feed with just an excerpt of your blog post."},{"date":"2007-04-17"},{"page_id":"individual"},{"category":"Blog Basics"},{"category_id":"27978"},{"category":"RSS & Syndication"},{"category_id":"27983"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"101386"}],"RK":0,"S":"... Amazed that I'ma convert Jesse? ;) (Jesse Newland is our IT director and had been
telling me of the lameness of excerpt feeds.). Related posts: ... ","T":"Full-text RSS feeds preferable to excerpt feeds : Real Lawyers ...","U":"http://kevin.lexblog.com/2007/04/articles/rss-syndication/fulltext-rss-feeds-preferable-to-excerpt-feeds/","UD":"http://kevin.lexblog.com/2007/04/articles/rss-syndication/fulltext-rss-feeds-preferable-to-excerpt-feeds/","UE":"http://kevin.lexblog.com/2007/04/articles/rss-syndication/fulltext-rss-feeds-preferable-to-excerpt-feeds/"},{"FS":[{"date":"2007-11-19"}],"HAS":{"L":true,"C":{"SZ":"63k","CID":"oWGCkSP6-rsJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Progress so far on LexMonitor content import - 20,000 entries by noon"},{"description":"As some of you may already be aware, LexBlog is in the process of launching a daily review of law blogs and journals, called LexMonitor. We're very ex"},{"date":"2007-11-19"},{"page_id":"individual"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Rob La Gatta"},{"author_email":"rob@lexblog.com"},{"entry_id":"109443"}],"RK":0,"S":"... Jesse Newland, LexBlog's IT Director, just sent over this graph, which shows how
many URLs have been indexed by our Google Search Appliance as of noon today: ... ","T":"Progress so far on LexMonitor content import - 20,000 entries by ...","U":"http://kevin.lexblog.com/2007/11/articles/cool-stuff/progress-so-far-on-lexmonitor-content-import-20000-entries-by-noon/","UD":"http://kevin.lexblog.com/2007/11/articles/cool-stuff/progress-so-far-on-lexmonitor-content-import-20000-entries-by-noon/","UE":"http://kevin.lexblog.com/2007/11/articles/cool-stuff/progress-so-far-on-lexmonitor-content-import-20000-entries-by-noon/"},{"CRAWLDATE":"27 Jan 2008","FS":[{"date":"2007-12-31"}],"HAS":{"L":true,"C":{"SZ":"63k","CID":"_S1JjsF8EVkJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Trip to the Apple Store"},{"description":"When you live on an Island, the trip to the Apple Store is more of an adventure (well at least a family outing). Here's the view from the ferry deck t"},{"date":"2007-12-31"},{"page_id":"individual"},{"category":"Bainbridge Island Ferry"},{"category_id":"37137"},{"category":"Cool Stuff"},{"category_id":"27981"},{"category":"Seattle"},{"category_id":"37136"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"113535"}],"RK":0,"S":"... coming next year. Each year this blog thing only gets more amazing. Jesse
Newland - December 31, 2007 8:41 PM Lemme know after the ... ","T":"Trip to the Apple Store : Real Lawyers Have Blogs","U":"http://kevin.lexblog.com/2007/12/articles/cool-stuff/trip-to-the-apple-store/","UD":"http://kevin.lexblog.com/2007/12/articles/cool-stuff/trip-to-the-apple-store/","UE":"http://kevin.lexblog.com/2007/12/articles/cool-stuff/trip-to-the-apple-store/"},{"FS":[{"date":"2007-05-02"}],"HAS":{"L":true,"C":{"SZ":"63k","CID":"f_JMx831UlEJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"LexBlog and Amazon working together? Amazon Web Services"},{"description":"LexBlog and Amazon working together? May sound strange, but Amazon Web Services offers endless possibilities to companies like LexBlog. LexBlog's IT d"},{"date":"2007-05-02"},{"page_id":"individual"},{"category":"Blog Basics"},{"category_id":"27978"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"101416"}],"RK":0,"S":"... like LexBlog. LexBlog's IT director, Jesse Newland, reports from last week's
Amazon Web Services Team event in Seattle: Jeff Barr ... ","T":"LexBlog and Amazon working together? Amazon Web Services : Real ...","U":"http://kevin.lexblog.com/2007/05/articles/cool-stuff/lexblog-and-amazon-working-together-amazon-web-services/","UD":"http://kevin.lexblog.com/2007/05/articles/cool-stuff/lexblog-and-amazon-working-together-amazon-web-services/","UE":"http://kevin.lexblog.com/2007/05/articles/cool-stuff/lexblog-and-amazon-working-together-amazon-web-services/"},{"FS":[{"date":"2005-10-20"}],"HAS":{"L":true,"C":{"SZ":"65k","CID":"L-8X8_ZabQsJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Flash and search engines do not mix for professional services websites"},{"description":"The Google Blogoscoped blog reminds us that "},{"date":"2005-10-20"},{"page_id":"individual"},{"category":"Search Engine Optimization"},{"category_id":"27980"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"100017"}],"RK":0,"S":"... Jesse Newland - October 20, 2005 2:12 PM There are still some instances where Flash
can be used to augment text content in a way that Google will index. ... ","T":"Flash and search engines do not mix for professional services ...","U":"http://kevin.lexblog.com/2005/10/articles/search-engine-optimization/flash-and-search-engines-do-not-mix-for-professional-services-websites/","UD":"http://kevin.lexblog.com/2005/10/articles/search-engine-optimization/flash-and-search-engines-do-not-mix-for-professional-services-websites/","UE":"http://kevin.lexblog.com/2005/10/articles/search-engine-optimization/flash-and-search-engines-do-not-mix-for-professional-services-websites/"}],},"TM":0.038048}, 3 | jnewland2: {"PARAM":{"entqr":{"value":"0","original_value":"0"},"access":{"value":"p","original_value":"p"},"getfields":{"value":"*","original_value":"*"},"start":{"value":"10","original_value":"10"},"sort":{"value":"date:D:L:d1","original_value":"date%3AD%3AL%3Ad1"},"output":{"value":"xml_no_dtd","original_value":"xml_no_dtd"},"cachebuster":{"value":"1202137537337","original_value":"1202137537337"},"ie":{"value":"UTF-8","original_value":"UTF-8"},"client":{"value":"json","original_value":"json"},"q":{"value":"jesse newland","original_value":"jesse+newland"},"requiredfields":{"value":"page_id:individual","original_value":"page_id%3Aindividual"},"callback":{"value":"Json.callback","original_value":"Json.callback"},"ud":{"value":"1","original_value":"1"},"site":{"value":"default_collection","original_value":"default_collection"},"oe":{"value":"UTF-8","original_value":"UTF-8"},"proxystylesheet":{"value":"json_new","original_value":"json_new"},"ip":{"value":"76.240.16.230","original_value":"76.240.16.230"},"google_search":{"value":"","original_value":""}},"Q":"jesse newland","RES":{"SN":"11","EN":"15","FI":true,"M":"17","NB":{"PU":"/search?q=jesse+newland&site=default_collection&hl=en&lr=&ie=UTF-8&oe=UTF-8&output=xml_no_dtd&client=json&access=p&sort=date:D:L:d1&getfields=*&requiredfields=page_id:individual&start=0&sa=N"},"R":[{"FS":[{"date":"2007-06-01"}],"HAS":{"L":true,"C":{"SZ":"63k","CID":"jZncuCwn1hYJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"FeedBurner acquired by Google"},{"description":"Google now has offices in the Chicago Loop with its acquisition of Feedburner. Congrats to everyone on the FeedBurner team, including fellow lawyer an"},{"date":"2007-06-01"},{"page_id":"individual"},{"category":"RSS & Syndication"},{"category_id":"27983"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"101490"}],"RK":0,"S":"... Thanks to our IT Director Jesse Newland's insight, LexBlog has been using Feedburner
for the management and distribution of our clients' blog feeds to their ... ","T":"FeedBurner acquired by Google : Real Lawyers Have Blogs","U":"http://kevin.lexblog.com/2007/06/articles/rss-syndication/feedburner-acquired-by-google/","UD":"http://kevin.lexblog.com/2007/06/articles/rss-syndication/feedburner-acquired-by-google/","UE":"http://kevin.lexblog.com/2007/06/articles/rss-syndication/feedburner-acquired-by-google/"},{"FS":[{"date":"2006-10-21"}],"HAS":{"L":true,"C":{"SZ":"62k","CID":"qVFhRTqLn6UJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Copying from Word when blogging : Problem city"},{"description":"Copying and pasting from Word causes big problems in blog publishing. Why? As Denise Wakeman explains: Because Word has tons of hidden code behind the"},{"date":"2006-10-21"},{"page_id":"individual"},{"category":"Blog Basics"},{"category_id":"27978"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"100925"}],"RK":0,"S":"... Jesse Newland, our IT guru at LexBlog, listened to the formatting problems caused
by copying and pasting from Word or from websites for months. ... ","T":"Copying from Word when blogging : Problem city : Real Lawyers Have ...","U":"http://kevin.lexblog.com/2006/10/articles/blog-basics/copying-from-word-when-blogging-problem-city/","UD":"http://kevin.lexblog.com/2006/10/articles/blog-basics/copying-from-word-when-blogging-problem-city/","UE":"http://kevin.lexblog.com/2006/10/articles/blog-basics/copying-from-word-when-blogging-problem-city/"},{"FS":[{"date":"2006-08-27"}],"HAS":{"L":true,"C":{"SZ":"62k","CID":"TWsLJEehbowJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"Akismet down : Spam getting through on some blogs"},{"description":"Scoble reports an onslaught of comment spam for WordPress bloggers with Akismet being down the last couple days. Akismet, produced by the WordPress fo"},{"date":"2006-08-27"},{"page_id":"individual"},{"category":"Blog Basics"},{"category_id":"27978"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"100768"}],"RK":0,"S":"... LexBlog's IT director, Jesse Newland, has whipped up a nice cocktail of
spam filters for LexBlog's network of blogs. We use Askimet ... ","T":"Akismet down : Spam getting through on some blogs : Real Lawyers ...","U":"http://kevin.lexblog.com/2006/08/articles/blog-basics/akismet-down-spam-getting-through-on-some-blogs/","UD":"http://kevin.lexblog.com/2006/08/articles/blog-basics/akismet-down-spam-getting-through-on-some-blogs/","UE":"http://kevin.lexblog.com/2006/08/articles/blog-basics/akismet-down-spam-getting-through-on-some-blogs/"},{"FS":[{"date":"2007-07-12"}],"HAS":{"L":true,"C":{"SZ":"61k","CID":"b0dat4IGr9EJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"27"},{"description":"Jill and I have been married 27 years today. 5 kids and 5 or 6 big dogs later, and I'm still loved more often than not - I think. By the way, 27 years"},{"date":"2007-07-12"},{"page_id":"individual"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"101594"}],"RK":0,"S":"... By the way, 27 years is longer than most of the LexBlog team members have been alive -
including our IT Director, Jesse Newland, who got married to Katie a ... ","T":"27 : Real Lawyers Have Blogs","U":"http://kevin.lexblog.com/2007/07/articles/cool-stuff/27/","UD":"http://kevin.lexblog.com/2007/07/articles/cool-stuff/27/","UE":"http://kevin.lexblog.com/2007/07/articles/cool-stuff/27/"},{"FS":[{"date":"2005-12-06"}],"HAS":{"L":true,"C":{"SZ":"64k","CID":"FFf2-yYVxrkJ"}},"LANG":"en","MT":[{"generator":"http://www.movabletype.org/"},{"blog_id":"395"},{"blog_name":"Real Lawyers Have Blogs"},{"title":"LexBlog is committed : Look at Bozeman weather"},{"description":"Lest there be any doubt about LexBlog's commitment to pulling together a talented team of marketing, web development, customer service and IT folk"},{"date":"2005-12-06"},{"page_id":"individual"},{"category":"Cool Stuff"},{"category_id":"27981"},{"author":"Kevin"},{"author_email":"kevin@lexblog.com"},{"entry_id":"100124"}],"RK":0,"S":"... Sure, Kelsie Eggensberger in customer service, and Andrea Gerlach, our new account
account exec, are both Montana State grads but Jesse Newland, head of IT, is ... ","T":"LexBlog is committed : Look at Bozeman weather : Real Lawyers Have ...","U":"http://kevin.lexblog.com/2005/12/articles/cool-stuff/lexblog-is-committed-look-at-bozeman-weather/","UD":"http://kevin.lexblog.com/2005/12/articles/cool-stuff/lexblog-is-committed-look-at-bozeman-weather/","UE":"http://kevin.lexblog.com/2005/12/articles/cool-stuff/lexblog-is-committed-look-at-bozeman-weather/"}],},"TM":0.043118} 4 | }; -------------------------------------------------------------------------------- /spec/lib/diff_match_patch.js: -------------------------------------------------------------------------------- 1 | function diff_match_patch(){this.Diff_Timeout=1.0;this.Diff_EditCost=4;this.Diff_DualThreshold=32;this.Match_Balance=0.5;this.Match_Threshold=0.5;this.Match_MinLength=100;this.Match_MaxLength=1000;this.Patch_Margin=4;function getMaxBits(){var maxbits=0;var oldi=1;var newi=2;while(oldi!=newi){maxbits++;oldi=newi;newi=newi<<1}return maxbits}this.Match_MaxBits=getMaxBits()}var DIFF_DELETE=-1;var DIFF_INSERT=1;var DIFF_EQUAL=0;diff_match_patch.prototype.diff_main=function(text1,text2,opt_checklines){if(text1==text2){return[[DIFF_EQUAL,text1]]}if(typeof opt_checklines=='undefined'){opt_checklines=true}var checklines=opt_checklines;var commonlength=this.diff_commonPrefix(text1,text2);var commonprefix=text1.substring(0,commonlength);text1=text1.substring(commonlength);text2=text2.substring(commonlength);commonlength=this.diff_commonSuffix(text1,text2);var commonsuffix=text1.substring(text1.length-commonlength);text1=text1.substring(0,text1.length-commonlength);text2=text2.substring(0,text2.length-commonlength);var diffs=this.diff_compute(text1,text2,checklines);if(commonprefix){diffs.unshift([DIFF_EQUAL,commonprefix])}if(commonsuffix){diffs.push([DIFF_EQUAL,commonsuffix])}this.diff_cleanupMerge(diffs);return diffs};diff_match_patch.prototype.diff_compute=function(text1,text2,checklines){var diffs;if(!text1){return[[DIFF_INSERT,text2]]}if(!text2){return[[DIFF_DELETE,text1]]}var longtext=text1.length>text2.length?text1:text2;var shorttext=text1.length>text2.length?text2:text1;var i=longtext.indexOf(shorttext);if(i!=-1){diffs=[[DIFF_INSERT,longtext.substring(0,i)],[DIFF_EQUAL,shorttext],[DIFF_INSERT,longtext.substring(i+shorttext.length)]];if(text1.length>text2.length){diffs[0][0]=diffs[2][0]=DIFF_DELETE}return diffs}longtext=shorttext=null;var hm=this.diff_halfMatch(text1,text2);if(hm){var text1_a=hm[0];var text1_b=hm[1];var text2_a=hm[2];var text2_b=hm[3];var mid_common=hm[4];var diffs_a=this.diff_main(text1_a,text2_a,checklines);var diffs_b=this.diff_main(text1_b,text2_b,checklines);return diffs_a.concat([[DIFF_EQUAL,mid_common]],diffs_b)}if(checklines&&text1.length+text2.length<250){checklines=false}var linearray;if(checklines){var a=this.diff_linesToChars(text1,text2);text1=a[0];text2=a[1];linearray=a[2]}diffs=this.diff_map(text1,text2);if(!diffs){diffs=[[DIFF_DELETE,text1],[DIFF_INSERT,text2]]}if(checklines){this.diff_charsToLines(diffs,linearray);this.diff_cleanupSemantic(diffs);diffs.push([DIFF_EQUAL,'']);var pointer=0;var count_delete=0;var count_insert=0;var text_delete='';var text_insert='';while(pointer=1&&count_insert>=1){var a=this.diff_main(text_delete,text_insert,false);diffs.splice(pointer-count_delete-count_insert,count_delete+count_insert);pointer=pointer-count_delete-count_insert;for(var j=a.length-1;j>=0;j--){diffs.splice(pointer,0,a[j])}pointer=pointer+a.length}count_insert=0;count_delete=0;text_delete='';text_insert=''}pointer++}diffs.pop()}return diffs};diff_match_patch.prototype.diff_linesToChars=function(text1,text2){var linearray=[];var linehash={};linearray.push('');function diff_linesToCharsMunge(text){var chars='';while(text){var i=text.indexOf('\n');if(i==-1){i=text.length}var line=text.substring(0,i+1);text=text.substring(i+1);if(linehash.hasOwnProperty?linehash.hasOwnProperty(line):(linehash[line]!==undefined)){chars+=String.fromCharCode(linehash[line])}else{linearray.push(line);linehash[line]=linearray.length-1;chars+=String.fromCharCode(linearray.length-1)}}return chars}var chars1=diff_linesToCharsMunge(text1);var chars2=diff_linesToCharsMunge(text2);return[chars1,chars2,linearray]};diff_match_patch.prototype.diff_charsToLines=function(diffs,linearray){for(var x=0;x0&&(new Date()).getTime()>ms_end){return null}v_map1[d]={};for(var k=-d;k<=d;k+=2){if(k==-d||k!=d&&v1[k-1]=0;d--){while(1){if(v_map[d].hasOwnProperty?v_map[d].hasOwnProperty((x-1)+','+y):(v_map[d][(x-1)+','+y]!==undefined)){x--;if(last_op===DIFF_DELETE){path[0][1]=text1.charAt(x)+path[0][1]}else{path.unshift([DIFF_DELETE,text1.charAt(x)])}last_op=DIFF_DELETE;break}else if(v_map[d].hasOwnProperty?v_map[d].hasOwnProperty(x+','+(y-1)):(v_map[d][x+','+(y-1)]!==undefined)){y--;if(last_op===DIFF_INSERT){path[0][1]=text2.charAt(y)+path[0][1]}else{path.unshift([DIFF_INSERT,text2.charAt(y)])}last_op=DIFF_INSERT;break}else{x--;y--;if(last_op===DIFF_EQUAL){path[0][1]=text1.charAt(x)+path[0][1]}else{path.unshift([DIFF_EQUAL,text1.charAt(x)])}last_op=DIFF_EQUAL}}}return path};diff_match_patch.prototype.diff_path2=function(v_map,text1,text2){var path=[];var x=text1.length;var y=text2.length;var last_op=null;for(var d=v_map.length-2;d>=0;d--){while(1){if(v_map[d].hasOwnProperty?v_map[d].hasOwnProperty((x-1)+','+y):(v_map[d][(x-1)+','+y]!==undefined)){x--;if(last_op===DIFF_DELETE){path[path.length-1][1]+=text1.charAt(text1.length-x-1)}else{path.push([DIFF_DELETE,text1.charAt(text1.length-x-1)])}last_op=DIFF_DELETE;break}else if(v_map[d].hasOwnProperty?v_map[d].hasOwnProperty(x+','+(y-1)):(v_map[d][x+','+(y-1)]!==undefined)){y--;if(last_op===DIFF_INSERT){path[path.length-1][1]+=text2.charAt(text2.length-y-1)}else{path.push([DIFF_INSERT,text2.charAt(text2.length-y-1)])}last_op=DIFF_INSERT;break}else{x--;y--;if(last_op===DIFF_EQUAL){path[path.length-1][1]+=text1.charAt(text1.length-x-1)}else{path.push([DIFF_EQUAL,text1.charAt(text1.length-x-1)])}last_op=DIFF_EQUAL}}}return path};diff_match_patch.prototype.diff_commonPrefix=function(text1,text2){if(!text1||!text2||text1.charCodeAt(0)!==text2.charCodeAt(0)){return 0}var pointermin=0;var pointermax=Math.min(text1.length,text2.length);var pointermid=pointermax;var pointerstart=0;while(pointermintext2.length?text1:text2;var shorttext=text1.length>text2.length?text2:text1;if(longtext.length<10||shorttext.length<1){return null}var dmp=this;function diff_halfMatchI(longtext,shorttext,i){var seed=longtext.substring(i,i+Math.floor(longtext.length/4));var j=-1;var best_common='';var best_longtext_a,best_longtext_b,best_shorttext_a,best_shorttext_b;while((j=shorttext.indexOf(seed,j+1))!=-1){var prefixLength=dmp.diff_commonPrefix(longtext.substring(i),shorttext.substring(j));var suffixLength=dmp.diff_commonSuffix(longtext.substring(0,i),shorttext.substring(0,j));if(best_common.length=longtext.length/2){return[best_longtext_a,best_longtext_b,best_shorttext_a,best_shorttext_b,best_common]}else{return null}}var hm1=diff_halfMatchI(longtext,shorttext,Math.ceil(longtext.length/4));var hm2=diff_halfMatchI(longtext,shorttext,Math.ceil(longtext.length/2));var hm;if(!hm1&&!hm2){return null}else if(!hm2){hm=hm1}else if(!hm1){hm=hm2}else{hm=hm1[4].length>hm2[4].length?hm1:hm2}var text1_a,text1_b,text2_a,text2_b;if(text1.length>text2.length){text1_a=hm[0];text1_b=hm[1];text2_a=hm[2];text2_b=hm[3]}else{text2_a=hm[0];text2_b=hm[1];text1_a=hm[2];text1_b=hm[3]}var mid_common=hm[4];return[text1_a,text1_b,text2_a,text2_b,mid_common]};diff_match_patch.prototype.diff_cleanupSemantic=function(diffs){var changes=false;var equalities=[];var lastequality=null;var pointer=0;var length_changes1=0;var length_changes2=0;while(pointer=bestScore){bestScore=score;bestEquality1=equality1;bestEdit=edit;bestEquality2=equality2}}if(diffs[pointer-1][1]!=bestEquality1){diffs[pointer-1][1]=bestEquality1;diffs[pointer][1]=bestEdit;diffs[pointer+1][1]=bestEquality2}}pointer++}};diff_match_patch.prototype.diff_cleanupEfficiency=function(diffs){var changes=false;var equalities=[];var lastequality='';var pointer=0;var pre_ins=false;var pre_del=false;var post_ins=false;var post_del=false;while(pointer0&&diffs[pointer-count_delete-count_insert-1][0]==DIFF_EQUAL){diffs[pointer-count_delete-count_insert-1][1]+=text_insert.substring(0,commonlength)}else{diffs.splice(0,0,[DIFF_EQUAL,text_insert.substring(0,commonlength)]);pointer++}text_insert=text_insert.substring(commonlength);text_delete=text_delete.substring(commonlength)}commonlength=this.diff_commonSuffix(text_insert,text_delete);if(commonlength!==0){diffs[pointer][1]=text_insert.substring(text_insert.length-commonlength)+diffs[pointer][1];text_insert=text_insert.substring(0,text_insert.length-commonlength);text_delete=text_delete.substring(0,text_delete.length-commonlength)}}if(count_delete===0){diffs.splice(pointer-count_delete-count_insert,count_delete+count_insert,[DIFF_INSERT,text_insert])}else if(count_insert===0){diffs.splice(pointer-count_delete-count_insert,count_delete+count_insert,[DIFF_DELETE,text_delete])}else{diffs.splice(pointer-count_delete-count_insert,count_delete+count_insert,[DIFF_DELETE,text_delete],[DIFF_INSERT,text_insert])}pointer=pointer-count_delete-count_insert+(count_delete?1:0)+(count_insert?1:0)+1}else if(pointer!==0&&diffs[pointer-1][0]==DIFF_EQUAL){diffs[pointer-1][1]+=diffs[pointer][1];diffs.splice(pointer,1)}else{pointer++}count_insert=0;count_delete=0;text_delete='';text_insert=''}}if(diffs[diffs.length-1][1]===''){diffs.pop()}var changes=false;pointer=1;while(pointerloc){break}last_chars1=chars1;last_chars2=chars2}if(diffs.length!=x&&diffs[x][0]===DIFF_DELETE){return last_chars2}return last_chars2+(loc-last_chars1)};diff_match_patch.prototype.diff_prettyHtml=function(diffs){this.diff_addIndex(diffs);var html=[];for(var x=0;x/g,'>');t=t.replace(/\n/g,'¶
');if(m===DIFF_DELETE){html.push('',t,'')}else if(m===DIFF_INSERT){html.push('',t,'')}else{html.push('',t,'')}}return html.join('')};diff_match_patch.prototype.diff_text1=function(diffs){var txt=[];for(var x=0;xthis.Match_MaxBits){return alert('Pattern too long for this browser.')}var s=this.match_alphabet(pattern);var score_text_length=text.length;score_text_length=Math.max(score_text_length,this.Match_MinLength);score_text_length=Math.min(score_text_length,this.Match_MaxLength);var dmp=this;function match_bitapScore(e,x){var d=Math.abs(loc-x);return(e/pattern.length/dmp.Match_Balance)+(d/score_text_length/(1.0-dmp.Match_Balance))}var score_threshold=this.Match_Threshold;var best_loc=text.indexOf(pattern,loc);if(best_loc!=-1){score_threshold=Math.min(match_bitapScore(0,best_loc),score_threshold)}best_loc=text.lastIndexOf(pattern,loc+pattern.length);if(best_loc!=-1){score_threshold=Math.min(match_bitapScore(0,best_loc),score_threshold)}var matchmask=1<<(pattern.length-1);best_loc=null;var bin_min,bin_mid;var bin_max=Math.max(loc+loc,text.length);var last_rd;for(var d=0;d=start;j--){if(d===0){rd[j]=((rd[j+1]<<1)|1)&s[text.charAt(j)]}else{rd[j]=((rd[j+1]<<1)|1)&s[text.charAt(j)]|((last_rd[j+1]<<1)|1)|((last_rd[j]<<1)|1)|last_rd[j+1]}if(rd[j]&matchmask){var score=match_bitapScore(d,j);if(score<=score_threshold){score_threshold=score;best_loc=j;if(j>loc){start=Math.max(0,loc-(j-loc))}else{break}}}}if(match_bitapScore(d+1,loc)>score_threshold){break}last_rd=rd}return best_loc};diff_match_patch.prototype.match_alphabet=function(pattern){var s=Object();for(var i=0;i2){this.diff_cleanupSemantic(diffs);this.diff_cleanupEfficiency(diffs)}}if(diffs.length===0){return[]}var patches=[];var patch=new patch_obj();var char_count1=0;var char_count2=0;var prepatch_text=text1;var postpatch_text=text1;for(var x=0;x=2*this.Patch_Margin){if(patch.diffs.length!==0){this.patch_addContext(patch,prepatch_text);patches.push(patch);patch=new patch_obj();prepatch_text=postpatch_text}}if(diff_type!==DIFF_INSERT){char_count1+=diff_text.length}if(diff_type!==DIFF_DELETE){char_count2+=diff_text.length}}if(patch.diffs.length!==0){this.patch_addContext(patch,prepatch_text);patches.push(patch)}return patches};diff_match_patch.prototype.patch_apply=function(patches,text){this.patch_splitMax(patches);var results=[];var delta=0;for(var x=0;xthis.Match_MaxBits){var bigpatch=patches[x];patches.splice(x,1);var patch_size=this.Match_MaxBits;var start1=bigpatch.start1;var start2=bigpatch.start2;var precontext='';while(bigpatch.diffs.length!==0){var patch=new patch_obj();var empty=true;patch.start1=start1-precontext.length;patch.start2=start2-precontext.length;if(precontext!==''){patch.length1=patch.length2=precontext.length;patch.diffs.push([DIFF_EQUAL,precontext])}while(bigpatch.diffs.length!==0&&patch.length1 -1, 40 | Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, 41 | Presto: navigator.appName == "Opera" 42 | } 43 | }; 44 | 45 | 46 | 47 | /** 48 | * Executor 49 | */ 50 | JSSpec.Executor = function(target, onSuccess, onException) { 51 | this.target = target; 52 | this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION; 53 | this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION; 54 | 55 | if(JSSpec.Browser.Trident) { 56 | // Exception handler for Trident. It helps to collect exact line number where exception occured. 57 | window.onerror = function(message, fileName, lineNumber) { 58 | var self = window._curExecutor; 59 | var ex = {message:message, fileName:fileName, lineNumber:lineNumber}; 60 | 61 | if(JSSpec._secondPass) { 62 | ex = self.mergeExceptions(JSSpec._assertionFailure, ex); 63 | delete JSSpec._secondPass; 64 | delete JSSpec._assertionFailure; 65 | 66 | ex.type = "failure"; 67 | self.onException(self, ex); 68 | } else if(JSSpec._assertionFailure) { 69 | JSSpec._secondPass = true; 70 | self.run(); 71 | } else { 72 | self.onException(self, ex); 73 | } 74 | 75 | return true; 76 | }; 77 | } 78 | }; 79 | JSSpec.Executor.prototype.mergeExceptions = function(assertionFailure, normalException) { 80 | var merged = { 81 | message:assertionFailure.message, 82 | fileName:normalException.fileName, 83 | lineNumber:normalException.lineNumber 84 | }; 85 | 86 | return merged; 87 | }; 88 | 89 | JSSpec.Executor.prototype.run = function() { 90 | var self = this; 91 | var target = this.target; 92 | var onSuccess = this.onSuccess; 93 | var onException = this.onException; 94 | 95 | window.setTimeout( 96 | function() { 97 | var result; 98 | if(JSSpec.Browser.Trident) { 99 | window._curExecutor = self; 100 | 101 | result = self.target(); 102 | self.onSuccess(self, result); 103 | } else { 104 | try { 105 | result = self.target(); 106 | self.onSuccess(self, result); 107 | } catch(ex) { 108 | if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line}; 109 | 110 | if(JSSpec._secondPass) { 111 | ex = self.mergeExceptions(JSSpec._assertionFailure, ex); 112 | delete JSSpec._secondPass; 113 | delete JSSpec._assertionFailure; 114 | 115 | ex.type = "failure"; 116 | self.onException(self, ex); 117 | } else if(JSSpec._assertionFailure) { 118 | JSSpec._secondPass = true; 119 | self.run(); 120 | } else { 121 | self.onException(self, ex); 122 | } 123 | } 124 | } 125 | }, 126 | 0 127 | ); 128 | }; 129 | 130 | 131 | 132 | /** 133 | * CompositeExecutor composites one or more executors and execute them sequencially. 134 | */ 135 | JSSpec.CompositeExecutor = function(onSuccess, onException, continueOnException) { 136 | this.queue = []; 137 | this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION; 138 | this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION; 139 | this.continueOnException = !!continueOnException; 140 | }; 141 | 142 | JSSpec.CompositeExecutor.prototype.addFunction = function(func) { 143 | this.addExecutor(new JSSpec.Executor(func)); 144 | }; 145 | 146 | JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) { 147 | var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1]; 148 | if(last) { 149 | last.next = executor; 150 | } 151 | 152 | executor.parent = this; 153 | executor.onSuccessBackup = executor.onSuccess; 154 | executor.onSuccess = function(result) { 155 | this.onSuccessBackup(result); 156 | if(this.next) { 157 | this.next.run(); 158 | } else { 159 | this.parent.onSuccess(); 160 | } 161 | }; 162 | executor.onExceptionBackup = executor.onException; 163 | executor.onException = function(executor, ex) { 164 | this.onExceptionBackup(executor, ex); 165 | 166 | if(this.parent.continueOnException) { 167 | if(this.next) { 168 | this.next.run(); 169 | } else { 170 | this.parent.onSuccess(); 171 | } 172 | } else { 173 | this.parent.onException(executor, ex); 174 | } 175 | }; 176 | 177 | this.queue.push(executor); 178 | }; 179 | 180 | JSSpec.CompositeExecutor.prototype.run = function() { 181 | if(this.queue.length > 0) { 182 | this.queue[0].run(); 183 | } 184 | }; 185 | 186 | /** 187 | * Spec is a set of Examples in a specific context 188 | */ 189 | JSSpec.Spec = function(context, entries) { 190 | this.id = JSSpec.Spec.id++; 191 | this.context = context; 192 | this.url = location.href; 193 | 194 | this.filterEntriesByEmbeddedExpressions(entries); 195 | this.extractOutSpecialEntries(entries); 196 | this.examples = this.makeExamplesFromEntries(entries); 197 | this.examplesMap = this.makeMapFromExamples(this.examples); 198 | }; 199 | 200 | JSSpec.Spec.id = 0; 201 | JSSpec.Spec.prototype.getExamples = function() { 202 | return this.examples; 203 | }; 204 | 205 | JSSpec.Spec.prototype.hasException = function() { 206 | return this.getTotalFailures() > 0 || this.getTotalErrors() > 0; 207 | }; 208 | 209 | JSSpec.Spec.prototype.getTotalFailures = function() { 210 | var examples = this.examples; 211 | var failures = 0; 212 | for(var i = 0; i < examples.length; i++) { 213 | if(examples[i].isFailure()) failures++; 214 | } 215 | return failures; 216 | }; 217 | 218 | JSSpec.Spec.prototype.getTotalErrors = function() { 219 | var examples = this.examples; 220 | var errors = 0; 221 | for(var i = 0; i < examples.length; i++) { 222 | if(examples[i].isError()) errors++; 223 | } 224 | return errors; 225 | }; 226 | 227 | JSSpec.Spec.prototype.filterEntriesByEmbeddedExpressions = function(entries) { 228 | var isTrue; 229 | for(name in entries) { 230 | var m = name.match(/\[\[(.+)\]\]/); 231 | if(m && m[1]) { 232 | eval("isTrue = (" + m[1] + ")"); 233 | if(!isTrue) delete entries[name]; 234 | } 235 | } 236 | }; 237 | 238 | JSSpec.Spec.prototype.extractOutSpecialEntries = function(entries) { 239 | this.beforeEach = JSSpec.EMPTY_FUNCTION; 240 | this.beforeAll = JSSpec.EMPTY_FUNCTION; 241 | this.afterEach = JSSpec.EMPTY_FUNCTION; 242 | this.afterAll = JSSpec.EMPTY_FUNCTION; 243 | 244 | for(name in entries) { 245 | if(name == 'before' || name == 'before each' || name == 'before_each') { 246 | this.beforeEach = entries[name]; 247 | } else if(name == 'before all' || name == 'before_all') { 248 | this.beforeAll = entries[name]; 249 | } else if(name == 'after' || name == 'after each' || name == 'after_each') { 250 | this.afterEach = entries[name]; 251 | } else if(name == 'after all' || name == 'after_all') { 252 | this.afterAll = entries[name]; 253 | } 254 | } 255 | 256 | delete entries['before']; 257 | delete entries['before each']; 258 | delete entries['before_each']; 259 | delete entries['before all']; 260 | delete entries['before_all']; 261 | delete entries['after']; 262 | delete entries['after each']; 263 | delete entries['after_each']; 264 | delete entries['after all']; 265 | delete entries['after_all']; 266 | }; 267 | 268 | JSSpec.Spec.prototype.makeExamplesFromEntries = function(entries) { 269 | var examples = []; 270 | for(name in entries) { 271 | examples.push(new JSSpec.Example(name, entries[name], this.beforeEach, this.afterEach)); 272 | } 273 | return examples; 274 | }; 275 | 276 | JSSpec.Spec.prototype.makeMapFromExamples = function(examples) { 277 | var map = {}; 278 | for(var i = 0; i < examples.length; i++) { 279 | var example = examples[i]; 280 | map[example.id] = examples[i]; 281 | } 282 | return map; 283 | }; 284 | 285 | JSSpec.Spec.prototype.getExampleById = function(id) { 286 | return this.examplesMap[id]; 287 | }; 288 | 289 | JSSpec.Spec.prototype.getExecutor = function() { 290 | var self = this; 291 | var onException = function(executor, ex) { 292 | self.exception = ex; 293 | }; 294 | 295 | var composite = new JSSpec.CompositeExecutor(); 296 | composite.addFunction(function() {JSSpec.log.onSpecStart(self);}); 297 | composite.addExecutor(new JSSpec.Executor(this.beforeAll, null, function(exec, ex) { 298 | self.exception = ex; 299 | JSSpec.log.onSpecEnd(self); 300 | })); 301 | 302 | var exampleAndAfter = new JSSpec.CompositeExecutor(null,null,true); 303 | for(var i = 0; i < this.examples.length; i++) { 304 | exampleAndAfter.addExecutor(this.examples[i].getExecutor()); 305 | } 306 | exampleAndAfter.addExecutor(new JSSpec.Executor(this.afterAll, null, onException)); 307 | exampleAndAfter.addFunction(function() {JSSpec.log.onSpecEnd(self);}); 308 | composite.addExecutor(exampleAndAfter); 309 | 310 | return composite; 311 | }; 312 | 313 | /** 314 | * Example 315 | */ 316 | JSSpec.Example = function(name, target, before, after) { 317 | this.id = JSSpec.Example.id++; 318 | this.name = name; 319 | this.target = target; 320 | this.before = before; 321 | this.after = after; 322 | }; 323 | 324 | JSSpec.Example.id = 0; 325 | JSSpec.Example.prototype.isFailure = function() { 326 | return this.exception && this.exception.type == "failure"; 327 | }; 328 | 329 | JSSpec.Example.prototype.isError = function() { 330 | return this.exception && !this.exception.type; 331 | }; 332 | 333 | JSSpec.Example.prototype.getExecutor = function() { 334 | var self = this; 335 | var onException = function(executor, ex) { 336 | self.exception = ex; 337 | }; 338 | 339 | var composite = new JSSpec.CompositeExecutor(); 340 | composite.addFunction(function() {JSSpec.log.onExampleStart(self);}); 341 | composite.addExecutor(new JSSpec.Executor(this.before, null, function(exec, ex) { 342 | self.exception = ex; 343 | JSSpec.log.onExampleEnd(self); 344 | })); 345 | 346 | var targetAndAfter = new JSSpec.CompositeExecutor(null,null,true); 347 | 348 | targetAndAfter.addExecutor(new JSSpec.Executor(this.target, null, onException)); 349 | targetAndAfter.addExecutor(new JSSpec.Executor(this.after, null, onException)); 350 | targetAndAfter.addFunction(function() {JSSpec.log.onExampleEnd(self);}); 351 | 352 | composite.addExecutor(targetAndAfter); 353 | 354 | return composite; 355 | }; 356 | 357 | /** 358 | * Runner 359 | */ 360 | JSSpec.Runner = function(specs, logger) { 361 | JSSpec.log = logger; 362 | 363 | this.totalExamples = 0; 364 | this.specs = []; 365 | this.specsMap = {}; 366 | this.addAllSpecs(specs); 367 | }; 368 | 369 | JSSpec.Runner.prototype.addAllSpecs = function(specs) { 370 | for(var i = 0; i < specs.length; i++) { 371 | this.addSpec(specs[i]); 372 | } 373 | }; 374 | 375 | JSSpec.Runner.prototype.addSpec = function(spec) { 376 | this.specs.push(spec); 377 | this.specsMap[spec.id] = spec; 378 | this.totalExamples += spec.getExamples().length; 379 | }; 380 | 381 | JSSpec.Runner.prototype.getSpecById = function(id) { 382 | return this.specsMap[id]; 383 | }; 384 | 385 | JSSpec.Runner.prototype.getSpecByContext = function(context) { 386 | for(var i = 0; i < this.specs.length; i++) { 387 | if(this.specs[i].context == context) return this.specs[i]; 388 | } 389 | return null; 390 | }; 391 | 392 | JSSpec.Runner.prototype.getSpecs = function() { 393 | return this.specs; 394 | }; 395 | 396 | JSSpec.Runner.prototype.hasException = function() { 397 | return this.getTotalFailures() > 0 || this.getTotalErrors() > 0; 398 | }; 399 | 400 | JSSpec.Runner.prototype.getTotalFailures = function() { 401 | var specs = this.specs; 402 | var failures = 0; 403 | for(var i = 0; i < specs.length; i++) { 404 | failures += specs[i].getTotalFailures(); 405 | } 406 | return failures; 407 | }; 408 | 409 | JSSpec.Runner.prototype.getTotalErrors = function() { 410 | var specs = this.specs; 411 | var errors = 0; 412 | for(var i = 0; i < specs.length; i++) { 413 | errors += specs[i].getTotalErrors(); 414 | } 415 | return errors; 416 | }; 417 | 418 | 419 | JSSpec.Runner.prototype.run = function() { 420 | JSSpec.log.onRunnerStart(); 421 | var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true); 422 | for(var i = 0; i < this.specs.length; i++) { 423 | executor.addExecutor(this.specs[i].getExecutor()); 424 | } 425 | executor.run(); 426 | }; 427 | 428 | 429 | JSSpec.Runner.prototype.rerun = function(context) { 430 | JSSpec.runner = new JSSpec.Runner([this.getSpecByContext(context)], JSSpec.log); 431 | JSSpec.runner.run(); 432 | }; 433 | 434 | /** 435 | * Logger 436 | */ 437 | JSSpec.Logger = function() { 438 | this.finishedExamples = 0; 439 | this.startedAt = null; 440 | }; 441 | 442 | JSSpec.Logger.prototype.onRunnerStart = function() { 443 | this._title = document.title; 444 | 445 | this.startedAt = new Date(); 446 | var container = document.getElementById('jsspec_container'); 447 | if(container) { 448 | container.innerHTML = ""; 449 | } else { 450 | container = document.createElement("DIV"); 451 | container.id = "jsspec_container"; 452 | document.body.appendChild(container); 453 | } 454 | 455 | var title = document.createElement("DIV"); 456 | title.id = "title"; 457 | title.innerHTML = [ 458 | '

JSSpec runner

', 459 | '
    ', 460 | '
  • ' + JSSpec.runner.totalExamples + ' examples
  • ', 461 | '
  • 0 failures
  • ', 462 | '
  • 0 errors
  • ', 463 | '
  • 0% done
  • ', 464 | '
  • 0 secs
  • ', 465 | '
', 466 | '

JSSpec homepage

', 467 | ].join(""); 468 | container.appendChild(title); 469 | 470 | var list = document.createElement("DIV"); 471 | list.id = "list"; 472 | list.innerHTML = [ 473 | '

List

', 474 | '
    ', 475 | function() { 476 | var specs = JSSpec.runner.getSpecs(); 477 | var sb = []; 478 | for(var i = 0; i < specs.length; i++) { 479 | var spec = specs[i]; 480 | sb.push('
  • ' + JSSpec.util.escapeTags(specs[i].context) + ' [rerun]

  • '); 481 | } 482 | return sb.join(""); 483 | }(), 484 | '
' 485 | ].join(""); 486 | container.appendChild(list); 487 | 488 | var log = document.createElement("DIV"); 489 | log.id = "log"; 490 | log.innerHTML = [ 491 | '

Log

', 492 | '
    ', 493 | function() { 494 | var specs = JSSpec.runner.getSpecs(); 495 | var sb = []; 496 | for(var i = 0; i < specs.length; i++) { 497 | var spec = specs[i]; 498 | sb.push('
  • '); 499 | sb.push('

    ' + JSSpec.util.escapeTags(specs[i].context) + ' [rerun]

    '); 500 | sb.push('
      '); 501 | for(var j = 0; j < spec.examples.length; j++) { 502 | var example = spec.examples[j]; 503 | sb.push('
    • ') 504 | sb.push('

      ' + JSSpec.util.escapeTags(example.name) + '

      ') 505 | sb.push('
    • ') 506 | } 507 | sb.push('
    '); 508 | sb.push('
  • '); 509 | } 510 | return sb.join(""); 511 | }(), 512 | '
' 513 | ].join(""); 514 | 515 | container.appendChild(log); 516 | 517 | // add event handler for toggling 518 | var specs = JSSpec.runner.getSpecs(); 519 | var sb = []; 520 | for(var i = 0; i < specs.length; i++) { 521 | var spec = document.getElementById("spec_" + specs[i].id); 522 | var title = spec.getElementsByTagName("H3")[0]; 523 | title.onclick = function(e) { 524 | var target = document.getElementById(this.parentNode.id + "_examples"); 525 | target.style.display = target.style.display == "none" ? "block" : "none"; 526 | return true; 527 | } 528 | } 529 | }; 530 | 531 | JSSpec.Logger.prototype.onRunnerEnd = function() { 532 | if(JSSpec.runner.hasException()) { 533 | var times = 4; 534 | var title1 = "*" + this._title; 535 | var title2 = "*F" + JSSpec.runner.getTotalFailures() + " E" + JSSpec.runner.getTotalErrors() + "* " + this._title; 536 | } else { 537 | var times = 2; 538 | var title1 = this._title; 539 | var title2 = "Success"; 540 | } 541 | this.blinkTitle(times,title1,title2); 542 | }; 543 | 544 | JSSpec.Logger.prototype.blinkTitle = function(times, title1, title2) { 545 | var times = times * 2; 546 | var mode = true; 547 | 548 | var f = function() { 549 | if(times > 0) { 550 | document.title = mode ? title1 : title2; 551 | mode = !mode; 552 | times--; 553 | window.setTimeout(f, 500); 554 | } else { 555 | document.title = title1; 556 | } 557 | }; 558 | 559 | f(); 560 | }; 561 | 562 | JSSpec.Logger.prototype.onSpecStart = function(spec) { 563 | var spec_list = document.getElementById("spec_" + spec.id + "_list"); 564 | var spec_log = document.getElementById("spec_" + spec.id); 565 | 566 | spec_list.className = "ongoing"; 567 | spec_log.className = "ongoing"; 568 | }; 569 | 570 | JSSpec.Logger.prototype.onSpecEnd = function(spec) { 571 | var spec_list = document.getElementById("spec_" + spec.id + "_list"); 572 | var spec_log = document.getElementById("spec_" + spec.id); 573 | var examples = document.getElementById("spec_" + spec.id + "_examples"); 574 | var className = spec.hasException() ? "exception" : "success"; 575 | 576 | spec_list.className = className; 577 | spec_log.className = className; 578 | 579 | if(JSSpec.options.autocollapse && !spec.hasException()) examples.style.display = "none"; 580 | 581 | if(spec.exception) { 582 | heading.appendChild(document.createTextNode(" - " + spec.exception.message)); 583 | } 584 | }; 585 | 586 | JSSpec.Logger.prototype.onExampleStart = function(example) { 587 | var li = document.getElementById("example_" + example.id); 588 | li.className = "ongoing"; 589 | }; 590 | 591 | JSSpec.Logger.prototype.onExampleEnd = function(example) { 592 | var li = document.getElementById("example_" + example.id); 593 | li.className = example.exception ? "exception" : "success"; 594 | 595 | if(example.exception) { 596 | var div = document.createElement("DIV"); 597 | div.innerHTML = example.exception.message + "


" + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "

"; 598 | li.appendChild(div); 599 | } 600 | 601 | var title = document.getElementById("title"); 602 | var runner = JSSpec.runner; 603 | 604 | title.className = runner.hasException() ? "exception" : "success"; 605 | 606 | this.finishedExamples++; 607 | document.getElementById("total_failures").innerHTML = runner.getTotalFailures(); 608 | document.getElementById("total_errors").innerHTML = runner.getTotalErrors(); 609 | var progress = parseInt(this.finishedExamples / runner.totalExamples * 100); 610 | document.getElementById("progress").innerHTML = progress; 611 | document.getElementById("total_elapsed").innerHTML = (new Date().getTime() - this.startedAt.getTime()) / 1000; 612 | 613 | document.title = progress + "%: " + this._title; 614 | }; 615 | 616 | /** 617 | * IncludeMatcher 618 | */ 619 | JSSpec.IncludeMatcher = function(actual, expected, condition) { 620 | this.actual = actual; 621 | this.expected = expected; 622 | this.condition = condition; 623 | this.match = false; 624 | this.explaination = this.makeExplain(); 625 | }; 626 | 627 | JSSpec.IncludeMatcher.createInstance = function(actual, expected, condition) { 628 | return new JSSpec.IncludeMatcher(actual, expected, condition); 629 | }; 630 | 631 | JSSpec.IncludeMatcher.prototype.matches = function() { 632 | return this.match; 633 | }; 634 | 635 | JSSpec.IncludeMatcher.prototype.explain = function() { 636 | return this.explaination; 637 | }; 638 | 639 | JSSpec.IncludeMatcher.prototype.makeExplain = function() { 640 | if(typeof this.actual.length == 'undefined') { 641 | return this.makeExplainForNotArray(); 642 | } else { 643 | return this.makeExplainForArray(); 644 | } 645 | }; 646 | 647 | JSSpec.IncludeMatcher.prototype.makeExplainForNotArray = function() { 648 | var sb = []; 649 | sb.push('

actual value:

'); 650 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 651 | sb.push('

should ' + (this.condition ? '' : 'not') + ' include:

'); 652 | sb.push('

' + JSSpec.util.inspect(this.expected) + '

'); 653 | sb.push('

but since it\s not an array, include or not doesn\'t make any sense.

'); 654 | return sb.join(""); 655 | } 656 | ; 657 | JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() { 658 | var matches; 659 | if(this.condition) { 660 | for(var i = 0; i < this.actual.length; i++) { 661 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); 662 | if(matches) { 663 | this.match = true; 664 | break; 665 | } 666 | } 667 | } else { 668 | for(var i = 0; i < this.actual.length; i++) { 669 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); 670 | if(matches) { 671 | this.match = false; 672 | break; 673 | } 674 | } 675 | } 676 | 677 | if(this.match) return ""; 678 | 679 | var sb = []; 680 | sb.push('

actual value:

'); 681 | sb.push('

' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '

'); 682 | sb.push('

should ' + (this.condition ? '' : 'not') + ' include:

'); 683 | sb.push('

' + JSSpec.util.inspect(this.expected) + '

'); 684 | return sb.join(""); 685 | }; 686 | 687 | /** 688 | * PropertyLengthMatcher 689 | */ 690 | JSSpec.PropertyLengthMatcher = function(num, property, o, condition) { 691 | this.num = num; 692 | this.o = o; 693 | this.property = property; 694 | if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') { 695 | this.property = 'length'; 696 | } 697 | 698 | this.condition = condition; 699 | this.conditionMet = function(x) { 700 | if(condition == 'exactly') return x.length == num; 701 | if(condition == 'at least') return x.length >= num; 702 | if(condition == 'at most') return x.length <= num; 703 | 704 | throw "Unknown condition '" + condition + "'"; 705 | }; 706 | this.match = false; 707 | this.explaination = this.makeExplain(); 708 | }; 709 | 710 | JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() { 711 | if(this.o._type == 'String' && this.property == 'length') { 712 | this.match = this.conditionMet(this.o); 713 | return this.match ? '' : this.makeExplainForString(); 714 | } else if(typeof this.o.length != 'undefined' && this.property == "length") { 715 | this.match = this.conditionMet(this.o); 716 | return this.match ? '' : this.makeExplainForArray(); 717 | } else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) { 718 | this.match = this.conditionMet(this.o[this.property]); 719 | return this.match ? '' : this.makeExplainForObject(); 720 | } else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) { 721 | this.match = false; 722 | return this.makeExplainForNoProperty(); 723 | } 724 | 725 | this.match = true; 726 | }; 727 | 728 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() { 729 | var sb = []; 730 | 731 | var exp = this.num == 0 ? 732 | 'be an empty string' : 733 | 'have ' + this.condition + ' ' + this.num + ' characters'; 734 | 735 | sb.push('

actual value has ' + this.o.length + ' characters:

'); 736 | sb.push('

' + JSSpec.util.inspect(this.o) + '

'); 737 | sb.push('

but it should ' + exp + '.

'); 738 | 739 | return sb.join(""); 740 | }; 741 | 742 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() { 743 | var sb = []; 744 | 745 | var exp = this.num == 0 ? 746 | 'be an empty array' : 747 | 'have ' + this.condition + ' ' + this.num + ' items'; 748 | 749 | sb.push('

actual value has ' + this.o.length + ' items:

'); 750 | sb.push('

' + JSSpec.util.inspect(this.o) + '

'); 751 | sb.push('

but it should ' + exp + '.

'); 752 | 753 | return sb.join(""); 754 | }; 755 | 756 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() { 757 | var sb = []; 758 | 759 | var exp = this.num == 0 ? 760 | 'be empty' : 761 | 'have ' + this.condition + ' ' + this.num + ' ' + this.property + '.'; 762 | 763 | sb.push('

actual value has ' + this.o[this.property].length + ' ' + this.property + ':

'); 764 | sb.push('

' + JSSpec.util.inspect(this.o, false, this.property) + '

'); 765 | sb.push('

but it should ' + exp + '.

'); 766 | 767 | return sb.join(""); 768 | }; 769 | 770 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() { 771 | var sb = []; 772 | 773 | sb.push('

actual value:

'); 774 | sb.push('

' + JSSpec.util.inspect(this.o) + '

'); 775 | sb.push('

should have ' + this.condition + ' ' + this.num + ' ' + this.property + ' but there\'s no such property.

'); 776 | 777 | return sb.join(""); 778 | }; 779 | 780 | JSSpec.PropertyLengthMatcher.prototype.matches = function() { 781 | return this.match; 782 | }; 783 | 784 | JSSpec.PropertyLengthMatcher.prototype.explain = function() { 785 | return this.explaination; 786 | }; 787 | 788 | JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) { 789 | return new JSSpec.PropertyLengthMatcher(num, property, o, condition); 790 | }; 791 | 792 | /** 793 | * EqualityMatcher 794 | */ 795 | JSSpec.EqualityMatcher = {}; 796 | 797 | JSSpec.EqualityMatcher.createInstance = function(expected, actual) { 798 | if(expected == null || actual == null) { 799 | return new JSSpec.NullEqualityMatcher(expected, actual); 800 | } else if(expected._type && expected._type == actual._type) { 801 | if(expected._type == "String") { 802 | return new JSSpec.StringEqualityMatcher(expected, actual); 803 | } else if(expected._type == "Date") { 804 | return new JSSpec.DateEqualityMatcher(expected, actual); 805 | } else if(expected._type == "Number") { 806 | return new JSSpec.NumberEqualityMatcher(expected, actual); 807 | } else if(expected._type == "Array") { 808 | return new JSSpec.ArrayEqualityMatcher(expected, actual); 809 | } else if(expected._type == "Boolean") { 810 | return new JSSpec.BooleanEqualityMatcher(expected, actual); 811 | } 812 | } 813 | 814 | return new JSSpec.ObjectEqualityMatcher(expected, actual); 815 | }; 816 | 817 | JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) { 818 | var sb = []; 819 | 820 | sb.push(actualDesc || '

actual value:

'); 821 | sb.push('

' + JSSpec.util.inspect(actual) + '

'); 822 | sb.push(expectedDesc || '

should be:

'); 823 | sb.push('

' + JSSpec.util.inspect(expected) + '

'); 824 | 825 | return sb.join(""); 826 | }; 827 | 828 | JSSpec.EqualityMatcher.diffExplain = function(expected, actual) { 829 | var sb = []; 830 | 831 | sb.push('

diff:

'); 832 | sb.push('

'); 833 | 834 | var dmp = new diff_match_patch(); 835 | var diff = dmp.diff_main(expected, actual); 836 | dmp.diff_cleanupEfficiency(diff); 837 | 838 | sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true)); 839 | 840 | sb.push('

'); 841 | 842 | return sb.join(""); 843 | }; 844 | 845 | /** 846 | * BooleanEqualityMatcher 847 | */ 848 | JSSpec.BooleanEqualityMatcher = function(expected, actual) { 849 | this.expected = expected; 850 | this.actual = actual; 851 | }; 852 | 853 | JSSpec.BooleanEqualityMatcher.prototype.explain = function() { 854 | var sb = []; 855 | 856 | sb.push('

actual value:

'); 857 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 858 | sb.push('

should be:

'); 859 | sb.push('

' + JSSpec.util.inspect(this.expected) + '

'); 860 | 861 | return sb.join(""); 862 | }; 863 | 864 | JSSpec.BooleanEqualityMatcher.prototype.matches = function() { 865 | return this.expected == this.actual; 866 | }; 867 | 868 | /** 869 | * NullEqualityMatcher 870 | */ 871 | JSSpec.NullEqualityMatcher = function(expected, actual) { 872 | this.expected = expected; 873 | this.actual = actual; 874 | }; 875 | 876 | JSSpec.NullEqualityMatcher.prototype.matches = function() { 877 | return this.expected == this.actual && typeof this.expected == typeof this.actual; 878 | }; 879 | 880 | JSSpec.NullEqualityMatcher.prototype.explain = function() { 881 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); 882 | }; 883 | 884 | JSSpec.DateEqualityMatcher = function(expected, actual) { 885 | this.expected = expected; 886 | this.actual = actual; 887 | }; 888 | 889 | JSSpec.DateEqualityMatcher.prototype.matches = function() { 890 | return this.expected.getTime() == this.actual.getTime(); 891 | }; 892 | 893 | JSSpec.DateEqualityMatcher.prototype.explain = function() { 894 | var sb = []; 895 | 896 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 897 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString())); 898 | 899 | return sb.join(""); 900 | }; 901 | 902 | /** 903 | * ObjectEqualityMatcher 904 | */ 905 | JSSpec.ObjectEqualityMatcher = function(expected, actual) { 906 | this.expected = expected; 907 | this.actual = actual; 908 | this.match = this.expected == this.actual; 909 | this.explaination = this.makeExplain(); 910 | }; 911 | 912 | JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match}; 913 | 914 | JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination}; 915 | 916 | JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() { 917 | if(this.expected == this.actual) { 918 | this.match = true; 919 | return ""; 920 | } 921 | 922 | if(JSSpec.util.isDomNode(this.expected)) { 923 | return this.makeExplainForDomNode(); 924 | } 925 | 926 | var key, expectedHasItem, actualHasItem; 927 | 928 | for(key in this.expected) { 929 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; 930 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; 931 | if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key); 932 | } 933 | for(key in this.actual) { 934 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; 935 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; 936 | if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key); 937 | } 938 | 939 | for(key in this.expected) { 940 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]); 941 | if(!matcher.matches()) return this.makeExplainForItemMismatch(key); 942 | } 943 | 944 | this.match = true; 945 | }; 946 | 947 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) { 948 | var sb = []; 949 | 950 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 951 | 952 | return sb.join(""); 953 | }; 954 | 955 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) { 956 | var sb = []; 957 | 958 | sb.push('

actual value has no item named ' + JSSpec.util.inspect(key) + '

'); 959 | sb.push('

' + JSSpec.util.inspect(this.actual, false, key) + '

'); 960 | sb.push('

but it should have the item whose value is ' + JSSpec.util.inspect(this.expected[key]) + '

'); 961 | sb.push('

' + JSSpec.util.inspect(this.expected, false, key) + '

'); 962 | 963 | return sb.join(""); 964 | }; 965 | 966 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) { 967 | var sb = []; 968 | 969 | sb.push('

actual value has item named ' + JSSpec.util.inspect(key) + '

'); 970 | sb.push('

' + JSSpec.util.inspect(this.actual, false, key) + '

'); 971 | sb.push('

but there should be no such item

'); 972 | sb.push('

' + JSSpec.util.inspect(this.expected, false, key) + '

'); 973 | 974 | return sb.join(""); 975 | }; 976 | 977 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) { 978 | var sb = []; 979 | 980 | sb.push('

actual value has an item named ' + JSSpec.util.inspect(key) + ' whose value is ' + JSSpec.util.inspect(this.actual[key]) + '

'); 981 | sb.push('

' + JSSpec.util.inspect(this.actual, false, key) + '

'); 982 | sb.push('

but it\'s value should be ' + JSSpec.util.inspect(this.expected[key]) + '

'); 983 | sb.push('

' + JSSpec.util.inspect(this.expected, false, key) + '

'); 984 | 985 | return sb.join(""); 986 | }; 987 | 988 | 989 | 990 | 991 | /** 992 | * ArrayEqualityMatcher 993 | */ 994 | JSSpec.ArrayEqualityMatcher = function(expected, actual) { 995 | this.expected = expected; 996 | this.actual = actual; 997 | this.match = this.expected == this.actual; 998 | this.explaination = this.makeExplain(); 999 | }; 1000 | 1001 | JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match}; 1002 | 1003 | JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination}; 1004 | 1005 | JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() { 1006 | if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch(); 1007 | 1008 | for(var i = 0; i < this.expected.length; i++) { 1009 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]); 1010 | if(!matcher.matches()) return this.makeExplainForItemMismatch(i); 1011 | } 1012 | 1013 | this.match = true; 1014 | }; 1015 | 1016 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() { 1017 | return JSSpec.EqualityMatcher.basicExplain( 1018 | this.expected, 1019 | this.actual, 1020 | '

but it should be ' + this.expected.length + '

', 1021 | '

actual value has ' + this.actual.length + ' items

' 1022 | ); 1023 | }; 1024 | 1025 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) { 1026 | var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)]; 1027 | 1028 | var sb = []; 1029 | 1030 | sb.push('

' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is ' + JSSpec.util.inspect(this.actual[index]) + ':

'); 1031 | sb.push('

' + JSSpec.util.inspect(this.actual, false, index) + '

'); 1032 | sb.push('

but it should be ' + JSSpec.util.inspect(this.expected[index]) + ':

'); 1033 | sb.push('

' + JSSpec.util.inspect(this.expected, false, index) + '

'); 1034 | 1035 | return sb.join(""); 1036 | }; 1037 | 1038 | /** 1039 | * NumberEqualityMatcher 1040 | */ 1041 | JSSpec.NumberEqualityMatcher = function(expected, actual) { 1042 | this.expected = expected; 1043 | this.actual = actual; 1044 | }; 1045 | 1046 | JSSpec.NumberEqualityMatcher.prototype.matches = function() { 1047 | if(this.expected == this.actual) return true; 1048 | }; 1049 | 1050 | JSSpec.NumberEqualityMatcher.prototype.explain = function() { 1051 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); 1052 | }; 1053 | 1054 | /** 1055 | * StringEqualityMatcher 1056 | */ 1057 | JSSpec.StringEqualityMatcher = function(expected, actual) { 1058 | this.expected = expected; 1059 | this.actual = actual; 1060 | }; 1061 | 1062 | JSSpec.StringEqualityMatcher.prototype.matches = function() { 1063 | if(this.expected == this.actual) return true; 1064 | }; 1065 | 1066 | JSSpec.StringEqualityMatcher.prototype.explain = function() { 1067 | var sb = []; 1068 | 1069 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 1070 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual)); 1071 | return sb.join(""); 1072 | }; 1073 | 1074 | /** 1075 | * PatternMatcher 1076 | */ 1077 | JSSpec.PatternMatcher = function(actual, pattern, condition) { 1078 | this.actual = actual; 1079 | this.pattern = pattern; 1080 | this.condition = condition; 1081 | this.match = false; 1082 | this.explaination = this.makeExplain(); 1083 | }; 1084 | 1085 | JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) { 1086 | return new JSSpec.PatternMatcher(actual, pattern, condition); 1087 | }; 1088 | 1089 | JSSpec.PatternMatcher.prototype.makeExplain = function() { 1090 | var sb; 1091 | if(this.actual == null || this.actual._type != 'String') { 1092 | sb = []; 1093 | sb.push('

actual value:

'); 1094 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 1095 | sb.push('

should ' + (this.condition ? '' : 'not') + ' match with pattern:

'); 1096 | sb.push('

' + JSSpec.util.inspect(this.pattern) + '

'); 1097 | sb.push('

but pattern matching cannot be performed.

'); 1098 | return sb.join(""); 1099 | } else { 1100 | this.match = this.condition == !!this.actual.match(this.pattern); 1101 | if(this.match) return ""; 1102 | 1103 | sb = []; 1104 | sb.push('

actual value:

'); 1105 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 1106 | sb.push('

should ' + (this.condition ? '' : 'not') + ' match with pattern:

'); 1107 | sb.push('

' + JSSpec.util.inspect(this.pattern) + '

'); 1108 | return sb.join(""); 1109 | } 1110 | }; 1111 | 1112 | JSSpec.PatternMatcher.prototype.matches = function() { 1113 | return this.match; 1114 | }; 1115 | 1116 | JSSpec.PatternMatcher.prototype.explain = function() { 1117 | return this.explaination; 1118 | }; 1119 | 1120 | /** 1121 | * Domain Specific Languages 1122 | */ 1123 | JSSpec.DSL = {}; 1124 | 1125 | JSSpec.DSL.forString = { 1126 | normalizeHtml: function() { 1127 | var html = this; 1128 | 1129 | // Uniformize quotation, turn tag names and attribute names into lower case 1130 | html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) { 1131 | var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase()) 1132 | return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">" 1133 | }); 1134 | 1135 | // validation self-closing tags 1136 | html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) { 1137 | return "<" + tag + attrs + " />"; 1138 | }); 1139 | 1140 | // append semi-colon at the end of style value 1141 | html = html.replace(/style="(.*?)"/mg, function(str, styleStr) { 1142 | styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari 1143 | if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";" 1144 | 1145 | return 'style="' + styleStr + '"' 1146 | }); 1147 | 1148 | // sort style entries 1149 | 1150 | // remove empty style attributes 1151 | html = html.replace(/ style=";"/mg, ""); 1152 | 1153 | // remove new-lines 1154 | html = html.replace(/\r/mg, ''); 1155 | html = html.replace(/\n/mg, ''); 1156 | 1157 | return html; 1158 | } 1159 | }; 1160 | 1161 | 1162 | 1163 | JSSpec.DSL.describe = function(context, entries) { 1164 | JSSpec.specs.push(new JSSpec.Spec(context, entries)); 1165 | }; 1166 | 1167 | JSSpec.DSL.value_of = function(target) { 1168 | if(JSSpec._secondPass) return {}; 1169 | 1170 | var subject = new JSSpec.DSL.Subject(target); 1171 | return subject; 1172 | }; 1173 | 1174 | JSSpec.DSL.Subject = function(target) { 1175 | this.target = target; 1176 | }; 1177 | 1178 | JSSpec.DSL.Subject.prototype._type = 'Subject'; 1179 | 1180 | JSSpec.DSL.Subject.prototype.should_fail = function(message) { 1181 | JSSpec._assertionFailure = {message:message}; 1182 | throw JSSpec._assertionFailure; 1183 | }; 1184 | 1185 | JSSpec.DSL.Subject.prototype.should_be = function(expected) { 1186 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); 1187 | if(!matcher.matches()) { 1188 | JSSpec._assertionFailure = {message:matcher.explain()}; 1189 | throw JSSpec._assertionFailure; 1190 | } 1191 | }; 1192 | 1193 | JSSpec.DSL.Subject.prototype.should_not_be = function(expected) { 1194 | // TODO JSSpec.EqualityMatcher should support 'condition' 1195 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); 1196 | if(matcher.matches()) { 1197 | JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"}; 1198 | throw JSSpec._assertionFailure; 1199 | } 1200 | }; 1201 | 1202 | JSSpec.DSL.Subject.prototype.should_be_empty = function() { 1203 | this.should_have(0, this.getType() == 'String' ? 'characters' : 'items'); 1204 | }; 1205 | 1206 | JSSpec.DSL.Subject.prototype.should_not_be_empty = function() { 1207 | this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items'); 1208 | }; 1209 | 1210 | JSSpec.DSL.Subject.prototype.should_be_true = function() { 1211 | this.should_be(true); 1212 | }; 1213 | 1214 | JSSpec.DSL.Subject.prototype.should_be_false = function() { 1215 | this.should_be(false); 1216 | }; 1217 | 1218 | JSSpec.DSL.Subject.prototype.should_be_null = function() { 1219 | this.should_be(null); 1220 | }; 1221 | 1222 | JSSpec.DSL.Subject.prototype.should_be_undefined = function() { 1223 | this.should_be(undefined); 1224 | }; 1225 | 1226 | JSSpec.DSL.Subject.prototype.should_not_be_null = function() { 1227 | this.should_not_be(null); 1228 | }; 1229 | 1230 | JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() { 1231 | this.should_not_be(undefined); 1232 | }; 1233 | 1234 | JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) { 1235 | var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition); 1236 | if(!matcher.matches()) { 1237 | JSSpec._assertionFailure = {message:matcher.explain()}; 1238 | throw JSSpec._assertionFailure; 1239 | } 1240 | }; 1241 | 1242 | JSSpec.DSL.Subject.prototype.should_have = function(num, property) { 1243 | this._should_have(num, property, "exactly"); 1244 | }; 1245 | 1246 | JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) { 1247 | this._should_have(num, property, "exactly"); 1248 | }; 1249 | 1250 | JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) { 1251 | this._should_have(num, property, "at least"); 1252 | }; 1253 | 1254 | JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) { 1255 | this._should_have(num, property, "at most"); 1256 | }; 1257 | 1258 | JSSpec.DSL.Subject.prototype.should_include = function(expected) { 1259 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true); 1260 | if(!matcher.matches()) { 1261 | JSSpec._assertionFailure = {message:matcher.explain()}; 1262 | throw JSSpec._assertionFailure; 1263 | } 1264 | }; 1265 | 1266 | JSSpec.DSL.Subject.prototype.should_not_include = function(expected) { 1267 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false); 1268 | if(!matcher.matches()) { 1269 | JSSpec._assertionFailure = {message:matcher.explain()}; 1270 | throw JSSpec._assertionFailure; 1271 | } 1272 | }; 1273 | 1274 | JSSpec.DSL.Subject.prototype.should_match = function(pattern) { 1275 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true); 1276 | if(!matcher.matches()) { 1277 | JSSpec._assertionFailure = {message:matcher.explain()}; 1278 | throw JSSpec._assertionFailure; 1279 | } 1280 | } 1281 | JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) { 1282 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false); 1283 | if(!matcher.matches()) { 1284 | JSSpec._assertionFailure = {message:matcher.explain()}; 1285 | throw JSSpec._assertionFailure; 1286 | } 1287 | }; 1288 | 1289 | JSSpec.DSL.Subject.prototype.getType = function() { 1290 | if(typeof this.target == 'undefined') { 1291 | return 'undefined'; 1292 | } else if(this.target == null) { 1293 | return 'null'; 1294 | } else if(this.target._type) { 1295 | return this.target._type; 1296 | } else if(JSSpec.util.isDomNode(this.target)) { 1297 | return 'DomNode'; 1298 | } else { 1299 | return 'object'; 1300 | } 1301 | }; 1302 | 1303 | /** 1304 | * Utilities 1305 | */ 1306 | JSSpec.util = { 1307 | escapeTags: function(string) { 1308 | return string.replace(//img, '>'); 1309 | }, 1310 | parseOptions: function(defaults) { 1311 | var options = defaults; 1312 | 1313 | var url = location.href; 1314 | var queryIndex = url.indexOf('?'); 1315 | if(queryIndex == -1) return options; 1316 | 1317 | var query = url.substring(queryIndex + 1); 1318 | var pairs = query.split('&'); 1319 | for(var i = 0; i < pairs.length; i++) { 1320 | var tokens = pairs[i].split('='); 1321 | options[tokens[0]] = tokens[1]; 1322 | } 1323 | 1324 | return options; 1325 | }, 1326 | correctHtmlAttrQuotation: function(html) { 1327 | html = html.replace(/(\w+)=['"]([^'"]+)['"]/mg,function (str, name, value) {return name + '=' + '"' + value + '"';}); 1328 | html = html.replace(/(\w+)=([^ '"]+)/mg,function (str, name, value) {return name + '=' + '"' + value + '"';}); 1329 | html = html.replace(/'/mg, '"'); 1330 | 1331 | return html; 1332 | }, 1333 | sortHtmlAttrs: function(html) { 1334 | var attrs = []; 1335 | html.replace(/((\w+)="[^"]+")/mg, function(str, matched) { 1336 | attrs.push(matched); 1337 | }); 1338 | return attrs.length == 0 ? "" : " " + attrs.sort().join(" "); 1339 | }, 1340 | sortStyleEntries: function(styleText) { 1341 | var entries = styleText.split(/; /); 1342 | return entries.sort().join("; "); 1343 | }, 1344 | escapeHtml: function(str) { 1345 | if(!this._div) { 1346 | this._div = document.createElement("DIV"); 1347 | this._text = document.createTextNode(''); 1348 | this._div.appendChild(this._text); 1349 | } 1350 | this._text.data = str; 1351 | return this._div.innerHTML; 1352 | }, 1353 | isDomNode: function(o) { 1354 | // TODO: make it more stricter 1355 | return (typeof o.nodeName == 'string') && (typeof o.nodeType == 'number'); 1356 | }, 1357 | inspectDomPath: function(o) { 1358 | var sb = []; 1359 | while(o && o.nodeName != '#document' && o.parent) { 1360 | var siblings = o.parentNode.childNodes; 1361 | for(var i = 0; i < siblings.length; i++) { 1362 | if(siblings[i] == o) { 1363 | sb.push(o.nodeName + (i == 0 ? '' : '[' + i + ']')); 1364 | break; 1365 | } 1366 | } 1367 | o = o.parentNode; 1368 | } 1369 | return sb.join(" > "); 1370 | }, 1371 | inspectDomNode: function(o) { 1372 | if(o.nodeType == 1) { 1373 | var nodeName = o.nodeName.toLowerCase(); 1374 | var sb = []; 1375 | sb.push(''); 1376 | sb.push("<"); 1377 | sb.push(nodeName); 1378 | 1379 | var attrs = o.attributes; 1380 | for(var i = 0; i < attrs.length; i++) { 1381 | if( 1382 | attrs[i].nodeValue && 1383 | attrs[i].nodeName != 'contentEditable' && 1384 | attrs[i].nodeName != 'style' && 1385 | typeof attrs[i].nodeValue != 'function' 1386 | ) sb.push(' ' + attrs[i].nodeName.toLowerCase() + '="' + attrs[i].nodeValue + '"'); 1387 | } 1388 | if(o.style && o.style.cssText) { 1389 | sb.push(' style="' + o.style.cssText + '"'); 1390 | } 1391 | sb.push('>'); 1392 | sb.push(JSSpec.util.escapeHtml(o.innerHTML)); 1393 | sb.push('</' + nodeName + '>'); 1394 | sb.push(' (' + JSSpec.util.inspectDomPath(o) + ')' ); 1395 | sb.push(''); 1396 | return sb.join(""); 1397 | } else if(o.nodeType == 3) { 1398 | return '#text ' + o.nodeValue + ''; 1399 | } else { 1400 | return 'UnknownDomNode'; 1401 | } 1402 | }, 1403 | inspect: function(o, dontEscape, emphasisKey) { 1404 | var sb, inspected; 1405 | 1406 | if(typeof o == 'undefined') return 'undefined'; 1407 | if(o == null) return 'null'; 1408 | if(o._type == 'String') return '"' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '"'; 1409 | 1410 | if(o._type == 'Date') { 1411 | return '"' + o.toString() + '"'; 1412 | } 1413 | 1414 | if(o._type == 'Number') return '' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + ''; 1415 | 1416 | if(o._type == 'Boolean') return '' + o + ''; 1417 | 1418 | if(o._type == 'RegExp') return '' + JSSpec.util.escapeHtml(o.toString()) + ''; 1419 | 1420 | if(JSSpec.util.isDomNode(o)) return JSSpec.util.inspectDomNode(o); 1421 | 1422 | if(o._type == 'Array' || typeof o.length != 'undefined') { 1423 | sb = []; 1424 | for(var i = 0; i < o.length; i++) { 1425 | inspected = JSSpec.util.inspect(o[i]); 1426 | sb.push(i == emphasisKey ? ('' + inspected + '') : inspected); 1427 | } 1428 | return '[' + sb.join(', ') + ']'; 1429 | } 1430 | 1431 | // object 1432 | sb = []; 1433 | for(var key in o) { 1434 | if(key == 'should') continue; 1435 | 1436 | inspected = JSSpec.util.inspect(key) + ":" + JSSpec.util.inspect(o[key]); 1437 | sb.push(key == emphasisKey ? ('' + inspected + '') : inspected); 1438 | } 1439 | return '{' + sb.join(', ') + '}'; 1440 | } 1441 | }; 1442 | 1443 | describe = JSSpec.DSL.describe; 1444 | behavior_of = JSSpec.DSL.describe; 1445 | value_of = JSSpec.DSL.value_of; 1446 | expect = JSSpec.DSL.value_of; // @deprecated 1447 | 1448 | String.prototype._type = "String"; 1449 | Number.prototype._type = "Number"; 1450 | Date.prototype._type = "Date"; 1451 | Array.prototype._type = "Array"; 1452 | Boolean.prototype._type = "Boolean"; 1453 | RegExp.prototype._type = "RegExp"; 1454 | 1455 | var targets = [Array.prototype, Date.prototype, Number.prototype, String.prototype, Boolean.prototype, RegExp.prototype]; 1456 | 1457 | String.prototype.normalizeHtml = JSSpec.DSL.forString.normalizeHtml; 1458 | String.prototype.asHtml = String.prototype.normalizeHtml; //@deprecated 1459 | 1460 | 1461 | 1462 | /** 1463 | * Main 1464 | */ 1465 | JSSpec.defaultOptions = { 1466 | autorun: 1, 1467 | specIdBeginsWith: 0, 1468 | exampleIdBeginsWith: 0, 1469 | autocollapse: 1 1470 | }; 1471 | JSSpec.options = JSSpec.util.parseOptions(JSSpec.defaultOptions); 1472 | 1473 | JSSpec.Spec.id = JSSpec.options.specIdBeginsWith; 1474 | JSSpec.Example.id = JSSpec.options.exampleIdBeginsWith; 1475 | 1476 | 1477 | 1478 | window.onload = function() { 1479 | if(JSSpec.specs.length > 0) { 1480 | if(!JSSpec.options.inSuite) { 1481 | JSSpec.runner = new JSSpec.Runner(JSSpec.specs, new JSSpec.Logger()); 1482 | if(JSSpec.options.rerun) { 1483 | JSSpec.runner.rerun(decodeURIComponent(JSSpec.options.rerun)); 1484 | } else { 1485 | JSSpec.runner.run(); 1486 | } 1487 | } else { 1488 | // in suite, send all specs to parent 1489 | var parentWindow = window.frames.parent.window; 1490 | for(var i = 0; i < JSSpec.specs.length; i++) { 1491 | parentWindow.JSSpec.specs.push(JSSpec.specs[i]); 1492 | } 1493 | } 1494 | } else { 1495 | var links = document.getElementById('list').getElementsByTagName('A'); 1496 | var frameContainer = document.createElement('DIV'); 1497 | frameContainer.style.display = 'none'; 1498 | document.body.appendChild(frameContainer); 1499 | 1500 | for(var i = 0; i < links.length; i++) { 1501 | var frame = document.createElement('IFRAME'); 1502 | frame.src = links[i].href + '?inSuite=0&specIdBeginsWith=' + (i * 10000) + '&exampleIdBeginsWith=' + (i * 10000); 1503 | frameContainer.appendChild(frame); 1504 | } 1505 | } 1506 | } --------------------------------------------------------------------------------