a ton more hacking

This commit is contained in:
John Bintz 2012-03-02 13:28:52 -05:00
parent e5dd9634a8
commit 04a77f9595
19 changed files with 847 additions and 94 deletions

View File

@ -1,7 +1,6 @@
#!/usr/bin/env ruby
require 'flowerbox'
require 'flowerbox-delivery'
require 'thor'
class Flowerbox::CLI < Thor
@ -9,23 +8,7 @@ class Flowerbox::CLI < Thor
method_options :pwd => :string
def test(dir)
Dir.chdir(pwd) do
load File.join(dir, 'spec_helper.rb')
require 'tilt/coffee'
Tilt::CoffeeScriptTemplate.default_bare = Flowerbox.bare_coffeescript
sprockets = Flowerbox::Delivery::SprocketsHandler.new(:asset_paths => [ Flowerbox.path.join("lib/assets/javascripts"), dir, Flowerbox.asset_paths ].flatten)
Flowerbox.test_environment.inject_into(sprockets)
Flowerbox.spec_patterns.each do |pattern|
Dir[File.join(dir, pattern)].each do |file|
sprockets.add(file.gsub(dir + '/', ''))
end
end
exit Flowerbox.runner_environment.run(sprockets)
exit Flowerbox.run(dir)
end
end

View File

@ -17,8 +17,8 @@ Gem::Specification.new do |gem|
gem.add_development_dependency 'cucumber'
gem.add_development_dependency 'rspec'
gem.add_development_dependency 'jasmine-core'
gem.add_dependency 'jasmine-core'
gem.add_dependency 'flowerbox-delivery'
gem.add_dependency 'thor'
gem.add_dependency 'selenium-webdriver'

View File

@ -0,0 +1,7 @@
Flowerbox =
baseUrl: '/'
contact: (url, data...) ->
xhr = new XMLHttpRequest()
xhr.open("POST", Flowerbox.baseUrl + url, false)
xhr.send(JSON.stringify(data))

View File

@ -0,0 +1,27 @@
#= require flowerbox/jasmine/reporter
getSplitName = (parts) ->
parts.push(String(@description).replace(/[\n\r]/g, ' '))
parts
jasmine.Suite.prototype.getSuiteSplitName = ->
this.getSplitName(if @parentSuite then @parentSuite.getSuiteSplitName() else [])
jasmine.Spec.prototype.getSpecSplitName = ->
this.getSplitName(@suite.getSuiteSplitName())
jasmine.Suite.prototype.getSplitName = getSplitName
jasmine.Spec.prototype.getSplitName = getSplitName
jasmine.Spec.prototype.addMatcherResult = (result) ->
for method in jasmine.Spec.beforeAddMatcherResult()
method.apply(result, [ this ])
@results_.addResult(result)
jasmine.Spec.beforeAddMatcherResult = ->
@_beforeAddMatcherResult ||= []
jasmine.Spec.beforeAddMatcherResult().push (spec) ->
@splitName = spec.getSpecSplitName()

View File

@ -1,9 +1,11 @@
class jasmine.SimpleNodeReporter
reportRunnerResults: (runner) ->
console.log(runner.results().totalCount + '/' + runner.results().failedCount)
jasmine.Spec.beforeAddMatcherResult().push ->
if !@passed_
Error.prepareStackTrace_ = Error.prepareStackTrace
Error.prepareStackTrace = (err, stack) -> stack
if runner.results().failedCount == 0
process.exit(0)
else
process.exit(1)
errorInfo = new Error().stack[3]
@trace = { stack: [ "#{errorInfo.getFileName()}:#{errorInfo.getLineNumber()}" ] }
Error.prepareStackTrace = Error.prepareStackTrace_

View File

