// Generic Microformat Parser v0.1 Dan Webb (dan@danwebb.net) // Licenced under the MIT Licence // // var people = HCard.discover(); // people[0].fn => 'Dan Webb' // people[0].urlList => ['http://danwebb.net', 'http://eventwax.com'] // // TODO // // Fix _propFor to work with old safari // Find and use unit testing framework on microformats.org test cases // isue with hcard email? // More formats: HFeed, HEntry, HAtom, RelTag, XFN? // JavaScript 1.6 Iterators and generics cross-browser if (!Array.prototype.forEach) { Array.prototype.forEach = function(func, scope) { scope = scope || this; for (var i = 0, l = this.length; i < l; i++) func.call(scope, this[i], i, this); } } if (typeof Prototype != 'undefined' || !Array.prototype.map) { Array.prototype.map = function(func, scope) { scope = scope || this; var list = []; for (var i = 0, l = this.length; i < l; i++) list.push(func.call(scope, this[i], i, this)); return list; } } if (typeof Prototype != 'undefined' || !Array.prototype.filter) { Array.prototype.filter = function(func, scope) { scope = scope || this; var list = []; for (var i = 0, l = this.length; i < l; i++) if (func.call(scope, this[i], i, this)) list.push(this[i]); return list; } } ['forEach', 'map', 'filter', 'slice', 'concat'].forEach(function(func) { if (!Array[func]) Array[func] = function(object) { return this.prototype[func].apply(object, Array.prototype.slice.call(arguments, 1)); } }); // ISO8601 Date extension Date.ISO8601PartMap = { Year : 1, Month : 3, Date : 5, Hours : 7, Minutes : 8, Seconds : 9 } Date.matchISO8601 = function(text) { return text.match(/^(\d{4})(-?(\d{2}))?(-?(\d{2}))?(T(\d{2}):?(\d{2})(:?(\d{2}))?)?(Z?(([+\-])(\d{2}):?(\d{2})))?$/); } Date.parseISO8601 = function(text) { var dateParts = this.matchISO8601(text); if (dateParts) { var date = new Date, parts, offset = 0; for (var prop in this.ISO8601PartMap) { if (part = dateParts[this.ISO8601PartMap[prop]]) date['set' + prop]((prop == 'Month') ? parseInt(part)-1 : parseInt(part)); else date['set' + prop]((prop == 'Date') ? 1 : 0); } if (dateParts[11]) { offset = (parseInt(dateParts[14]) * 60) + parseInt(dateParts[15]); offset *= ((parseInt[13] == '-') ? 1 : -1); } offset -= date.getTimezoneOffset(); date.setTime(date.getTime() + (offset * 60 * 1000)); return date; } } // Main Microformat namespace Microformat = { define : function(name, spec) { var mf = function(node, data) { this.parentElement = node; Microformat.extend(this, data); }; mf.container = name; mf.format = spec; mf.prototype = Microformat.Base; return Microformat.extend(mf, Microformat.SingletonMethods); }, SingletonMethods : { discover : function(context) { return Microformat.$$(this.container, context).map(function(node) { return new this(node, this._parse(this.format, node)); }, this); }, _parse : function(format, node) { var data = {}; this._process(data, format.one, node, true); this._process(data, format.many, node); return data; }, _process : function(data, format, context, firstOnly) { var selection, first; format = format || []; format.forEach(function(item) { if (typeof item == 'string') { selection = Microformat.$$(item, context); if (firstOnly && (first = selection[0])) { data[this._propFor(item)] = this._extractData(first, 'simple', data); } else if (selection.length > 0) { data[this._propFor(item) + 'List'] = selection.map(function(node) { return this._extractData(node, 'simple', data); }, this); } } else { for (var cls in item) { selection = Microformat.$$(cls, context); if (firstOnly && (first = selection[0])) { data[this._propFor(cls)] = this._extractData(first, item[cls], data); } else if (selection.length > 0) { data[this._propFor(cls + 'List')] = selection.map(function(node) { return this._extractData(node, item[cls], data); }, this); } } } }, this); return data; }, _extractData : function(node, dataType, data) { if (dataType._parse) return dataType._parse(dataType.format, node); if (typeof dataType == 'function') return dataType.call(this, node, data); var values = Microformat.$$('value', node); if (values.length > 0) return this._extractClassValues(node, values); switch (dataType) { case 'simple': return this._extractSimple(node); case 'url': return this._extractURL(node); } return this._parse(dataType, node); }, _extractURL : function(node) { var href; switch (node.nodeName.toLowerCase()) { case 'img': href = node.src; break; case 'area': case 'a': href = node.href; break; case 'object': href = node.data; } if (href) { if (href.indexOf('mailto:') == 0) href = href.replace(/^mailto:/, '').replace(/\?.*$/, ''); return href; } return this._coerce(this._getText(node)); }, _extractSimple : function(node) { switch (node.nodeName.toLowerCase()) { case 'abbr': return this._coerce(node.title); case 'img': return this._coerce(node.alt); } return this._coerce(this._getText(node)); }, _extractClassValues : function(node, values) { var value = new String(values.map(function(value) { return this._extractSimple(value); }, this).join('')); var types = Microformat.$$('type', node); var t = types.map(function(type) { return this._extractSimple(type); }, this); value.types = t; return value; }, _getText : function(node) { if (node.innerText) return node.innerText; return Array.map(node.childNodes, function(node) { if (node.nodeType == 3) return node.nodeValue; else return this._getText(node); }, this).join('').replace(/\s+/g, ' ').replace(/(^\s+)|(\s+)$/g, ''); }, _coerce : function(value) { var date, number; if (value == 'true') return true; if (value == 'false') return false; if (date = Date.parseISO8601(value)) return date; return String(value); }, _propFor : function(name) { this.__propCache = this.__propCache || {}; if (prop = this.__propCache[name]) return prop; return this.__propCache[name] = name.replace(/(-(.))/g, function() { // this isn't going to work on old safari without the fix....hmmm return arguments[2].toUpperCase(); }); }, _handle : function(prop, item, data) { if (this.handlers[prop]) this.handlers[prop].call(this, item, data); } }, // In built getElementsByClassName $$ : function(className, context) { context = context || document; var nodeList; if (context == document || context.nodeType == 1) { if (typeof document.evaluate == 'function') { var xpath = document.evaluate(".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]", context, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); var els = []; for (var i = 0, l = xpath.snapshotLength; i < l; i++) els.push(xpath.snapshotItem(i)); return els; } else nodeList = context.getElementsByTagName('*'); } else nodeList = context; var re = new RegExp('(^|\\s)' + className + '(\\s|$)'); return Array.filter(nodeList, function(node) { return node.className.match(re) }); }, // In built Object.extend equivilent extend : function(dest, source) { for (var prop in source) dest[prop] = source[prop]; return dest; }, // methods available to all instances of a microformat Base : {} };