Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Moved Geddy/Jake shared utilities into a separate repo.

  • Loading branch information...
commit 7f376a5379b31a224646ae1f4afdbf969aa4a1ff 1 parent c8bca75
mde authored
View
13 Jakefile
@@ -0,0 +1,13 @@
+
+var t = new jake.TestTask('Jake', function () {
+ this.testFiles.include('test/*.js');
+});
+
+var p = new jake.NpmPublishTask('jake', [
+ 'Jakefile'
+, 'README.md'
+, 'package.json'
+, 'lib/**'
+, 'test/**'
+]);
+
View
256 deps/inflection.js
@@ -0,0 +1,256 @@
+/**
+
+Copyright (c) 2010 George Moschovitis, http://www.gmosx.com
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
+CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+ * A port of the Rails/ActiveSupport Inflector class
+ * http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html
+ */
+
+var inflections = exports.inflections = {
+ plurals: [],
+ singulars: [],
+ uncountables: [],
+ humans: []
+};
+
+var PLURALS = inflections.plurals,
+ SINGULARS = inflections.singulars,
+ UNCOUNTABLES = inflections.uncountables,
+ HUMANS = inflections.humans;
+
+/**
+ * Specifies a new pluralization rule and its replacement. The rule can either
+ * be a string or a regular expression. The replacement should always be a
+ * string that may include references to the matched data from the rule.
+ */
+var plural = function (rule, replacement) {
+ //inflections.uncountables.delete(rule) if rule.is_a?(String)
+ //inflections.uncountables.delete(replacement)
+ inflections.plurals.unshift([rule, replacement]);
+}
+
+/**
+ * Specifies a new singularization rule and its replacement. The rule can either
+ * be a string or a regular expression. The replacement should always be a
+ * string that may include references to the matched data from the rule.
+ */
+var singular = function (rule, replacement) {
+ //inflections.uncountables.delete(rule) if rule.is_a?(String)
+ //inflections.uncountables.delete(replacement)
+ inflections.singulars.unshift([rule, replacement]);
+}
+
+/**
+ * Add uncountable words that shouldn't be attempted inflected.
+ */
+var uncountable = function (word) {
+ inflections.uncountables[word] = true;
+}
+
+/**
+ * Specifies a new irregular that applies to both pluralization and
+ * singularization at the same time. This can only be used for strings, not
+ * regular expressions. You simply pass the irregular in singular and plural
+ * form.
+ *
+ * Examples:
+ * irregular("octopus", "octopi");
+ * irregular("person", "people");
+ */
+var irregular = function (s, p) {
+ //inflections.uncountables.delete(singular);
+ //inflections.uncountables.delete(plural);
+ if (s.substr(0, 1).toUpperCase() == p.substr(0, 1).toUpperCase()) {
+ plural(new RegExp("(" + s.substr(0, 1) + ")" + s.substr(1) + "$", "i"), '$1' + p.substr(1));
+ plural(new RegExp("(" + p.substr(0, 1) + ")" + p.substr(1) + "$", "i"), '$1' + p.substr(1));
+ singular(new RegExp("(" + p.substr(0, 1) + ")" + p.substr(1) + "$", "i"), '$1' + s.substr(1));
+ } else {
+ plural(new RegExp(s.substr(0, 1).toUpperCase() + s.substr(1) + "$"), p.substr(0, 1).toUpperCase() + p.substr(1));
+ plural(new RegExp(s.substr(0, 1).toLowerCase() + s.substr(1) + "$"), p.substr(0, 1).toLowerCase() + p.substr(1));
+ plural(new RegExp(p.substr(0, 1).toUpperCase() + p.substr(1) + "$"), p.substr(0, 1).toUpperCase() + p.substr(1));
+ plural(new RegExp(p.substr(0, 1).toLowerCase() + p.substr(1) + "$"), p.substr(0, 1).toLowerCase() + p.substr(1));
+ singular(new RegExp(p.substr(0, 1).toUpperCase() + p.substr(1) + "$"), s.substr(0, 1).toUpperCase() + s.substr(1));
+ singular(new RegExp(p.substr(0, 1).toLowerCase() + p.substr(1) + "$"), s.substr(0, 1).toLowerCase() + s.substr(1));
+ }
+}
+
+/**
+ * Specifies a humanized form of a string by a regular expression rule or by a
+ * string mapping. When using a regular expression based replacement, the normal
+ * humanize formatting is called after the replacement.
+ */
+var human = function (rule, replacement) {
+ //inflections.uncountables.delete(rule) if rule.is_a?(String)
+ //inflections.uncountables.delete(replacement)
+ inflections.humans.push([rule, replacement]);
+}
+
+plural(/$/, "s");
+plural(/s$/i, "s");
+plural(/(ax|test)is$/i, "$1es");
+plural(/(octop|vir)us$/i, "$1i");
+plural(/(alias|status)$/i, "$1es");
+plural(/(bu)s$/i, "$1ses");
+plural(/(buffal|tomat)o$/i, "$1oes");
+plural(/([ti])um$/i, "$1a");
+plural(/sis$/i, "ses");
+plural(/(?:([^f])fe|([lr])f)$/i, "$1$2ves");
+plural(/(hive)$/i, "$1s");
+plural(/([^aeiouy]|qu)y$/i, "$1ies");
+plural(/(x|ch|ss|sh)$/i, "$1es");
+plural(/(matr|vert|ind)(?:ix|ex)$/i, "$1ices");
+plural(/([m|l])ouse$/i, "$1ice");
+plural(/^(ox)$/i, "$1en");
+plural(/(quiz)$/i, "$1zes");
+
+singular(/s$/i, "")
+singular(/(n)ews$/i, "$1ews")
+singular(/([ti])a$/i, "$1um")
+singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, "$1$2sis")
+singular(/(^analy)ses$/i, "$1sis")
+singular(/([^f])ves$/i, "$1fe")
+singular(/(hive)s$/i, "$1")
+singular(/(tive)s$/i, "$1")
+singular(/([lr])ves$/i, "$1f")
+singular(/([^aeiouy]|qu)ies$/i, "$1y")
+singular(/(s)eries$/i, "$1eries")
+singular(/(m)ovies$/i, "$1ovie")
+singular(/(x|ch|ss|sh)es$/i, "$1")
+singular(/([m|l])ice$/i, "$1ouse")
+singular(/(bus)es$/i, "$1")
+singular(/(o)es$/i, "$1")
+singular(/(shoe)s$/i, "$1")
+singular(/(cris|ax|test)es$/i, "$1is")
+singular(/(octop|vir)i$/i, "$1us")
+singular(/(alias|status)es$/i, "$1")
+singular(/^(ox)en/i, "$1")
+singular(/(vert|ind)ices$/i, "$1ex")
+singular(/(matr)ices$/i, "$1ix")
+singular(/(quiz)zes$/i, "$1")
+singular(/(database)s$/i, "$1")
+
+irregular("person", "people");
+irregular("man", "men");
+irregular("child", "children");
+irregular("sex", "sexes");
+irregular("move", "moves");
+irregular("cow", "kine");
+
+uncountable("equipment");
+uncountable("information");
+uncountable("rice");
+uncountable("money");
+uncountable("species");
+uncountable("series");
+uncountable("fish");
+uncountable("sheep");
+uncountable("jeans");
+
+/**
+ * Returns the plural form of the word in the string.
+ */
+exports.pluralize = function (word) {
+ var wlc = word.toLowerCase();
+
+ if (UNCOUNTABLES[wlc]) {
+ return word;
+ }
+
+ for (var i = 0; i < PLURALS.length; i++) {
+ var rule = PLURALS[i][0],
+ replacement = PLURALS[i][1];
+ if (rule.test(word)) {
+ return word.replace(rule, replacement);
+ }
+ }
+
+ return word;
+}
+
+/**
+ * Returns the singular form of the word in the string.
+ */
+exports.singularize = function (word) {
+ var wlc = word.toLowerCase();
+
+ if (UNCOUNTABLES[wlc]) {
+ return word;
+ }
+
+ for (var i = 0; i < SINGULARS.length; i++) {
+ var rule = SINGULARS[i][0],
+ replacement = SINGULARS[i][1];
+ if (rule.test(word)) {
+ return word.replace(rule, replacement);
+ }
+ }
+
+ return word;
+}
+
+/**
+ * Capitalizes the first word and turns underscores into spaces and strips a
+ * trailing "Key", if any. Like +titleize+, this is meant for creating pretty
+ * output.
+ *
+ * Examples:
+ * "employeeSalary" => "employee salary"
+ * "authorKey" => "author"
+ */
+exports.humanize = function (word) {
+ for (var i = 0; i < HUMANS.length; i++) {
+ var rule = HUMANS[i][0],
+ replacement = HUMANS[i][1];
+ if (rule.test(word)) {
+ word = word.replace(rule, replacement);
+ }
+ }
+
+ return exports.split(word, " ").toLowerCase();
+}
+
+/**
+ * Split a camel case word in its terms.
+ */
+exports.split = function (word, delim) {
+ delim = delim || " ";
+ var replacement = "$1" + delim + "$2";
+ return word.
+ replace(/([A-Z]+)([A-Z][a-z])/g, replacement).
+ replace(/([a-z\d])([A-Z])/g, replacement);
+}
+
+/**
+ * Converts a CamelCase word to underscore format.
+ */
+exports.underscore = function (word) {
+ return exports.split(word, "_").toLowerCase();
+}
+
+/**
+ * Converts a CamelCase word to dash (lisp style) format.
+ */
+exports.dash = exports.dasherize = function (word) {
+ return exports.split(word, "-").toLowerCase();
+}
View
44 lib/array.js
@@ -0,0 +1,44 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+var array = new (function () {
+
+ this.humanize = function(array) {
+ // Return an array in a readable form, useful for outputting lists of items
+
+ var last = array.pop();
+ array = array.join(', ');
+ return array + ' and ' + last;
+ };
+
+ this.included = function(item, array) {
+ // Check if an `item` is included in an `array`
+ // If the `item` is found, it'll return and object with the key and value,
+ // - otherwise return undefined
+
+ if(!item) return undefined;
+ var result = array.indexOf(item);
+
+ if(result === -1) {
+ return undefined;
+ } else return { key: result, value: array[result] };
+ };
+
+})();
+
+module.exports = array;
View
265 lib/async.js
@@ -0,0 +1,265 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+async = {};
+
+/*
+AsyncChain -- performs a list of asynchronous calls in a desired order.
+Optional "last" method can be set to run after all the items in the
+chain have completed.
+
+ // Example usage
+ var asyncChain = new async.AsyncChain([
+ {
+ func: app.trainToBangkok,
+ args: [geddy, neil, alex],
+ callback: null, // No callback for this action
+ },
+ {
+ func: fs.readdir,
+ args: [geddy.config.dirname + '/thailand/express'],
+ callback: function (err, result) {
+ if (err) {
+ // Bail out completely
+ arguments.callee.chain.abort();
+ }
+ else if (result.theBest) {
+ // Don't run the next item in the chain; go directly
+ // to the 'last' method.
+ arguments.callee.chain.shortCircuit();
+ }
+ else {
+ // Otherwise do some other stuff and
+ // then go to the next link
+ }
+ }
+ },
+ {
+ func: child_process.exec,
+ args: ['ls ./'],
+ callback: this.hitTheStops
+ }
+ ]);
+
+ // Function to exec after all the links in the chain finish
+ asyncChain.last = function () { // Do some final stuff };
+
+ // Start the async-chain
+ asyncChain.run();
+
+*/
+async.execNonBlocking = function (func) {
+ if (typeof process != 'undefined' && typeof process.nextTick == 'function') {
+ process.nextTick(func);
+ }
+ else {
+ setTimeout(func, 0);
+ }
+};
+
+async.AsyncBase = new (function () {
+
+ this.init = function (chain) {
+ var item;
+ this.chain = [];
+ this.currentItem = null;
+ this.shortCircuited = false;
+ this.shortCircuitedArgs = undefined;
+ this.aborted = false;
+
+ for (var i = 0; i < chain.length; i++) {
+ item = chain[i];
+ this.chain.push(new async.AsyncCall(
+ item.func, item.args, item.callback, item.context));
+ }
+ };
+
+ this.runItem = function (item) {
+ // Reference to the current item in the chain -- used
+ // to look up the callback to execute with execCallback
+ this.currentItem = item;
+ // Scopage
+ var _this = this;
+ // Pass the arguments passed to the current async call
+ // to the callback executor, execute it in the correct scope
+ var executor = function () {
+ _this.execCallback.apply(_this, arguments);
+ };
+ // Append the callback executor to the end of the arguments
+ // Node helpfully always has the callback func last
+ var args = item.args.concat(executor);
+ var func = item.func;
+ // Run the async call
+ func.apply(item.context, args);
+ };
+
+ this.next = function () {
+ if (this.chain.length) {
+ this.runItem(this.chain.shift());
+ }
+ else {
+ this.last();
+ }
+ };
+
+ this.execCallback = function () {
+ // Look up the callback, if any, specified for this async call
+ var callback = this.currentItem.callback;
+ // If there's a callback, do it
+ if (callback && typeof callback == 'function') {
+ // Allow access to the chain from inside the callback by setting
+ // callback.chain = this, and then using arguments.callee.chain
+ callback.chain = this;
+ callback.apply(this.currentItem.context, arguments);
+ }
+
+ this.currentItem.finished = true;
+
+ // If one of the async callbacks called chain.shortCircuit,
+ // skip to the 'last' function for the chain
+ if (this.shortCircuited) {
+ this.last.apply(null, this.shortCircuitedArgs);
+ }
+ // If one of the async callbacks called chain.abort,
+ // bail completely out
+ else if (this.aborted) {
+ return;
+ }
+ // Otherwise run the next item, if any, in the chain
+ // Let's try not to block if we don't have to
+ else {
+ // Scopage
+ var _this = this;
+ async.execNonBlocking(function () { _this.next.call(_this); });
+ }
+ }
+
+ // Short-circuit the chain, jump straight to the 'last' function
+ this.shortCircuit = function () {
+ this.shortCircuitedArgs = arguments;
+ this.shortCircuited = true;
+ }
+
+ // Stop execution of the chain, bail completely out
+ this.abort = function () {
+ this.aborted = true;
+ }
+
+ // Kick off the chain by grabbing the first item and running it
+ this.run = this.next;
+
+ // Function to run when the chain is done -- default is a no-op
+ this.last = function () {};
+
+})();
+
+async.AsyncChain = function (chain) {
+ this.init(chain);
+};
+
+async.AsyncChain.prototype = async.AsyncBase;
+
+async.AsyncGroup = function (group) {
+ var item;
+ var callback;
+ var args;
+
+ this.group = [];
+ this.outstandingCount = 0;
+
+ for (var i = 0; i < group.length; i++) {
+ item = group[i];
+ this.group.push(new async.AsyncCall(
+ item.func, item.args, item.callback, item.context));
+ this.outstandingCount++;
+ }
+
+};
+
+/*
+Simpler way to group async calls -- doesn't ensure completion order,
+but still has a "last" method called when the entire group of calls
+have completed.
+*/
+async.AsyncGroup.prototype = new function () {
+ this.run = function () {
+ var _this = this
+ , group = this.group
+ , item
+ , createItem = function (item, args) {
+ return function () {
+ item.func.apply(item.context, args);
+ };
+ }
+ , createCallback = function (item) {
+ return function () {
+ if (item.callback) {
+ item.callback.apply(null, arguments);
+ }
+ _this.finish.call(_this);
+ }
+ };
+
+ for (var i = 0; i < group.length; i++) {
+ item = group[i];
+ callback = createCallback(item);
+ args = item.args.concat(callback);
+ // Run the async call
+ async.execNonBlocking(createItem(item, args));
+ }
+ };
+
+ this.finish = function () {
+ this.outstandingCount--;
+ if (!this.outstandingCount) {
+ this.last();
+ };
+ };
+
+ this.last = function () {};
+
+};
+
+var _createSimpleAsyncCall = function (func, context) {
+ return {
+ func: func
+ , args: []
+ , callback: function () {}
+ , context: context
+ };
+};
+
+async.SimpleAsyncChain = function (funcs, context) {
+ chain = [];
+ for (var i = 0, ii = funcs.length; i < ii; i++) {
+ chain.push(_createSimpleAsyncCall(funcs[i], context));
+ }
+ this.init(chain);
+};
+
+async.SimpleAsyncChain.prototype = async.AsyncBase;
+
+async.AsyncCall = function (func, args, callback, context) {
+ this.func = func;
+ this.args = args;
+ this.callback = callback || null;
+ this.context = context || null;
+};
+
+module.exports = async;
+
View
103 lib/core.js
@@ -0,0 +1,103 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+var core = new (function () {
+
+ var _mix = function (targ, src, merge, includeProto) {
+ for (var p in src) {
+ // Don't copy stuff from the prototype
+ if (src.hasOwnProperty(p) || includeProto) {
+ if (merge &&
+ // Assumes the source property is an Object you can
+ // actually recurse down into
+ (typeof src[p] == 'object') &&
+ (src[p] !== null) &&
+ !(src[p] instanceof Array)) {
+ // Create the source property if it doesn't exist
+ // TODO: What if it's something weird like a String or Number?
+ if (typeof targ[p] == 'undefined') {
+ targ[p] = {};
+ }
+ _mix(targ[p], src[p], merge, includeProto); // Recurse
+ }
+ // If it's not a merge-copy, just set and forget
+ else {
+ targ[p] = src[p];
+ }
+ }
+ }
+ };
+
+ this.objectToString = function (object) {
+ var objectArray = [];
+ for (var key in object) {
+ if ('object' == typeof object[key]) {
+ objectArray.push(this.objectToString(object[key]));
+ } else {
+ objectArray.push(key + '=' + object[key]);
+ }
+ }
+ return objectArray.join(', ');
+ };
+
+ /*
+ * Mix in the properties on an object to another object
+ * yam.mixin(target, source, [source,] [source, etc.] [merge-flag]);
+ * 'merge' recurses, to merge object sub-properties together instead
+ * of just overwriting with the source object.
+ */
+ this.mixin = (function () {
+ return function () {
+ var args = Array.prototype.slice.apply(arguments),
+ merge = false,
+ targ, sources;
+ if (args.length > 2) {
+ if (typeof args[args.length - 1] == 'boolean') {
+ merge = args.pop();
+ }
+ }
+ targ = args.shift();
+ sources = args;
+ for (var i = 0, ii = sources.length; i < ii; i++) {
+ _mix(targ, sources[i], merge);
+ }
+ return targ;
+ };
+ }).call(this);
+
+ this.enhance = (function () {
+ return function () {
+ var args = Array.prototype.slice.apply(arguments),
+ merge = false,
+ targ, sources;
+ if (args.length > 2) {
+ if (typeof args[args.length - 1] == 'boolean') {
+ merge = args.pop();
+ }
+ }
+ targ = args.shift();
+ sources = args;
+ for (var i = 0, ii = sources.length; i < ii; i++) {
+ _mix(targ, sources[i], merge, true);
+ }
+ return targ;
+ };
+ }).call(this);
+
+})();
+
+module.exports = core;
View
749 lib/date.js
@@ -0,0 +1,749 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+var date = new (function () {
+ var _this = this;
+
+ var _US_DATE_PAT = /^(\d{1,2})(?:\-|\/|\.)(\d{1,2})(?:\-|\/|\.)(\d{4})/;
+ var _DATETIME_PAT = /^(\d{4})(?:\-|\/|\.)(\d{1,2})(?:\-|\/|\.)(\d{1,2})(?:T| )?(\d{2})?(?::)?(\d{2})?(?::)?(\d{2})?(?:\.)?(\d+)?(?: *)?(Z|[+-]\d{4}|[+-]\d{2}:\d{2}|[+-]\d{2})?/;
+ // TODO Add am/pm parsing instead of dumb, 24-hour clock.
+ var _TIME_PAT = /^(\d{1,2})?(?::)?(\d{2})?(?::)?(\d{2})?(?:\.)?(\d+)?$/;
+
+ var _dateMethods = [
+ 'FullYear'
+ , 'Month'
+ , 'Date'
+ , 'Hours'
+ , 'Minutes'
+ , 'Seconds'
+ , 'Milliseconds'
+ ];
+
+ var _isArray = function (obj) {
+ return obj &&
+ typeof obj === 'object' &&
+ typeof obj.length === 'number' &&
+ typeof obj.splice === 'function' &&
+ !(obj.propertyIsEnumerable('length'));
+ };
+
+ this.weekdayLong = ['Sunday', 'Monday', 'Tuesday',
+ 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+ this.weekdayShort = ['Sun', 'Mon', 'Tue', 'Wed',
+ 'Thu', 'Fri', 'Sat'];
+ this.monthLong = ['January', 'February', 'March',
+ 'April', 'May', 'June', 'July', 'August', 'September',
+ 'October', 'November', 'December'];
+ this.monthShort = ['Jan', 'Feb', 'Mar', 'Apr',
+ 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
+ this.meridian = {
+ 'AM': 'AM',
+ 'PM': 'PM'
+ }
+
+ this.supportedFormats = {
+ // abbreviated weekday name according to the current locale
+ 'a': function (dt) { return _this.weekdayShort[dt.getDay()]; },
+ // full weekday name according to the current locale
+ 'A': function (dt) { return _this.weekdayLong[dt.getDay()]; },
+ // abbreviated month name according to the current locale
+ 'b': function (dt) { return _this.monthShort[dt.getMonth()]; },
+ 'h': function (dt) { return _this.strftime(dt, '%b'); },
+ // full month name according to the current locale
+ 'B': function (dt) { return _this.monthLong[dt.getMonth()]; },
+ // preferred date and time representation for the current locale
+ 'c': function (dt) { return _this.strftime(dt, '%a %b %d %T %Y'); },
+ // century number (the year divided by 100 and truncated
+ // to an integer, range 00 to 99)
+ 'C': function (dt) { return _this.calcCentury(dt.getFullYear());; },
+ // day of the month as a decimal number (range 01 to 31)
+ 'd': function (dt) { return _this.leftPad(dt.getDate(), 2, '0'); },
+ // same as %m/%d/%y
+ 'D': function (dt) { return _this.strftime(dt, '%m/%d/%y') },
+ // day of the month as a decimal number, a single digit is
+ // preceded by a space (range ' 1' to '31')
+ 'e': function (dt) { return _this.leftPad(dt.getDate(), 2, ' '); },
+ // month as a decimal number, a single digit is
+ // preceded by a space (range ' 1' to '12')
+ 'f': function () { return _this.strftimeNotImplemented('f'); },
+ // same as %Y-%m-%d
+ 'F': function (dt) { return _this.strftime(dt, '%Y-%m-%d'); },
+ // like %G, but without the century.
+ 'g': function () { return _this.strftimeNotImplemented('g'); },
+ // The 4-digit year corresponding to the ISO week number
+ // (see %V). This has the same format and value as %Y,
+ // except that if the ISO week number belongs to the
+ // previous or next year, that year is used instead.
+ 'G': function () { return _this.strftimeNotImplemented('G'); },
+ // hour as a decimal number using a 24-hour clock (range
+ // 00 to 23)
+ 'H': function (dt) { return _this.leftPad(dt.getHours(), 2, '0'); },
+ // hour as a decimal number using a 12-hour clock (range
+ // 01 to 12)
+ 'I': function (dt) { return _this.leftPad(
+ _this.hrMil2Std(dt.getHours()), 2, '0'); },
+ // day of the year as a decimal number (range 001 to 366)
+ 'j': function (dt) { return _this.leftPad(
+ _this.calcDays(dt), 3, '0'); },
+ // Hour as a decimal number using a 24-hour clock (range
+ // 0 to 23 (space-padded))
+ 'k': function (dt) { return _this.leftPad(dt.getHours(), 2, ' '); },
+ // Hour as a decimal number using a 12-hour clock (range
+ // 1 to 12 (space-padded))
+ 'l': function (dt) { return _this.leftPad(
+ _this.hrMil2Std(dt.getHours()), 2, ' '); },
+ // month as a decimal number (range 01 to 12)
+ 'm': function (dt) { return _this.leftPad((dt.getMonth()+1), 2, '0'); },
+ // minute as a decimal number
+ 'M': function (dt) { return _this.leftPad(dt.getMinutes(), 2, '0'); },
+ // Linebreak
+ 'n': function () { return '\n'; },
+ // either `am' or `pm' according to the given time value,
+ // or the corresponding strings for the current locale
+ 'p': function (dt) { return _this.getMeridian(dt.getHours()); },
+ // time in a.m. and p.m. notation
+ 'r': function (dt) { return _this.strftime(dt, '%I:%M:%S %p'); },
+ // time in 24 hour notation
+ 'R': function (dt) { return _this.strftime(dt, '%H:%M'); },
+ // second as a decimal number
+ 'S': function (dt) { return _this.leftPad(dt.getSeconds(), 2, '0'); },
+ // Tab char
+ 't': function () { return '\t'; },
+ // current time, equal to %H:%M:%S
+ 'T': function (dt) { return _this.strftime(dt, '%H:%M:%S'); },
+ // weekday as a decimal number [1,7], with 1 representing
+ // Monday
+ 'u': function (dt) { return _this.convertOneBase(dt.getDay()); },
+ // week number of the current year as a decimal number,
+ // starting with the first Sunday as the first day of the
+ // first week
+ 'U': function () { return _this.strftimeNotImplemented('U'); },
+ // week number of the year (Monday as the first day of the
+ // week) as a decimal number [01,53]. If the week containing
+ // 1 January has four or more days in the new year, then it
+ // is considered week 1. Otherwise, it is the last week of
+ // the previous year, and the next week is week 1.
+ 'V': function () { return _this.strftimeNotImplemented('V'); },
+ // week number of the current year as a decimal number,
+ // starting with the first Monday as the first day of the
+ // first week
+ 'W': function () { return _this.strftimeNotImplemented('W'); },
+ // day of the week as a decimal, Sunday being 0
+ 'w': function (dt) { return dt.getDay(); },
+ // preferred date representation for the current locale
+ // without the time
+ 'x': function (dt) { return _this.strftime(dt, '%D'); },
+ // preferred time representation for the current locale
+ // without the date
+ 'X': function (dt) { return _this.strftime(dt, '%T'); },
+ // year as a decimal number without a century (range 00 to
+ // 99)
+ 'y': function (dt) { return _this.getTwoDigitYear(dt.getFullYear()); },
+ // year as a decimal number including the century
+ 'Y': function (dt) { return _this.leftPad(dt.getFullYear(), 4, '0'); },
+ // time zone or name or abbreviation
+ 'z': function () { return _this.strftimeNotImplemented('z'); },
+ 'Z': function () { return _this.strftimeNotImplemented('Z'); },
+ // Literal percent char
+ '%': function (dt) { return '%'; }
+ };
+
+ this.getSupportedFormats = function () {
+ var str = '';
+ for (var i in this.supportedFormats) { str += i; }
+ return str;
+ }
+
+ this.supportedFormatsPat = new RegExp('%[' +
+ this.getSupportedFormats() + ']{1}', 'g');
+
+ this.strftime = function (dt, format) {
+ if (!dt) { return '' }
+
+ var d = dt;
+ var pats = [];
+ var dts = [];
+ var str = format;
+
+ // Allow either Date obj or UTC stamp
+ d = typeof dt == 'number' ? new Date(dt) : dt;
+
+ // Grab all instances of expected formats into array
+ while (pats = this.supportedFormatsPat.exec(format)) {
+ dts.push(pats[0]);
+ }
+
+ // Process any hits
+ for (var i = 0; i < dts.length; i++) {
+ key = dts[i].replace(/%/, '');
+ str = str.replace('%' + key,
+ this.supportedFormats[key](d));
+ }
+ return str;
+
+ };
+
+ this.strftimeNotImplemented = function (s) {
+ throw('this.strftime format "' + s + '" not implemented.');
+ };
+
+ this.leftPad = function (instr, len, spacer) {
+ var str = instr.toString();
+ // spacer char optional, default to space
+ var sp = spacer ? spacer : ' ';
+ while (str.length < len) {
+ str = sp + str;
+ }
+ return str;
+ };
+
+ /**
+ * Calculate the century to which a particular year belongs
+ * @param y Integer year number
+ * @return Integer century number
+ */
+ this.calcCentury = function (y) {
+ var ret = parseInt(y/100);
+ ret = ret.toString();
+ return this.leftPad(ret);
+ };
+
+ /**
+ * Calculate the day number in the year a particular date is on
+ * @param dt JavaScript date object
+ * @return Integer day number in the year for the given date
+ */
+ this.calcDays = function(dt) {
+ var first = new Date(dt.getFullYear(), 0, 1);
+ var diff = 0;
+ var ret = 0;
+ first = first.getTime();
+ diff = (dt.getTime() - first);
+ ret = parseInt(((((diff/1000)/60)/60)/24))+1;
+ return ret;
+ };
+
+ /**
+ * Adjust from 0-6 base week to 1-7 base week
+ * @param d integer for day of week
+ * @return Integer day number for 1-7 base week
+ */
+ this.convertOneBase = function (d) {
+ return d == 0 ? 7 : d;
+ };
+
+ this.getTwoDigitYear = function (yr) {
+ // Add a millenium to take care of years before the year 1000,
+ // (e.g, the year 7) since we're only taking the last two digits
+ // If we overshoot, it doesn't matter
+ var millenYear = yr + 1000;
+ var str = millenYear.toString();
+ str = str.substr(2); // Get the last two digits
+ return str
+ };
+
+ /**
+ * Return 'AM' or 'PM' based on hour in 24-hour format
+ * @param h Integer for hour in 24-hour format
+ * @return String of either 'AM' or 'PM' based on hour number
+ */
+ this.getMeridian = function (h) {
+ return h > 11 ? this.meridian.PM :
+ this.meridian.AM;
+ };
+
+ /**
+ * Convert a 24-hour formatted hour to 12-hour format
+ * @param hour Integer hour number
+ * @return String for hour in 12-hour format -- may be string length of one
+ */
+ this.hrMil2Std = function (hour) {
+ var h = typeof hour == 'number' ? hour : parseInt(hour);
+ var str = h > 12 ? h - 12 : h;
+ str = str == 0 ? 12 : str;
+ return str;
+ };
+
+ /**
+ * Convert a 12-hour formatted hour with meridian flag to 24-hour format
+ * @param hour Integer hour number
+ * @param pm Boolean flag, if PM hour then set to true
+ * @return String for hour in 24-hour format
+ */
+ this.hrStd2Mil = function (hour, pm) {
+ var h = typeof hour == 'number' ? hour : parseInt(hour);
+ var str = '';
+ // PM
+ if (pm) {
+ str = h < 12 ? (h+12) : h;
+ }
+ // AM
+ else {
+ str = h == 12 ? 0 : h;
+ }
+ return str;
+ };
+
+ // Constants for use in this.add
+ var dateParts = {
+ YEAR: 'year'
+ , MONTH: 'month'
+ , DAY: 'day'
+ , HOUR: 'hour'
+ , MINUTE: 'minute'
+ , SECOND: 'second'
+ , MILLISECOND: 'millisecond'
+ , QUARTER: 'quarter'
+ , WEEK: 'week'
+ , WEEKDAY: 'weekday'
+ };
+ // Create a map for singular/plural lookup, e.g., day/days
+ var datePartsMap = {};
+ for (var p in dateParts) {
+ datePartsMap[dateParts[p]] = dateParts[p];
+ datePartsMap[dateParts[p] + 's'] = dateParts[p];
+ }
+ this.dateParts = dateParts;
+
+ /**
+ * Add to a Date in intervals of different size, from
+ * milliseconds to years
+ * @param dt -- Date (or timestamp Number), date to increment
+ * @param interv -- String, a constant representing the interval,
+ * e.g. YEAR, MONTH, DAY. See this.dateParts
+ * @param incr -- Number, how much to add to the date
+ * @return Integer day number for 1-7 base week
+ */
+ this.add = function (dt, interv, incr) {
+ if (typeof dt == 'number') { dt = new Date(dt); }
+ function fixOvershoot(){
+ if (sum.getDate() < dt.getDate()){
+ sum.setDate(0);
+ }
+ }
+ var key = datePartsMap[interv];
+ var sum = new Date(dt);
+ switch(key) {
+ case dateParts.YEAR:
+ sum.setFullYear(dt.getFullYear()+incr);
+ // Keep increment/decrement from 2/29 out of March
+ fixOvershoot();
+ break;
+ case dateParts.QUARTER:
+ // Naive quarter is just three months
+ incr*=3;
+ // fallthrough...
+ case dateParts.MONTH:
+ sum.setMonth(dt.getMonth()+incr);
+ // Reset to last day of month if you overshoot
+ fixOvershoot();
+ break;
+ case dateParts.WEEK:
+ incr*=7;
+ // fallthrough...
+ case dateParts.DAY:
+ sum.setDate(dt.getDate() + incr);
+ break;
+ case dateParts.WEEKDAY:
+ //FIXME: assumes Saturday/Sunday weekend, but even this is not fixed.
+ // There are CLDR entries to localize this.
+ var dat = dt.getDate();
+ var weeks = 0;
+ var days = 0;
+ var strt = 0;
+ var trgt = 0;
+ var adj = 0;
+ // Divide the increment time span into weekspans plus leftover days
+ // e.g., 8 days is one 5-day weekspan / and two leftover days
+ // Can't have zero leftover days, so numbers divisible by 5 get
+ // a days value of 5, and the remaining days make up the number of weeks
+ var mod = incr % 5;
+ if (mod == 0) {
+ days = (incr > 0) ? 5 : -5;
+ weeks = (incr > 0) ? ((incr-5)/5) : ((incr+5)/5);
+ }
+ else {
+ days = mod;
+ weeks = parseInt(incr/5);
+ }
+ // Get weekday value for orig date param
+ strt = dt.getDay();
+ // Orig date is Sat / positive incrementer
+ // Jump over Sun
+ if (strt == 6 && incr > 0) {
+ adj = 1;
+ }
+ // Orig date is Sun / negative incrementer
+ // Jump back over Sat
+ else if (strt == 0 && incr < 0) {
+ adj = -1;
+ }
+ // Get weekday val for the new date
+ trgt = strt + days;
+ // New date is on Sat or Sun
+ if (trgt == 0 || trgt == 6) {
+ adj = (incr > 0) ? 2 : -2;
+ }
+ // Increment by number of weeks plus leftover days plus
+ // weekend adjustments
+ sum.setDate(dat + (7*weeks) + days + adj);
+ break;
+ case dateParts.HOUR:
+ sum.setHours(sum.getHours()+incr);
+ break;
+ case dateParts.MINUTE:
+ sum.setMinutes(sum.getMinutes()+incr);
+ break;
+ case dateParts.SECOND:
+ sum.setSeconds(sum.getSeconds()+incr);
+ break;
+ case dateParts.MILLISECOND:
+ sum.setMilliseconds(sum.getMilliseconds()+incr);
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ return sum; // Date
+ };
+
+ /**
+ * Get the difference in a specific unit of time (e.g., number
+ * of months, weeks, days, etc.) between two dates.
+ * @param date1 -- Date (or timestamp Number)
+ * @param date2 -- Date (or timestamp Number)
+ * @param interv -- String, a constant representing the interval,
+ * e.g. YEAR, MONTH, DAY. See this.dateParts
+ * @return Integer, number of (interv) units apart that
+ * the two dates are
+ */
+ this.diff = function (date1, date2, interv) {
+ // date1
+ // Date object or Number equivalent
+ //
+ // date2
+ // Date object or Number equivalent
+ //
+ // interval
+ // A constant representing the interval, e.g. YEAR, MONTH, DAY. See this.dateParts.
+
+ // Accept timestamp input
+ if (typeof date1 == 'number') { date1 = new Date(date1); }
+ if (typeof date2 == 'number') { date2 = new Date(date2); }
+ var yeaDiff = date2.getFullYear() - date1.getFullYear();
+ var monDiff = (date2.getMonth() - date1.getMonth()) + (yeaDiff * 12);
+ var msDiff = date2.getTime() - date1.getTime(); // Millisecs
+ var secDiff = msDiff/1000;
+ var minDiff = secDiff/60;
+ var houDiff = minDiff/60;
+ var dayDiff = houDiff/24;
+ var weeDiff = dayDiff/7;
+ var delta = 0; // Integer return value
+
+ var key = datePartsMap[interv];
+ switch (key) {
+ case dateParts.YEAR:
+ delta = yeaDiff;
+ break;
+ case dateParts.QUARTER:
+ var m1 = date1.getMonth();
+ var m2 = date2.getMonth();
+ // Figure out which quarter the months are in
+ var q1 = Math.floor(m1/3) + 1;
+ var q2 = Math.floor(m2/3) + 1;
+ // Add quarters for any year difference between the dates
+ q2 += (yeaDiff * 4);
+ delta = q2 - q1;
+ break;
+ case dateParts.MONTH:
+ delta = monDiff;
+ break;
+ case dateParts.WEEK:
+ // Truncate instead of rounding
+ // Don't use Math.floor -- value may be negative
+ delta = parseInt(weeDiff);
+ break;
+ case dateParts.DAY:
+ delta = dayDiff;
+ break;
+ case dateParts.WEEKDAY:
+ var days = Math.round(dayDiff);
+ var weeks = parseInt(days/7);
+ var mod = days % 7;
+
+ // Even number of weeks
+ if (mod == 0) {
+ days = weeks*5;
+ }
+ else {
+ // Weeks plus spare change (< 7 days)
+ var adj = 0;
+ var aDay = date1.getDay();
+ var bDay = date2.getDay();
+
+ weeks = parseInt(days/7);
+ mod = days % 7;
+ // Mark the date advanced by the number of
+ // round weeks (may be zero)
+ var dtMark = new Date(date1);
+ dtMark.setDate(dtMark.getDate()+(weeks*7));
+ var dayMark = dtMark.getDay();
+
+ // Spare change days -- 6 or less
+ if (dayDiff > 0) {
+ switch (true) {
+ // Range starts on Sat
+ case aDay == 6:
+ adj = -1;
+ break;
+ // Range starts on Sun
+ case aDay == 0:
+ adj = 0;
+ break;
+ // Range ends on Sat
+ case bDay == 6:
+ adj = -1;
+ break;
+ // Range ends on Sun
+ case bDay == 0:
+ adj = -2;
+ break;
+ // Range contains weekend
+ case (dayMark + mod) > 5:
+ adj = -2;
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ }
+ else if (dayDiff < 0) {
+ switch (true) {
+ // Range starts on Sat
+ case aDay == 6:
+ adj = 0;
+ break;
+ // Range starts on Sun
+ case aDay == 0:
+ adj = 1;
+ break;
+ // Range ends on Sat
+ case bDay == 6:
+ adj = 2;
+ break;
+ // Range ends on Sun
+ case bDay == 0:
+ adj = 1;
+ break;
+ // Range contains weekend
+ case (dayMark + mod) < 0:
+ adj = 2;
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ }
+ days += adj;
+ days -= (weeks*2);
+ }
+ delta = days;
+
+ break;
+ case dateParts.HOUR:
+ delta = houDiff;
+ break;
+ case dateParts.MINUTE:
+ delta = minDiff;
+ break;
+ case dateParts.SECOND:
+ delta = secDiff;
+ break;
+ case dateParts.MILLISECOND:
+ delta = msDiff;
+ break;
+ default:
+ // Do nothing
+ break;
+ }
+ // Round for fractional values and DST leaps
+ return Math.round(delta); // Number (integer)
+ };
+
+ this.parse = function (val) {
+ var dt
+ , matches
+ , reordered
+ , off
+ , curr
+ , stamp
+ , prefix = ''
+ , utc;
+
+ // Yay, we have a date, use it as-is
+ if (val instanceof Date || typeof val.getFullYear == 'function') {
+ dt = val;
+ }
+
+ // Timestamp?
+ else if (typeof val == 'number') {
+ dt = new Date(val);
+ }
+
+ // String or Array
+ else {
+ // Value preparsed, looks like [yyyy, mo, dd, hh, mi, ss, ms, (offset?)]
+ if (_isArray(val)) {
+ matches = val;
+ matches.unshift(null);
+ matches[8] = null;
+ }
+
+ // Oh, crap, it's a string -- parse this bitch
+ else if (typeof val == 'string') {
+ matches = val.match(_DATETIME_PAT);
+
+ // Stupid US-only format?
+ if (!matches) {
+ matches = val.match(_US_DATE_PAT);
+ if (matches) {
+ reordered = [matches[0], matches[3], matches[1], matches[2]];
+ // Pad the results to the same length as ISO8601
+ reordered[8] = null;
+ matches = reordered;
+ }
+ }
+
+ // Time-stored-in-Date hack?
+ if (!matches) {
+ matches = val.match(_TIME_PAT);
+ if (matches) {
+ reordered = [matches[0], 0, 1, 0, matches[1], matches[2], matches[3], matches[4], null];
+ matches = reordered;
+ }
+ }
+
+ }
+
+ // Sweet, the regex actually parsed it into something useful
+ if (matches) {
+ matches.shift(); // First match is entire match, DO NOT WANT
+
+ off = matches.pop();
+ // If there's an offset, and it's GMT, we know to use
+ // UTC methods to set everything
+ if (off) {
+ utc = false;
+ if (off == 'Z') {
+ utc = true;
+ }
+ else {
+ off = off.replace(/\+|-|:/g, '');
+ if (parseInt(off, 10) === 0) {
+ utc = true;
+ }
+ }
+ if (utc) {
+ prefix = 'UTC';
+ }
+ }
+
+ dt = new Date(0);
+
+ // Stupid zero-based months
+ matches[1] = parseInt(matches[1], 10) - 1;
+
+ // Iterate the array and set each date property using either
+ // plain or UTC setters
+ for (var i = matches.length - 1; i >= 0; i--) {
+ curr = parseInt(matches[i], 10) || 0;
+ dt['set' + prefix + _dateMethods[i]](curr);
+ }
+ }
+
+ // Shit, last-ditch effort using Date.parse
+ else {
+ stamp = Date.parse(val);
+ // Failures to parse yield NaN
+ if (!isNaN(stamp)) {
+ dt = new Date(stamp);
+ }
+ }
+
+ }
+
+ return dt || null;
+ };
+
+ this.relativeTime = function (dt, options) {
+ var opts = options || {}
+ , now = opts.now || new Date()
+ , abbr = opts.abbreviated || false
+ // Diff in seconds
+ , diff = (now.getTime() - dt.getTime()) / 1000
+ , ret
+ , num
+ , hour = 60*60
+ , day = 24*hour
+ , week = 7*day
+ , month = 30*day;
+ switch (true) {
+ case diff < 60:
+ ret = abbr ? '<1m' : 'less than a minute ago';
+ break;
+ case diff < 120:
+ ret = abbr ? '1m' : 'about a minute ago';
+ break;
+ case diff < (45*60):
+ num = parseInt((diff / 60), 10);
+ ret = abbr ? num + 'm' : num + ' minutes ago';
+ break;
+ case diff < (2*hour):
+ ret = abbr ? '1h' : 'about an hour ago';
+ break;
+ case diff < (1*day):
+ num = parseInt((diff / hour), 10);
+ ret = abbr ? num + 'h' : 'about ' + num + ' hours ago';
+ break;
+ case diff < (2*day):
+ ret = abbr ? '1d' : 'one day ago';
+ break;
+ case diff < (7*day):
+ num = parseInt((diff / day), 10);
+ ret = abbr ? num + 'd' : 'about ' + num + ' days ago';
+ break;
+ case diff < (8*day):
+ ret = abbr ? '1w': 'one week ago';
+ break;
+ case diff < (1*month):
+ num = parseInt((diff / week), 10);
+ ret = abbr ? num + 'w' : 'about ' + num + ' weeks ago';
+ break;
+ default:
+ num = parseInt((diff / day), 10);
+ ret = abbr ? num + 'd' : num + ' days ago';
+ break;
+ }
+ return ret;
+ };
+
+})();
+
+module.exports = date;
+
+
View
79 lib/event_buffer.js
@@ -0,0 +1,79 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+
+This is a very simple buffer for a predetermined set of events. It is unbounded.
+It forwards all arguments to any outlet emitter attached with sync().
+
+Example:
+ var source = new Stream()
+ , dest = new EventEmitter()
+ , buff = new EventBuffer(source)
+ , data = '';
+ dest.on('data', function(d) { data += d; });
+ source.writeable = true;
+ source.readable = true;
+ source.emit('data', 'abcdef');
+ source.emit('data', '123456');
+ buff.sync(dest);
+
+*/
+
+var EventBuffer = function (src, events) {
+ // By default, we service the default stream events
+ var self = this
+ , streamEvents = ['data', 'end', 'error', 'close', 'fd', 'drain', 'pipe'];
+ this.events = events || streamEvents;
+ this.emitter = src;
+ this.eventBuffer = [];
+ this.outlet = null;
+ this.events.forEach(function (name) {
+ self.emitter.addListener(name, function () {
+ self.proxyEmit(name, arguments);
+ });
+ });
+};
+
+EventBuffer.prototype = new (function () {
+ this.proxyEmit = function (name, args) {
+ if (this.outlet) {
+ this.emit(name, args);
+ }
+ else {
+ this.eventBuffer.push({name: name, args: args});
+ }
+ };
+
+ this.emit = function (name, args) {
+ // Prepend name to args
+ var outlet = this.outlet;
+ Array.prototype.splice.call(args, 0, 0, name);
+ outlet.emit.apply(outlet, args);
+ };
+
+ // Flush the buffer and continue piping new events to the outlet
+ this.sync = function (outlet) {
+ var buffer = this.eventBuffer
+ , bufferItem;
+ this.outlet = outlet;
+ while ((bufferItem = buffer.shift())) {
+ this.emit(bufferItem.name, bufferItem.args);
+ }
+ };
+})();
+EventBuffer.prototype.constructor = EventBuffer;
+
+module.exports.EventBuffer = EventBuffer;
View
304 lib/file.js
@@ -0,0 +1,304 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+var fs = require('fs')
+ , path = require('path')
+ , logger;
+
+var logger = new (function () {
+ var out;
+ try {
+ out = require('./logger');
+ }
+ catch (e) {
+ out = console;
+ }
+
+ this.log = function (o) {
+ out.log(o);
+ };
+})();
+
+var fileUtils = new (function () {
+ var _copyFile = function(fromPath, toPath, opts) {
+ var from = path.normalize(fromPath)
+ , to = path.normalize(toPath)
+ , options = opts || {}
+ , fromStat
+ , toStat
+ , destExists
+ , destDoesNotExistErr
+ , content
+ , filename
+ , dirContents
+ , targetDir;
+
+ fromStat = fs.statSync(from);
+
+ try {
+ //console.dir(to + ' destExists');
+ toStat = fs.statSync(to);
+ destExists = true;
+ }
+ catch(e) {
+ //console.dir(to + ' does not exist');
+ destDoesNotExistErr = e;
+ destExists = false;
+ }
+ // Destination dir or file exists, copy into (directory)
+ // or overwrite (file)
+ if (destExists) {
+
+ // If there's a rename-via-copy file/dir name passed, use it.
+ // Otherwise use the actual file/dir name
+ filename = options.rename || path.basename(from);
+
+ // Copying a directory
+ if (fromStat.isDirectory()) {
+ dirContents = fs.readdirSync(from);
+ targetDir = path.join(to, filename);
+ // We don't care if the target dir already exists
+ try {
+ fs.mkdirSync(targetDir, options.mode || 0755);
+ }
+ catch(e) {
+ if (e.code != 'EEXIST') {
+ throw e;
+ }
+ }
+ for (var i = 0, ii = dirContents.length; i < ii; i++) {
+ //console.log(dirContents[i]);
+ _copyFile(path.join(from, dirContents[i]), targetDir);
+ }
+ }
+ // Copying a file
+ else {
+ content = fs.readFileSync(from);
+ // Copy into dir
+ if (toStat.isDirectory()) {
+ //console.log('copy into dir ' + to);
+ fs.writeFileSync(path.join(to, filename), content);
+ }
+ // Overwrite file
+ else {
+ //console.log('overwriting ' + to);
+ fs.writeFileSync(to, content);
+ }
+ }
+ }
+ // Dest doesn't exist, can't create it
+ else {
+ throw destDoesNotExistErr;
+ }
+ }
+
+ , _copyDir = function (from, to, opts) {
+ var createDir = opts.createDir;
+ }
+
+ , _readDir = function (dirPath) {
+ var dir = path.normalize(dirPath)
+ , paths = []
+ , ret = [dir];
+
+ try {
+ paths = fs.readdirSync(dir);
+ }
+ catch (e) {
+ throw new Error('Could not read path ' + dir);
+ }
+
+ paths.forEach(function (p) {
+ var curr = path.join(dir, p);
+ var stat = fs.statSync(curr);
+ if (stat.isDirectory()) {
+ ret = ret.concat(_readDir(curr));
+ }
+ else {
+ ret.push(curr);
+ }
+ });
+
+ return ret;
+ }
+
+ , _rmDir = function (dirPath) {
+ var dir = path.normalize(dirPath)
+ , paths = [];
+ paths = fs.readdirSync(dir);
+ paths.forEach(function (p) {
+ var curr = path.join(dir, p);
+ var stat = fs.statSync(curr);
+ if (stat.isDirectory()) {
+ _rmDir(curr);
+ }
+ else {
+ fs.unlinkSync(curr);
+ }
+ });
+ fs.rmdirSync(dir);
+ };
+
+ this.cpR = function (fromPath, toPath, options) {
+ var from = path.normalize(fromPath)
+ , to = path.normalize(toPath)
+ , toStat
+ , doesNotExistErr
+ , paths
+ , filename
+ , opts = options || {};
+
+ if (!opts.silent) {
+ logger.log('cp -r ' + fromPath + ' ' + toPath);
+ }
+
+ opts = {}; // Reset
+
+ if (from == to) {
+ throw new Error('Cannot copy ' + from + ' to itself.');
+ }
+
+ // Handle rename-via-copy
+ try {
+ toStat = fs.statSync(to);
+ }
+ catch(e) {
+ doesNotExistErr = e;
+
+ // Get abs path so it's possible to check parent dir
+ if (!this.isAbsolute(to)) {
+ to = path.join(process.cwd() , to);
+ }
+
+ // Save the file/dir name
+ filename = path.basename(to);
+ // See if a parent dir exists, so there's a place to put the
+ /// renamed file/dir (resets the destination for the copy)
+ to = path.dirname(to);
+ try {
+ toStat = fs.statSync(to);
+ }
+ catch(e) {}
+ if (toStat && toStat.isDirectory()) {
+ // Set the rename opt to pass to the copy func, will be used
+ // as the new file/dir name
+ opts.rename = filename;
+ //console.log('filename ' + filename);
+ }
+ else {
+ throw doesNotExistErr;
+ }
+ }
+
+ _copyFile(from, to, opts);
+ };
+
+ this.mkdirP = function (dir, mode) {
+ var dirPath = path.normalize(dir)
+ , paths = dirPath.split(/\/|\\/)
+ , currPath
+ , next;
+
+ if (paths[0] == '' || /^[A-Za-z]+:/.test(paths[0])) {
+ currPath = paths.shift() || '/';
+ currPath = path.join(currPath, paths.shift());
+ //console.log('basedir');
+ }
+ while ((next = paths.shift())) {
+ if (next == '..') {
+ currPath = path.join(currPath, next);
+ continue;
+ }
+ currPath = path.join(currPath, next);
+ try {
+ //console.log('making ' + currPath);
+ fs.mkdirSync(currPath, mode || 0755);
+ }
+ catch(e) {
+ if (e.code != 'EEXIST') {
+ throw e;
+ }
+ }
+ }
+ };
+
+ this.readdirR = function (dir, opts) {
+ var options = opts || {}
+ , format = options.format || 'array'
+ , ret;
+ ret = _readDir(dir);
+ return format == 'string' ? ret.join('\n') : ret;
+ };
+
+ this.rmRf = function (p, options) {
+ var stat
+ , opts = options || {};
+ if (!opts.silent) {
+ logger.log('rm -rf ' + p);
+ }
+ try {
+ stat = fs.statSync(p);
+ if (stat.isDirectory()) {
+ _rmDir(p);
+ }
+ else {
+ fs.unlinkSync(p);
+ }
+ }
+ catch (e) {}
+ };
+
+ this.isAbsolute = function (p) {
+ var match = /^[A-Za-z]+:\\|^\//.exec(p);
+ if (match && match.length) {
+ return match[0];
+ }
+ return false;
+ };
+
+ this.absolutize = function (p) {
+ if (this.isAbsolute(p)) {
+ return p;
+ }
+ else {
+ return path.join(process.cwd(), p);
+ }
+ };
+
+ this.basedir = function (p) {
+ var str = p || ''
+ , abs = this.isAbsolute(p);
+ if (abs) {
+ return abs;
+ }
+ // Split into segments
+ str = str.split(/\\|\//)[0];
+ // If the path has a leading asterisk, basedir is the current dir
+ if (str.indexOf('*') > -1) {
+ return '.';
+ }
+ // Otherwise it's the first segment in the path
+ else {
+ return str;
+ }
+ };
+
+})();
+
+module.exports = fileUtils;
+
View
50 lib/index.js
@@ -0,0 +1,50 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+var var utils = {}
+// Core methods
+ , core = require('./core')
+// Namespaces with methods
+ , string = require('./string')
+ , file = require('./file')
+ , async = require('./async')
+ , uri = require('./uri')
+ , array = require('./array')
+ , object = require('./object')
+ , date = require('./date')
+ , request = require('./request')
+// Constructors
+ , EventBuffer = require('./event_buffer').EventBuffer
+ , XML = require('./xml').XML
+ , SortedCollection = require('./sorted_collection').SortedCollection;
+
+core.mixin(utils, core);
+
+utils.string = string;
+utils.file = file;
+utils.async = async;
+utils.uri = uri;
+utils.array = array;
+utils.object = object;
+utils.date = date;
+utils.request = request;
+utils.SortedCollection = SortedCollection;
+utils.EventBuffer = EventBuffer;
+utils.XML = XML;
+
+module.exports = utils;
+
View
71 lib/object.js
@@ -0,0 +1,71 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+var object = new (function () {
+
+ this.merge = function(object, otherObject) {
+ // Merges two objects together, then returns the final object
+ // - if no matching value is found it will create a new one otherwise it will overwrite the old
+ // - one, also supports deep merging automatically
+ object = object || {};
+ otherObject = otherObject || {};
+ var i, key, value;
+
+ for(i in otherObject) {
+ key = i, value = otherObject[key];
+
+ try {
+ // If value is an object
+ if(typeof value === 'object' && !value instanceof Array) {
+ // Update value of object to the one from otherObject
+ object[key] = merge(object[key], value);
+ } else object[key] = value;
+ } catch(err) {
+ // Object isn't set so set it
+ object[key] = value;
+ }
+ }
+ return object;
+ };
+
+ this.reverseMerge = function(object, defaultObject) {
+ // Same as `merge` except `defaultObject` is the object being changed
+ // - this is useful if we want to easily deal with default object values
+ return this.merge(defaultObject, object);
+ };
+
+ this.isEmpty = function(object) {
+ // Returns true if a object is empty false if not
+ for(var i in object) { return false; }
+ return true;
+ };
+
+ this.toArray = function(object) {
+ // Converts an object into an array of objects with the original key, values
+ array = [];
+
+ for(var i in object) {
+ array.push({ key: i, value: object[i] });
+ }
+
+ return array;
+ };
+
+})();
+
+module.exports = object;
View
105 lib/request.js
@@ -0,0 +1,105 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+var http = require('http')
+ , https = require('https')
+ , url = require('url')
+ , uri = require('./uri').uri;
+
+var formatters = {
+ xml: function (data) {
+ return data;
+ }
+, html: function (data) {
+ return data;
+ }
+, txt: function (data) {
+ return data;
+ }
+, json: function (data) {
+ return JSON.parse(data);
+ }
+}
+
+var request = function (options, callback) {
+ var client
+ , opts = options || {}
+ , parsed = url.parse(opts.url)
+ , requester = parsed.protocol == 'http:' ? http : https
+ , method = (opts.method && opts.method.toUpperCase()) || 'GET'
+ , headers = opts.headers || {}
+ , contentLength
+ , port;
+
+ if (parsed.port) {
+ port = parsed.port;
+ }
+ else {
+ port = parsed.protocol == 'http:' ? '80' : '443';
+ }
+
+ if (method == 'POST' || method == 'PUT') {
+ if (opts.data) {
+ contentLength = opts.data.length;
+ }
+ else {
+ contentLength = 0
+ }
+ headers['Content-Length'] = contentLength;
+ }
+
+ client = requester.request({
+ host: parsed.hostname
+ , port: port
+ , method: method
+ , agent: false
+ , path: parsed.pathname + parsed.search
+ , headers: headers
+ });
+
+ client.addListener('response', function (resp) {
+ var data = ''
+ , dataType;
+ resp.addListener('data', function (chunk) {
+ data += chunk.toString();
+ });
+ resp.addListener('end', function () {
+ dataType = opts.dataType || uri.getFileExtension(parsed.pathname);
+ if (formatters[dataType]) {
+ try {
+ data = formatters[dataType](data);
+ }
+ catch (e) {
+ callback(e, null);
+ }
+ }
+ callback(null, data);
+ });
+ });
+
+ client.addListener('error', function (e) {
+ callback(e, null);
+ });
+
+ if ((method == 'POST' || method == 'PUT') && opts.data) {
+ client.write(opts.data);
+ }
+
+ client.end();
+};
+
+module.exports = request;
View
311 lib/sorted_collection.js
@@ -0,0 +1,311 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+
+var SortedCollection = function (d) {
+ this.count = 0;
+ this.items = {}; // Hash keys and their values
+ this.order = []; // Array for sort order
+ if (d) {
+ this.defaultValue = d;
+ };
+};
+
+SortedCollection.prototype = new (function () {
+ // Interface methods
+ this.addItem = function (key, val) {
+ if (typeof key != 'string') {
+ throw('Hash only allows string keys.');
+ }
+ return this.setByKey(key, val);
+ };
+
+ this.getItem = function (p) {
+ if (typeof p == 'string') {
+ return this.getByKey(p);
+ }
+ else if (typeof p == 'number') {
+ return this.getByIndex(p);
+ }
+ };
+
+ this.setItem = function (p, val) {
+ if (typeof p == 'string') {
+ this.setByKey(p, val);
+ }
+ else if (typeof p == 'number') {
+ this.setByIndex(p, val);
+ }
+ };
+
+ this.removeItem = function (p) {
+ if (typeof p == 'string') {
+ this.removeByKey(p);
+ }
+ else if (typeof p == 'number') {
+ this.removeByIndex(p);
+ }
+ };
+
+ this.getByKey = function (key) {
+ return this.items[key];
+ };
+
+ this.setByKey = function (key, val) {
+ var v = null;
+ if (typeof val == 'undefined') {
+ v = this.defaultValue;
+ }
+ else { v = val; }
+ if (typeof this.items[key] == 'undefined') {
+ this.order[this.count] = key;
+ this.count++;
+ }
+ this.items[key] = v;
+ return this.items[key];
+ };
+
+ this.removeByKey = function (key) {
+ if (typeof this.items[key] != 'undefined') {
+ var pos = null;
+ delete this.items[key]; // Remove the value
+ // Find the key in the order list
+ for (var i = 0; i < this.order.length; i++) {
+ if (this.order[i] == key) {
+ pos = i;
+ }
+ }
+ this.order.splice(pos, 1); // Remove the key
+ this.count--; // Decrement the length
+ }
+ };
+
+ this.getByIndex = function (ind) {
+ return this.items[this.order[ind]];
+ };
+
+ this.setByIndex = function (ind, val) {
+ if (ind < 0 || ind >= this.count) {
+ throw('Index out of bounds. Hash length is ' + this.count);
+ }
+ this.items[this.order[ind]] = val;
+ };
+
+ this.removeByIndex = function (pos) {
+ var ret = this.items[this.order[pos]];
+ if (typeof ret != 'undefined') {
+ delete this.items[this.order[pos]]
+ this.order.splice(pos, 1);
+ this.count--;
+ return true;
+ }
+ else {
+ return false;
+ }
+ };
+
+ this.hasKey = function (key) {
+ return typeof this.items[key] != 'undefined';
+ };
+
+ this.hasValue = function (val) {
+ for (var i = 0; i < this.order.length; i++) {
+ if (this.items[this.order[i]] == val) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ this.allKeys = function (str) {
+ return this.order.join(str);
+ };
+
+ this.replaceKey = function (oldKey, newKey) {
+ // If item for newKey exists, nuke it
+ if (this.hasKey(newKey)) {
+ this.removeItem(newKey);
+ }
+ this.items[newKey] = this.items[oldKey];
+ delete this.items[oldKey];
+ for (var i = 0; i < this.order.length; i++) {
+ if (this.order[i] == oldKey) {
+ this.order[i] = newKey;
+ }
+ }
+ };
+
+ this.insertAtIndex = function (pos, key, val) {
+ this.order.splice(pos, 0, key);
+ this.items[key] = val;
+ this.count++;
+ return true;
+ };
+
+ this.insertAfterKey = function (refKey, key, val) {
+ var pos = this.getPos(refKey);
+ this.insertAtPos(pos, key, val);
+ };
+
+ this.getPosition = function (key) {
+ var order = this.order;
+ if (typeof order.indexOf == 'function') {
+ return order.indexOf(key);
+ }
+ else {
+ for (var i = 0; i < order.length; i++) {
+ if (order[i] == key) { return i;}
+ }
+ }
+ };
+
+ this.each = function (func, o) {
+ var opts = o || {}
+ , order = this.order;
+ for (var i = 0, ii = order.length; i < ii; i++) {
+ var key = order[i];
+ var val = this.items[key];
+ if (opts.keyOnly) {
+ func(key);
+ }
+ else if (opts.valueOnly) {
+ func(val);
+ }
+ else {
+ func(val, key);
+ }
+ }
+ return true;
+ };
+
+ this.eachKey = function (func) {
+ this.each(func, { keyOnly: true });
+ };
+
+ this.eachValue = function (func) {
+ this.each(func, { valueOnly: true });
+ };
+
+ this.clone = function () {
+ var coll = new SortedCollection()
+ , key
+ , val;
+ for (var i = 0; i < this.order.length; i++) {
+ key = this.order[i];
+ val = this.items[key];
+ coll.setItem(key, val);
+ }
+ return coll;
+ };
+
+ this.concat = function (hNew) {
+ for (var i = 0; i < hNew.order.length; i++) {
+ var key = hNew.order[i];
+ var val = hNew.items[key];
+ this.setItem(key, val);
+ }
+ };
+
+ this.push = function (key, val) {
+ this.insertAtIndex(this.count, key, val);
+ return this.count;
+ };
+
+ this.pop = function () {
+ var pos = this.count-1;
+ var ret = this.items[this.order[pos]];
+ if (typeof ret != 'undefined') {
+ this.removeByIndex(pos);
+ return ret;
+ }
+ else {
+ return;
+ }
+ };
+
+ this.unshift = function (key, val) {
+ this.insertAtIndex(0, key, val);
+ return this.count;
+ };
+
+ this.shift = function (key, val) {
+ var pos = 0;
+ var ret = this.items[this.order[pos]];
+ if (typeof ret != 'undefined') {
+ this.removeByIndex(pos);
+ return ret;
+ }
+ else {
+ return;
+ }
+ };
+
+ this.splice = function (index, numToRemove, hash) {
+ var _this = this;
+ // Removal
+ if (numToRemove > 0) {
+ // Items
+ var limit = index + numToRemove;
+ for (var i = index; i < limit; i++) {
+ delete this.items[this.order[i]];
+ }
+ // Order
+ this.order.splice(index, numToRemove);
+ }
+ // Adding
+ if (hash) {
+ // Items
+ for (var i in hash.items) {
+ this.items[i] = hash.items[i];
+ }
+ // Order
+ var args = hash.order;
+ args.unshift(0);
+ args.unshift(index);
+ this.order.splice.apply(this.order, args);
+ }
+ this.count = this.order.length;
+ };
+
+ this.sort = function (c) {
+ var arr = [];
+ // Assumes vals are comparable scalars
+ var comp = function (a, b) {
+ return c(a.val, b.val);
+ }
+ for (var i = 0; i < this.order.length; i++) {
+ var key = this.order[i];
+ arr[i] = { key: key, val: this.items[key] };
+ }
+ arr.sort(comp);
+ this.order = [];
+ for (var i = 0; i < arr.length; i++) {
+ this.order.push(arr[i].key);
+ }
+ };
+
+ this.sortByKey = function (comp) {
+ this.order.sort(comp);
+ };
+
+ this.reverse = function () {
+ this.order.reverse();
+ };
+
+})();
+
+module.exports.SortedCollection = SortedCollection;
View
389 lib/string.js
@@ -0,0 +1,389 @@
+/*
+ * JSTools JavaScript utilities
+ * Copyright 2112 Matthew Eernisse (mde@fleegix.org)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+*/
+var core = require('./core')
+ , string;
+
+string = new (function () {
+ // Regexes used in trimming functions
+ var _LTR = /^\s+/;
+ var _RTR = /\s+$/;
+ var _TR = /^\s+|\s+$/g;
+ var _NL = /\n|\r|\r\n/g;
+ // From/to char mappings -- for the XML escape,
+ // unescape, and test for escapable chars
+ var _CHARS = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ '\'': '&#39;'
+ };
+ var _UUID_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
+ // Builds the escape/unescape methods using a common
+ // map of characters
+ var _buildEscapes = function (direction) {
+ return function (str) {
+ s = str || '';
+ s = s.toString();
+ var fr, to;
+ for (var p in _CHARS) {
+ fr = direction == 'to' ? p : _CHARS[p];
+ to = direction == 'to' ? _CHARS[p] : p;
+ s = s.replace(new RegExp(fr, 'gm'), to);
+ }
+ return s;
+ };
+ };
+ // Builds a method that tests for any of the escapable
+ // characters -- useful for avoiding double-escaping if
+ // you're not sure whether a string is already escaped
+ var _buildEscapeTest = function (direction) {
+ return function (s) {
+ var pat = '';
+ for (var p in _CHARS) {
+ pat += direction == 'to' ? p : _CHARS[p];
+ pat += '|';
+ }
+ pat = pat.substr(0, pat.length - 1);
+ pat = new RegExp(pat, "gm");
+ return pat.test(s);
+ };
+ };
+
+ // Escape special chars to entities
+ this.escapeXML = _buildEscapes('to');
+
+ // Unescape entities to special chars
+ this.unescapeXML = _buildEscapes('from');
+
+ // Test if a string includes special chars that
+ // require escaping
+ this.needsEscape = _buildEscapeTest('to');
+
+ this.needsUnescape = _buildEscapeTest('from');
+
+ this.toArray = function (str) {
+ var arr = [];
+ for (var i = 0; i < str.length; i++) {
+ arr[i] = str.substr(i, 1);
+ }
+ return arr;
+ };
+
+ this.reverse = function (str) {
+ return this.toArray(str).reverse().join('');
+ };
+
+ this.ltrim = function (str, chr) {
+ var pat = chr ? new RegExp('^' + chr + '+') : _LTR;
+ return str.replace(pat, '');
+ };
+
+ this.rtrim = function (str, chr) {
+ var pat = chr ? new RegExp(chr + '+$') : _RTR;
+ return str.replace(pat, '');
+ };
+
+ this.trim = function (str, chr) {
+ var pat = chr ? new RegExp('^' + chr + '+|' + chr + '+$', 'g') : _TR;
+ return str.replace(pat, '');
+ };
+
+ this.lpad = function (str, chr, width) {
+ var s = str || ''
+ , len = s.length;
+ if (width > len) {
+ s = (new Array(width - len + 1)).join(chr) + s;
+ }
+ return s;
+ };
+
+ this.rpad = function (str, chr, width) {
+ var s = str || ''
+ , len = s.length;
+ if (width > len) {
+ s = s + (new Array(width - len + 1)).join(chr);
+ }
+ return s;
+ };
+
+ this.truncate = function(string, options, callback) {
+ if (!string) {
+ return;
+ }
+
+ var stringLen = string.length
+ , opts
+ , stringLenWithOmission
+ , last
+ , ignoreCase
+ , multiLine
+ , stringToWorkWith
+ , lastIndexOf
+ , nextStop
+ , result
+ , returnString;
+
+ // If `options` is a number, assume it's the length and
+ // create a options object with length
+ if (typeof options === 'number') {
+ opts = {
+ length: options
+ };
+ }
+ else {
+ opts = options || {};
+ }
+
+ // Set `opts` defaults
+ opts.length = opts.length || 30;
+ opts.omission = opts.omission || opts.ellipsis || '...';
+
+ stringLenWithOmission = opts.length - opts.omission.length;
+
+ // Set the index to stop at for `string`
+ if (opts.seperator) {
+ if (opts.seperator instanceof RegExp) {
+ // If `seperator` is a regex
+ if (opts.seperator.global) {
+ opts.seperator = opts.seperator;
+ } else {
+ ignoreCase = opts.seperator.ignoreCase ? 'i' : ''
+ multiLine = opts.seperator.multiLine ? 'm' : '';
+ opts.seperator = new RegExp(opts.seperator.source,
+ 'g' + ignoreCase + multiLine);
+ }
+ stringToWorkWith = string.substring(0, stringLenWithOmission + 1)
+ lastIndexOf = -1
+ nextStop = 0
+
+ while ((result = opts.seperator.exec(stringToWorkWith))) {
+ lastIndexOf = result.index;
+ opts.seperator.lastIndex = ++nextStop;
+ }
+ last = lastIndexOf;
+ }
+ else {
+ // Seperator is a String
+ last = string.lastIndexOf(opts.seperator, stringLenWithOmission);
+ }
+
+ // If the above couldn't be found, they'll default to -1 so,
+ // we need to just set it as `stringLenWithOmission`
+ if (last === -1) {
+ last = stringLenWithOmission;
+ }
+ }
+ else {
+ last = stringLenWithOmission;
+ }
+
+ if (stringLen < opts.length) {
+ return string;
+ }