@ -0,0 +1,18 @@
class jasmine.FlowerboxReporter
reportRunnerStarting: (runner) ->
@time = (new Date()).getTime()
Flowerbox.contact("starting")
reportSpecStarting: (spec) ->
Flowerbox.contact("start_test", spec.description)
reportSpecResults: (spec) ->
failures = []
for result in spec.results().getItems()
if result.type == 'expect' && !result.passed_
failures.push(result)
Flowerbox.contact("finish_test", spec.description, failures)
reportRunnerResults: (runner) ->
Flowerbox.contact("results", (new Date().getTime()) - @time)

View File

@ -1,3 +1,15 @@
class jasmine.SimpleSeleniumReporter
reportRunnerResults: (runner) ->
Flowerbox.contact("results", runner.results().totalCount + '/' + runner.results().failedCount)
jasmine.Spec.beforeAddMatcherResult().push ->
if !@passed_
@trace = { stack: [] }
try
lol.wut
catch e
if e.stack
file = switch Flowerbox.environment
when 'firefox'
e.stack.split("\n")[3].replace(/^[^@]*@/, '')
when 'chrome'
e.stack.split("\n")[4].replace(/^.*\((.*)\)$/, '$1').replace(/:[^:]+$/, '')
@trace = { stack: [ file.replace(/^.*__F__/, '') ] }

View File

@ -1,12 +1,23 @@
require "flowerbox/version"
require 'flowerbox-delivery'
module Flowerbox
autoload :Runner, 'flowerbox/runner'
module Runner
autoload :Node, 'flowerbox/runner/node'
autoload :Selenium, 'flowerbox/runner/selenium'
autoload :Firefox, 'flowerbox/runner/firefox'
autoload :Chrome, 'flowerbox/runner/chrome'
autoload :Base, 'flowerbox/runner/base'
end
autoload :TestEnvironment, 'flowerbox/test_environment'
module TestEnvironment
autoload :Jasmine, 'flowerbox/test_environment/jasmine'
end
autoload :Rack, 'flowerbox/rack'
class << self
@ -19,11 +30,11 @@ module Flowerbox
end
def test_with(what)
require "flowerbox/test_environment/#{what}"
self.test_environment = Flowerbox::TestEnvironment.for(what)
end
def run_with(what)
require "flowerbox/runner/#{what}"
def run_with(*whats)
self.runner_environment = whats.collect { |what| Flowerbox::Runner.for(what) }
end
def path
@ -34,6 +45,64 @@ module Flowerbox
def configure
yield self
if spec_patterns.empty?
spec_patterns << "**/*_spec*"
spec_patterns << "*/*_spec*"
end
end
def bare_coffeescript
@bare_coffeescript ||= true
end
def run(dir)
load File.join(dir, 'spec_helper.rb')
require 'coffee_script'
require 'tilt/coffee'
Tilt::CoffeeScriptTemplate.default_bare = Flowerbox.bare_coffeescript
result = Flowerbox.runner_environment.collect do |env|
env.run(build_sprockets_for(dir))
end
result.max
end
def build_sprockets_for(dir)
sprockets = Flowerbox::Delivery::SprocketsHandler.new(
:asset_paths => [
Flowerbox.path.join("lib/assets/javascripts"),
Flowerbox.path.join("vendor/assets/javascripts"),
dir,
Flowerbox.asset_paths
].flatten
)
sprockets.add('flowerbox')
sprockets.add('json2')
Flowerbox.test_environment.inject_into(sprockets)
spec_files_for(dir).each { |file| sprockets.add(file) }
sprockets
end
def spec_files_for(dir)
return @spec_files if @spec_files
@spec_files = []
Flowerbox.spec_patterns.each do |pattern|
Dir[File.join(dir, pattern)].each do |file|
@spec_files << file.gsub(dir + '/', '')
end
end
@spec_files
end
end
end

View File

@ -1,4 +1,5 @@
require 'sinatra'
require 'json'
module Flowerbox
class Rack < Sinatra::Base
@ -10,12 +11,32 @@ module Flowerbox
self.class.runner
end
post '/results' do
runner.results = request.body.string
def data
JSON.parse(request.body.string)
end
post '/log' do
runner.log(request.body.string)
def self.empty_post(*args, &block)
post(*args) do
instance_eval(&block)
""
end
end
empty_post '/results' do
runner.finish!(data.flatten.first)
end
empty_post '/start_test' do
runner.tests << data.flatten
end
empty_post '/finish_test' do
runner.add_failures(data.flatten[1..-1])
end
empty_post '/log' do
runner.log(data.first)
end
get %r{^/__F__(/.*)$} do |file|

