From b42be9f421e2b5c0a0e336035236b6f43f667d18 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Sun, 25 Jan 2015 16:12:06 -0800 Subject: [PATCH 01/11] Modularize JSON 3. --- lib/attempt.js | 8 + lib/date.js | 81 +++ lib/forOwn.js | 48 ++ lib/hasDontEnumBug.js | 19 + lib/json3.js | 1152 ++++++++++------------------------------- lib/json3.min.js | 17 - lib/parse.js | 325 ++++++++++++ lib/serializeDate.js | 43 ++ lib/stringify.js | 166 ++++++ lib/toPaddedString.js | 8 + 10 files changed, 962 insertions(+), 905 deletions(-) create mode 100644 lib/attempt.js create mode 100644 lib/date.js create mode 100644 lib/forOwn.js create mode 100644 lib/hasDontEnumBug.js delete mode 100644 lib/json3.min.js create mode 100644 lib/parse.js create mode 100644 lib/serializeDate.js create mode 100644 lib/stringify.js create mode 100644 lib/toPaddedString.js diff --git a/lib/attempt.js b/lib/attempt.js new file mode 100644 index 0000000..ccf1871 --- /dev/null +++ b/lib/attempt.js @@ -0,0 +1,8 @@ +// Internal: Contains `try...catch` logic used by other functions. +// This prevents other functions from being deoptimized. +module.exports = attempt; +function attempt(func) { + try { + return func(); + } catch (exception) {} +} diff --git a/lib/date.js b/lib/date.js new file mode 100644 index 0000000..6b57832 --- /dev/null +++ b/lib/date.js @@ -0,0 +1,81 @@ +// An associative array where the index is the month of the year (as +// reported by `Date#getUTCMonth`; e.g., 0 = January, 1 = February, +// etc) and the value is the number of days between January 1st and +// the first of the respective month. +var daysByMonth = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; + +// getDay returns the number of days between the Unix epoch and the +// first day of the given month. +function getDay(floor, year, month) { + return daysByMonth[month] + 365 * (year - 1970) + + floor((year - 1969 + (month = +(month > 1))) / 4) - + floor((year - 1901 + month) / 100) + + floor((year - 1601 + month) / 400); +} + +module.exports = createUTCDate; +function createUTCDate(floor) { + // UTCDate decomposes a `Date` value and exposes a subset of the + // `Date` API. This is used to obtain the year, month, date, hours, + // minutes, seconds, and milliseconds if the `getUTC*` methods are + // buggy. Adapted from @Yaffle's `date-shim` project. + function UTCDate(value) { + var date = floor(value / 864e5); + this.date = date; + + var year = floor(date / 365.2425) + 1970 - 1; + while (getDay(floor, year + 1, 0) <= date) { + year++; + } + this.year = year; + + var month = floor((date - getDay(floor, year, 0)) / 30.42); + while (getDay(floor, year, month + 1) <= date) { + month++; + } + this.month = month; + + // The `time` value specifies the time within the day (see ES + // 5.1 section 15.9.1.2). The formula `(A % B + B) % B` is used + // to compute `A modulo B`, as the `%` operator does not + // correspond to the `modulo` operation for negative numbers. + this.time = (value % 864e5 + 864e5) % 864e5; + } + + UTCDate.prototype.date = 0; + UTCDate.prototype.year = 0; + UTCDate.prototype.month = 0; + UTCDate.prototype.time = 0; + + UTCDate.prototype.getUTCFullYear = function() { + return this.year; + }; + + UTCDate.prototype.getUTCMonth = function() { + return this.month; + }; + + UTCDate.prototype.getUTCDate = function() { + return 1 + this.date - getDay(floor, this.year, this.month); + }; + + // The hours, minutes, seconds, and milliseconds are obtained by + // decomposing the time within the day. See section 15.9.1.10. + UTCDate.prototype.getUTCHours = function() { + return floor(this.time / 36e5) % 24; + }; + + UTCDate.prototype.getUTCMinutes = function() { + return floor(this.time / 6e4) % 60; + }; + + UTCDate.prototype.getUTCSeconds = function() { + return floor(this.time / 1e3) % 60; + }; + + UTCDate.prototype.getUTCMilliseconds = function() { + return this.time % 1e3; + }; + + return UTCDate; +} diff --git a/lib/forOwn.js b/lib/forOwn.js new file mode 100644 index 0000000..a94b22a --- /dev/null +++ b/lib/forOwn.js @@ -0,0 +1,48 @@ +var hasDontEnumBug = require("./hasDontEnumBug"); + +// A list of non-enumerable properties inherited from `Object.prototype`. +var dontEnums = ["valueOf", "toString", "toLocaleString", + "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"]; + +module.exports = createForOwn; +function createForOwn(getClass, isProperty) { + // Internal: Normalizes the `for...in` iteration algorithm across + // environments. Each enumerated key is yielded to a `callback` function. + var forOwn; + if (hasDontEnumBug(isProperty)) { + // IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable + // properties. + forOwn = function forOwn(object, callback) { + var isFunction = getClass.call(object) == "[object Function]", property, length; + var hasProperty = isProperty; + if (!isFunction && typeof object.constructor != "function" && (typeof object.hasOwnProperty == "function" || typeof object.hasOwnProperty == "object" && object.hasOwnProperty)) { + hasProperty = object.hasOwnProperty; + } + for (property in object) { + // Gecko <= 1.0 enumerates the `prototype` property of functions under + // certain conditions; IE does not. + if (!(isFunction && property == "prototype") && hasProperty.call(object, property)) { + callback(property); + } + } + // Manually invoke the callback for each non-enumerable property. + for (length = dontEnums.length; property = dontEnums[--length]; hasProperty.call(object, property) && callback(property)); + }; + } else { + // No bugs detected; use the standard `for...in` algorithm. + forOwn = function forOwn(object, callback) { + var isFunction = getClass.call(object) == "[object Function]", property, isConstructor; + for (property in object) { + if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) { + callback(property); + } + } + // Manually invoke the callback for the `constructor` property due to + // cross-environment inconsistencies. + if (isConstructor || isProperty.call(object, (property = "constructor"))) { + callback(property); + } + }; + } + return forOwn; +} diff --git a/lib/hasDontEnumBug.js b/lib/hasDontEnumBug.js new file mode 100644 index 0000000..4f1011a --- /dev/null +++ b/lib/hasDontEnumBug.js @@ -0,0 +1,19 @@ +// The `valueOf` property inherits the non-enumerable flag from +// `Object.prototype` in older versions of IE, Netscape, and Mozilla. +function Properties() { + this.valueOf = 0; +} +Properties.prototype.valueOf = 0; + +// Tests for bugs in the current environment's `for...in` algorithm. +module.exports = hasDontEnumBug; +function hasDontEnumBug(isProperty) { + var members = new Properties(), size = 0; + for (var property in members) { + // Ignore all properties inherited from `Object.prototype`. + if (isProperty.call(members, property)) { + size++; + } + } + return size === 0; +} diff --git a/lib/json3.js b/lib/json3.js index 2a066a8..27926b5 100644 --- a/lib/json3.js +++ b/lib/json3.js @@ -1,911 +1,287 @@ /*! JSON v3.3.2 | https://bestiejs.github.io/json3 | Copyright 2012-2015, Kit Cambridge, Benjamin Tan | http://kit.mit-license.org */ -;(function () { - // Detect the `define` function exposed by asynchronous module loaders. The - // strict `define` check is necessary for compatibility with `r.js`. - var isLoader = typeof define === "function" && define.amd; - // A set of types used to distinguish objects from primitives. - var objectTypes = { - "function": true, - "object": true - }; - - // Detect the `exports` object exposed by CommonJS implementations. - var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; - - // Use the `global` object exposed by Node (including Browserify via - // `insert-module-globals`), Narwhal, and Ringo as the default context, - // and the `window` object in browsers. Rhino exports a `global` function - // instead. - var root = objectTypes[typeof window] && window || this, - freeGlobal = freeExports && objectTypes[typeof module] && module && !module.nodeType && typeof global == "object" && global; - - if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { - root = freeGlobal; +var attempt = require("./attempt"), + createUTCDate = require("./date"), + createForOwn = require("./forOwn"), + createParse = require("./parse"), + createSerializeDate = require("./serializeDate"), + createStringify = require("./stringify"); + +// Detect the `define` function exposed by asynchronous module loaders. The +// strict `define` check is necessary for compatibility with `r.js`. +var isLoader = typeof define === "function" && define.amd; + +// Detect the `exports` object exposed by CommonJS implementations. +var freeExports = typeof exports == "object" && exports && !exports.nodeType && exports; + +// Use the `global` object exposed by Node (including Browserify via +// `insert-module-globals`), Narwhal, and Ringo as the default context, +// and the `window` object in browsers. Rhino exports a `global` function +// instead. +var root = typeof window == "object" && window || this, + freeGlobal = freeExports && typeof module == "object" && module && !module.nodeType && typeof global == "object" && global; + +if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { + root = freeGlobal; +} + +// Public: Initializes JSON 3 using the given `context` object, attaching the +// `stringify` and `parse` functions to the specified `exports` object. +function runInContext(context, exports) { + context || (context = root.Object()); + exports || (exports = root.Object()); + + // Native constructor aliases. + var String = context.String || root.String, + Object = context.Object || root.Object, + Date = context.Date || root.Date, + SyntaxError = context.SyntaxError || root.SyntaxError, + TypeError = context.TypeError || root.TypeError, + Math = context.Math || root.Math, + nativeJSON = context.JSON || root.JSON; + + // Delegate to the native `stringify` and `parse` implementations. + if (typeof nativeJSON == "object" && nativeJSON) { + exports.stringify = nativeJSON.stringify; + exports.parse = nativeJSON.parse; } - // Public: Initializes JSON 3 using the given `context` object, attaching the - // `stringify` and `parse` functions to the specified `exports` object. - function runInContext(context, exports) { - context || (context = root.Object()); - exports || (exports = root.Object()); - - // Native constructor aliases. - var Number = context.Number || root.Number, - String = context.String || root.String, - Object = context.Object || root.Object, - Date = context.Date || root.Date, - SyntaxError = context.SyntaxError || root.SyntaxError, - TypeError = context.TypeError || root.TypeError, - Math = context.Math || root.Math, - nativeJSON = context.JSON || root.JSON; - - // Delegate to the native `stringify` and `parse` implementations. - if (typeof nativeJSON == "object" && nativeJSON) { - exports.stringify = nativeJSON.stringify; - exports.parse = nativeJSON.parse; + // Convenience aliases. + var getClass = Object.prototype.toString; + + // Internal: Determines whether the native `JSON.stringify` and `parse` + // implementations are spec-compliant. Based on work by Ken Snyder. + function has(name) { + var stringify = exports.stringify, parse = exports.parse; + if (name == "bug-string-char-index") { + // IE <= 7 doesn't support accessing string characters using square + // bracket notation. IE 8 only supports this for primitives. + var isSupported = "a"[0] != "a"; + has[name] = isSupported; + return isSupported; } - - // Convenience aliases. - var objectProto = Object.prototype, - getClass = objectProto.toString, - isProperty = objectProto.hasOwnProperty, - undefined; - - // Internal: Contains `try...catch` logic used by other functions. - // This prevents other functions from being deoptimized. - function attempt(func, errorFunc) { - try { - func(); - } catch (exception) { - if (errorFunc) { - errorFunc(); - } + if (name == "json") { + // Indicates whether both `JSON.stringify` and `JSON.parse` are + // supported. TODO: Remove `json-stringify`; `date-serialization` + // checks for it implicitly. + var isSupported = has("json-stringify") && has("date-serialization") && has("json-parse"); + has[name] = isSupported; + return isSupported; + } + if (name == "extended-years") { + // Test the `Date#getUTC*` methods. Based on work by @Yaffle. + var isSupported = !!attempt(function () { + // The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical + // results for certain dates in Opera >= 10.53. + var extendedYear = new Date(-3509827334573292); + return extendedYear.getUTCFullYear() == -109252 && + extendedYear.getUTCMonth() === 0 && + extendedYear.getUTCDate() === 1 && + extendedYear.getUTCHours() == 10 && + extendedYear.getUTCMinutes() == 37 && + extendedYear.getUTCSeconds() == 6 && + extendedYear.getUTCMilliseconds() == 708; + }); + has[name] = isSupported; + return isSupported; + } + if (name == "date-serialization") { + // Indicates whether `Date`s can be serialized accurately by `JSON.stringify`. + var isSupported = has("json-stringify") && has("extended-years"); + if (isSupported) { + isSupported = !!attempt(function () { + return ( + // JSON 2, Prototype <= 1.7, and older WebKit builds incorrectly + // serialize extended years. + stringify(new Date(-8.64e15)) == '"-271821-04-20T00:00:00.000Z"' && + // The milliseconds are optional in ES 5, but required in 5.1. + stringify(new Date(8.64e15)) == '"+275760-09-13T00:00:00.000Z"' && + // Firefox <= 11.0 incorrectly serializes years prior to 0 as negative + // four-digit years instead of six-digit years. Credits: @Yaffle. + stringify(new Date(-621987552e5)) == '"-000001-01-01T00:00:00.000Z"' && + // Safari <= 5.1.5 and Opera >= 10.53 incorrectly serialize millisecond + // values less than 1000. Credits: @Yaffle. + stringify(new Date(-1)) == '"1969-12-31T23:59:59.999Z"' + ); + }); } + has[name] = isSupported; + return isSupported; } - - // Test the `Date#getUTC*` methods. Based on work by @Yaffle. - var isExtended = new Date(-3509827334573292); - attempt(function () { - // The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical - // results for certain dates in Opera >= 10.53. - isExtended = isExtended.getUTCFullYear() == -109252 && isExtended.getUTCMonth() === 0 && isExtended.getUTCDate() === 1 && - isExtended.getUTCHours() == 10 && isExtended.getUTCMinutes() == 37 && isExtended.getUTCSeconds() == 6 && isExtended.getUTCMilliseconds() == 708; - }); - - // Internal: Determines whether the native `JSON.stringify` and `parse` - // implementations are spec-compliant. Based on work by Ken Snyder. - function has(name) { - if (has[name] != null) { - // Return cached feature test result. - return has[name]; + var value, serialized = '{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}'; + if (name == "json-stringify") { + // Test `JSON.stringify`. + var isSupported = typeof stringify == "function"; + if (isSupported) { + // A test function object with a custom `toJSON` method. + (value = function () { + return 1; + }).toJSON = value; + isSupported = !!attempt(function () { + return ( + // Firefox 3.1b1 and b2 serialize string, number, and boolean + // primitives as object literals. + stringify(0) === "0" && + // FF 3.1b1, b2, and JSON 2 serialize wrapped primitives as object + // literals. + stringify(new Number()) === "0" && + stringify(new String()) == '""' && + // FF 3.1b1, 2 throw an error if the value is `null`, `undefined`, or + // does not define a canonical JSON representation (this applies to + // objects with `toJSON` properties as well, *unless* they are nested + // within an object or array). + stringify(getClass) === undefined && + // IE 8 serializes `undefined` as `"undefined"`. Safari <= 5.1.7 and + // FF 3.1b3 pass this test. + stringify(undefined) === undefined && + // Safari <= 5.1.7 and FF 3.1b3 throw `Error`s and `TypeError`s, + // respectively, if the value is omitted entirely. + stringify() === undefined && + // FF 3.1b1, 2 throw an error if the given value is not a number, + // string, array, object, Boolean, or `null` literal. This applies to + // objects with custom `toJSON` methods as well, unless they are nested + // inside object or array literals. YUI 3.0.0b1 ignores custom `toJSON` + // methods entirely. + stringify(value) === "1" && + stringify([value]) == "[1]" && + // Prototype <= 1.6.1 serializes `[undefined]` as `"[]"` instead of + // `"[null]"`. + stringify([undefined]) == "[null]" && + // YUI 3.0.0b1 fails to serialize `null` literals. + stringify(null) == "null" && + // FF 3.1b1, 2 halts serialization if an array contains a function: + // `[1, true, getClass, 1]` serializes as "[1,true,],". FF 3.1b3 + // elides non-JSON values from objects and arrays, unless they + // define custom `toJSON` methods. + stringify([undefined, getClass, null]) == "[null,null,null]" && + // Simple serialization test. FF 3.1b1 uses Unicode escape sequences + // where character escape codes are expected (e.g., `\b` => `\u0008`). + stringify({ "a": [value, true, false, null, "\x00\b\n\f\r\t"] }) == serialized && + // FF 3.1b1 and b2 ignore the `filter` and `width` arguments. + stringify(null, value) === "1" && + stringify([1, 2], null, 1) == "[\n 1,\n 2\n]" + ); + }); } - var isSupported; - if (name == "bug-string-char-index") { - // IE <= 7 doesn't support accessing string characters using square - // bracket notation. IE 8 only supports this for primitives. - isSupported = "a"[0] != "a"; - } else if (name == "json") { - // Indicates whether both `JSON.stringify` and `JSON.parse` are - // supported. - isSupported = has("json-stringify") && has("date-serialization") && has("json-parse"); - } else if (name == "date-serialization") { - // Indicates whether `Date`s can be serialized accurately by `JSON.stringify`. - isSupported = has("json-stringify") && isExtended; + has[name] = isSupported; + return isSupported; + } + if (name == "json-parse") { + // Test `JSON.parse`. + var parse = parse, isSupported = typeof parse == "function"; + if (isSupported) { + isSupported = !!attempt(function () { + // FF 3.1b1, b2 will throw an exception if a bare literal is provided. + // Conforming implementations should also coerce the initial argument to + // a string prior to parsing. + if (parse("0") !== 0 || parse(false)) { + return; + } + // Simple parsing test. + var value = parse(serialized); + return value["a"].length == 5 && value["a"][0] === 1; + }); if (isSupported) { - var stringify = exports.stringify; - attempt(function () { - isSupported = - // JSON 2, Prototype <= 1.7, and older WebKit builds incorrectly - // serialize extended years. - stringify(new Date(-8.64e15)) == '"-271821-04-20T00:00:00.000Z"' && - // The milliseconds are optional in ES 5, but required in 5.1. - stringify(new Date(8.64e15)) == '"+275760-09-13T00:00:00.000Z"' && - // Firefox <= 11.0 incorrectly serializes years prior to 0 as negative - // four-digit years instead of six-digit years. Credits: @Yaffle. - stringify(new Date(-621987552e5)) == '"-000001-01-01T00:00:00.000Z"' && - // Safari <= 5.1.5 and Opera >= 10.53 incorrectly serialize millisecond - // values less than 1000. Credits: @Yaffle. - stringify(new Date(-1)) == '"1969-12-31T23:59:59.999Z"'; + isSupported = !!attempt(function () { + // Safari <= 5.1.2 and FF 3.1b1 allow unescaped tabs in strings. + return !parse('"\t"'); }); } - } else { - var value, serialized = '{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}'; - // Test `JSON.stringify`. - if (name == "json-stringify") { - var stringify = exports.stringify, stringifySupported = typeof stringify == "function"; - if (stringifySupported) { - // A test function object with a custom `toJSON` method. - (value = function () { - return 1; - }).toJSON = value; - attempt(function () { - stringifySupported = - // Firefox 3.1b1 and b2 serialize string, number, and boolean - // primitives as object literals. - stringify(0) === "0" && - // FF 3.1b1, b2, and JSON 2 serialize wrapped primitives as object - // literals. - stringify(new Number()) === "0" && - stringify(new String()) == '""' && - // FF 3.1b1, 2 throw an error if the value is `null`, `undefined`, or - // does not define a canonical JSON representation (this applies to - // objects with `toJSON` properties as well, *unless* they are nested - // within an object or array). - stringify(getClass) === undefined && - // IE 8 serializes `undefined` as `"undefined"`. Safari <= 5.1.7 and - // FF 3.1b3 pass this test. - stringify(undefined) === undefined && - // Safari <= 5.1.7 and FF 3.1b3 throw `Error`s and `TypeError`s, - // respectively, if the value is omitted entirely. - stringify() === undefined && - // FF 3.1b1, 2 throw an error if the given value is not a number, - // string, array, object, Boolean, or `null` literal. This applies to - // objects with custom `toJSON` methods as well, unless they are nested - // inside object or array literals. YUI 3.0.0b1 ignores custom `toJSON` - // methods entirely. - stringify(value) === "1" && - stringify([value]) == "[1]" && - // Prototype <= 1.6.1 serializes `[undefined]` as `"[]"` instead of - // `"[null]"`. - stringify([undefined]) == "[null]" && - // YUI 3.0.0b1 fails to serialize `null` literals. - stringify(null) == "null" && - // FF 3.1b1, 2 halts serialization if an array contains a function: - // `[1, true, getClass, 1]` serializes as "[1,true,],". FF 3.1b3 - // elides non-JSON values from objects and arrays, unless they - // define custom `toJSON` methods. - stringify([undefined, getClass, null]) == "[null,null,null]" && - // Simple serialization test. FF 3.1b1 uses Unicode escape sequences - // where character escape codes are expected (e.g., `\b` => `\u0008`). - stringify({ "a": [value, true, false, null, "\x00\b\n\f\r\t"] }) == serialized && - // FF 3.1b1 and b2 ignore the `filter` and `width` arguments. - stringify(null, value) === "1" && - stringify([1, 2], null, 1) == "[\n 1,\n 2\n]"; - }, function () { - stringifySupported = false; - }); - } - isSupported = stringifySupported; + if (isSupported) { + isSupported = !!attempt(function () { + // FF 4.0 and 4.0.1 allow leading `+` signs and leading + // decimal points. FF 4.0, 4.0.1, and IE 9-10 also allow + // certain octal literals. + return parse("01") !== 1; + }); } - // Test `JSON.parse`. - if (name == "json-parse") { - var parse = exports.parse, parseSupported; - if (typeof parse == "function") { - attempt(function () { - // FF 3.1b1, b2 will throw an exception if a bare literal is provided. - // Conforming implementations should also coerce the initial argument to - // a string prior to parsing. - if (parse("0") === 0 && !parse(false)) { - // Simple parsing test. - value = parse(serialized); - parseSupported = value["a"].length == 5 && value["a"][0] === 1; - if (parseSupported) { - attempt(function () { - // Safari <= 5.1.2 and FF 3.1b1 allow unescaped tabs in strings. - parseSupported = !parse('"\t"'); - }); - if (parseSupported) { - attempt(function () { - // FF 4.0 and 4.0.1 allow leading `+` signs and leading - // decimal points. FF 4.0, 4.0.1, and IE 9-10 also allow - // certain octal literals. - parseSupported = parse("01") !== 1; - }); - } - if (parseSupported) { - attempt(function () { - // FF 4.0, 4.0.1, and Rhino 1.7R3-R4 allow trailing decimal - // points. These environments, along with FF 3.1b1 and 2, - // also allow trailing commas in JSON objects and arrays. - parseSupported = parse("1.") !== 1; - }); - } - } - } - }, function () { - parseSupported = false; - }); - } - isSupported = parseSupported; + if (isSupported) { + isSupported = !!attempt(function () { + // FF 4.0, 4.0.1, and Rhino 1.7R3-R4 allow trailing decimal + // points. These environments, along with FF 3.1b1 and 2, + // also allow trailing commas in JSON objects and arrays. + return parse("1.") !== 1; + }); } } - return has[name] = !!isSupported; + has[name] = isSupported; + return isSupported; } - has["bug-string-char-index"] = has["date-serialization"] = has["json"] = has["json-stringify"] = has["json-parse"] = null; - - if (!has("json")) { - // Common `[[Class]]` name aliases. - var functionClass = "[object Function]", - dateClass = "[object Date]", - numberClass = "[object Number]", - stringClass = "[object String]", - arrayClass = "[object Array]", - booleanClass = "[object Boolean]"; - - // Detect incomplete support for accessing string characters by index. - var charIndexBuggy = has("bug-string-char-index"); - - // Internal: Normalizes the `for...in` iteration algorithm across - // environments. Each enumerated key is yielded to a `callback` function. - var forOwn = function (object, callback) { - var size = 0, Properties, members, property; - - // Tests for bugs in the current environment's `for...in` algorithm. The - // `valueOf` property inherits the non-enumerable flag from - // `Object.prototype` in older versions of IE, Netscape, and Mozilla. - (Properties = function () { - this.valueOf = 0; - }).prototype.valueOf = 0; - - // Iterate over a new instance of the `Properties` class. - members = new Properties(); - for (property in members) { - // Ignore all properties inherited from `Object.prototype`. - if (isProperty.call(members, property)) { - size++; - } - } - Properties = members = null; + return false; + } + has["bug-string-char-index"] = null; + has["extended-years"] = null; + has["json-stringify"] = null; + has["date-serialization"] = null; // Depends on `json-stringify`, `extended-years`. + has["json-parse"] = null; // Depends on `json-stringify`, `json-parse`, `date-serialization`. + + if (has("json")) { + return; + } - // Normalize the iteration algorithm. - if (!size) { - // A list of non-enumerable properties inherited from `Object.prototype`. - members = ["valueOf", "toString", "toLocaleString", "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"]; - // IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable - // properties. - forOwn = function (object, callback) { - var isFunction = getClass.call(object) == functionClass, property, length; - var hasProperty = !isFunction && typeof object.constructor != "function" && objectTypes[typeof object.hasOwnProperty] && object.hasOwnProperty || isProperty; - for (property in object) { - // Gecko <= 1.0 enumerates the `prototype` property of functions under - // certain conditions; IE does not. - if (!(isFunction && property == "prototype") && hasProperty.call(object, property)) { - callback(property); - } - } - // Manually invoke the callback for each non-enumerable property. - for (length = members.length; property = members[--length]; hasProperty.call(object, property) && callback(property)); - }; - } else { - // No bugs detected; use the standard `for...in` algorithm. - forOwn = function (object, callback) { - var isFunction = getClass.call(object) == functionClass, property, isConstructor; - for (property in object) { - if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) { - callback(property); - } - } - // Manually invoke the callback for the `constructor` property due to - // cross-environment inconsistencies. - if (isConstructor || isProperty.call(object, (property = "constructor"))) { - callback(property); - } - }; - } - return forOwn(object, callback); + var forOwn = createForOwn(getClass, Object.prototype.hasOwnProperty); + + if (!has("json-stringify")) { + exports.stringify = createStringify(getClass, forOwn, TypeError); + } else if (!has("date-serialization")) { + // For environments with `JSON.stringify` but buggy date serialization, + // we override the native `Date#toJSON` implementation with a + // spec-compliant one. + var UTCDate; + if (!has("extended-years")) { + UTCDate = createUTCDate(Math.floor); + } + var serializeDate = createSerializeDate(UTCDate); + if (has("json-stringify")) { + // TODO: Revert via `JSON3.noConflict()`. + Date.prototype.toJSON = function() { + return serializeDate(this); }; - - // Public: Serializes a JavaScript `value` as a JSON string. The optional - // `filter` argument may specify either a function that alters how object and - // array members are serialized, or an array of strings and numbers that - // indicates which properties should be serialized. The optional `width` - // argument may be either a string or number that specifies the indentation - // level of the output. - if (!has("json-stringify") && !has("date-serialization")) { - // Internal: A map of control characters and their escaped equivalents. - var Escapes = { - 92: "\\\\", - 34: '\\"', - 8: "\\b", - 12: "\\f", - 10: "\\n", - 13: "\\r", - 9: "\\t" - }; - - // Internal: Converts `value` into a zero-padded string such that its - // length is at least equal to `width`. The `width` must be <= 6. - var leadingZeroes = "000000"; - var toPaddedString = function (width, value) { - // The `|| 0` expression is necessary to work around a bug in - // Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`. - return (leadingZeroes + (value || 0)).slice(-width); - }; - - // Internal: Serializes a date object. - var serializeDate = function (value) { - var getData, year, month, date, time, hours, minutes, seconds, milliseconds; - // Define additional utility methods if the `Date` methods are buggy. - if (!isExtended) { - var floor = Math.floor; - // A mapping between the months of the year and the number of days between - // January 1st and the first of the respective month. - var Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; - // Internal: Calculates the number of days between the Unix epoch and the - // first day of the given month. - var getDay = function (year, month) { - return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400); - }; - getData = function (value) { - // Manually compute the year, month, date, hours, minutes, - // seconds, and milliseconds if the `getUTC*` methods are - // buggy. Adapted from @Yaffle's `date-shim` project. - date = floor(value / 864e5); - for (year = floor(date / 365.2425) + 1970 - 1; getDay(year + 1, 0) <= date; year++); - for (month = floor((date - getDay(year, 0)) / 30.42); getDay(year, month + 1) <= date; month++); - date = 1 + date - getDay(year, month); - // The `time` value specifies the time within the day (see ES - // 5.1 section 15.9.1.2). The formula `(A % B + B) % B` is used - // to compute `A modulo B`, as the `%` operator does not - // correspond to the `modulo` operation for negative numbers. - time = (value % 864e5 + 864e5) % 864e5; - // The hours, minutes, seconds, and milliseconds are obtained by - // decomposing the time within the day. See section 15.9.1.10. - hours = floor(time / 36e5) % 24; - minutes = floor(time / 6e4) % 60; - seconds = floor(time / 1e3) % 60; - milliseconds = time % 1e3; - }; - } else { - getData = function (value) { - year = value.getUTCFullYear(); - month = value.getUTCMonth(); - date = value.getUTCDate(); - hours = value.getUTCHours(); - minutes = value.getUTCMinutes(); - seconds = value.getUTCSeconds(); - milliseconds = value.getUTCMilliseconds(); - }; - } - serializeDate = function (value) { - if (value > -1 / 0 && value < 1 / 0) { - // Dates are serialized according to the `Date#toJSON` method - // specified in ES 5.1 section 15.9.5.44. See section 15.9.1.15 - // for the ISO 8601 date time string format. - getData(value); - // Serialize extended years correctly. - value = (year <= 0 || year >= 1e4 ? (year < 0 ? "-" : "+") + toPaddedString(6, year < 0 ? -year : year) : toPaddedString(4, year)) + - "-" + toPaddedString(2, month + 1) + "-" + toPaddedString(2, date) + - // Months, dates, hours, minutes, and seconds should have two - // digits; milliseconds should have three. - "T" + toPaddedString(2, hours) + ":" + toPaddedString(2, minutes) + ":" + toPaddedString(2, seconds) + - // Milliseconds are optional in ES 5.0, but required in 5.1. - "." + toPaddedString(3, milliseconds) + "Z"; - year = month = date = hours = minutes = seconds = milliseconds = null; - } else { - value = null; - } - return value; - }; - return serializeDate(value); - }; - - // For environments with `JSON.stringify` but buggy date serialization, - // we override the native `Date#toJSON` implementation with a - // spec-compliant one. - if (has("json-stringify") && !has("date-serialization")) { - // Internal: the `Date#toJSON` implementation used to override the native one. - function dateToJSON (key) { - return serializeDate(this); - } - - // Public: `JSON.stringify`. See ES 5.1 section 15.12.3. - var nativeStringify = exports.stringify; - exports.stringify = function (source, filter, width) { - var nativeToJSON = Date.prototype.toJSON; - Date.prototype.toJSON = dateToJSON; - var result = nativeStringify(source, filter, width); - Date.prototype.toJSON = nativeToJSON; - return result; - } - } else { - // Internal: Double-quotes a string `value`, replacing all ASCII control - // characters (characters with code unit values between 0 and 31) with - // their escaped equivalents. This is an implementation of the - // `Quote(value)` operation defined in ES 5.1 section 15.12.3. - var unicodePrefix = "\\u00"; - var escapeChar = function (character) { - var charCode = character.charCodeAt(0), escaped = Escapes[charCode]; - if (escaped) { - return escaped; - } - return unicodePrefix + toPaddedString(2, charCode.toString(16)); - }; - var reEscape = /[\x00-\x1f\x22\x5c]/g; - var quote = function (value) { - reEscape.lastIndex = 0; - return '"' + - ( - reEscape.test(value) - ? value.replace(reEscape, escapeChar) - : value - ) + - '"'; - }; - - // Internal: Recursively serializes an object. Implements the - // `Str(key, holder)`, `JO(value)`, and `JA(value)` operations. - var serialize = function (property, object, callback, properties, whitespace, indentation, stack) { - var value, type, className, results, element, index, length, prefix, result; - attempt(function () { - // Necessary for host object support. - value = object[property]; - }); - if (typeof value == "object" && value) { - if (value.getUTCFullYear && getClass.call(value) == dateClass && value.toJSON === Date.prototype.toJSON) { - value = serializeDate(value); - } else if (typeof value.toJSON == "function") { - value = value.toJSON(property); - } - } - if (callback) { - // If a replacement function was provided, call it to obtain the value - // for serialization. - value = callback.call(object, property, value); - } - // Exit early if value is `undefined` or `null`. - if (value == undefined) { - return value === undefined ? value : "null"; - } - type = typeof value; - // Only call `getClass` if the value is an object. - if (type == "object") { - className = getClass.call(value); - } - switch (className || type) { - case "boolean": - case booleanClass: - // Booleans are represented literally. - return "" + value; - case "number": - case numberClass: - // JSON numbers must be finite. `Infinity` and `NaN` are serialized as - // `"null"`. - return value > -1 / 0 && value < 1 / 0 ? "" + value : "null"; - case "string": - case stringClass: - // Strings are double-quoted and escaped. - return quote("" + value); - } - // Recursively serialize objects and arrays. - if (typeof value == "object") { - // Check for cyclic structures. This is a linear search; performance - // is inversely proportional to the number of unique nested objects. - for (length = stack.length; length--;) { - if (stack[length] === value) { - // Cyclic structures cannot be serialized by `JSON.stringify`. - throw TypeError(); - } - } - // Add the object to the stack of traversed objects. - stack.push(value); - results = []; - // Save the current indentation level and indent one additional level. - prefix = indentation; - indentation += whitespace; - if (className == arrayClass) { - // Recursively serialize array elements. - for (index = 0, length = value.length; index < length; index++) { - element = serialize(index, value, callback, properties, whitespace, indentation, stack); - results.push(element === undefined ? "null" : element); - } - result = results.length ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]"; - } else { - // Recursively serialize object members. Members are selected from - // either a user-specified list of property names, or the object - // itself. - forOwn(properties || value, function (property) { - var element = serialize(property, value, callback, properties, whitespace, indentation, stack); - if (element !== undefined) { - // According to ES 5.1 section 15.12.3: "If `gap` {whitespace} - // is not the empty string, let `member` {quote(property) + ":"} - // be the concatenation of `member` and the `space` character." - // The "`space` character" refers to the literal space - // character, not the `space` {width} argument provided to - // `JSON.stringify`. - results.push(quote(property) + ":" + (whitespace ? " " : "") + element); - } - }); - result = results.length ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}"; - } - // Remove the object from the traversed object stack. - stack.pop(); - return result; - } - }; - - // Public: `JSON.stringify`. See ES 5.1 section 15.12.3. - exports.stringify = function (source, filter, width) { - var whitespace, callback, properties, className; - if (objectTypes[typeof filter] && filter) { - className = getClass.call(filter); - if (className == functionClass) { - callback = filter; - } else if (className == arrayClass) { - // Convert the property names array into a makeshift set. - properties = {}; - for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((className = getClass.call(value)), className == stringClass || className == numberClass) && (properties[value] = 1)); - } - } - if (width) { - className = getClass.call(width); - if (className == numberClass) { - // Convert the `width` to an integer and create a string containing - // `width` number of space characters. - if ((width -= width % 1) > 0) { - for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " "); - } - } else if (className == stringClass) { - whitespace = width.length <= 10 ? width : width.slice(0, 10); - } - } - // Opera <= 7.54u2 discards the values associated with empty string keys - // (`""`) only if they are used directly within an object member list - // (e.g., `!("" in { "": 1})`). - return serialize("", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []); - }; - } - } - - // Public: Parses a JSON source string. - if (!has("json-parse")) { - var fromCharCode = String.fromCharCode; - - // Internal: A map of escaped control characters and their unescaped - // equivalents. - var Unescapes = { - 92: "\\", - 34: '"', - 47: "/", - 98: "\b", - 116: "\t", - 110: "\n", - 102: "\f", - 114: "\r" - }; - - // Internal: Stores the parser state. - var Index, Source; - - // Internal: Resets the parser state and throws a `SyntaxError`. - var abort = function () { - Index = Source = null; - throw SyntaxError(); - }; - - // Internal: Returns the next token, or `"$"` if the parser has reached - // the end of the source string. A token may be a string, number, `null` - // literal, or Boolean literal. - var lex = function () { - var source = Source, length = source.length, value, begin, position, isSigned, charCode; - while (Index < length) { - charCode = source.charCodeAt(Index); - switch (charCode) { - case 9: case 10: case 13: case 32: - // Skip whitespace tokens, including tabs, carriage returns, line - // feeds, and space characters. - Index++; - break; - case 123: case 125: case 91: case 93: case 58: case 44: - // Parse a punctuator token (`{`, `}`, `[`, `]`, `:`, or `,`) at - // the current position. - value = charIndexBuggy ? source.charAt(Index) : source[Index]; - Index++; - return value; - case 34: - // `"` delimits a JSON string; advance to the next character and - // begin parsing the string. String tokens are prefixed with the - // sentinel `@` character to distinguish them from punctuators and - // end-of-string tokens. - for (value = "@", Index++; Index < length;) { - charCode = source.charCodeAt(Index); - if (charCode < 32) { - // Unescaped ASCII control characters (those with a code unit - // less than the space character) are not permitted. - abort(); - } else if (charCode == 92) { - // A reverse solidus (`\`) marks the beginning of an escaped - // control character (including `"`, `\`, and `/`) or Unicode - // escape sequence. - charCode = source.charCodeAt(++Index); - switch (charCode) { - case 92: case 34: case 47: case 98: case 116: case 110: case 102: case 114: - // Revive escaped control characters. - value += Unescapes[charCode]; - Index++; - break; - case 117: - // `\u` marks the beginning of a Unicode escape sequence. - // Advance to the first character and validate the - // four-digit code point. - begin = ++Index; - for (position = Index + 4; Index < position; Index++) { - charCode = source.charCodeAt(Index); - // A valid sequence comprises four hexdigits (case- - // insensitive) that form a single hexadecimal value. - if (!(charCode >= 48 && charCode <= 57 || charCode >= 97 && charCode <= 102 || charCode >= 65 && charCode <= 70)) { - // Invalid Unicode escape sequence. - abort(); - } - } - // Revive the escaped character. - value += fromCharCode("0x" + source.slice(begin, Index)); - break; - default: - // Invalid escape sequence. - abort(); - } - } else { - if (charCode == 34) { - // An unescaped double-quote character marks the end of the - // string. - break; - } - charCode = source.charCodeAt(Index); - begin = Index; - // Optimize for the common case where a string is valid. - while (charCode >= 32 && charCode != 92 && charCode != 34) { - charCode = source.charCodeAt(++Index); - } - // Append the string as-is. - value += source.slice(begin, Index); - } - } - if (source.charCodeAt(Index) == 34) { - // Advance to the next character and return the revived string. - Index++; - return value; - } - // Unterminated string. - abort(); - default: - // Parse numbers and literals. - begin = Index; - // Advance past the negative sign, if one is specified. - if (charCode == 45) { - isSigned = true; - charCode = source.charCodeAt(++Index); - } - // Parse an integer or floating-point value. - if (charCode >= 48 && charCode <= 57) { - // Leading zeroes are interpreted as octal literals. - if (charCode == 48 && ((charCode = source.charCodeAt(Index + 1)), charCode >= 48 && charCode <= 57)) { - // Illegal octal literal. - abort(); - } - isSigned = false; - // Parse the integer component. - for (; Index < length && ((charCode = source.charCodeAt(Index)), charCode >= 48 && charCode <= 57); Index++); - // Floats cannot contain a leading decimal point; however, this - // case is already accounted for by the parser. - if (source.charCodeAt(Index) == 46) { - position = ++Index; - // Parse the decimal component. - for (; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++); - if (position == Index) { - // Illegal trailing decimal. - abort(); - } - Index = position; - } - // Parse exponents. The `e` denoting the exponent is - // case-insensitive. - charCode = source.charCodeAt(Index); - if (charCode == 101 || charCode == 69) { - charCode = source.charCodeAt(++Index); - // Skip past the sign following the exponent, if one is - // specified. - if (charCode == 43 || charCode == 45) { - Index++; - } - // Parse the exponential component. - for (position = Index; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++); - if (position == Index) { - // Illegal empty exponent. - abort(); - } - Index = position; - } - // Coerce the parsed value to a JavaScript number. - return +source.slice(begin, Index); - } - // A negative sign may only precede numbers. - if (isSigned) { - abort(); - } - // `true`, `false`, and `null` literals. - var temp = source.slice(Index, Index + 4); - if (temp == "true") { - Index += 4; - return true; - } else if (temp == "fals" && source.charCodeAt(Index + 4 ) == 101) { - Index += 5; - return false; - } else if (temp == "null") { - Index += 4; - return null; - } - // Unrecognized token. - abort(); - } - } - // Return the sentinel `$` character if the parser has reached the end - // of the source string. - return "$"; - }; - - // Internal: Parses a JSON `value` token. - var get = function (value) { - var results, hasMembers; - if (value == "$") { - // Unexpected end of input. - abort(); - } - if (typeof value == "string") { - if ((charIndexBuggy ? value.charAt(0) : value[0]) == "@") { - // Remove the sentinel `@` character. - return value.slice(1); - } - // Parse object and array literals. - if (value == "[") { - // Parses a JSON array, returning a new JavaScript array. - results = []; - for (;;) { - value = lex(); - // A closing square bracket marks the end of the array literal. - if (value == "]") { - break; - } - // If the array literal contains elements, the current token - // should be a comma separating the previous element from the - // next. - if (hasMembers) { - if (value == ",") { - value = lex(); - if (value == "]") { - // Unexpected trailing `,` in array literal. - abort(); - } - } else { - // A `,` must separate each array element. - abort(); - } - } else { - hasMembers = true; - } - // Elisions and leading commas are not permitted. - if (value == ",") { - abort(); - } - results.push(get(value)); - } - return results; - } else if (value == "{") { - // Parses a JSON object, returning a new JavaScript object. - results = {}; - for (;;) { - value = lex(); - // A closing curly brace marks the end of the object literal. - if (value == "}") { - break; - } - // If the object literal contains members, the current token - // should be a comma separator. - if (hasMembers) { - if (value == ",") { - value = lex(); - if (value == "}") { - // Unexpected trailing `,` in object literal. - abort(); - } - } else { - // A `,` must separate each object member. - abort(); - } - } else { - hasMembers = true; - } - // Leading commas are not permitted, object property names must be - // double-quoted strings, and a `:` must separate each property - // name and value. - if (value == "," || typeof value != "string" || (charIndexBuggy ? value.charAt(0) : value[0]) != "@" || lex() != ":") { - abort(); - } - results[value.slice(1)] = get(lex()); - } - return results; - } - // Unexpected token encountered. - abort(); - } - return value; - }; - - // Internal: Updates a traversed object member. - var update = function (source, property, callback) { - var element = walk(source, property, callback); - if (element === undefined) { - delete source[property]; - } else { - source[property] = element; - } - }; - - // Internal: Recursively traverses a parsed JSON object, invoking the - // `callback` function for each value. This is an implementation of the - // `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2. - var walk = function (source, property, callback) { - var value = source[property], length; - if (typeof value == "object" && value) { - // `forOwn` can't be used to traverse an array in Opera <= 8.54 - // because its `Object#hasOwnProperty` implementation returns `false` - // for array indices (e.g., `![1, 2, 3].hasOwnProperty("0")`). - if (getClass.call(value) == arrayClass) { - for (length = value.length; length--; update(value, length, callback)); - } else { - forOwn(value, function (property) { - update(value, property, callback); - }); - } - } - return callback.call(source, property, value); - }; - - // Public: `JSON.parse`. See ES 5.1 section 15.12.2. - exports.parse = function (source, callback) { - var result, value; - Index = 0; - Source = "" + source; - result = get(lex()); - // If a JSON string contains multiple tokens, it is invalid. - if (lex() != "$") { - abort(); - } - // Reset the parser state. - Index = Source = null; - return callback && getClass.call(callback) == functionClass ? walk((value = {}, value[""] = result, value), "", callback) : result; - }; - } } - - exports.runInContext = runInContext; - return exports; } - if (freeExports && !isLoader) { - // Export for CommonJS environments. - runInContext(root, freeExports); - } else { - // Export for web browsers and JavaScript engines. - var nativeJSON = root.JSON, - previousJSON = root.JSON3, - isRestored = false; - - var JSON3 = runInContext(root, (root.JSON3 = { - // Public: Restores the original value of the global `JSON` object and - // returns a reference to the `JSON3` object. - "noConflict": function () { - if (!isRestored) { - isRestored = true; - root.JSON = nativeJSON; - root.JSON3 = previousJSON; - nativeJSON = previousJSON = null; - } - return JSON3; - } - })); - - root.JSON = { - "parse": JSON3.parse, - "stringify": JSON3.stringify - }; + if (!has("json-parse")) { + // Detect incomplete support for accessing string characters by index. + var charIndexBuggy = has("bug-string-char-index"); + exports.parse = createParse(charIndexBuggy, String.fromCharCode, SyntaxError, getClass, forOwn); } - // Export for asynchronous module loaders. - if (isLoader) { - define(function () { + exports.runInContext = runInContext; + return exports; +} + +if (freeExports && !isLoader) { + // Export for CommonJS environments. + runInContext(root, freeExports); +} else { + // Export for web browsers and JavaScript engines. + var nativeJSON = root.JSON, + previousJSON = root.JSON3, + isRestored = false; + + var JSON3 = runInContext(root, (root.JSON3 = { + // Public: Restores the original value of the global `JSON` object and + // returns a reference to the `JSON3` object. + "noConflict": function () { + if (!isRestored) { + isRestored = true; + root.JSON = nativeJSON; + root.JSON3 = previousJSON; + nativeJSON = previousJSON = null; + } return JSON3; - }); - } -}).call(this); + } + })); + + root.JSON = { + "parse": JSON3.parse, + "stringify": JSON3.stringify + }; +} + +// Export for asynchronous module loaders. +if (isLoader) { + define(function () { + return JSON3; + }); +} diff --git a/lib/json3.min.js b/lib/json3.min.js deleted file mode 100644 index 002cbcf..0000000 --- a/lib/json3.min.js +++ /dev/null @@ -1,17 +0,0 @@ -/*! JSON v3.3.2 | https://bestiejs.github.io/json3 | Copyright 2012-2015, Kit Cambridge, Benjamin Tan | http://kit.mit-license.org */ -(function(){function M(r,q){function p(a,l){try{a()}catch(c){l&&l()}}function k(a){if(null!=k[a])return k[a];var l;if("bug-string-char-index"==a)l="a"!="a"[0];else if("json"==a)l=k("json-stringify")&&k("date-serialization")&&k("json-parse");else if("date-serialization"==a){if(l=k("json-stringify")&&v){var c=q.stringify;p(function(){l='"-271821-04-20T00:00:00.000Z"'==c(new z(-864E13))&&'"+275760-09-13T00:00:00.000Z"'==c(new z(864E13))&&'"-000001-01-01T00:00:00.000Z"'==c(new z(-621987552E5))&&'"1969-12-31T23:59:59.999Z"'== -c(new z(-1))})}}else{var b;if("json-stringify"==a){var c=q.stringify,e="function"==typeof c;e&&((b=function(){return 1}).toJSON=b,p(function(){e="0"===c(0)&&"0"===c(new B)&&'""'==c(new A)&&c(t)===u&&c(u)===u&&c()===u&&"1"===c(b)&&"[1]"==c([b])&&"[null]"==c([u])&&"null"==c(null)&&"[null,null,null]"==c([u,t,null])&&'{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}'==c({a:[b,!0,!1,null,"\x00\b\n\f\r\t"]})&&"1"===c(null,b)&&"[\n 1,\n 2\n]"==c([1,2],null,1)},function(){e=!1}));l=e}if("json-parse"==a){var n= -q.parse,d;"function"==typeof n&&p(function(){0===n("0")&&!n(!1)&&(b=n('{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}'),d=5==b.a.length&&1===b.a[0])&&(p(function(){d=!n('"\t"')}),d&&p(function(){d=1!==n("01")}),d&&p(function(){d=1!==n("1.")}))},function(){d=!1});l=d}}return k[a]=!!l}r||(r=f.Object());q||(q=f.Object());var B=r.Number||f.Number,A=r.String||f.String,E=r.Object||f.Object,z=r.Date||f.Date,I=r.SyntaxError||f.SyntaxError,J=r.TypeError||f.TypeError,K=r.Math||f.Math,F=r.JSON||f.JSON;"object"== -typeof F&&F&&(q.stringify=F.stringify,q.parse=F.parse);var E=E.prototype,t=E.toString,G=E.hasOwnProperty,u,v=new z(-0xc782b5b800cec);p(function(){v=-109252==v.getUTCFullYear()&&0===v.getUTCMonth()&&1===v.getUTCDate()&&10==v.getUTCHours()&&37==v.getUTCMinutes()&&6==v.getUTCSeconds()&&708==v.getUTCMilliseconds()});k["bug-string-char-index"]=k["date-serialization"]=k.json=k["json-stringify"]=k["json-parse"]=null;if(!k("json")){var N=k("bug-string-char-index"),C=function(a,b){var c=0,g,e,n;(g=function(){this.valueOf= -0}).prototype.valueOf=0;e=new g;for(n in e)G.call(e,n)&&c++;g=e=null;c?C=function(a,c){var b="[object Function]"==t.call(a),l,e;for(l in a)b&&"prototype"==l||!G.call(a,l)||(e="constructor"===l)||c(l);(e||G.call(a,l="constructor"))&&c(l)}:(e="valueOf toString toLocaleString propertyIsEnumerable isPrototypeOf hasOwnProperty constructor".split(" "),C=function(a,c){var b="[object Function]"==t.call(a),l,g=!b&&"function"!=typeof a.constructor&&D[typeof a.hasOwnProperty]&&a.hasOwnProperty||G;for(l in a)b&& -"prototype"==l||!g.call(a,l)||c(l);for(b=e.length;l=e[--b];g.call(a,l)&&c(l));});return C(a,b)};if(!k("json-stringify")||!k(" date-serialization")){var L={92:"\\\\",34:'\\"',8:"\\b",12:"\\f",10:"\\n",13:"\\r",9:"\\t"},x=function(a,b){return("000000"+(b||0)).slice(-a)},V=function(a){a=a.charCodeAt(0);var b=L[a];return b?b:"\\u00"+x(2,a.toString(16))},O=/[\x00-\x1f\x22\x5c]/g,S=function(a){O.lastIndex=0;return'"'+(O.test(a)?a.replace(O,V):a)+'"'},P=function(a){var b,c,g,e,n,d,h,f,m;if(v)b=function(a){c= -a.getUTCFullYear();g=a.getUTCMonth();e=a.getUTCDate();d=a.getUTCHours();h=a.getUTCMinutes();f=a.getUTCSeconds();m=a.getUTCMilliseconds()};else{var w=K.floor,k=[0,31,59,90,120,151,181,212,243,273,304,334],p=function(a,c){return k[c]+365*(a-1970)+w((a-1969+(c=+(1-1/0&&a<1/0?(b(a),a=(0>=c||1E4<=c?(0>c?"-":"+")+x(6,0>c?-c:c):x(4,c))+"-"+x(2,g+1)+"-"+x(2,e)+"T"+x(2,d)+":"+x(2,h)+":"+x(2,f)+"."+x(3,m)+"Z",c=g=e=d=h=f=m=null):a=null;return a};return P(a)},Q=function(a,b,c,g,e,n,d){var h,f,m,k,q,r;p(function(){h=b[a]});"object"==typeof h&&h&&(h.getUTCFullYear&&"[object Date]"==t.call(h)&&h.toJSON===z.prototype.toJSON?h=P(h):"function"==typeof h.toJSON&&(h=h.toJSON(a)));c&&(h=c.call(b,a,h));if(h==u)return h===u?h:"null";f=typeof h;"object"== -f&&(m=t.call(h));switch(m||f){case "boolean":case "[object Boolean]":return""+h;case "number":case "[object Number]":return h>-1/0&&h<1/0?""+h:"null";case "string":case "[object String]":return S(""+h)}if("object"==typeof h){for(f=d.length;f--;)if(d[f]===h)throw J();d.push(h);k=[];r=n;n+=e;if("[object Array]"==m){q=0;for(f=h.length;q=c.length?c:c.slice(0,10));return Q("",(k={},k[""]=a,k),e,f,g,"",[])}}if(!k("json-parse")){var W=A.fromCharCode,X={92:"\\",34:'"',47:"/",98:"\b",116:"\t",110:"\n",102:"\f",114:"\r"},b,H,m=function(){b=H=null;throw I();},y=function(){for(var a=H,l=a.length,c,g,e,f,d;bd)m();else if(92==d)switch(d= -a.charCodeAt(++b),d){case 92:case 34:case 47:case 98:case 116:case 110:case 102:case 114:c+=X[d];b++;break;case 117:g=++b;for(e=b+4;b=d||97<=d&&102>=d||65<=d&&70>=d||m();c+=W("0x"+a.slice(g,b));break;default:m()}else{if(34==d)break;d=a.charCodeAt(b);for(g=b;32<=d&&92!=d&&34!=d;)d=a.charCodeAt(++b);c+=a.slice(g,b)}if(34==a.charCodeAt(b))return b++,c;m();default:g=b;45==d&&(f=!0,d=a.charCodeAt(++b));if(48<=d&&57>=d){for(48==d&&(d=a.charCodeAt(b+1),48<=d&&57>=d)&&m();b< -l&&(d=a.charCodeAt(b),48<=d&&57>=d);b++);if(46==a.charCodeAt(b)){for(e=++b;e=d);e++);e==b&&m();b=e}d=a.charCodeAt(b);if(101==d||69==d){d=a.charCodeAt(++b);43!=d&&45!=d||b++;for(e=b;e=d);e++);e==b&&m();b=e}return+a.slice(g,b)}f&&m();c=a.slice(b,b+4);if("true"==c)return b+=4,!0;if("fals"==c&&101==a.charCodeAt(b+4))return b+=5,!1;if("null"==c)return b+=4,null;m()}return"$"},R=function(a){var b,c;"$"==a&&m();if("string"==typeof a){if("@"== -(N?a.charAt(0):a[0]))return a.slice(1);if("["==a){for(b=[];;){a=y();if("]"==a)break;c?","==a?(a=y(),"]"==a&&m()):m():c=!0;","==a&&m();b.push(R(a))}return b}if("{"==a){for(b={};;){a=y();if("}"==a)break;c?","==a?(a=y(),"}"==a&&m()):m():c=!0;","!=a&&"string"==typeof a&&"@"==(N?a.charAt(0):a[0])&&":"==y()||m();b[a.slice(1)]=R(y())}return b}m()}return a},U=function(a,b,c){c=T(a,b,c);c===u?delete a[b]:a[b]=c},T=function(a,b,c){var g=a[b],e;if("object"==typeof g&&g)if("[object Array]"==t.call(g))for(e=g.length;e--;U(g, -e,c));else C(g,function(a){U(g,a,c)});return c.call(a,b,g)};q.parse=function(a,f){var c,g;b=0;H=""+a;c=R(y());"$"!=y()&&m();b=H=null;return f&&"[object Function]"==t.call(f)?T((g={},g[""]=c,g),"",f):c}}}q.runInContext=M;return q}var I=typeof define==="function"&&define.amd,D={"function":!0,object:!0},A=D[typeof exports]&&exports&&!exports.nodeType&&exports,f=D[typeof window]&&window||this,p=A&&D[typeof module]&&module&&!module.nodeType&&"object"==typeof global&&global;!p||p.global!==p&&p.window!== -p&&p.self!==p||(f=p);if(A&&!I)M(f,A);else{var J=f.JSON,K=f.JSON3,L=!1,B=M(f,f.JSON3={noConflict:function(){L||(L=!0,f.JSON=J,f.JSON3=K,J=K=null);return B}});f.JSON={parse:B.parse,stringify:B.stringify}}I&&define(function(){return B})}).call(this); diff --git a/lib/parse.js b/lib/parse.js new file mode 100644 index 0000000..8e06674 --- /dev/null +++ b/lib/parse.js @@ -0,0 +1,325 @@ +module.exports = createParse; +function createParse(charIndexBuggy, fromCharCode, newSyntaxError, getClass, forOwn) { + // Public: `JSON.parse` parses a JSON source string. See + // ES 5.1 section 15.12.2. + function parse(source, callback) { + Index = 0; + Source = "" + source; + var firstToken = lex(charIndexBuggy, fromCharCode, newSyntaxError); + var result = get(charIndexBuggy, fromCharCode, newSyntaxError, firstToken); + // If a JSON string contains multiple tokens, it is invalid. + if (lex(charIndexBuggy, fromCharCode, newSyntaxError) != "$") { + abort(newSyntaxError); + } + // Reset the parser state. + Index = Source = null; + if (callback && getClass.call(callback) == "[object Function]") { + var value = {}; + value[""] = result; + return walk(getClass, forOwn, value, "", callback); + } + return result; + } + return parse; +} + +// Internal: A map of escaped control characters and their unescaped +// equivalents. +var Unescapes = { + 92: "\\", + 34: '"', + 47: "/", + 98: "\b", + 116: "\t", + 110: "\n", + 102: "\f", + 114: "\r" +}; + +// Internal: Stores the parser state. +var Index, Source; + +// Internal: Resets the parser state and throws a `newSyntaxError`. +function abort(newSyntaxError) { + Index = Source = null; + throw newSyntaxError(); +} + +// Internal: Returns the next token, or `"$"` if the parser has reached +// the end of the source string. A token may be a string, number, `null` +// literal, or Boolean literal. +function lex(charIndexBuggy, fromCharCode, newSyntaxError) { + var source = Source, length = source.length, value, begin, position, isSigned, charCode; + while (Index < length) { + charCode = source.charCodeAt(Index); + switch (charCode) { + case 9: case 10: case 13: case 32: + // Skip whitespace tokens, including tabs, carriage returns, line + // feeds, and space characters. + Index++; + break; + case 123: case 125: case 91: case 93: case 58: case 44: + // Parse a punctuator token (`{`, `}`, `[`, `]`, `:`, or `,`) at + // the current position. + value = charIndexBuggy ? source.charAt(Index) : source[Index]; + Index++; + return value; + case 34: + // `"` delimits a JSON string; advance to the next character and + // begin parsing the string. String tokens are prefixed with the + // sentinel `@` character to distinguish them from punctuators and + // end-of-string tokens. + for (value = "@", Index++; Index < length;) { + charCode = source.charCodeAt(Index); + if (charCode < 32) { + // Unescaped ASCII control characters (those with a code unit + // less than the space character) are not permitted. + abort(newSyntaxError); + } else if (charCode == 92) { + // A reverse solidus (`\`) marks the beginning of an escaped + // control character (including `"`, `\`, and `/`) or Unicode + // escape sequence. + charCode = source.charCodeAt(++Index); + switch (charCode) { + case 92: case 34: case 47: case 98: case 116: case 110: case 102: case 114: + // Revive escaped control characters. + value += Unescapes[charCode]; + Index++; + break; + case 117: + // `\u` marks the beginning of a Unicode escape sequence. + // Advance to the first character and validate the + // four-digit code point. + begin = ++Index; + for (position = Index + 4; Index < position; Index++) { + charCode = source.charCodeAt(Index); + // A valid sequence comprises four hexdigits (case- + // insensitive) that form a single hexadecimal value. + if (!(charCode >= 48 && charCode <= 57 || charCode >= 97 && charCode <= 102 || charCode >= 65 && charCode <= 70)) { + // Invalid Unicode escape sequence. + abort(newSyntaxError); + } + } + // Revive the escaped character. + value += fromCharCode("0x" + source.slice(begin, Index)); + break; + default: + // Invalid escape sequence. + abort(newSyntaxError); + } + } else { + if (charCode == 34) { + // An unescaped double-quote character marks the end of the + // string. + break; + } + charCode = source.charCodeAt(Index); + begin = Index; + // Optimize for the common case where a string is valid. + while (charCode >= 32 && charCode != 92 && charCode != 34) { + charCode = source.charCodeAt(++Index); + } + // Append the string as-is. + value += source.slice(begin, Index); + } + } + if (source.charCodeAt(Index) == 34) { + // Advance to the next character and return the revived string. + Index++; + return value; + } + // Unterminated string. + abort(newSyntaxError); + default: + // Parse numbers and literals. + begin = Index; + // Advance past the negative sign, if one is specified. + if (charCode == 45) { + isSigned = true; + charCode = source.charCodeAt(++Index); + } + // Parse an integer or floating-point value. + if (charCode >= 48 && charCode <= 57) { + // Leading zeroes are interpreted as octal literals. + if (charCode == 48 && ((charCode = source.charCodeAt(Index + 1)), charCode >= 48 && charCode <= 57)) { + // Illegal octal literal. + abort(newSyntaxError); + } + isSigned = false; + // Parse the integer component. + for (; Index < length && ((charCode = source.charCodeAt(Index)), charCode >= 48 && charCode <= 57); Index++); + // Floats cannot contain a leading decimal point; however, this + // case is already accounted for by the parser. + if (source.charCodeAt(Index) == 46) { + position = ++Index; + // Parse the decimal component. + for (; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++); + if (position == Index) { + // Illegal trailing decimal. + abort(newSyntaxError); + } + Index = position; + } + // Parse exponents. The `e` denoting the exponent is + // case-insensitive. + charCode = source.charCodeAt(Index); + if (charCode == 101 || charCode == 69) { + charCode = source.charCodeAt(++Index); + // Skip past the sign following the exponent, if one is + // specified. + if (charCode == 43 || charCode == 45) { + Index++; + } + // Parse the exponential component. + for (position = Index; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++); + if (position == Index) { + // Illegal empty exponent. + abort(newSyntaxError); + } + Index = position; + } + // Coerce the parsed value to a JavaScript number. + return +source.slice(begin, Index); + } + // A negative sign may only precede numbers. + if (isSigned) { + abort(newSyntaxError); + } + // `true`, `false`, and `null` literals. + var temp = source.slice(Index, Index + 4); + if (temp == "true") { + Index += 4; + return true; + } else if (temp == "fals" && source.charCodeAt(Index + 4 ) == 101) { + Index += 5; + return false; + } else if (temp == "null") { + Index += 4; + return null; + } + // Unrecognized token. + abort(newSyntaxError); + } + } + // Return the sentinel `$` character if the parser has reached the end + // of the source string. + return "$"; +} + +// Internal: Parses a JSON `value` token. +function get(charIndexBuggy, fromCharCode, newSyntaxError, value) { + var results, hasMembers; + if (value == "$") { + // Unexpected end of input. + abort(newSyntaxError); + } + if (typeof value == "string") { + if ((charIndexBuggy ? value.charAt(0) : value[0]) == "@") { + // Remove the sentinel `@` character. + return value.slice(1); + } + // Parse object and array literals. + if (value == "[") { + // Parses a JSON array, returning a new JavaScript array. + results = []; + for (;;) { + value = lex(charIndexBuggy, fromCharCode, newSyntaxError); + // A closing square bracket marks the end of the array literal. + if (value == "]") { + break; + } + // If the array literal contains elements, the current token + // should be a comma separating the previous element from the + // next. + if (hasMembers) { + if (value == ",") { + value = lex(charIndexBuggy, fromCharCode, newSyntaxError); + if (value == "]") { + // Unexpected trailing `,` in array literal. + abort(newSyntaxError); + } + } else { + // A `,` must separate each array element. + abort(newSyntaxError); + } + } else { + hasMembers = true; + } + // Elisions and leading commas are not permitted. + if (value == ",") { + abort(newSyntaxError); + } + results.push(get(charIndexBuggy, fromCharCode, newSyntaxError, value)); + } + return results; + } else if (value == "{") { + // Parses a JSON object, returning a new JavaScript object. + results = {}; + for (;;) { + value = lex(charIndexBuggy, fromCharCode, newSyntaxError); + // A closing curly brace marks the end of the object literal. + if (value == "}") { + break; + } + // If the object literal contains members, the current token + // should be a comma separator. + if (hasMembers) { + if (value == ",") { + value = lex(charIndexBuggy, fromCharCode, newSyntaxError); + if (value == "}") { + // Unexpected trailing `,` in object literal. + abort(newSyntaxError); + } + } else { + // A `,` must separate each object member. + abort(newSyntaxError); + } + } else { + hasMembers = true; + } + // Leading commas are not permitted, object property names must be + // double-quoted strings, and a `:` must separate each property + // name and value. + if (value == "," || typeof value != "string" || (charIndexBuggy ? value.charAt(0) : value[0]) != "@" || lex(charIndexBuggy, fromCharCode, newSyntaxError) != ":") { + abort(newSyntaxError); + } + var memberValue = lex(charIndexBuggy, fromCharCode, newSyntaxError); + results[value.slice(1)] = get(charIndexBuggy, fromCharCode, newSyntaxError, memberValue); + } + return results; + } + // Unexpected token encountered. + abort(newSyntaxError); + } + return value; +} + +// Internal: Updates a traversed object member. +function update(getClass, forOwn, source, property, callback) { + var element = walk(getClass, forOwn, source, property, callback); + if (element === undefined) { + delete source[property]; + } else { + source[property] = element; + } +} + +// Internal: Recursively traverses a parsed JSON object, invoking the +// `callback` function for each value. This is an implementation of the +// `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2. +function walk(getClass, forOwn, source, property, callback) { + var value = source[property], length; + if (typeof value == "object" && value) { + // `forOwn` can't be used to traverse an array in Opera <= 8.54 + // because its `Object#hasOwnProperty` implementation returns `false` + // for array indices (e.g., `![1, 2, 3].hasOwnProperty("0")`). + if (getClass.call(value) == "[object Array]") { + for (length = value.length; length--; update(getClass, forOwn, value, length, callback)); + } else { + forOwn(value, function (property) { + update(getClass, forOwn, value, property, callback); + }); + } + } + return callback.call(source, property, value); +} \ No newline at end of file diff --git a/lib/serializeDate.js b/lib/serializeDate.js new file mode 100644 index 0000000..c61e5cb --- /dev/null +++ b/lib/serializeDate.js @@ -0,0 +1,43 @@ +var toPaddedString = require("./toPaddedString"); + +// Internal: Serializes dates according to the `Date#toJSON` method specified +// in ES 5.1 section 15.9.5.44. See section 15.9.1.15 for the ISO 8601 date +// time string format. +function toISOString(v) { + var year = v.getUTCFullYear(); + var yearString = year <= 0 || year >= 1e4 ? + // Extended year: [+-]YYYYYY. + (year < 0 ? "-" : "+") + toPaddedString(6, year < 0 ? -year : year) : + // Four-digit year: YYYY. + toPaddedString(4, year); + return ( + yearString + + // Pad months, dates, hours, minutes, and seconds to two digits. + "-" + toPaddedString(2, v.getUTCMonth() + 1) + + "-" + toPaddedString(2, v.getUTCDate()) + + "T" + toPaddedString(2, v.getUTCHours()) + + ":" + toPaddedString(2, v.getUTCMinutes()) + + ":" + toPaddedString(2, v.getUTCSeconds()) + + // Pad milliseconds to three digits (optional in ES 5.0; required + // in ES 5.1). + "." + toPaddedString(3, v.getUTCMilliseconds()) + + "Z" + ); +} + +module.exports = createSerializeDate; +function createSerializeDate(UTCDate) { + // Internal: Serializes a date object. + function serializeDate(value) { + var epochTime = +value; + if (epochTime != epochTime || epochTime == -1 / 0 || epochTime == 1 / 0) { + // Handle `NaN`, `Infinity`, and `-Infinity`. + return null; + } + if (UTCDate) { + return toISOString(new UTCDate(value)); + } + return toISOString(value); + } + return serializeDate; +} diff --git a/lib/stringify.js b/lib/stringify.js new file mode 100644 index 0000000..4069428 --- /dev/null +++ b/lib/stringify.js @@ -0,0 +1,166 @@ +var attempt = require("./attempt"), + toPaddedString = require("./toPaddedString"); + +var undefined; + +module.exports = createStringify; +function createStringify(getClass, forOwn, newTypeError) { + // Public: `JSON.stringify` serializes a JavaScript `value` as a JSON string. + // The optional `filter` argument may specify either a function that alters + // how object and array members are serialized, or an array of strings and + // numbers that indicates which properties should be serialized. The optional + // `width` argument may be either a string or number that specifies the + // indentation level of the output. See ES 5.1 section 15.12.3. + function stringify(source, filter, width) { + var whitespace, callback, properties, className; + if (typeof filter == "function" || typeof filter == "object" && filter) { + className = getClass.call(filter); + if (className == "[object Function]") { + callback = filter; + } else if (className == "[object Array]") { + // Convert the property names array into a makeshift set. + properties = {}; + for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((className = getClass.call(value)), className == "[object String]" || className == "[object Number]") && (properties[value] = 1)); + } + } + if (width) { + className = getClass.call(width); + if (className == "[object Number]") { + // Convert the `width` to an integer and create a string containing + // `width` number of space characters. + if ((width -= width % 1) > 0) { + for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " "); + } + } else if (className == "[object String]") { + whitespace = width.length <= 10 ? width : width.slice(0, 10); + } + } + // Opera <= 7.54u2 discards the values associated with empty string keys + // (`""`) only if they are used directly within an object member list + // (e.g., `!("" in { "": 1})`). + return serialize(getClass, forOwn, newTypeError, "", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []); + } + return stringify; +} + +// Internal: A map of control characters and their escaped equivalents. +var Escapes = { + 92: "\\\\", + 34: '\\"', + 8: "\\b", + 12: "\\f", + 10: "\\n", + 13: "\\r", + 9: "\\t" +}; + +// Internal: Double-quotes a string `value`, replacing all ASCII control +// characters (characters with code unit values between 0 and 31) with +// their escaped equivalents. This is an implementation of the +// `Quote(value)` operation defined in ES 5.1 section 15.12.3. +var unicodePrefix = "\\u00"; +var escapeChar = function (character) { + var charCode = character.charCodeAt(0), escaped = Escapes[charCode]; + if (escaped) { + return escaped; + } + return unicodePrefix + toPaddedString(2, charCode.toString(16)); +}; +var reEscape = /[\x00-\x1f\x22\x5c]/g; +var quote = function (value) { + reEscape.lastIndex = 0; + return '"' + + ( + reEscape.test(value) + ? value.replace(reEscape, escapeChar) + : value + ) + + '"'; +}; + +// Internal: Recursively serializes an object. Implements the +// `Str(key, holder)`, `JO(value)`, and `JA(value)` operations. +function serialize(getClass, forOwn, newTypeError, property, object, callback, properties, whitespace, indentation, stack) { + var type, className, results, element, index, length, prefix, result; + var value = attempt(function () { + // Necessary for host object support. + return object[property]; + }); + if (typeof value == "object" && value && typeof value.toJSON == "function") { + value = value.toJSON(property); + } + if (callback) { + // If a replacement function was provided, call it to obtain the value + // for serialization. + value = callback.call(object, property, value); + } + // Exit early if value is `undefined` or `null`. + if (value == undefined) { + return value === undefined ? value : "null"; + } + type = typeof value; + // Only call `getClass` if the value is an object. + if (type == "object") { + className = getClass.call(value); + } + switch (className || type) { + case "boolean": + case "[object Boolean]": + // Booleans are represented literally. + return "" + value; + case "number": + case "[object Number]": + // JSON numbers must be finite. `Infinity` and `NaN` are serialized as + // `"null"`. + return value > -1 / 0 && value < 1 / 0 ? "" + value : "null"; + case "string": + case "[object String]": + // Strings are double-quoted and escaped. + return quote("" + value); + } + // Recursively serialize objects and arrays. + if (typeof value == "object") { + // Check for cyclic structures. This is a linear search; performance + // is inversely proportional to the number of unique nested objects. + for (length = stack.length; length--;) { + if (stack[length] === value) { + // Cyclic structures cannot be serialized by `JSON.stringify`. + throw newTypeError(); + } + } + // Add the object to the stack of traversed objects. + stack.push(value); + results = []; + // Save the current indentation level and indent one additional level. + prefix = indentation; + indentation += whitespace; + if (className == "[object Array]") { + // Recursively serialize array elements. + for (index = 0, length = value.length; index < length; index++) { + element = serialize(getClass, forOwn, newTypeError, index, value, callback, properties, whitespace, indentation, stack); + results.push(element === undefined ? "null" : element); + } + result = results.length ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]"; + } else { + // Recursively serialize object members. Members are selected from + // either a user-specified list of property names, or the object + // itself. + forOwn(properties || value, function (property) { + var element = serialize(getClass, forOwn, newTypeError, property, value, callback, properties, whitespace, indentation, stack); + if (element !== undefined) { + // According to ES 5.1 section 15.12.3: "If `gap` {whitespace} + // is not the empty string, let `member` {quote(property) + ":"} + // be the concatenation of `member` and the `space` character." + // The "`space` character" refers to the literal space + // character, not the `space` {width} argument provided to + // `JSON.stringify`. + results.push(quote(property) + ":" + (whitespace ? " " : "") + element); + } + }); + result = results.length ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}"; + } + // Remove the object from the traversed object stack. + stack.pop(); + return result; + } +} diff --git a/lib/toPaddedString.js b/lib/toPaddedString.js new file mode 100644 index 0000000..09c9230 --- /dev/null +++ b/lib/toPaddedString.js @@ -0,0 +1,8 @@ +// Internal: Converts `value` into a zero-padded string such that its +// length is at least equal to `width`. The `width` must be <= 6. +var leadingZeroes = "000000"; +module.exports = function (width, value) { + // The `|| 0` expression is necessary to work around a bug in + // Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`. + return (leadingZeroes + (value || 0)).slice(-width); +}; From 7e908642bac1604821ddb804c2682db1b3c7ada8 Mon Sep 17 00:00:00 2001 From: Kit Cambridge Date: Sat, 7 Mar 2015 22:24:56 -0800 Subject: [PATCH 02/11] Use webpack for modular builds. Closes #75. * Add `makeRunInContext`. * Modularize `build.js`. * Use Bower to download the Closure Compiler. --- .gitignore | 2 +- .jshintrc | 12 +- CONTRIBUTING.md | 8 +- README.md | 2 + bower.json | 5 +- build.js | 795 ++++-------------- build/assets/footer.js | 38 + build/assets/header.js | 21 + build/assets/index.js | 7 + {page => build/assets}/page.html | 4 +- build/genPage.js | 97 +++ build/minify.js | 82 ++ build/pp.js | 148 ++++ build/renderer.js | 69 ++ build/wrapper.js | 38 + lib/json3.js | 1356 ++++++++++++++++++++++++------ lib/json3.min.js | 20 + package.json | 4 +- {lib => src}/attempt.js | 0 {lib => src}/date.js | 0 {lib => src}/forOwn.js | 0 {lib => src}/hasDontEnumBug.js | 0 {lib => src}/parse.js | 0 src/runInContext.js | 235 ++++++ {lib => src}/serializeDate.js | 0 {lib => src}/stringify.js | 0 {lib => src}/toPaddedString.js | 0 27 files changed, 2018 insertions(+), 925 deletions(-) create mode 100644 build/assets/footer.js create mode 100644 build/assets/header.js create mode 100644 build/assets/index.js rename {page => build/assets}/page.html (85%) create mode 100644 build/genPage.js create mode 100644 build/minify.js create mode 100644 build/pp.js create mode 100644 build/renderer.js create mode 100644 build/wrapper.js create mode 100644 lib/json3.min.js rename {lib => src}/attempt.js (100%) rename {lib => src}/date.js (100%) rename {lib => src}/forOwn.js (100%) rename {lib => src}/hasDontEnumBug.js (100%) rename {lib => src}/parse.js (100%) create mode 100644 src/runInContext.js rename {lib => src}/serializeDate.js (100%) rename {lib => src}/stringify.js (100%) rename {lib => src}/toPaddedString.js (100%) diff --git a/.gitignore b/.gitignore index 66be20f..0173852 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ +bower_components node_modules coverage -vendor/closure-compiler* diff --git a/.jshintrc b/.jshintrc index 96bf7e7..ffcbf50 100644 --- a/.jshintrc +++ b/.jshintrc @@ -63,5 +63,13 @@ "-W053": true, "-W086": true, - "predef": ["define", "environment", "Packages", "Benchmark", "_", "phantom"] -} \ No newline at end of file + "globals": { + "define": false, + "environment": false, + "Packages": false, + "phantom": false, + + "Benchmark": true, + "_": true + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0975f8b..963fa07 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Please use the [GitHub issue tracker](https://github.com/bestiejs/json3/issues) ## Pull Requests ## -If you'd like to contribute a feature or bug fix, you can [fork](http://help.github.com/fork-a-repo/) JSON 3, commit your changes, and [send a pull request](http://help.github.com/send-pull-requests/) **against the `dev` branch**. Please make sure to update the unit tests in the `test` directory as well. +If you'd like to contribute a feature or bug fix, you can [fork](http://help.github.com/fork-a-repo/) JSON 3, commit your changes, and [send a pull request](http://help.github.com/send-pull-requests/) **against the `master` branch**. Please make sure to update the unit tests in the `test` directory as well. **Please do not send pull requests against `gh-pages`**; this branch is reserved for releases, builder updates, and project page changes. @@ -86,8 +86,8 @@ To add a test: The builder (`build.js`) is a Node script that: -* Regenerates the [GitHub project page](https://bestiejs.github.io/json3/) from `README.md`... -* Downloads the [Closure Compiler](https://developers.google.com/closure/compiler/) if it's not already on your system, and... -* Minifies `lib/json3.js`. +* Regenerates the [GitHub project pages](https://bestiejs.github.io/json3/)... +* Builds `lib/json3.js` using [webpack](https://webpack.github.io/), and... +* Minifies `lib/json3.js` using the [Closure Compiler](https://developers.google.com/closure/compiler/). You don't need to run the builder before submitting your pull request; we'll do this before each release. diff --git a/README.md b/README.md index 46fd060..3a68cf1 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,8 @@ Check out a working copy of the JSON 3 source code with [Git](http://git-scm.com $ git clone git://github.com/bestiejs/json3.git $ cd json3 + $ npm install + $ bower install We ♥ bug reports, suggestions, questions, and pull requests! Please see our [contribution guidelines](https://bestiejs.github.io/json3/contribute.html) if you'd like to contribute. diff --git a/bower.json b/bower.json index 7215dad..d555a10 100644 --- a/bower.json +++ b/bower.json @@ -34,5 +34,8 @@ "authors": [ "Kit Cambridge " ], - "license": "MIT" + "license": "MIT", + "devDependencies": { + "closure-compiler": "https://dl.google.com/closure-compiler/compiler-latest.tar.gz" + } } diff --git a/build.js b/build.js index 53ce94d..3858be5 100755 --- a/build.js +++ b/build.js @@ -1,670 +1,191 @@ #!/usr/bin/env node /* JSON 3 Builder | https://bestiejs.github.io/json3 */ -var fs = require("fs"), - https = require("https"), - path = require("path"), - spawn = require("child_process").spawn, - url = require("url"), - util = require("util"), - zlib = require("zlib"); +var fs = require("fs"); +var path = require("path"); +var zlib = require("zlib"); -var vendorPath = path.join(__dirname, "vendor"), - highlightAuto = require("highlight.js").highlightAuto, - marked = require("marked"), - package = require(path.join(__dirname, "package.json")), - tar = require("tar"); +var async = require("async"); +var webpack = require("webpack"); -// The path to the Closure Compiler `.jar` file and ETag. -var closurePath = path.join(vendorPath, "closure-compiler.jar"), - closureETag = path.join(vendorPath, "closure-compiler-etag.txt"); - -// The path to the project page template. -var pageTemplatePath = path.join(path.join(__dirname, "page", "page.html")); - -var indexPageSrc = path.join(__dirname, "README.md"), - contribPageSrc = path.join(__dirname, "CONTRIBUTING.md"), - relPageSrc = path.join(__dirname, "CHANGELOG.md"); - -var indexPageDest = path.join(__dirname, "index.html"), - contribPageDest = path.join(__dirname, "contribute.html"), - relPageDest = path.join(__dirname, "changes.html"); - -// The Closure Compiler options: enable advanced optimizations and suppress all -// warnings apart from syntax and optimization errors. -var closureOptions = ["--compilation_level=ADVANCED_OPTIMIZATIONS", "--warning_level=QUIET"]; - -// A RegExp used to detect the `define` pragma used by asynchronous module -// loaders. -var definePattern = RegExp('(?:' + - // `typeof define == "function"`. Matches `==` and `===`; `'` and `"`. - 'typeof\\s+define\\s*===?\\s*([\'"])function\\1|' + - // `"function" == typeof define`. Same rules as above. - '([\'"])function\\2\\s*===?\\s*typeof\\s+define' + -')' + -// `&&`. -'\\s*&&\\s*(?:' + - // `!!` (optional Boolean coercion) - '(?:!!)?' + - // `define`. - 'define\\s*(?:' + - // `.amd`. - '\\.\\s*amd|' + - // `["amd"]` | `['amd']`. - '\\[\\s*([\'"])amd\\3\\s*\\]' + - ')|' + - '(?:' + - '(?:' + - // `typeof define.amd`. - 'typeof\\s+define\\.\\s*amd|' + - // `typeof define["amd"]` or `typeof define['amd']`. - 'typeof\\s+define\\[\\s*([\'"])amd\\4\\s*\\]' + - ')' + - // `=== "object"`. Same rules for quotes and equality operators. - '\\s*===?\\s*([\'"])object\\5' + - '|' + - // `"object" ===`. - '([\'"])object\\6\\s*===?\\s*' + - '(?:' + - 'typeof\\s+define\\.\\s*amd|' + - 'typeof\\s+define\\[\\s*([\'"])amd\\7\\s*\\]' + - ')' + - ')' + - '(?:' + - // `&&` (optional Boolean test for `define.amd`). - '\\s*&&\\s*' + - '(?:' + - // `!!` (optional Boolean coercion) - '(?:!!)?' + - // `define.amd`. - 'define\\.\\s*amd|' + - // `define["amd"] | define['amd']`. - 'define\\[\\s*([\'"])amd\\8\\s*\\]' + - ')' + - ')?' + -')', 'g'); - -// List of properties to prevent the Closure Compiler from minifying. -var propertyWhitelist = [ - 'Date', - 'JSON', - 'JSON3', - 'Math', - 'Number', - 'Object', - 'String', - 'SyntaxError', - 'TypeError', - 'global', - 'parse', - 'runInContext', - 'self', - 'stringify', - 'window' -]; - -function PageRenderer(options) { - marked.Renderer.call(this); - if (options) { - this.issueFormatters = options.issueFormatters; - this.imgClasses = options.imgClasses; - this.linkClasses = options.linkClasses; - } -} - -util.inherits(PageRenderer, marked.Renderer); - -PageRenderer.issuePattern = /(\s*|[\[])\s*(issue|pr)\s*#(\d+)([;\]]|\s*)/gi; - -PageRenderer.prototype.issueFormatters = null; -PageRenderer.prototype.imgClasses = null; -PageRenderer.prototype.linkClasses = null; - -PageRenderer.prototype.listitem = function listitem(text) { - var self = this; - if (this.issueFormatters) { - text = text.replace(PageRenderer.issuePattern, function linkIssue(match, pre, type, id, post) { - var getURI = self.issueFormatters[type.toLowerCase()]; - if (!getURI) { - return match; - } - return pre + util.format('#%s', - getURI(id), id) + post; - }); - } - return marked.Renderer.prototype.listitem.call(this, text); -}; - -PageRenderer.prototype.image = function image(src, title, alt) { - var attrs = [util.format('src="%s" alt="%s"', src, alt)]; - if (title) { - attrs.push(util.format('title="%s"', title)); - } - if (this.imgClasses) { - var className = this.imgClasses[path.basename(src)]; - if (className) { - attrs.push(util.format('class="%s"', className)); - } - } - return util.format('', attrs.join(" ")); -}; - -PageRenderer.prototype.link = function link(href, title, text) { - var uri = url.parse(href); - if (this.options && this.options.sanitize && uri.protocol == "javascript") { - return ''; - } - var attrs = [util.format('href="%s"', href)]; - if (title) { - attrs.push(util.format('title="%s"', title)); - } - if (this.linkClasses) { - var className = this.linkClasses[uri.hostname]; - if (className) { - attrs.push(util.format('class="%s"', className)); - } - } - return util.format("%s", attrs.join(" "), text); -}; - -marked.setOptions({ - "smartypants": true, - "highlight": function highlight(source) { - return highlightAuto(source).value; - } -}); - -main(); +var genPage = require("./build/genPage"); +var minify = require("./build/minify"); +var package = require("./package.json"); +var PageRenderer = require("./build/renderer"); +var pp = require("./build/pp"); +var WrapperPlugin = require("./build/wrapper"); function main() { - genIndex(); - genContrib(); - genRel(); - minify(); -} - -function genIndex() { - var r = new PageRenderer({ - imgClasses: { "logo.png": "logo" }, - linkClasses: { "travis-ci.org": "travis-ci" } - }); - genPage("JSON 3", indexPageDest, indexPageSrc, - { "renderer": r }, function afterGen(err) { - console.log(err || "GitHub project page generated successfully."); - }); -} - -function genContrib() { - var r = new PageRenderer(); - genPage("Contribute | JSON 3", contribPageDest, contribPageSrc, - { "renderer": r }, function afterGen(err) { - - console.log(err || "Contributing page generated successfully."); - }); -} - -function genRel() { - var r = new PageRenderer({ - issueFormatters: { - issue: function formatIssue(id) { - return "https://github.com/bestiejs/json3/issues/" + id; - }, - pr: function formatPR(id) { - return "https://github.com/bestiejs/json3/pull/" + id; - } - } - }); - genPage("Releases | JSON 3", relPageDest, relPageSrc, - {"renderer": r }, function afterGen(err) { - - console.log(err || "Change log generated successfully."); - }); -} - -// Compress JSON 3 using the Closure Compiler. -function minify() { - getCompiler(function hasCompiler(error) { - if (error) { - console.log(error); - return; - } - - fs.readFile(path.join(__dirname, "lib", "json3.js"), "utf8", function readSource(error, source) { - if (error) { - console.log(error); - return; - } - - console.log("Development version size: %d KB.", Math.round(Buffer.byteLength(source) / 1024 * 100) / 100); - // Shell out to the Closure Compiler. Requires Java 7 or higher. - var error = [], errorLength = 0; - var output = [], outputLength = 0; - var compiler = spawn("java", ["-jar", closurePath].concat(closureOptions)); - compiler.stdout.on("data", function onData(data) { - // Append the data to the output stream. - output.push(data); - outputLength += data.length; - }); - compiler.stderr.on("data", function onError(data) { - // Append the error message to the error stream. - error.push(data); - errorLength += data.length; - }); - compiler.on("exit", function onExit(status) { - var exception; - // `status` specifies the process exit code. - if (status) { - exception = new Error(Buffer.concat(error, errorLength)); - exception.status = status; - } - compressSource(exception, '' + Buffer.concat(output, outputLength)); - }); - // Proxy the preprocessed source to the Closure Compiler. - compiler.stdin.end(preprocessSource(source)); - - // Post-processes the compressed source and writes the result to disk. - function compressSource(exception, compressed) { - if (exception) { - console.log(exception); - } else { - // Extract the JSON 3 header and clean up the minified source. - compressed = extractComments(source)[0] + '\n' + postprocessSource(compressed); - // Write the compressed version to disk. - fs.writeFile(path.join(__dirname, "lib", "json3.min.js"), compressed, writeSource); + async.auto({ + pages: function pages(callback) { + writePages([{ + "title": "JSON 3", + "src": path.join(__dirname, "README.md"), + "dest": path.join(__dirname, "index.html"), + "markedOptions": { + "renderer": new PageRenderer({ + "imgClasses": { "logo.png": "logo" }, + "linkClasses": { "travis-ci.org": "travis-ci" } + }) } - // Checks the `gzip`-ped size of the compressed version. - function writeSource(exception) { - console.log(exception || "Compressed version generated successfully."); - zlib.gzip(compressed, function (exception, results) { - console.log("Compressed version size: %d KB.", Math.round(results.length / 1024 * 100) / 100); - }); + }, { + "title": "Contribute | JSON 3", + "src": path.join(__dirname, "CONTRIBUTING.md"), + "dest": path.join(__dirname, "contribute.html"), + "markedOptions": { + "renderer": new PageRenderer() } - } - }); - }); -} - -// Internal: Extracts line and block comments from a JavaScript `source` -// string. Returns an array containing the comments. -function extractComments(source) { - var index = 0, length = source.length, results = [], symbol, position, original; - while (index < length) { - symbol = source[index]; - switch (symbol) { - // Parse line and block comments. - case "/": - original = symbol; - symbol = source[++index]; - switch (symbol) { - // Extract line comments. - case "/": - position = source.indexOf("\n", index); - if (position < 0) { - // Check for CR line endings. - position = source.indexOf("\r", index); - } - results.push(original + source.slice(index, index = position < 0 ? length : position)); - break; - // Extract block comments. - case "*": - position = source.indexOf("*/", index); - if (position < 0) { - throw SyntaxError("Unterminated block comment."); + }, { + "title": "Releases | JSON 3", + "src": path.join(__dirname, "CHANGELOG.md"), + "dest": path.join(__dirname, "changes.html"), + "markedOptions": { + "renderer": new PageRenderer({ + "issueFormatters": { + "issue": function formatIssue(id) { + return "https://github.com/bestiejs/json3/issues/" + id; + }, + "pr": function formatPR(id) { + return "https://github.com/bestiejs/json3/pull/" + id; + } } - // Advance past the end of the comment. - results.push(original + source.slice(index, index = position += 2)); - break; - default: - index++; + }) } - break; - // Parse strings separately to ensure that any JavaScript comments within - // them are preserved. - case '"': - case "'": - for (position = index, original = symbol; index < length;) { - symbol = source[++index]; - if (symbol == "\\") { - // Skip past escaped characters. - index++; - } else if ("\n\r\u2028\u2029".indexOf(symbol) > -1) { - // According to the ES 5.1 spec, strings may not contain unescaped - // line terminators. - throw SyntaxError("Illegal line continuation."); - } else if (symbol == original) { - break; - } + }], callback); + }, + + build: function build(callback) { + var compiler = webpack({ + context: path.join(__dirname, "src"), + entry: { + json3: "./runInContext.js", + }, + output: { + path: path.join(__dirname, "lib"), + filename: "[name].js" + }, + module: { + loaders: [] + }, + resolve: { + modulesDirectories: [] + }, + bail: true, + node: { + console: false, + global: false, + process: false, + Buffer: false, + __filename: false, + __dirname: false } - if (source[index] == original) { - index++; - break; + }); + compiler.plugin("compilation", function gotCompilation(compilation) { + compilation.apply(new WrapperPlugin(package.version)); + }); + compiler.run(function afterRun(err, stats) { + if (err) { + console.error("Error building JSON 3: %s", err); + callback(err); } - throw SyntaxError("Unterminated string."); - default: - // Advance to the next character. - index++; - } - } - return results; -} - -function preprocessSource(source) { - source = source.replace(definePattern, 'typeof define === "function" && define["amd"]'); - // Add brackets to whitelisted properties so the Closure Compiler won't minify them. - // https://developers.google.com/closure/compiler/docs/api-tutorial3#export - return source.replace(RegExp('(["\'])(?:(?!\\1)[^\\n\\\\]|\\\\.)*\\1|\\.(' + propertyWhitelist.join('|') + ')\\b', 'g'), function(match, quote, prop) { - return quote ? match : "['" + prop + "']"; - }); -} - -function postprocessSource(source) { - // Shift variables in the global scope into the IIFE and fix the - // `define` pragma. - var result = source.replace(/^(var [^;]*;)\s*(\(function\([^)]*\)\{)/m, '\n;$2$1'); - return result.replace(definePattern, 'typeof define==="function"&&define.amd'); -} - -// Internal: Lazily downloads the Closure Compiler. -function getCompiler(callback) { - var hasCompiler = false, - isFinalized = false, - eTag; - - var prePending = 2, - preDone = false; - - var postPending = 1, - postDone = false; - - // Step one: determine if the Closure Compiler has already been downloaded. - fs.stat(closurePath, function getStats(error, stats) { - if (error) { - if (error.code != "ENOENT") { - return updatePre(error); - } - return updatePre(); - } - if (!stats.isFile()) { - return updatePre(new Error(util.format("`%s` must be a file.", closurePath))); - } - hasCompiler = true; - console.log('The Closure Compiler has already been downloaded.'); - updatePre(); - }); - - // Step two: Retrieve the locally-cached `ETag` from the previous download. - fs.readFile(closureETag, "utf8", function readETag(error, source) { - if (error) { - if (error.code != "ENOENT") { - return updatePre(error); - } - return updatePre(); - } - eTag = source; - updatePre(); - }); - - // Step three: download the Closure Compiler. - function download() { - var headers = { - "user-agent": util.format("JSON/%s", package.version) - }; - if (eTag && hasCompiler) { - headers["if-none-match"] = eTag; - } - var request = https.request({ - "hostname": "dl.google.com", - "port": 443, - "path": "/closure-compiler/compiler-latest.tar.gz", - "headers": headers, - "agent": false // Disable keep-alive. - }); - - request.on("response", onResponse); - function onResponse(response) { - request.removeListener("response", onResponse); - request.removeListener("error", onError); - - if (response.statusCode == 304) { - // If the cached `ETag` matches that of the entity, there is no - // need to download the Compiler tarball or extract the `.jar`. - // Skip all post-processing steps. - console.log('No updates available.'); - return finalize(); - } - - // Step four: write the new `ETag` to disk. - var eTag = response.headers.etag; - saveETag(eTag); - - // Step five: extract the Compiler `.jar` from the downloaded - // tarball using `node-tar`'s streaming `.tar` parser. - var parser = new tar.Parse(); - - parser.on("entry", onEntry); - function onEntry(entry) { - if (path.basename(entry.path) != "compiler.jar") { + console.log(stats.toString()); + callback(); + }); + }, + + compress: ["build", function compress(callback) { + var srcPath = path.join(__dirname, "lib", "json3.js"); + var destPath = path.join(__dirname, "lib", "json3.min.js"); + writeMinified(srcPath, destPath, function afterMinify(err, fileSizes) { + if (err) { + console.error("Error compressing JSON 3: %s", err); return; } - parser.removeListener("entry", onEntry); - - // Step six: write the Compiler to disk. - console.log('Extracting the Closure Compiler...'); - var writeStream = fs.createWriteStream(closurePath); - - writeStream.on("close", onClose); - function onClose() { - writeStream.removeListener("close", onClose); - writeStream.removeListener("error", onError); - // The Compiler has been successfully downloaded. - hasCompiler = true; - updatePost(); - } - - writeStream.on("error", onError); - function onError(error) { - writeStream.removeListener("close", onClose); - writeStream.removeListener("error", onError); - updatePost(error); - } - - entry.pipe(writeStream); - } - - parser.on("error", onError); - function onError(error) { - parser.removeListener("entry", onEntry); - parser.removeListener("error", onError); - parser.removeListener("end", onEnd); - updatePost(error); - } - - // Clean up attached event handlers. - parser.on("end", onEnd); - function onEnd() { - parser.removeListener("entry", onEntry); - parser.removeListener("error", onError); - parser.removeListener("end", onEnd); - } - - // Begin downloading the Compiler tarball. - console.log('Downloading the Closure Compiler...'); - response.pipe(zlib.createGunzip()).pipe(parser); - } - - request.on("error", onError); - function onError(error) { - request.removeListener("response", onResponse); - request.removeListener("error", onError); - // Connection errors are not fatal, as it is possible to use the - // previously-downloaded copy of Closure Compiler. If a cached copy - // is not available, the `hasCompiler` flag will be set to `false`, - // and `finalize` will yield an error to the `callback`. - console.log('A new version of the Closure Compiler could not be downloaded.'); - updatePost(); - } - - console.log('Updating the Closure Compiler...'); - request.end(); - } - - function finalize(error) { - if (isFinalized) { - return; - } - isFinalized = true; - if (error) { - return callback(error); - } - if (!hasCompiler) { - return callback(new Error("The Closure Compiler is required to build JSON 3.")); - } - callback(); - } - - function saveETag(eTag) { - postPending++; - fs.writeFile(closureETag, eTag, function writeETag(error) { - if (error) { - // This error is not fatal. If the write fails, the Compiler will be - // downloaded again when `getCompiler` is called. - console.log("The Closure Compiler `ETag` could not be written to disk."); - } - updatePost(); - }); - } - - function updatePre(error) { - if (preDone) { - return; - } - if (error) { - preDone = true; - return finalize(error); - } - prePending--; - if (!prePending) { - preDone = true; - download(); - } - } - - function updatePost(error) { - if (postDone) { + console.log("Development size: %d KB; compressed size: %d KB", + fileSizes.src, fileSizes.dest); + }); + }] + }, function afterBuild(err) { + if (err) { + process.exit(1); return; } - if (error) { - postDone = true; - return finalize(error); - } - postPending--; - if (!postPending) { - postDone = true; - finalize(); - } - } -} - -// Generates the navigation section for a Markdown document. -// Ported from `mdtoc.rb` by Sam Stephenson. -function genNav(source) { - var headers = []; - var lines = source.split(/\r?\n/); - // First pass: Scan the Markdown source looking for titles of the format: - // `### Title ###`. Record the line number, header level (number of - // octothorpes), and text of each matching title. - lines.forEach(function (line, index) { - var match = /^(\#{1,6})\s+(.+?)\s+\1$/.exec(line); - if (match) { - headers.push([index, match[1].length, match[2]]); - } - }); - // Second pass: Iterate over all matched titles and compute their - // corresponding section numbers. Then replace the titles with annotated - // anchors. - var lastSection, lastLevel; - headers.forEach(function (value) { - var index = value[0], level = value[1], text = value[2], section, length; - if (lastSection) { - // Clone the last section metadata array. - section = lastSection.slice(0); - if (lastLevel < level) { - section.push(1); - } else { - length = lastLevel - level; - while (length--) { - section.pop(); - } - section[section.length - 1] += 1; - } - } else { - section = [1]; - } - lines[index] = Array(level + 1).join("#") + "" + text; - value.push(section); - lastSection = section; - lastLevel = level; - }); - // Third pass: Iterate over matched titles once more to produce the table of - // contents. - var navigation = headers.map(function (value) { - var index = value[0], level = value[1], text = value[2], section = value[3], name = section.join("."); - return "
  • " + text + "
  • "; + process.exit(0); }); - navigation.push(""); - return { - "navigation": navigation, - "lines": lines - }; } -function buildPage(title, template, src, options) { - var parts = genNav(src); - return template.replace(/<%=\s*(.+?)\s*%>/g, function interpolate(match, section) { - if (section == "title") { - return title; - } - if (section == "navigation") { - // Insert the table of contents directly into the template. - return parts.navigation.join("\n"); - } - if (section == "source") { - // Convert the page to HTML and insert it into the body. - return marked(parts.lines.join("\n"), options); +function writePages(pages, callback) { + async.each(pages, function eachPage(page, callback) { + genPage(page.title, page.dest, page.src, page.markedOptions, afterGen); + function afterGen(err) { + if (err) { + console.log("Error generating project page %j: %s", page.title, err); + callback(err); + return; + } + console.log("Project page %j generated successfully.", page.title); + callback(); } - return ""; }); } -// Generates a GitHub project page using the template. -function genPage(title, destPath, srcPath, options, callback) { - var pendingReads = 2; - var contents = { "src": null, "tmpl": null }; - function afterRead(err, content, prop) { - if (pendingReads === 0) { - return; - } +// Compress JSON 3 using the Closure Compiler. +function writeMinified(srcPath, destPath, callback) { + var fileSizes = { + "src": 0, + "dest": 0 + }; + var header, minSrc; + fs.readFile(srcPath, afterRead); + function afterRead(err, srcBytes) { if (err) { - pendingReads = 0; callback(err); return; } - pendingReads--; - contents[prop] = content; - if (pendingReads > 0) { + fileSizes.src = getKilobytes(srcBytes.length); + var src = srcBytes.toString("utf8"); + // Extract the JSON 3 header. + header = pp.extractComments(src)[0]; + minify(pp.preprocessSource(src), afterMinify); + } + function afterMinify(err, minSrcBytes) { + if (err) { + callback(err); return; } - // Interpolate the page template. - var page = buildPage(title, contents.tmpl, contents.src, options); - // Write the page to disk. - fs.writeFile(destPath, page, callback); + // Post-process the compressed source. + minSrc = new Buffer([ + header, + pp.postprocessSource(minSrcBytes.toString("utf8")) + ].join("\n")); + async.each([ + // Write the compressed version to disk. + function write(callback) { + fs.writeFile(destPath, minSrc, callback); + }, + // Calculate the `gzip`-ped size of the compressed version. + function gzipSize(callback) { + zlib.gzip(minSrc, function afterGzip(err, results) { + if (!err) { + fileSizes.dest = getKilobytes(results.length); + } + callback(); + }); + } + ], function eachFunc(func, callback) { + func(callback); + }, function afterFinish(err) { + if (err) { + callback(err); + return; + } + callback(null, fileSizes); + }); } - fs.readFile(srcPath, "utf8", function afterReadSrc(err, src) { - afterRead(err, src, "src"); - }); - fs.readFile(pageTemplatePath, "utf8", - function afterReadTemplate(err, template) { +} - afterRead(err, template, "tmpl"); - }); +function getKilobytes(byteLength) { + return Math.round(byteLength / 1024 * 100) / 100; } + +main(); diff --git a/build/assets/footer.js b/build/assets/footer.js new file mode 100644 index 0000000..84fa886 --- /dev/null +++ b/build/assets/footer.js @@ -0,0 +1,38 @@ + )(root); + + if (freeExports && !isLoader) { + // Export for CommonJS environments. + runInContext(root, freeExports); + } else { + // Export for web browsers and JavaScript engines. + var nativeJSON = root.JSON, + previousJSON = root.JSON3, + isRestored = false; + + var JSON3 = runInContext(root, (root.JSON3 = { + // Public: Restores the original value of the global `JSON` object and + // returns a reference to the `JSON3` object. + "noConflict": function () { + if (!isRestored) { + isRestored = true; + root.JSON = nativeJSON; + root.JSON3 = previousJSON; + nativeJSON = previousJSON = null; + } + return JSON3; + } + })); + + root.JSON = { + "parse": JSON3.parse, + "stringify": JSON3.stringify + }; + } + + // Export for asynchronous module loaders. + if (isLoader) { + define(function () { + return JSON3; + }); + } +}).call(this); diff --git a/build/assets/header.js b/build/assets/header.js new file mode 100644 index 0000000..f99a2b7 --- /dev/null +++ b/build/assets/header.js @@ -0,0 +1,21 @@ +/*! JSON v3.3.2 | https://bestiejs.github.io/json3 | Copyright 2012-2015, Kit Cambridge, Benjamin Tan | http://kit.mit-license.org */ +;(function () { + // Detect the `define` function exposed by asynchronous module loaders. The + // strict `define` check is necessary for compatibility with `r.js`. + var isLoader = typeof define === "function" && define.amd; + + // Detect the `exports` object exposed by CommonJS implementations. + var freeExports = typeof exports == "object" && exports && !exports.nodeType && exports; + + // Use the `global` object exposed by Node (including Browserify via + // `insert-module-globals`), Narwhal, and Ringo as the default context, + // and the `window` object in browsers. Rhino exports a `global` function + // instead. + var root = typeof window == "object" && window || this, + freeGlobal = freeExports && typeof module == "object" && module && !module.nodeType && typeof global == "object" && global; + + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { + root = freeGlobal; + } + + var runInContext = ( diff --git a/build/assets/index.js b/build/assets/index.js new file mode 100644 index 0000000..5bfeea5 --- /dev/null +++ b/build/assets/index.js @@ -0,0 +1,7 @@ +var fs = require("fs"); + +module.exports = { + "header": fs.readFileSync(__dirname + "/header.js", "utf8"), + "footer": fs.readFileSync(__dirname + "/footer.js", "utf8"), + "page": fs.readFileSync(__dirname + "/page.html", "utf8") +}; diff --git a/page/page.html b/build/assets/page.html similarity index 85% rename from page/page.html rename to build/assets/page.html index dfb89f2..f32e9ff 100644 --- a/page/page.html +++ b/build/assets/page.html @@ -8,10 +8,10 @@
    - <%= source %> + <%= marked(lines.join("\n"), markedOptions) %>