michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = ["Microformats", "adr", "tag", "hCard", "hCalendar", "geo"]; michael@0: michael@0: this.Microformats = { michael@0: /* When a microformat is added, the name is placed in this list */ michael@0: list: [], michael@0: /* Custom iterator so that microformats can be enumerated as */ michael@0: /* for (i in Microformats) */ michael@0: __iterator__: function () { michael@0: for (let i=0; i < this.list.length; i++) { michael@0: yield this.list[i]; michael@0: } michael@0: }, michael@0: /** michael@0: * Retrieves microformats objects of the given type from a document michael@0: * michael@0: * @param name The name of the microformat (required) michael@0: * @param rootElement The DOM element at which to start searching (required) michael@0: * @param options Literal object with the following options: michael@0: * recurseExternalFrames - Whether or not to search child frames michael@0: * that reference external pages (with a src attribute) michael@0: * for microformats (optional - defaults to true) michael@0: * showHidden - Whether or not to add hidden microformat michael@0: * (optional - defaults to false) michael@0: * debug - Whether or not we are in debug mode (optional michael@0: * - defaults to false) michael@0: * @param targetArray An array of microformat objects to which is added the results (optional) michael@0: * @return A new array of microformat objects or the passed in microformat michael@0: * object array with the new objects added michael@0: */ michael@0: get: function(name, rootElement, options, targetArray) { michael@0: function isAncestor(haystack, needle) { michael@0: var parent = needle; michael@0: while (parent = parent.parentNode) { michael@0: /* We need to check parentNode because defaultView.frames[i].frameElement */ michael@0: /* isn't a real DOM node */ michael@0: if (parent == needle.parentNode) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: } michael@0: if (!Microformats[name] || !rootElement) { michael@0: return; michael@0: } michael@0: targetArray = targetArray || []; michael@0: michael@0: /* Root element might not be the document - we need the document's default view */ michael@0: /* to get frames and to check their ancestry */ michael@0: var defaultView = rootElement.defaultView || rootElement.ownerDocument.defaultView; michael@0: var rootDocument = rootElement.ownerDocument || rootElement; michael@0: michael@0: /* If recurseExternalFrames is undefined or true, look through all child frames for microformats */ michael@0: if (!options || !options.hasOwnProperty("recurseExternalFrames") || options.recurseExternalFrames) { michael@0: if (defaultView && defaultView.frames.length > 0) { michael@0: for (let i=0; i < defaultView.frames.length; i++) { michael@0: if (isAncestor(rootDocument, defaultView.frames[i].frameElement)) { michael@0: Microformats.get(name, defaultView.frames[i].document, options, targetArray); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: /* Get the microformat nodes for the document */ michael@0: var microformatNodes = []; michael@0: if (Microformats[name].className) { michael@0: microformatNodes = Microformats.getElementsByClassName(rootElement, michael@0: Microformats[name].className); michael@0: /* alternateClassName is for cases where a parent microformat is inferred by the children */ michael@0: /* If we find alternateClassName, the entire document becomes the microformat */ michael@0: if ((microformatNodes.length == 0) && Microformats[name].alternateClassName) { michael@0: var altClass = Microformats.getElementsByClassName(rootElement, Microformats[name].alternateClassName); michael@0: if (altClass.length > 0) { michael@0: microformatNodes.push(rootElement); michael@0: } michael@0: } michael@0: } else if (Microformats[name].attributeValues) { michael@0: microformatNodes = michael@0: Microformats.getElementsByAttribute(rootElement, michael@0: Microformats[name].attributeName, michael@0: Microformats[name].attributeValues); michael@0: michael@0: } michael@0: michael@0: michael@0: function isVisible(node, checkChildren) { michael@0: if (node.getBoundingClientRect) { michael@0: var box = node.getBoundingClientRect(); michael@0: } else { michael@0: var box = node.ownerDocument.getBoxObjectFor(node); michael@0: } michael@0: /* If the parent has is an empty box, double check the children */ michael@0: if ((box.height == 0) || (box.width == 0)) { michael@0: if (checkChildren && node.childNodes.length > 0) { michael@0: for(let i=0; i < node.childNodes.length; i++) { michael@0: if (node.childNodes[i].nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) { michael@0: /* For performance reasons, we only go down one level */ michael@0: /* of children */ michael@0: if (isVisible(node.childNodes[i], false)) { michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return false michael@0: } michael@0: return true; michael@0: } michael@0: michael@0: /* Create objects for the microformat nodes and put them into the microformats */ michael@0: /* array */ michael@0: for (let i = 0; i < microformatNodes.length; i++) { michael@0: /* If showHidden undefined or false, don't add microformats to the list that aren't visible */ michael@0: if (!options || !options.hasOwnProperty("showHidden") || !options.showHidden) { michael@0: if (microformatNodes[i].ownerDocument) { michael@0: if (!isVisible(microformatNodes[i], true)) { michael@0: continue; michael@0: } michael@0: } michael@0: } michael@0: try { michael@0: if (options && options.debug) { michael@0: /* Don't validate in the debug case so that we don't get errors thrown */ michael@0: /* in the debug case, we want all microformats, even if they are invalid */ michael@0: targetArray.push(new Microformats[name].mfObject(microformatNodes[i], false)); michael@0: } else { michael@0: targetArray.push(new Microformats[name].mfObject(microformatNodes[i], true)); michael@0: } michael@0: } catch (ex) { michael@0: /* Creation of individual object probably failed because it is invalid. */ michael@0: /* This isn't a problem, because the page might have invalid microformats */ michael@0: } michael@0: } michael@0: return targetArray; michael@0: }, michael@0: /** michael@0: * Counts microformats objects of the given type from a document michael@0: * michael@0: * @param name The name of the microformat (required) michael@0: * @param rootElement The DOM element at which to start searching (required) michael@0: * @param options Literal object with the following options: michael@0: * recurseExternalFrames - Whether or not to search child frames michael@0: * that reference external pages (with a src attribute) michael@0: * for microformats (optional - defaults to true) michael@0: * showHidden - Whether or not to add hidden microformat michael@0: * (optional - defaults to false) michael@0: * debug - Whether or not we are in debug mode (optional michael@0: * - defaults to false) michael@0: * @return The new count michael@0: */ michael@0: count: function(name, rootElement, options) { michael@0: var mfArray = Microformats.get(name, rootElement, options); michael@0: if (mfArray) { michael@0: return mfArray.length; michael@0: } michael@0: return 0; michael@0: }, michael@0: /** michael@0: * Returns true if the passed in node is a microformat. Does NOT return true michael@0: * if the passed in node is a child of a microformat. michael@0: * michael@0: * @param node DOM node to check michael@0: * @return true if the node is a microformat, false if it is not michael@0: */ michael@0: isMicroformat: function(node) { michael@0: for (let i in Microformats) michael@0: { michael@0: if (Microformats[i].className) { michael@0: if (Microformats.matchClass(node, Microformats[i].className)) { michael@0: return true; michael@0: } michael@0: } else { michael@0: var attribute; michael@0: if (attribute = node.getAttribute(Microformats[i].attributeName)) { michael@0: var attributeList = Microformats[i].attributeValues.split(" "); michael@0: for (let j=0; j < attributeList.length; j++) { michael@0: if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) { michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return false; michael@0: }, michael@0: /** michael@0: * This function searches a given nodes ancestors looking for a microformat michael@0: * and if it finds it, returns it. It does NOT include self, so if the passed michael@0: * in node is a microformat, it will still search ancestors for a microformat. michael@0: * michael@0: * @param node DOM node to check michael@0: * @return If the node is contained in a microformat, it returns the parent michael@0: * DOM node, otherwise returns null michael@0: */ michael@0: getParent: function(node) { michael@0: var xpathExpression; michael@0: var xpathResult; michael@0: michael@0: xpathExpression = "ancestor::*["; michael@0: for (let i=0; i < Microformats.list.length; i++) { michael@0: var mfname = Microformats.list[i]; michael@0: if (i != 0) { michael@0: xpathExpression += " or "; michael@0: } michael@0: if (Microformats[mfname].className) { michael@0: xpathExpression += "contains(concat(' ', @class, ' '), ' " + Microformats[mfname].className + " ')"; michael@0: } else { michael@0: var attributeList = Microformats[mfname].attributeValues.split(" "); michael@0: for (let j=0; j < attributeList.length; j++) { michael@0: if (j != 0) { michael@0: xpathExpression += " or "; michael@0: } michael@0: xpathExpression += "contains(concat(' ', @" + Microformats[mfname].attributeName + ", ' '), ' " + attributeList[j] + " ')"; michael@0: } michael@0: } michael@0: } michael@0: xpathExpression += "][1]"; michael@0: xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null, Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null); michael@0: if (xpathResult.singleNodeValue) { michael@0: xpathResult.singleNodeValue.microformat = mfname; michael@0: return xpathResult.singleNodeValue; michael@0: } michael@0: return null; michael@0: }, michael@0: /** michael@0: * If the passed in node is a microformat, this function returns a space michael@0: * separated list of the microformat names that correspond to this node michael@0: * michael@0: * @param node DOM node to check michael@0: * @return If the node is a microformat, a space separated list of microformat michael@0: * names, otherwise returns nothing michael@0: */ michael@0: getNamesFromNode: function(node) { michael@0: var microformatNames = []; michael@0: var xpathExpression; michael@0: var xpathResult; michael@0: for (let i in Microformats) michael@0: { michael@0: if (Microformats[i]) { michael@0: if (Microformats[i].className) { michael@0: if (Microformats.matchClass(node, Microformats[i].className)) { michael@0: microformatNames.push(i); michael@0: continue; michael@0: } michael@0: } else if (Microformats[i].attributeValues) { michael@0: var attribute; michael@0: if (attribute = node.getAttribute(Microformats[i].attributeName)) { michael@0: var attributeList = Microformats[i].attributeValues.split(" "); michael@0: for (let j=0; j < attributeList.length; j++) { michael@0: /* If we match any attribute, we've got a microformat */ michael@0: if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) { michael@0: microformatNames.push(i); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return microformatNames.join(" "); michael@0: }, michael@0: /** michael@0: * Outputs the contents of a microformat object for debug purposes. michael@0: * michael@0: * @param microformatObject JavaScript object that represents a microformat michael@0: * @return string containing a visual representation of the contents of the microformat michael@0: */ michael@0: debug: function debug(microformatObject) { michael@0: function dumpObject(item, indent) michael@0: { michael@0: if (!indent) { michael@0: indent = ""; michael@0: } michael@0: var toreturn = ""; michael@0: var testArray = []; michael@0: michael@0: for (let i in item) michael@0: { michael@0: if (testArray[i]) { michael@0: continue; michael@0: } michael@0: if (typeof item[i] == "object") { michael@0: if ((i != "node") && (i != "resolvedNode")) { michael@0: if (item[i] && item[i].semanticType) { michael@0: toreturn += indent + item[i].semanticType + " [" + i + "] { \n"; michael@0: } else { michael@0: toreturn += indent + "object " + i + " { \n"; michael@0: } michael@0: toreturn += dumpObject(item[i], indent + "\t"); michael@0: toreturn += indent + "}\n"; michael@0: } michael@0: } else if ((typeof item[i] != "function") && (i != "semanticType")) { michael@0: if (item[i]) { michael@0: toreturn += indent + i + "=" + item[i] + "\n"; michael@0: } michael@0: } michael@0: } michael@0: if (!toreturn && item) { michael@0: toreturn = item.toString(); michael@0: } michael@0: return toreturn; michael@0: } michael@0: return dumpObject(microformatObject); michael@0: }, michael@0: add: function add(microformat, microformatDefinition) { michael@0: /* We always replace an existing definition with the new one */ michael@0: if (!Microformats[microformat]) { michael@0: Microformats.list.push(microformat); michael@0: } michael@0: Microformats[microformat] = microformatDefinition; michael@0: microformatDefinition.mfObject.prototype.debug = michael@0: function(microformatObject) { michael@0: return Microformats.debug(microformatObject) michael@0: }; michael@0: }, michael@0: remove: function remove(microformat) { michael@0: if (Microformats[microformat]) { michael@0: var list = Microformats.list; michael@0: var index = list.indexOf(microformat, 1); michael@0: if (index != -1) { michael@0: list.splice(index, 1); michael@0: } michael@0: delete Microformats[microformat]; michael@0: } michael@0: }, michael@0: /* All parser specific functions are contained in this object */ michael@0: parser: { michael@0: /** michael@0: * Uses the microformat patterns to decide what the correct text for a michael@0: * given microformat property is. This includes looking at things like michael@0: * abbr, img/alt, area/alt and value excerpting. michael@0: * michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: & @param datatype HTML/text - whether to use innerHTML or innerText - defaults to text michael@0: * @return A string with the value of the property michael@0: */ michael@0: defaultGetter: function(propnode, parentnode, datatype) { michael@0: function collapseWhitespace(instring) { michael@0: /* Remove new lines, carriage returns and tabs */ michael@0: outstring = instring.replace(/[\n\r\t]/gi, ' '); michael@0: /* Replace any double spaces with single spaces */ michael@0: outstring = outstring.replace(/\s{2,}/gi, ' '); michael@0: /* Remove any double spaces that are left */ michael@0: outstring = outstring.replace(/\s{2,}/gi, ''); michael@0: /* Remove any spaces at the beginning */ michael@0: outstring = outstring.replace(/^\s+/, ''); michael@0: /* Remove any spaces at the end */ michael@0: outstring = outstring.replace(/\s+$/, ''); michael@0: return outstring; michael@0: } michael@0: michael@0: michael@0: if (((((propnode.localName.toLowerCase() == "abbr") || (propnode.localName.toLowerCase() == "html:abbr")) && !propnode.namespaceURI) || michael@0: ((propnode.localName.toLowerCase() == "abbr") && (propnode.namespaceURI == "http://www.w3.org/1999/xhtml"))) && (propnode.hasAttribute("title"))) { michael@0: return propnode.getAttribute("title"); michael@0: } else if ((propnode.nodeName.toLowerCase() == "img") && (propnode.hasAttribute("alt"))) { michael@0: return propnode.getAttribute("alt"); michael@0: } else if ((propnode.nodeName.toLowerCase() == "area") && (propnode.hasAttribute("alt"))) { michael@0: return propnode.getAttribute("alt"); michael@0: } else if ((propnode.nodeName.toLowerCase() == "textarea") || michael@0: (propnode.nodeName.toLowerCase() == "select") || michael@0: (propnode.nodeName.toLowerCase() == "input")) { michael@0: return propnode.value; michael@0: } else { michael@0: var values = Microformats.getElementsByClassName(propnode, "value"); michael@0: /* Verify that values are children of the propnode */ michael@0: for (let i = values.length-1; i >= 0; i--) { michael@0: if (values[i].parentNode != propnode) { michael@0: values.splice(i,1); michael@0: } michael@0: } michael@0: if (values.length > 0) { michael@0: var value = ""; michael@0: for (let j=0;j 0) { michael@0: return s; michael@0: } michael@0: } michael@0: }, michael@0: /** michael@0: * Used to specifically retrieve a date in a microformat node. michael@0: * After getting the default text, it normalizes it to an ISO8601 date. michael@0: * michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return A string with the normalized date. michael@0: */ michael@0: dateTimeGetter: function(propnode, parentnode) { michael@0: var date = Microformats.parser.textGetter(propnode, parentnode); michael@0: if (date) { michael@0: return Microformats.parser.normalizeISO8601(date); michael@0: } michael@0: }, michael@0: /** michael@0: * Used to specifically retrieve a URI in a microformat node. This includes michael@0: * looking at an href/img/object/area to get the fully qualified URI. michael@0: * michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return A string with the fully qualified URI. michael@0: */ michael@0: uriGetter: function(propnode, parentnode) { michael@0: var pairs = {"a":"href", "img":"src", "object":"data", "area":"href"}; michael@0: var name = propnode.nodeName.toLowerCase(); michael@0: if (pairs.hasOwnProperty(name)) { michael@0: return propnode[pairs[name]]; michael@0: } michael@0: return Microformats.parser.textGetter(propnode, parentnode); michael@0: }, michael@0: /** michael@0: * Used to specifically retrieve a telephone number in a microformat node. michael@0: * Basically this is to handle the face that telephone numbers use value michael@0: * as the name as one of their subproperties, but value is also used for michael@0: * value excerpting (http://microformats.org/wiki/hcard#Value_excerpting) michael@0: michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return A string with the telephone number michael@0: */ michael@0: telGetter: function(propnode, parentnode) { michael@0: var pairs = {"a":"href", "object":"data", "area":"href"}; michael@0: var name = propnode.nodeName.toLowerCase(); michael@0: if (pairs.hasOwnProperty(name)) { michael@0: var protocol; michael@0: if (propnode[pairs[name]].indexOf("tel:") == 0) { michael@0: protocol = "tel:"; michael@0: } michael@0: if (propnode[pairs[name]].indexOf("fax:") == 0) { michael@0: protocol = "fax:"; michael@0: } michael@0: if (propnode[pairs[name]].indexOf("modem:") == 0) { michael@0: protocol = "modem:"; michael@0: } michael@0: if (protocol) { michael@0: if (propnode[pairs[name]].indexOf('?') > 0) { michael@0: return unescape(propnode[pairs[name]].substring(protocol.length, propnode[pairs[name]].indexOf('?'))); michael@0: } else { michael@0: return unescape(propnode[pairs[name]].substring(protocol.length)); michael@0: } michael@0: } michael@0: } michael@0: /* Special case - if this node is a value, use the parent node to get all the values */ michael@0: if (Microformats.matchClass(propnode, "value")) { michael@0: return Microformats.parser.textGetter(parentnode, parentnode); michael@0: } else { michael@0: /* Virtual case */ michael@0: if (!parentnode && (Microformats.getElementsByClassName(propnode, "type").length > 0)) { michael@0: var tempNode = propnode.cloneNode(true); michael@0: var typeNodes = Microformats.getElementsByClassName(tempNode, "type"); michael@0: for (let i=0; i < typeNodes.length; i++) { michael@0: typeNodes[i].parentNode.removeChild(typeNodes[i]); michael@0: } michael@0: return Microformats.parser.textGetter(tempNode); michael@0: } michael@0: return Microformats.parser.textGetter(propnode, parentnode); michael@0: } michael@0: }, michael@0: /** michael@0: * Used to specifically retrieve an email address in a microformat node. michael@0: * This includes at an href, as well as removing subject if specified and michael@0: * the mailto prefix. michael@0: * michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return A string with the email address. michael@0: */ michael@0: emailGetter: function(propnode, parentnode) { michael@0: if ((propnode.nodeName.toLowerCase() == "a") || (propnode.nodeName.toLowerCase() == "area")) { michael@0: var mailto = propnode.href; michael@0: /* IO Service won't fully parse mailto, so we do it manually */ michael@0: if (mailto.indexOf('?') > 0) { michael@0: return unescape(mailto.substring("mailto:".length, mailto.indexOf('?'))); michael@0: } else { michael@0: return unescape(mailto.substring("mailto:".length)); michael@0: } michael@0: } else { michael@0: /* Special case - if this node is a value, use the parent node to get all the values */ michael@0: /* If this case gets executed, per the value design pattern, the result */ michael@0: /* will be the EXACT email address with no extra parsing required */ michael@0: if (Microformats.matchClass(propnode, "value")) { michael@0: return Microformats.parser.textGetter(parentnode, parentnode); michael@0: } else { michael@0: /* Virtual case */ michael@0: if (!parentnode && (Microformats.getElementsByClassName(propnode, "type").length > 0)) { michael@0: var tempNode = propnode.cloneNode(true); michael@0: var typeNodes = Microformats.getElementsByClassName(tempNode, "type"); michael@0: for (let i=0; i < typeNodes.length; i++) { michael@0: typeNodes[i].parentNode.removeChild(typeNodes[i]); michael@0: } michael@0: return Microformats.parser.textGetter(tempNode); michael@0: } michael@0: return Microformats.parser.textGetter(propnode, parentnode); michael@0: } michael@0: } michael@0: }, michael@0: /** michael@0: * Used when a caller needs the text inside a particular DOM node. michael@0: * It calls defaultGetter to handle all the subtleties of getting michael@0: * text from a microformat. michael@0: * michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return A string with just the text including all tags. michael@0: */ michael@0: textGetter: function(propnode, parentnode) { michael@0: return Microformats.parser.defaultGetter(propnode, parentnode, "text"); michael@0: }, michael@0: /** michael@0: * Used when a caller needs the HTML inside a particular DOM node. michael@0: * michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return An emulated string object that also has a new function called toHTML michael@0: */ michael@0: HTMLGetter: function(propnode, parentnode) { michael@0: /* This is so we can have a string that behaves like a string */ michael@0: /* but also has a new function that can return the HTML that corresponds */ michael@0: /* to the string. */ michael@0: function mfHTML(value) { michael@0: this.valueOf = function() {return value ? value.valueOf() : "";} michael@0: this.toString = function() {return value ? value.toString() : "";} michael@0: } michael@0: mfHTML.prototype = new String; michael@0: mfHTML.prototype.toHTML = function() { michael@0: return Microformats.parser.defaultGetter(propnode, parentnode, "HTML"); michael@0: } michael@0: return new mfHTML(Microformats.parser.defaultGetter(propnode, parentnode, "text")); michael@0: }, michael@0: /** michael@0: * Internal parser API used to determine which getter to call based on the michael@0: * datatype specified in the microformat definition. michael@0: * michael@0: * @param prop The microformat property in the definition michael@0: * @param propnode The DOMNode to check michael@0: * @param parentnode The parent node of the property. If it is a subproperty, michael@0: * this is the parent property node. If it is not, this is the michael@0: * microformat node. michael@0: * @return A string with the property value. michael@0: */ michael@0: datatypeHelper: function(prop, node, parentnode) { michael@0: var result; michael@0: var datatype = prop.datatype; michael@0: switch (datatype) { michael@0: case "dateTime": michael@0: result = Microformats.parser.dateTimeGetter(node, parentnode); michael@0: break; michael@0: case "anyURI": michael@0: result = Microformats.parser.uriGetter(node, parentnode); michael@0: break; michael@0: case "email": michael@0: result = Microformats.parser.emailGetter(node, parentnode); michael@0: break; michael@0: case "tel": michael@0: result = Microformats.parser.telGetter(node, parentnode); michael@0: break; michael@0: case "HTML": michael@0: result = Microformats.parser.HTMLGetter(node, parentnode); michael@0: break; michael@0: case "float": michael@0: var asText = Microformats.parser.textGetter(node, parentnode); michael@0: if (!isNaN(asText)) { michael@0: result = parseFloat(asText); michael@0: } michael@0: break; michael@0: case "custom": michael@0: result = prop.customGetter(node, parentnode); michael@0: break; michael@0: case "microformat": michael@0: try { michael@0: result = new Microformats[prop.microformat].mfObject(node, true); michael@0: } catch (ex) { michael@0: /* There are two reasons we get here, one because the node is not */ michael@0: /* a microformat and two because the node is a microformat and */ michael@0: /* creation failed. If the node is not a microformat, we just fall */ michael@0: /* through and use the default getter since there are some cases */ michael@0: /* (location in hCalendar) where a property can be either a microformat */ michael@0: /* or a string. If creation failed, we break and simply don't add the */ michael@0: /* microformat property to the parent microformat */ michael@0: if (ex != "Node is not a microformat (" + prop.microformat + ")") { michael@0: break; michael@0: } michael@0: } michael@0: if (result != undefined) { michael@0: if (prop.microformat_property) { michael@0: result = result[prop.microformat_property]; michael@0: } michael@0: break; michael@0: } michael@0: default: michael@0: result = Microformats.parser.textGetter(node, parentnode); michael@0: break; michael@0: } michael@0: /* This handles the case where one property implies another property */ michael@0: /* For instance, org by itself is actually org.organization-name */ michael@0: if (prop.values && (result != undefined)) { michael@0: var validType = false; michael@0: for (let value in prop.values) { michael@0: if (result.toLowerCase() == prop.values[value]) { michael@0: result = result.toLowerCase(); michael@0: validType = true; michael@0: break; michael@0: } michael@0: } michael@0: if (!validType) { michael@0: return; michael@0: } michael@0: } michael@0: return result; michael@0: }, michael@0: newMicroformat: function(object, in_node, microformat, validate) { michael@0: /* check to see if we are even valid */ michael@0: if (!Microformats[microformat]) { michael@0: throw("Invalid microformat - " + microformat); michael@0: } michael@0: if (in_node.ownerDocument) { michael@0: if (Microformats[microformat].attributeName) { michael@0: if (!(in_node.hasAttribute(Microformats[microformat].attributeName))) { michael@0: throw("Node is not a microformat (" + microformat + ")"); michael@0: } michael@0: } else { michael@0: if (!Microformats.matchClass(in_node, Microformats[microformat].className)) { michael@0: throw("Node is not a microformat (" + microformat + ")"); michael@0: } michael@0: } michael@0: } michael@0: var node = in_node; michael@0: if ((Microformats[microformat].className) && in_node.ownerDocument) { michael@0: node = Microformats.parser.preProcessMicroformat(in_node); michael@0: } michael@0: michael@0: for (let i in Microformats[microformat].properties) { michael@0: object.__defineGetter__(i, Microformats.parser.getMicroformatPropertyGenerator(node, microformat, i, object)); michael@0: } michael@0: michael@0: /* The node in the object should be the original node */ michael@0: object.node = in_node; michael@0: /* we also store the node that has been "resolved" */ michael@0: object.resolvedNode = node; michael@0: object.semanticType = microformat; michael@0: if (validate) { michael@0: Microformats.parser.validate(node, microformat); michael@0: } michael@0: }, michael@0: getMicroformatPropertyGenerator: function getMicroformatPropertyGenerator(node, name, property, microformat) michael@0: { michael@0: return function() { michael@0: var result = Microformats.parser.getMicroformatProperty(node, name, property); michael@0: // delete microformat[property]; michael@0: // microformat[property] = result; michael@0: return result; michael@0: }; michael@0: }, michael@0: getPropertyInternal: function getPropertyInternal(propnode, parentnode, propobj, propname, mfnode) { michael@0: var result; michael@0: if (propobj.subproperties) { michael@0: for (let subpropname in propobj.subproperties) { michael@0: var subpropnodes; michael@0: var subpropobj = propobj.subproperties[subpropname]; michael@0: if (subpropobj.rel == true) { michael@0: subpropnodes = Microformats.getElementsByAttribute(propnode, "rel", subpropname); michael@0: } else { michael@0: subpropnodes = Microformats.getElementsByClassName(propnode, subpropname); michael@0: } michael@0: var resultArray = []; michael@0: var subresult; michael@0: for (let i = 0; i < subpropnodes.length; i++) { michael@0: subresult = Microformats.parser.getPropertyInternal(subpropnodes[i], propnode, michael@0: subpropobj, michael@0: subpropname, mfnode); michael@0: if (subresult != undefined) { michael@0: resultArray.push(subresult); michael@0: /* If we're not a plural property, don't bother getting more */ michael@0: if (!subpropobj.plural) { michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: if (resultArray.length == 0) { michael@0: subresult = Microformats.parser.getPropertyInternal(propnode, null, michael@0: subpropobj, michael@0: subpropname, mfnode); michael@0: if (subresult != undefined) { michael@0: resultArray.push(subresult); michael@0: } michael@0: } michael@0: if (resultArray.length > 0) { michael@0: result = result || {}; michael@0: if (subpropobj.plural) { michael@0: result[subpropname] = resultArray; michael@0: } else { michael@0: result[subpropname] = resultArray[0]; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: if (!parentnode || (!result && propobj.subproperties)) { michael@0: if (propobj.virtual) { michael@0: if (propobj.virtualGetter) { michael@0: result = propobj.virtualGetter(mfnode || propnode); michael@0: } else { michael@0: result = Microformats.parser.datatypeHelper(propobj, propnode); michael@0: } michael@0: } michael@0: } else if (!result) { michael@0: result = Microformats.parser.datatypeHelper(propobj, propnode, parentnode); michael@0: } michael@0: return result; michael@0: }, michael@0: getMicroformatProperty: function getMicroformatProperty(in_mfnode, mfname, propname) { michael@0: var mfnode = in_mfnode; michael@0: /* If the node has not been preprocessed, the requested microformat */ michael@0: /* is a class based microformat and the passed in node is not the */ michael@0: /* entire document, preprocess it. Preprocessing the node involves */ michael@0: /* creating a duplicate of the node and taking care of things like */ michael@0: /* the include and header design patterns */ michael@0: if (!in_mfnode.origNode && Microformats[mfname].className && in_mfnode.ownerDocument) { michael@0: mfnode = Microformats.parser.preProcessMicroformat(in_mfnode); michael@0: } michael@0: /* propobj is the corresponding property object in the microformat */ michael@0: var propobj; michael@0: /* If there is a corresponding property in the microformat, use it */ michael@0: if (Microformats[mfname].properties[propname]) { michael@0: propobj = Microformats[mfname].properties[propname]; michael@0: } else { michael@0: /* If we didn't get a property, bail */ michael@0: return; michael@0: } michael@0: /* Query the correct set of nodes (rel or class) based on the setting */ michael@0: /* in the property */ michael@0: var propnodes; michael@0: if (propobj.rel == true) { michael@0: propnodes = Microformats.getElementsByAttribute(mfnode, "rel", propname); michael@0: } else { michael@0: propnodes = Microformats.getElementsByClassName(mfnode, propname); michael@0: } michael@0: for (let i=propnodes.length-1; i >= 0; i--) { michael@0: /* The reason getParent is not used here is because this code does */ michael@0: /* not apply to attribute based microformats, plus adr and geo */ michael@0: /* when contained in hCard are a special case */ michael@0: var parentnode; michael@0: var node = propnodes[i]; michael@0: var xpathExpression = ""; michael@0: for (let j=0; j < Microformats.list.length; j++) { michael@0: /* Don't treat adr or geo in an hCard as a microformat in this case */ michael@0: if ((mfname == "hCard") && ((Microformats.list[j] == "adr") || (Microformats.list[j] == "geo"))) { michael@0: continue; michael@0: } michael@0: if (Microformats[Microformats.list[j]].className) { michael@0: if (xpathExpression.length == 0) { michael@0: xpathExpression = "ancestor::*["; michael@0: } else { michael@0: xpathExpression += " or "; michael@0: } michael@0: xpathExpression += "contains(concat(' ', @class, ' '), ' " + Microformats[Microformats.list[j]].className + " ')"; michael@0: } michael@0: } michael@0: xpathExpression += "][1]"; michael@0: var xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null, Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null); michael@0: if (xpathResult.singleNodeValue) { michael@0: xpathResult.singleNodeValue.microformat = mfname; michael@0: parentnode = xpathResult.singleNodeValue; michael@0: } michael@0: /* If the propnode is not a child of the microformat, and */ michael@0: /* the property belongs to the parent microformat as well, */ michael@0: /* remove it. */ michael@0: if (parentnode != mfnode) { michael@0: var mfNameString = Microformats.getNamesFromNode(parentnode); michael@0: var mfNames = mfNameString.split(" "); michael@0: var j; michael@0: for (j=0; j < mfNames.length; j++) { michael@0: /* If this property is in the parent microformat, remove the node */ michael@0: if (Microformats[mfNames[j]].properties[propname]) { michael@0: propnodes.splice(i,1); michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: if (propnodes.length > 0) { michael@0: var resultArray = []; michael@0: for (let i = 0; i < propnodes.length; i++) { michael@0: var subresult = Microformats.parser.getPropertyInternal(propnodes[i], michael@0: mfnode, michael@0: propobj, michael@0: propname); michael@0: if (subresult != undefined) { michael@0: resultArray.push(subresult); michael@0: /* If we're not a plural property, don't bother getting more */ michael@0: if (!propobj.plural) { michael@0: return resultArray[0]; michael@0: } michael@0: } michael@0: } michael@0: if (resultArray.length > 0) { michael@0: return resultArray; michael@0: } michael@0: } else { michael@0: /* If we didn't find any class nodes, check to see if this property */ michael@0: /* is virtual and if so, call getPropertyInternal again */ michael@0: if (propobj.virtual) { michael@0: return Microformats.parser.getPropertyInternal(mfnode, null, michael@0: propobj, propname); michael@0: } michael@0: } michael@0: return; michael@0: }, michael@0: /** michael@0: * Internal parser API used to resolve includes and headers. Includes are michael@0: * resolved by simply cloning the node and replacing it in a clone of the michael@0: * original DOM node. Headers are resolved by creating a span and then copying michael@0: * the innerHTML and the class name. michael@0: * michael@0: * @param in_mfnode The node to preProcess. michael@0: * @return If the node had includes or headers, a cloned node otherwise michael@0: * the original node. You can check to see if the node was cloned michael@0: * by looking for .origNode in the new node. michael@0: */ michael@0: preProcessMicroformat: function preProcessMicroformat(in_mfnode) { michael@0: var mfnode; michael@0: if ((in_mfnode.nodeName.toLowerCase() == "td") && (in_mfnode.hasAttribute("headers"))) { michael@0: mfnode = in_mfnode.cloneNode(true); michael@0: mfnode.origNode = in_mfnode; michael@0: var headers = in_mfnode.getAttribute("headers").split(" "); michael@0: for (let i = 0; i < headers.length; i++) { michael@0: var tempNode = in_mfnode.ownerDocument.createElement("span"); michael@0: var headerNode = in_mfnode.ownerDocument.getElementById(headers[i]); michael@0: if (headerNode) { michael@0: tempNode.innerHTML = headerNode.innerHTML; michael@0: tempNode.className = headerNode.className; michael@0: mfnode.appendChild(tempNode); michael@0: } michael@0: } michael@0: } else { michael@0: mfnode = in_mfnode; michael@0: } michael@0: var includes = Microformats.getElementsByClassName(mfnode, "include"); michael@0: if (includes.length > 0) { michael@0: /* If we didn't clone, clone now */ michael@0: if (!mfnode.origNode) { michael@0: mfnode = in_mfnode.cloneNode(true); michael@0: mfnode.origNode = in_mfnode; michael@0: } michael@0: includes = Microformats.getElementsByClassName(mfnode, "include"); michael@0: var includeId; michael@0: var include_length = includes.length; michael@0: for (let i = include_length -1; i >= 0; i--) { michael@0: if (includes[i].nodeName.toLowerCase() == "a") { michael@0: includeId = includes[i].getAttribute("href").substr(1); michael@0: } michael@0: if (includes[i].nodeName.toLowerCase() == "object") { michael@0: includeId = includes[i].getAttribute("data").substr(1); michael@0: } michael@0: if (in_mfnode.ownerDocument.getElementById(includeId)) { michael@0: includes[i].parentNode.replaceChild(in_mfnode.ownerDocument.getElementById(includeId).cloneNode(true), includes[i]); michael@0: } michael@0: } michael@0: } michael@0: return mfnode; michael@0: }, michael@0: validate: function validate(mfnode, mfname) { michael@0: var error = ""; michael@0: if (Microformats[mfname].validate) { michael@0: return Microformats[mfname].validate(mfnode); michael@0: } else if (Microformats[mfname].required) { michael@0: for (let i=0;i 0) { michael@0: throw(error); michael@0: } michael@0: return true; michael@0: } michael@0: }, michael@0: /* This function normalizes an ISO8601 date by adding punctuation and */ michael@0: /* ensuring that hours and seconds have values */ michael@0: normalizeISO8601: function normalizeISO8601(string) michael@0: { michael@0: var dateArray = string.match(/(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(?:([-+Z])(?:(\d\d)(?::?(\d\d))?)?)?)?)?)?/); michael@0: michael@0: var dateString; michael@0: var tzOffset = 0; michael@0: if (!dateArray) { michael@0: return; michael@0: } michael@0: if (dateArray[1]) { michael@0: dateString = dateArray[1]; michael@0: if (dateArray[2]) { michael@0: dateString += "-" + dateArray[2]; michael@0: if (dateArray[3]) { michael@0: dateString += "-" + dateArray[3]; michael@0: if (dateArray[4]) { michael@0: dateString += "T" + dateArray[4]; michael@0: if (dateArray[5]) { michael@0: dateString += ":" + dateArray[5]; michael@0: } else { michael@0: dateString += ":" + "00"; michael@0: } michael@0: if (dateArray[6]) { michael@0: dateString += ":" + dateArray[6]; michael@0: } else { michael@0: dateString += ":" + "00"; michael@0: } michael@0: if (dateArray[7]) { michael@0: dateString += "." + dateArray[7]; michael@0: } michael@0: if (dateArray[8]) { michael@0: dateString += dateArray[8]; michael@0: if ((dateArray[8] == "+") || (dateArray[8] == "-")) { michael@0: if (dateArray[9]) { michael@0: dateString += dateArray[9]; michael@0: if (dateArray[10]) { michael@0: dateString += dateArray[10]; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return dateString; michael@0: } michael@0: }, michael@0: /** michael@0: * Converts an ISO8601 date into a JavaScript date object, honoring the TZ michael@0: * offset and Z if present to convert the date to local time michael@0: * NOTE: I'm using an extra parameter on the date object for this function. michael@0: * I set date.time to true if there is a date, otherwise date.time is false. michael@0: * michael@0: * @param string ISO8601 formatted date michael@0: * @return JavaScript date object that represents the ISO date. michael@0: */ michael@0: dateFromISO8601: function dateFromISO8601(string) { michael@0: var dateArray = string.match(/(\d\d\d\d)(?:-?(\d\d)(?:-?(\d\d)(?:[T ](\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(?:([-+Z])(?:(\d\d)(?::?(\d\d))?)?)?)?)?)?/); michael@0: michael@0: var date = new Date(dateArray[1], 0, 1); michael@0: date.time = false; michael@0: michael@0: if (dateArray[2]) { michael@0: date.setMonth(dateArray[2] - 1); michael@0: } michael@0: if (dateArray[3]) { michael@0: date.setDate(dateArray[3]); michael@0: } michael@0: if (dateArray[4]) { michael@0: date.setHours(dateArray[4]); michael@0: date.time = true; michael@0: if (dateArray[5]) { michael@0: date.setMinutes(dateArray[5]); michael@0: if (dateArray[6]) { michael@0: date.setSeconds(dateArray[6]); michael@0: if (dateArray[7]) { michael@0: date.setMilliseconds(Number("0." + dateArray[7]) * 1000); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: if (dateArray[8]) { michael@0: if (dateArray[8] == "-") { michael@0: if (dateArray[9] && dateArray[10]) { michael@0: date.setHours(date.getHours() + parseInt(dateArray[9], 10)); michael@0: date.setMinutes(date.getMinutes() + parseInt(dateArray[10], 10)); michael@0: } michael@0: } else if (dateArray[8] == "+") { michael@0: if (dateArray[9] && dateArray[10]) { michael@0: date.setHours(date.getHours() - parseInt(dateArray[9], 10)); michael@0: date.setMinutes(date.getMinutes() - parseInt(dateArray[10], 10)); michael@0: } michael@0: } michael@0: /* at this point we have the time in gmt */ michael@0: /* convert to local if we had a Z - or + */ michael@0: if (dateArray[8]) { michael@0: var tzOffset = date.getTimezoneOffset(); michael@0: if (tzOffset < 0) { michael@0: date.setMinutes(date.getMinutes() + tzOffset); michael@0: } else if (tzOffset > 0) { michael@0: date.setMinutes(date.getMinutes() - tzOffset); michael@0: } michael@0: } michael@0: } michael@0: return date; michael@0: }, michael@0: /** michael@0: * Converts a Javascript date object into an ISO 8601 formatted date michael@0: * NOTE: I'm using an extra parameter on the date object for this function. michael@0: * If date.time is NOT true, this function only outputs the date. michael@0: * michael@0: * @param date Javascript Date object michael@0: * @param punctuation true if the date should have -/: michael@0: * @return string with the ISO date. michael@0: */ michael@0: iso8601FromDate: function iso8601FromDate(date, punctuation) { michael@0: var string = date.getFullYear().toString(); michael@0: if (punctuation) { michael@0: string += "-"; michael@0: } michael@0: string += (date.getMonth() + 1).toString().replace(/\b(\d)\b/g, '0$1'); michael@0: if (punctuation) { michael@0: string += "-"; michael@0: } michael@0: string += date.getDate().toString().replace(/\b(\d)\b/g, '0$1'); michael@0: if (date.time) { michael@0: string += "T"; michael@0: string += date.getHours().toString().replace(/\b(\d)\b/g, '0$1'); michael@0: if (punctuation) { michael@0: string += ":"; michael@0: } michael@0: string += date.getMinutes().toString().replace(/\b(\d)\b/g, '0$1'); michael@0: if (punctuation) { michael@0: string += ":"; michael@0: } michael@0: string += date.getSeconds().toString().replace(/\b(\d)\b/g, '0$1'); michael@0: if (date.getMilliseconds() > 0) { michael@0: if (punctuation) { michael@0: string += "."; michael@0: } michael@0: string += date.getMilliseconds().toString(); michael@0: } michael@0: } michael@0: return string; michael@0: }, michael@0: simpleEscape: function simpleEscape(s) michael@0: { michael@0: s = s.replace(/\&/g, '%26'); michael@0: s = s.replace(/\#/g, '%23'); michael@0: s = s.replace(/\+/g, '%2B'); michael@0: s = s.replace(/\-/g, '%2D'); michael@0: s = s.replace(/\=/g, '%3D'); michael@0: s = s.replace(/\'/g, '%27'); michael@0: s = s.replace(/\,/g, '%2C'); michael@0: // s = s.replace(/\r/g, '%0D'); michael@0: // s = s.replace(/\n/g, '%0A'); michael@0: s = s.replace(/ /g, '+'); michael@0: return s; michael@0: }, michael@0: /** michael@0: * Not intended for external consumption. Microformat implementations might use it. michael@0: * michael@0: * Retrieve elements matching all classes listed in a space-separated string. michael@0: * I had to implement my own because I need an Array, not an nsIDomNodeList michael@0: * michael@0: * @param rootElement The DOM element at which to start searching (optional) michael@0: * @param className A space separated list of classenames michael@0: * @return microformatNodes An array of DOM Nodes, each representing a michael@0: microformat in the document. michael@0: */ michael@0: getElementsByClassName: function getElementsByClassName(rootNode, className) michael@0: { michael@0: var returnElements = []; michael@0: michael@0: if ((rootNode.ownerDocument || rootNode).getElementsByClassName) { michael@0: /* Firefox 3 - native getElementsByClassName */ michael@0: var col = rootNode.getElementsByClassName(className); michael@0: for (let i = 0; i < col.length; i++) { michael@0: returnElements[i] = col[i]; michael@0: } michael@0: } else if ((rootNode.ownerDocument || rootNode).evaluate) { michael@0: /* Firefox 2 and below - XPath */ michael@0: var xpathExpression; michael@0: xpathExpression = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]"; michael@0: var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null); michael@0: michael@0: var node; michael@0: while (node = xpathResult.iterateNext()) { michael@0: returnElements.push(node); michael@0: } michael@0: } else { michael@0: /* Slow fallback for testing */ michael@0: className = className.replace(/\-/g, "\\-"); michael@0: var elements = rootNode.getElementsByTagName("*"); michael@0: for (let i=0;i 1) || (fn != orgs[0]["organization-name"]))) { michael@0: var fns = fn.split(" "); michael@0: if (fns.length === 2) { michael@0: if (fns[0].charAt(fns[0].length-1) == ',') { michael@0: given_name[0] = fns[1]; michael@0: family_name[0] = fns[0].substr(0, fns[0].length-1); michael@0: } else if (fns[1].length == 1) { michael@0: given_name[0] = fns[1]; michael@0: family_name[0] = fns[0]; michael@0: } else if ((fns[1].length == 2) && (fns[1].charAt(fns[1].length-1) == '.')) { michael@0: given_name[0] = fns[1]; michael@0: family_name[0] = fns[0]; michael@0: } else { michael@0: given_name[0] = fns[0]; michael@0: family_name[0] = fns[1]; michael@0: } michael@0: return {"given-name" : given_name, "family-name" : family_name}; michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: "nickname" : { michael@0: plural: true, michael@0: virtual: true, michael@0: /* Implied "nickname" Optimization */ michael@0: /* http://microformats.org/wiki/hcard#Implied_.22nickname.22_Optimization */ michael@0: virtualGetter: function(mfnode) { michael@0: var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn"); michael@0: var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org"); michael@0: var given_name; michael@0: var family_name; michael@0: if (fn && (!orgs || (orgs.length) > 1 || (fn != orgs[0]["organization-name"]))) { michael@0: var fns = fn.split(" "); michael@0: if (fns.length === 1) { michael@0: return [fns[0]]; michael@0: } michael@0: } michael@0: return; michael@0: } michael@0: }, michael@0: "note" : { michael@0: plural: true, michael@0: datatype: "HTML" michael@0: }, michael@0: "org" : { michael@0: subproperties: { michael@0: "organization-name" : { michael@0: virtual: true michael@0: }, michael@0: "organization-unit" : { michael@0: plural: true michael@0: } michael@0: }, michael@0: plural: true michael@0: }, michael@0: "photo" : { michael@0: plural: true, michael@0: datatype: "anyURI" michael@0: }, michael@0: "rev" : { michael@0: datatype: "dateTime" michael@0: }, michael@0: "role" : { michael@0: plural: true michael@0: }, michael@0: "sequence" : { michael@0: }, michael@0: "sort-string" : { michael@0: }, michael@0: "sound" : { michael@0: plural: true michael@0: }, michael@0: "title" : { michael@0: plural: true michael@0: }, michael@0: "tel" : { michael@0: subproperties: { michael@0: "type" : { michael@0: plural: true, michael@0: values: ["msg", "home", "work", "pref", "voice", "fax", "cell", "video", "pager", "bbs", "car", "isdn", "pcs"] michael@0: }, michael@0: "value" : { michael@0: datatype: "tel", michael@0: virtual: true michael@0: } michael@0: }, michael@0: plural: true michael@0: }, michael@0: "tz" : { michael@0: }, michael@0: "uid" : { michael@0: datatype: "anyURI" michael@0: }, michael@0: "url" : { michael@0: plural: true, michael@0: datatype: "anyURI" michael@0: } michael@0: } michael@0: }; michael@0: michael@0: Microformats.add("hCard", hCard_definition); michael@0: michael@0: this.hCalendar = function hCalendar(node, validate) { michael@0: if (node) { michael@0: Microformats.parser.newMicroformat(this, node, "hCalendar", validate); michael@0: } michael@0: } michael@0: hCalendar.prototype.toString = function() { michael@0: if (this.resolvedNode) { michael@0: /* If this microformat has an include pattern, put the */ michael@0: /* dtstart in parenthesis after the summary to differentiate */ michael@0: /* them. */ michael@0: var summaries = Microformats.getElementsByClassName(this.node, "summary"); michael@0: if (summaries.length === 0) { michael@0: if (this.summary) { michael@0: if (this.dtstart) { michael@0: return this.summary + " (" + Microformats.dateFromISO8601(this.dtstart).toLocaleString() + ")"; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: if (this.dtstart) { michael@0: return this.summary; michael@0: } michael@0: return; michael@0: } michael@0: michael@0: var hCalendar_definition = { michael@0: mfObject: hCalendar, michael@0: className: "vevent", michael@0: required: ["summary", "dtstart"], michael@0: properties: { michael@0: "category" : { michael@0: plural: true, michael@0: datatype: "microformat", michael@0: microformat: "tag", michael@0: microformat_property: "tag" michael@0: }, michael@0: "class" : { michael@0: values: ["public", "private", "confidential"] michael@0: }, michael@0: "description" : { michael@0: datatype: "HTML" michael@0: }, michael@0: "dtstart" : { michael@0: datatype: "dateTime" michael@0: }, michael@0: "dtend" : { michael@0: datatype: "dateTime" michael@0: }, michael@0: "dtstamp" : { michael@0: datatype: "dateTime" michael@0: }, michael@0: "duration" : { michael@0: }, michael@0: "geo" : { michael@0: datatype: "microformat", michael@0: microformat: "geo" michael@0: }, michael@0: "location" : { michael@0: datatype: "microformat", michael@0: microformat: "hCard" michael@0: }, michael@0: "status" : { michael@0: values: ["tentative", "confirmed", "cancelled"] michael@0: }, michael@0: "summary" : {}, michael@0: "transp" : { michael@0: values: ["opaque", "transparent"] michael@0: }, michael@0: "uid" : { michael@0: datatype: "anyURI" michael@0: }, michael@0: "url" : { michael@0: datatype: "anyURI" michael@0: }, michael@0: "last-modified" : { michael@0: datatype: "dateTime" michael@0: }, michael@0: "rrule" : { michael@0: subproperties: { michael@0: "interval" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "interval"); michael@0: } michael@0: }, michael@0: "freq" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "freq"); michael@0: } michael@0: }, michael@0: "bysecond" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bysecond"); michael@0: } michael@0: }, michael@0: "byminute" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byminute"); michael@0: } michael@0: }, michael@0: "byhour" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byhour"); michael@0: } michael@0: }, michael@0: "bymonthday" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonthday"); michael@0: } michael@0: }, michael@0: "byyearday" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byyearday"); michael@0: } michael@0: }, michael@0: "byweekno" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byweekno"); michael@0: } michael@0: }, michael@0: "bymonth" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonth"); michael@0: } michael@0: }, michael@0: "byday" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byday"); michael@0: } michael@0: }, michael@0: "until" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "until"); michael@0: } michael@0: }, michael@0: "count" : { michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "count"); michael@0: } michael@0: } michael@0: }, michael@0: retrieve: function(mfnode, property) { michael@0: var value = Microformats.parser.textGetter(mfnode); michael@0: var rrule; michael@0: rrule = value.split(';'); michael@0: for (let i=0; i < rrule.length; i++) { michael@0: if (rrule[i].match(property)) { michael@0: return rrule[i].split('=')[1]; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }; michael@0: michael@0: Microformats.add("hCalendar", hCalendar_definition); michael@0: michael@0: this.geo = function geo(node, validate) { michael@0: if (node) { michael@0: Microformats.parser.newMicroformat(this, node, "geo", validate); michael@0: } michael@0: } michael@0: geo.prototype.toString = function() { michael@0: if (this.latitude != undefined) { michael@0: if (!isFinite(this.latitude) || (this.latitude > 360) || (this.latitude < -360)) { michael@0: return; michael@0: } michael@0: } michael@0: if (this.longitude != undefined) { michael@0: if (!isFinite(this.longitude) || (this.longitude > 360) || (this.longitude < -360)) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: if ((this.latitude != undefined) && (this.longitude != undefined)) { michael@0: var s; michael@0: if ((this.node.localName.toLowerCase() == "abbr") || (this.node.localName.toLowerCase() == "html:abbr")) { michael@0: s = this.node.textContent; michael@0: } michael@0: michael@0: if (s) { michael@0: return s; michael@0: } michael@0: michael@0: /* check if geo is contained in a vcard */ michael@0: var xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vcard ')]"; michael@0: var xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null, Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null); michael@0: if (xpathResult.singleNodeValue) { michael@0: var hcard = new hCard(xpathResult.singleNodeValue); michael@0: if (hcard.fn) { michael@0: return hcard.fn; michael@0: } michael@0: } michael@0: /* check if geo is contained in a vevent */ michael@0: xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vevent ')]"; michael@0: xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null, Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, xpathResult); michael@0: if (xpathResult.singleNodeValue) { michael@0: var hcal = new hCalendar(xpathResult.singleNodeValue); michael@0: if (hcal.summary) { michael@0: return hcal.summary; michael@0: } michael@0: } michael@0: if (s) { michael@0: return s; michael@0: } else { michael@0: return this.latitude + ", " + this.longitude; michael@0: } michael@0: } michael@0: } michael@0: michael@0: var geo_definition = { michael@0: mfObject: geo, michael@0: className: "geo", michael@0: required: ["latitude","longitude"], michael@0: properties: { michael@0: "latitude" : { michael@0: datatype: "float", michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: var value = Microformats.parser.textGetter(mfnode); michael@0: var latlong; michael@0: if (value.match(';')) { michael@0: latlong = value.split(';'); michael@0: if (latlong[0]) { michael@0: if (!isNaN(latlong[0])) { michael@0: return parseFloat(latlong[0]); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: "longitude" : { michael@0: datatype: "float", michael@0: virtual: true, michael@0: /* This will only be called in the virtual case */ michael@0: virtualGetter: function(mfnode) { michael@0: var value = Microformats.parser.textGetter(mfnode); michael@0: var latlong; michael@0: if (value.match(';')) { michael@0: latlong = value.split(';'); michael@0: if (latlong[1]) { michael@0: if (!isNaN(latlong[1])) { michael@0: return parseFloat(latlong[1]); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: validate: function(node) { michael@0: var latitude = Microformats.parser.getMicroformatProperty(node, "geo", "latitude"); michael@0: var longitude = Microformats.parser.getMicroformatProperty(node, "geo", "longitude"); michael@0: if (latitude != undefined) { michael@0: if (!isFinite(latitude) || (latitude > 360) || (latitude < -360)) { michael@0: throw("Invalid latitude"); michael@0: } michael@0: } else { michael@0: throw("No latitude specified"); michael@0: } michael@0: if (longitude != undefined) { michael@0: if (!isFinite(longitude) || (longitude > 360) || (longitude < -360)) { michael@0: throw("Invalid longitude"); michael@0: } michael@0: } else { michael@0: throw("No longitude specified"); michael@0: } michael@0: return true; michael@0: } michael@0: }; michael@0: michael@0: Microformats.add("geo", geo_definition); michael@0: michael@0: this.tag = function tag(node, validate) { michael@0: if (node) { michael@0: Microformats.parser.newMicroformat(this, node, "tag", validate); michael@0: } michael@0: } michael@0: tag.prototype.toString = function() { michael@0: return this.tag; michael@0: } michael@0: michael@0: var tag_definition = { michael@0: mfObject: tag, michael@0: attributeName: "rel", michael@0: attributeValues: "tag", michael@0: properties: { michael@0: "tag" : { michael@0: virtual: true, michael@0: virtualGetter: function(mfnode) { michael@0: if (mfnode.href) { michael@0: var ioService = Components.classes["@mozilla.org/network/io-service;1"]. michael@0: getService(Components.interfaces.nsIIOService); michael@0: var uri = ioService.newURI(mfnode.href, null, null); michael@0: var url_array = uri.path.split("/"); michael@0: for(let i=url_array.length-1; i > 0; i--) { michael@0: if (url_array[i] !== "") { michael@0: var tag michael@0: if (tag = Microformats.tag.validTagName(url_array[i].replace(/\+/g, ' '))) { michael@0: try { michael@0: return decodeURIComponent(tag); michael@0: } catch (ex) { michael@0: return unescape(tag); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: } michael@0: }, michael@0: "link" : { michael@0: virtual: true, michael@0: datatype: "anyURI" michael@0: }, michael@0: "text" : { michael@0: virtual: true michael@0: } michael@0: }, michael@0: validTagName: function(tag) michael@0: { michael@0: var returnTag = tag; michael@0: if (tag.indexOf('?') != -1) { michael@0: if (tag.indexOf('?') === 0) { michael@0: return false; michael@0: } else { michael@0: returnTag = tag.substr(0, tag.indexOf('?')); michael@0: } michael@0: } michael@0: if (tag.indexOf('#') != -1) { michael@0: if (tag.indexOf('#') === 0) { michael@0: return false; michael@0: } else { michael@0: returnTag = tag.substr(0, tag.indexOf('#')); michael@0: } michael@0: } michael@0: if (tag.indexOf('.html') != -1) { michael@0: if (tag.indexOf('.html') == tag.length - 5) { michael@0: return false; michael@0: } michael@0: } michael@0: return returnTag; michael@0: }, michael@0: validate: function(node) { michael@0: var tag = Microformats.parser.getMicroformatProperty(node, "tag", "tag"); michael@0: if (!tag) { michael@0: if (node.href) { michael@0: var url_array = node.getAttribute("href").split("/"); michael@0: for(let i=url_array.length-1; i > 0; i--) { michael@0: if (url_array[i] !== "") { michael@0: throw("Invalid tag name (" + url_array[i] + ")"); michael@0: } michael@0: } michael@0: } else { michael@0: throw("No href specified on tag"); michael@0: } michael@0: } michael@0: return true; michael@0: } michael@0: }; michael@0: michael@0: Microformats.add("tag", tag_definition);