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