1 /**	@fileOverview Utilities that just get grafted onto the puredom namespace. */
  2 
  3 puredom.extend(puredom, /** @lends puredom */ {
  4 	
  5 	/** When called as a function, <code>puredom.text()</code> acts as an alias of {@link puredom.text.filter}.
  6 	 *	@namespace A collection of text utilities and a way to apply them as filters.
  7 	 *	@function
  8 	 *	@name puredom.text
  9 	 *	@param {String} str		The string to modify
 10 	 *	@param {String|Array}	A pipe-separated String or Array, where each value is a filter. Arguments to a filter can be passed using a colon followed by a CSV of the arguments.
 11 	 *	@returns {String} The modified string
 12 	 */
 13 	text : (function() {
 14 		/** @exports base as puredom.text */
 15 		
 16 		/** @public A collection of text utilities and a way to apply them as filters. <br />
 17 		 *	<strong>Note:</strong> puredom.text() is a convenient shortcut for this method.
 18 		 *	@param {String} str		The string to modify
 19 		 *	@param {String|Array}	A pipe-separated String or Array, where each value is a filter. Arguments to a filter can be passed using a colon followed by a CSV of the arguments.
 20 		 *	@returns {String} The modified string
 21 		 *	@function
 22 		 */
 23 		var base = function(){
 24 				return base.filter.apply(this, arguments);
 25 			},
 26 			regexes = {
 27 				htmlEntities : /[&<>"]/gim,
 28 				ucWords : /(^|\s)[a-z]/gim,
 29 				ucFirst : /^[a-z]/gim,
 30 				nl2br : /\r?\n/g,
 31 				numbersOnly : /[^0-9.\-]/gim,
 32 				trim : /^\s*?(.*?)\s*?$/gim
 33 			};
 34 		
 35 		/** Modify a string using a filter to apply any functions available in {@link puredom#text}.
 36 		 *	@param {String} str				The string to modify
 37 		 *	@param {String|Array} filters	A bar-separated string or {Array}, where each value is a filter. Arguments to a filter can be passed using a colon followed by a CSV of the arguments.
 38 		 *	@returns {String} The modified string
 39 		 *	@example
 40 		 * puredom.text.filter(" <hi there!> ", 'trim|ucWords|htmlEntities') === "<Hi There!>";
 41 		 * puredom.text("This string might be too long!", 'truncate:10,byWord') === "This string...";
 42 		 */
 43 		base.filter = function(str, filters) {
 44 			var x, filter, ind, args, i;
 45 			if (puredom.typeOf(filters)!=='array') {
 46 				filters = ((filters||'') + '').split('|');
 47 			}
 48 			if (arguments.length>2) {
 49 				for (x=2; x<arguments.length; x++) {
 50 					if (puredom.typeOf(arguments[x])==='array') {
 51 						filters = filters.concat(arguments[x]);
 52 					}
 53 					else {
 54 						filters.push(arguments[x]);
 55 					}
 56 				}
 57 			}
 58 			for (x=0; x<filters.length; x++) {
 59 				filter = filters[x];
 60 				args = [str];
 61 				ind = filter.indexOf(':');
 62 				if (ind>-1) {
 63 					filter = filter.substring(0, ind);
 64 					args = args.concat(filters[x].substring(ind+1).split(','));
 65 				}
 66 				for (i in base) {
 67 					if ((i+'').toLowerCase()===filter.toLowerCase()) {
 68 						str = base[i].apply(base, args);
 69 						break;
 70 					}
 71 				}
 72 			}
 73 			return str;
 74 		};
 75 		
 76 		/**	URL-encode a string. (using encodeURIComponent)
 77 		 *	@param {String} str		The string to modify
 78 		 *	@returns {String} The modified string
 79 		 */
 80 		base.escape = function(str) {
 81 			return encodeURIComponent(str);
 82 		};
 83 		
 84 		/**	URL-decode a string. (using decodeURIComponent)
 85 		 *	@public
 86 		 *	@param {String} str		The string to modify
 87 		 *	@returns {String} The modified string
 88 		 */
 89 		base.unescape = function(str) {
 90 			return decodeURIComponent(str);
 91 		};
 92 		
 93 		/**	Convert special characters to their HTML-encoded equivalents.
 94 		 *	@param {String} str		The string to modify
 95 		 *	@returns {String} The modified string
 96 		 */
 97 		base.htmlEntities = function(str) {
 98 			var map = {
 99 				'&' : '&',
100 				'<' : '<',
101 				'>' : '>',
102 				'"' : '"'
103 			};
104 			return (str+'').replace(regexes.htmlEntities, function(s) {
105 				return map[s];
106 			});
107 		};
108 		
109 		/**	Convert the first character of each word to uppercase.
110 		 *	@param {String} str		The string to modify
111 		 *	@returns {String} The modified string
112 		 */
113 		base.ucWords = function(str) {
114 			return (str+'').toLowerCase().replace(regexes.ucWords, function(s) {
115 				return s.toUpperCase();
116 			});
117 		};
118 		
119 		/**	Convert the first character of the first word to uppercase.
120 		 *	@param {String} str		The string to modify
121 		 *	@returns {String} The modified string
122 		 */
123 		base.ucFirst = function(str) {
124 			return (str+'').toLowerCase().replace(regexes.ucFirst, function(s) {
125 				return s.toUpperCase();
126 			});
127 		};
128 		
129 		/**	Convert newline characters to HTML <br /> elements.
130 		 *	@param {String} str		The string to modify
131 		 *	@returns {String} The modified string
132 		 */
133 		base.nl2br = function(str) {
134 			return (str+'').replace(regexes.nl2br, '<br />');
135 		};
136 		
137 		/**	Strip all non-numeric characters from a string.
138 		 *	@param {String} str		The string to modify
139 		 *	@returns {String} The modified string
140 		 */
141 		base.numbersOnly = function(str) {
142 			return (str+'').replace(regexes.numbersOnly, '');
143 		};
144 		
145 		/** Truncate a string, optionally on word boundaries. <br />
146 		 *	Optionally adds a textual truncation indicator (eg: "...").
147 		 *	@param {String} str						The string to truncate
148 		 *	@param {Number} [maxLen=80]				Maximum string length, in characters.
149 		 *	@param {Boolean|String} [byWord=false]	Don't truncate in the middle of words. Resultant string may be shorter if set to true.
150 		 *	@param {String} [indicatorChars="..."]	Custom indicator characters if truncation occurs. Defaults to "...".
151 		 *	@returns {String} The truncated string
152 		 */
153 		base.truncate = function(str, maxLen, byWord, indicatorChars) {
154 			var trimmed = false,
155 				origStr = str+'';
156 			str = origStr;
157 			maxLen = parseInt(maxLen,10) || 80;
158 			byWord = byWord===true || byWord==='true' || byWord==='byWord';
159 			indicatorChars = indicatorChars || '...';
160 			if (str.length>maxLen) {
161 				if (byWord) {
162 					str = str.substring(0, maxLen);
163 					if (!origStr.charAt(maxLen).match(/\s/)) {
164 						str = str.replace(/\s[^\s]*$/,'');
165 					}
166 				}
167 				else {
168 					str = str.substring(0, maxLen-indicatorChars.length);
169 				}
170 				trimmed = true;
171 			}
172 			if (trimmed) {
173 				str += indicatorChars;
174 			}
175 			return str;
176 		};
177 		
178 		/** Fast JS trim implementation across all browsers. <br />
179 		 *	<em>Note: Research credit goes to http://blog.stevenlevithan.com/archives/faster-trim-javascript</em>
180 		 *	@param {String} str		The string to modify
181 		 *	@returns {String} The modified string
182 		 */
183 		base.trim = function(str) {
184 			//return str.replace(regexes.trim, '$1');
185 			var ws = /\s/, i;
186 			str = str.replace(/^\s\s*/, '');
187 			i = str.length;
188 			while (ws.test(str.charAt(--i)));
189 			return str.slice(0, i + 1);
190 		};
191 		
192 		
193 		/** Default/fallback text. <br />
194 		 *	Used by templates to provide fallback values for empty fields.
195 		 *	@param {String} str		The string to modify
196 		 *	@param {String} text	Default text if str is empty.
197 		 *	@returns {String} The modified string
198 		 */
199 		base['default'] = function(str, text) {
200 			str = base.trim(str);
201 			return str ? str : text;
202 		};
203 		
204 		
205 		/** Format a date using whatever i18n module is registered with puredom. <br />
206 		 *	<em><strong>Note:</strong> Requires a conversion function to be registered as puredom.i18n() in order to convert dates.</em>
207 		 *	@requires puredom.i18n
208 		 *	@param {String} str				The string to modify
209 		 *	@param {String} [type=date]		A date type to pass to i18n. Defaults to "date".
210 		 *	@returns {String} The formatted date string
211 		 */
212 		base.dateformat = function(str, type) {
213 			var i18n = puredom.i18n,
214 				d = puredom.date,
215 				date;
216 			if (d && d.create) {
217 				date = d.create(str);
218 			}
219 			if (!date || (date+'').indexOf('Invalid')>-1) {
220 				date = new Date(str);
221 				if (!date || (date+'').indexOf('Invalid')>-1) {
222 					date = new Date();
223 					date.setTime(Math.round(str));
224 				}
225 			}
226 			if (type && type.indexOf('%')>-1) {
227 				if (d && d.format) {
228 					str = d.format(date, type);
229 				}
230 			}
231 			else if (i18n) {
232 				str = i18n(date, null, null, {
233 					datetype : type || 'date'
234 				}) || (date+'');
235 			}
236 			return str;
237 		};
238 		
239 		
240 		return base;
241 	}()),
242 	
243 	
244 	
245 	/**	Convert an object to a sequence of URL-encoded key-value parameters.
246 	 *		This function is the same as {@link puredom.querystring.stringify}, except that
247 	 *		it prepends a '?' to the result by default. (ie: startDelimiter is '?' by default)
248 	 *	@name puredom.parameterize
249 	 *	@param {Object} obj		The object to serialize
250 	 *	@param config			Configuration overrides. See {@link puredom.querystring.stringify}
251 	 *	@see puredom.querystring.stringify
252 	 *	@deprecated
253 	 *	@private
254 	 *	@returns {String} The generated querystring
255 	 */
256 	parameterize : function(obj, customConfig) {
257 		var t = [],
258 			key, value, x, type,
259 			config = puredom.extend({
260 				delimiter		: '&',
261 				startDelimiter	: '?',
262 				assignment		: '=',
263 				typeHandlers	: null
264 			}, customConfig);
265 		
266 		for (key in obj) {
267 			if (obj.hasOwnProperty(key)) {
268 				value = obj[key];
269 				type = this.typeOf(value);
270 				if (config.typeHandlers && config.typeHandlers.hasOwnProperty(type)) {
271 					t.push( config.delimiter + encodeURIComponent(key) + "=" + encodeURIComponent(config.typeHandlers[type](value)) );
272 				}
273 				else if (type==='array' && config.disableArrayParams!==true) {
274 					for (x=0; x<value.length; x++) {
275 						t.push( config.delimiter + encodeURIComponent(key) + "[]=" + encodeURIComponent(value[x]) );
276 					}
277 				}
278 				else {
279 					switch (type) {
280 						case 'boolean':
281 							value = value ? 'true' : 'false';
282 							break;
283 						case 'null':
284 						case 'undefined':
285 							value = '';
286 							break;
287 						case 'object':
288 							if (config.useJsonForObjects!==false) {
289 								// nested objects get serialized as JSON by default:
290 								value = this.json(value);
291 							}
292 							else {
293 								// alternatively, they can be serialized by double-encoding:
294 								value = this.parameterize(value);
295 							}
296 							break;
297 					}
298 					t.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
299 				}
300 			}
301 		}
302 		t = t.join(config.delimiter || '&');
303 		t = (config.startDelimiter || '') + t;
304 		return t;
305 	},
306 	
307 	
308 	
309 	/** @namespace Handles querystring encoding and decoding.
310 	 *	@name puredom.querystring
311 	 */
312 	querystring : {
313 		/** @lends puredom.querystring */
314 		
315 		/** Parse a querystring and return an {Object} with the key-value pairs as its properties.
316 		 *	<em>Note: Preceeding '?' and '&' characters will be stripped. Empty parameters will be returned as empty strings.</em>
317 		 *	@param {String} querystring		The querystring to parse.
318 		 *	@returns {Object} The key-value parameters as an object.
319 		 */
320 		parse : function(str) {
321 			var parts, i, j, p={};
322 			if (str.substring(0,1)==='?') {
323 				str = str.substring(1);
324 			}
325 			parts = str.split('&');
326 			for (i=0; i<parts.length; i++) {
327 				if (parts[i]) {
328 					j = parts[i].indexOf('=');
329 					p[decodeURIComponent(parts[i].substring(0,j))] = j<0 ? '' : decodeURIComponent(parts[i].substring(j+1));
330 				}
331 			}
332 			return p;
333 		},
334 		
335 		/** Convert an object into a querystring, optionally with custom separator/delimiter characters.
336 		 *	<em>Note: Nested objects are serialized as double-encoded querystring parameters by default. To use JSON for nested objects, set the "useJsonForObjects" flag to true.</em>
337 		 *	Available options:
338 		 *		{Boolean} useJsonForObjects		Use JSON to serialize nested objects? (uses double-encoding by default)
339 		 *		{Boolean} disableArrayParams	Disable PHP-style "array parameters? ex: p[]=foo&p[]=bar
340 		 *		{Object} typeHandlers			Specify custom serializers for each data type by setting type:handler. Handlers accept the original data and return the serialized parameter value, *not* URL-encoded.
341 		 *		{String} assignment				The key-value separator. Defaults to "=".
342 		 *		{String} delimiter				The group separator. Defaults to "&".
343 		 *		{String} startDelimiter			A character to insert at the beginning of the string. Defaults to none.
344 		 *	@param {Object} parameters		A key-value map of parameters to serialize.
345 		 *	@param {Object} [options]		A hashmap of configuration options.
346 		 */
347 		stringify : function(parameters, options) {
348 			options = puredom.extend({ startDelimiter:'' }, options || {});
349 			return puredom.parameterize(parameters, options);
350 		},
351 		build : function(){return puredom.querystring.stringify.apply(puredom.querystring,arguments);}
352 	},
353 	
354 	
355 	
356 	/**	@namespace Handles storage and retrieval of cookies.
357 	 *	@name puredom.cookies
358 	 */
359 	cookies : (function(){
360 		var cache = {};
361 		
362 		return /** @lends puredom.cookies */ {
363 			
364 			/**	Set a cookie with name *key* to value *value*
365 			 *	@exports set as puredom.cookies.set
366 			 *	@param {String} key		The key for storage
367 			 *	@param {String} value	A value to store
368 			 *	@param {Number} days	The cookie lifetime in number of days.
369 			 */
370 			set : function (key, value, days, domain, path, secure) {
371 				var expires = '',
372 					cookie = '',
373 					date;
374 				path = typeof path==='string' ? path : '';
375 				if (days) {
376 					date = new Date();
377 					date.setTime(date.getTime() + days*24*60*60*1000);
378 					expires = "; expires="+date.toGMTString();
379 				}
380 				if(cache.hasOwnProperty(key) && cache[key].expires) {
381 					expires = "; expires="+cache[key].expires.toGMTString();
382 				}
383 				cookie = key + "=" + encodeURIComponent(value) + expires + "; path=/"+path.replace(/^\//,'');
384 				if (typeof domain==='string' && domain.length>0) {
385 					cookie += '; domain=' + domain.replace(/[\;\,]/,'');
386 				}
387 				if (secure===true) {
388 					cookie += '; secure';
389 				}
390 				//puredom.log('puredom.cookies.set() :: ' + cookie);
391 				document.cookie = cookie;
392 				cache[key] = {
393 					value : value,
394 					expires : date
395 				};
396 			},
397 			
398 			/**	Get a cookie. Pulls values from cache when possible.
399 			 *	@exports get as puredom.cookies.get
400 			 *	@param {String} key					The key to lookup
401 			 *	@param {Boolean} [useCached=true]	Use cached value if present
402 			 *	@returns {String} value				The value, or <code>null</code> if the lookup failed.
403 			 */
404 			get : function (key, useCached) {
405 				if(cache.hasOwnProperty(key) && useCached!==true) {
406 					return cache[key].value;
407 				}
408 				var c, i, ca = document.cookie.split(';');
409 				for (i=0; i<ca.length; i++) {
410 					c = ca[i].replace(/^\s+/gim,'');
411 					if (c.indexOf(key+"=")===0) {
412 						return decodeURIComponent(c.substring(key.length+1,c.length));
413 					}
414 				}
415 				return null;
416 			},
417 			/**	Remove a cookie and any cached values
418 			 *	@param {String} key		The key to remove
419 			 */
420 			remove	: function (key) {
421 				this.set(key, "", -1);
422 				delete cache[key];
423 			},
424 			/**	Remove all cookies and cached values */
425 			purge	: function () {
426 				for (var x in cache) {
427 					if(cache.hasOwnProperty(x)) {
428 						this.remove(x);
429 						delete cache[x];
430 					}
431 				}
432 			},
433 			/** Alias of {@link puredom.cookies.get}
434 			 *	@see puredom.cookies.get
435 			 *	@private
436 			 */
437 			read : function() {
438 				return this.get.apply(this,arguments);
439 			},
440 			/** Alias of {@link puredom.cookies.set}
441 			 *	@see puredom.cookies.set
442 			 *	@private
443 			 */
444 			write : function() {
445 				return this.set.apply(this,arguments);
446 			}
447 		};
448 	}()),
449 	
450 	
451 	/**	@ignore */
452 	Cache : (function() {
453 		/** @class In-memeory cache class with a twist! <br />
454 		 *	Set and get work like a normal cache.
455 		 *	Creates a new Cache instance.
456 		 *	@name puredom.Cache
457 		 */
458 		function Cache() {
459 			if (this.constructor!==arguments.callee && this.constructor!==Cache) {
460 				return new Cache();
461 			}
462 			this.data = {};
463 		}
464 		
465 		puredom.extend(Cache.prototype, /** @lends puredom.Cache# */ {
466 			
467 			/** The default *type* used for namespacing keys is "_default" */
468 			defaultType : '_default',
469 			
470 			/** Purge all entries from the cache */
471 			purge : function() {
472 				this.data = {};
473 			},
474 			
475 			/** Get a cached value with optional type. 
476 			 *	@param {String} [type]		A type prefix.
477 			 *	@param {String|Number} id	The cache entry ID
478 			 *	@param {Function} callback	A callback, gets passed the cached value once retrieved.
479 			 */
480 			get : function(type, id, cb) {
481 				var d;
482 				if (arguments.length===2) {
483 					id = type;
484 					cb = id;
485 					type = null;
486 				}
487 				type = (type || this.defaultType)+'';
488 				id = id+'';
489 				d = this.data.hasOwnProperty(type) && this.data[type][id] || false;
490 				if (cb) {
491 					if (d) {
492 						cb(d);
493 					}
494 					return !!d;
495 				}
496 				return d;
497 			},
498 			
499 			/** Get a cached value with optional type. 
500 			 *	@param {String} [type]		A type prefix.
501 			 *	@param {String|Number} id	The cache entry ID
502 			 *	@param value				Any value to cache.
503 			 */
504 			set : function(type, id, val) {
505 				if (arguments.length===2) {
506 					id = type;
507 					val = id;
508 					type = null;
509 				}
510 				type = (type || this.defaultType)+'';
511 				id = id+'';
512 				if (!this.data[type]) {
513 					this.data[type] = {};
514 				}
515 				this.data[type][id] = val;
516 			},
517 			
518 			/** Proxy a callback function for automatically caching asynchronous responses.
519 			 *	@param {String} [type]		A type prefix.
520 			 *	@param {String|Number} id	The cache entry ID
521 			 *	@param {Function} callback	The callback function to inject c
522 			 *	@param {Number} paramIndex	Which callback parameter to cache (0-based).
523 			 *	@returns {Function} The proxied callback function, with the cache set injected.
524 			 */
525 			proxySet : function(type, id, callback, paramIndex) {
526 				var self = this;	//, cb;
527 				//cb = function() {
528 				return function() {
529 					self.set(type, id, arguments[paramIndex || 0]);
530 					if (callback) {
531 						callback.apply(callback, arguments);
532 					}
533 					//self = cb = type = id = callback = paramIndex = null;
534 				};
535 				//return cb;
536 			},
537 			
538 			/** Iterate over all the cache entries.
539 			 *	@param {Function} iterator	Gets passed each entry.
540 			 */
541 			each : function(iterator) {
542 				return puredom.foreach(this.data, iterator);
543 			}
544 		});
545 		return Cache;
546 	}()),
547 	
548 	
549 	
550 	/** @namespace Parse and generate JSON.
551 	 *	When called as a function, <code>puredom.json()</code> automatically converts between JSON-Strings and Objects.
552 	 *	@function
553 	 *	@name puredom.json
554 	 *	@param {String|Object|Array} what		If a String is passed, it is parsed as JSON. Otherwise, returns JSON-encoded value of <code>what</code>.
555 	 *	@returns {String|Object|Array} jsonStringOrJsonResult
556 	 */
557 	json : (function() {
558 		/** @exports json as puredom.json */
559 		
560 		/**	@private */
561 		var json = function(what) {
562 			if (puredom.typeOf(what)==="string") {
563 				return json.parse(what);
564 			}
565 			return json.stringify(what);
566 		};
567 		
568 		/** Serialize a JavaScript object structure to a JSON string.<br />
569 		 *	<em>Note: Circular references cause this function to fail.</em>
570 		 *	@param what			Any object of any type.
571 		 *	@returns {String} The JSON-encoded string
572 		 */
573 		json.stringify = function(what) {
574 			var result;
575 			try {
576 				result = JSON.stringify(what);
577 			}catch(err) {
578 				puredom.log("puredom.json:: Stringify failed: " + err + " | " + what);
579 			}
580 			return result;
581 		};
582 		
583 		/** Parse JSON from a {String} and return the resulting object.
584 		 *	@param {String} json	A string containing JSON.
585 		 *	@returns {Object|Array|String|Number} jsonResult
586 		 *	@example
587 		 *		var obj = puredom.json.parse('{"items":[{"title":"Example"}]}');
588 		 */
589 		json.parse = function(what) {
590 			var result;
591 			if (typeof what==='string' && what.length>0) {
592 				try {
593 					result = JSON.parse(what);
594 				}catch(err) {
595 					puredom.log("puredom.json:: Parse failed: " + err + " | " + what);
596 				}
597 			}
598 			return result;
599 		};
600 		
601 		/**	Alias of {@link puredom.json.stringify}
602 		 *	@function
603 		 *	@deprecated
604 		 *	@private
605 		 */
606 		json.serialize = json.stringify;
607 		
608 		/**	Alias of {@link puredom.json.parse}
609 		 *	@function
610 		 *	@deprecated
611 		 *	@private
612 		 */
613 		json.unserialize = json.parse;
614 		
615 		return json;
616 	}()),
617 	
618 	
619 	
620 	/** @namespace Parse and generate XML.
621 	 *	@name puredom.xml
622 	 */
623 	xml : /** @lends puredom.xml */ {
624 		
625 		/** Parse XML from a string and return the resulting {Document}.
626 		 *	@param {String} xmlString		The XML to parse
627 		 *	@returns {Document} The XML document.
628 		 *	@example
629 		 *		var doc = puredom.xml.parse('<items><item><title>Example</title></item></items>');
630 		 */
631 		parse : function(xmlString) {
632 			var xmlDoc;
633 			if (window.DOMParser) {
634 				xmlDoc = new window.DOMParser().parseFromString(xmlString, "text/xml");
635 			}
636 			else {
637 				// Internet Explorer
638 				xmlDoc = new window.ActiveXObject("Microsoft.XMLDOM");
639 				xmlDoc.async = "false";
640 				xmlDoc.loadXML(xmlString);
641 			}
642 			return xmlDoc;
643 		}
644 	}
645 	
646 });