10
lib/flowerbox/runner.rb Normal file
View File

@ -0,0 +1,10 @@
module Flowerbox
module Runner
class << self
def for(env)
self.const_get(self.constants.find { |c| c.to_s.downcase.to_s == env.to_s }).new
end
end
end
end

View File

@ -3,8 +3,47 @@ module Flowerbox
class Base
attr_reader :sprockets
attr_accessor :time, :results
def run(sprockets)
@sprockets = sprockets
puts "Flowerbox running your #{Flowerbox.test_environment.name} tests on #{name}..."
server.start
yield
server.stop
puts
failures.each do |failure_set|
if failure_set.first['splitName']
puts failure_set.first['splitName'].join(' ')
end
failure_set.each do |failure|
case failure['trace']['stack']
when String
# exception
puts failure['trace']['stack']
else
# failed test
puts %{#{failure['message']} (#{failure['trace']['stack'].first})}
end
end
puts
end
puts "#{total_count} tests, #{failure_count} failures, #{time.to_f / 1000} sec"
if failures.length == 0
$?.exitstatus
else
1
end
end
def type
@ -12,7 +51,7 @@ module Flowerbox
end
def start_test_environment
Flowerbox.test_environment.start_for(type)
Flowerbox.test_environment.start_for(self)
end
def server
@ -23,6 +62,48 @@ module Flowerbox
@server
end
def log(message)
puts message
end
def tests
@tests ||= []
end
def failures
@failures ||= []
end
def add_failures(test_failures)
if test_failures.length == 0
print '.'
else
print 'F'
end
$stdout.flush
failures << test_failures
end
def total_count
@tests.length
end
def failure_count
@failures.length
end
def finish!(time)
@time = time
@finished = true
end
def finished?
@finished
end
end
end
end

View File

@ -0,0 +1,11 @@
class Flowerbox::Runner::Chrome < Flowerbox::Runner::Selenium
def name
"Chrome"
end
def browser
:chrome
end
end

View File

@ -0,0 +1,10 @@
class Flowerbox::Runner::Firefox < Flowerbox::Runner::Selenium
def name
"Firefox"
end
def browser
:firefox
end
end

View File

@ -5,21 +5,25 @@ require 'json'
module Flowerbox
module Runner
class Node < Base
def name
"Node.js"
end
def type
:node
end
def run(sprockets)
super
super do
begin
file = File.join(Dir.pwd, ".node-tmp.#{Time.now.to_i}.js")
File.open(file, 'wb') { |fh| fh.print template }
file = File.join(Dir.pwd, ".node-tmp.#{Time.now.to_i}.js")
File.open(file, 'wb') { |fh| fh.print template.tap { |o| puts o } }
server.start
system %{node #{file}}
server.stop
$?.exitstatus
ensure
File.unlink(file) if file
system %{node #{file}}
ensure
File.unlink(file) if file
end
end
end
def template
@ -29,7 +33,8 @@ module Flowerbox
var fs = require('fs'),
vm = require('vm'),
http = require('http'),
jsdom = require('jsdom');
jsdom = require('jsdom'),
xhr = require('xmlhttprequest')
// expand the sandbox a bit
var context = function() {};
@ -38,6 +43,7 @@ for (method in global) { context[method] = global[method]; }
jsdom.env(
"<html><head><title></title></head><body></body></html>", [], function(errors, window) {
context.window = window;
context.XMLHttpRequest = xhr.XMLHttpRequest;
var files = #{sprockets.files.to_json};
var fileRunner = function() {
@ -65,6 +71,10 @@ jsdom.env(
if (!context[thing]) { context[thing] = window[thing] }
}
if (context.Flowerbox) {
context.Flowerbox.baseUrl = "http://localhost:#{server.port}/";
}
fileRunner();
});
});

View File

@ -3,36 +3,33 @@ require 'selenium-webdriver'
module Flowerbox
module Runner
class Selenium < Base
attr_accessor :browser, :results
MAX_COUNT = 30
def name
raise StandardError.new("Override me")
end
def type
:selenium
end
def run(sprockets)
super
super do
begin
selenium = ::Selenium::WebDriver.for(browser)
selenium = ::Selenium::WebDriver.for :firefox
selenium.navigate.to "http://localhost:#{server.port}/"
Rack.runner = self
@count = 0
server.start
selenium.navigate.to "http://localhost:#{server.port}/"
1.upto(30) do
if results
break
else
sleep 1
while @count < MAX_COUNT && !finished?
@count += 1
sleep 0.1
end
ensure
selenium.quit if selenium
end
end
if results
puts results
exit (results.split('/').last.to_i == 0) ? 0 : 1
else
exit 1
end
ensure
selenium.quit if selenium
end
def log(msg)
@ -47,14 +44,6 @@ module Flowerbox
<head>
<title>Flowerbox - Selenium Runner</title>
<script type="text/javascript">
this.Flowerbox = {
contact: function(url, message) {
xhr = new XMLHttpRequest();
xhr.open("POST", "/" + url);
xhr.send(message);
}
}
console._log = console.log;
console.log = function(msg) {
@ -67,6 +56,8 @@ console.log = function(msg) {
<body>
<h1>Flowerbox - Selenium Runner</h1>
<script type="text/javascript">
Flowerbox.environment = '#{browser}';
#{env}
</script>
</body>
@ -77,8 +68,13 @@ HTML
def template_files
sprockets.files.collect { |file| %{<script type="text/javascript" src="/__F__#{file}"></script>} }
end
def add_failures(data)
super
@count = 0
end
end
end
end
Flowerbox.runner_environment = Flowerbox::Runner::Selenium.new

View File

@ -0,0 +1,10 @@
module Flowerbox
module TestEnvironment
class << self
def for(env)
self.const_get(self.constants.find { |c| c.to_s.downcase.to_s == env.to_s }).new
end
end
end
end

View File

@ -3,6 +3,10 @@ require 'jasmine-core'
module Flowerbox
module TestEnvironment
class Jasmine
def name
self.class.name.split("::").last
end
def inject_into(sprockets)
@sprockets = sprockets
@ -13,9 +17,10 @@ module Flowerbox
end
def start_for(runner)
@sprockets.add("flowerbox/jasmine/#{runner}")
@sprockets.add("flowerbox/jasmine")
@sprockets.add("flowerbox/jasmine/#{runner.type}")
case runner
case runner.type
when :node
<<-JS
var jasmine = context.jasmine;
@ -25,8 +30,7 @@ jasmine.getEnv().execute();
JS
when :selenium
<<-JS
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
jasmine.getEnv().addReporter(new jasmine.SimpleSeleniumReporter());
jasmine.getEnv().addReporter(new jasmine.FlowerboxReporter());
jasmine.getEnv().execute();
JS
end
@ -37,11 +41,9 @@ JS
end
def reporters
@reporters ||= []
@reporters ||= [ 'FlowerboxReporter' ]
end
end
end
end
Flowerbox.test_environment = Flowerbox::TestEnvironment::Jasmine.new

View File

@ -1,9 +1,6 @@
describe("cats", function() {
it("should hiss", function() {
$("body").append("<div /");
console.log($('body').html())
expect("hiss").toEqual("hiss");
});
});

487
vendor/assets/javascripts/json2.js vendored Normal file
View File

@ -0,0 +1,487 @@
/*
http://www.JSON.org/json2.js
2011-10-19
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, regexp: true */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
'use strict';
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf())
? this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z'
: null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string'
? c
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0
? '[]'
: gap
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
: '[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0
? '{}'
: gap
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
: '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function'
? walk({'': j}, '')
: j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());