toolkit/components/microformats/Microformats.js

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 this.EXPORTED_SYMBOLS = ["Microformats", "adr", "tag", "hCard", "hCalendar", "geo"];
     7 this.Microformats = {
     8   /* When a microformat is added, the name is placed in this list */
     9   list: [],
    10   /* Custom iterator so that microformats can be enumerated as */
    11   /* for (i in Microformats) */
    12   __iterator__: function () {
    13     for (let i=0; i < this.list.length; i++) {
    14       yield this.list[i];
    15     }
    16   },
    17   /**
    18    * Retrieves microformats objects of the given type from a document
    19    * 
    20    * @param  name          The name of the microformat (required)
    21    * @param  rootElement   The DOM element at which to start searching (required)
    22    * @param  options       Literal object with the following options:
    23    *                       recurseExternalFrames - Whether or not to search child frames
    24    *                       that reference external pages (with a src attribute)
    25    *                       for microformats (optional - defaults to true)
    26    *                       showHidden -  Whether or not to add hidden microformat
    27    *                       (optional - defaults to false)
    28    *                       debug - Whether or not we are in debug mode (optional
    29    *                       - defaults to false)
    30    * @param  targetArray  An array of microformat objects to which is added the results (optional)
    31    * @return A new array of microformat objects or the passed in microformat 
    32    *         object array with the new objects added
    33    */
    34   get: function(name, rootElement, options, targetArray) {
    35     function isAncestor(haystack, needle) {
    36       var parent = needle;
    37       while (parent = parent.parentNode) {
    38         /* We need to check parentNode because defaultView.frames[i].frameElement */
    39         /* isn't a real DOM node */
    40         if (parent == needle.parentNode) {
    41           return true;
    42         }
    43       }
    44       return false;
    45     }
    46     if (!Microformats[name] || !rootElement) {
    47       return;
    48     }
    49     targetArray = targetArray || [];
    51     /* Root element might not be the document - we need the document's default view */
    52     /* to get frames and to check their ancestry */
    53     var defaultView = rootElement.defaultView || rootElement.ownerDocument.defaultView;
    54     var rootDocument = rootElement.ownerDocument || rootElement;
    56     /* If recurseExternalFrames is undefined or true, look through all child frames for microformats */
    57     if (!options || !options.hasOwnProperty("recurseExternalFrames") || options.recurseExternalFrames) {
    58       if (defaultView && defaultView.frames.length > 0) {
    59         for (let i=0; i < defaultView.frames.length; i++) {
    60           if (isAncestor(rootDocument, defaultView.frames[i].frameElement)) {
    61             Microformats.get(name, defaultView.frames[i].document, options, targetArray);
    62           }
    63         }
    64       }
    65     }
    67     /* Get the microformat nodes for the document */
    68     var microformatNodes = [];
    69     if (Microformats[name].className) {
    70       microformatNodes = Microformats.getElementsByClassName(rootElement,
    71                                         Microformats[name].className);
    72       /* alternateClassName is for cases where a parent microformat is inferred by the children */
    73       /* If we find alternateClassName, the entire document becomes the microformat */
    74       if ((microformatNodes.length == 0) && Microformats[name].alternateClassName) {
    75         var altClass = Microformats.getElementsByClassName(rootElement, Microformats[name].alternateClassName);
    76         if (altClass.length > 0) {
    77           microformatNodes.push(rootElement); 
    78         }
    79       }
    80     } else if (Microformats[name].attributeValues) {
    81       microformatNodes =
    82         Microformats.getElementsByAttribute(rootElement,
    83                                             Microformats[name].attributeName,
    84                                             Microformats[name].attributeValues);
    86     }
    89     function isVisible(node, checkChildren) {
    90       if (node.getBoundingClientRect) {
    91         var box = node.getBoundingClientRect();
    92       } else {
    93         var box = node.ownerDocument.getBoxObjectFor(node);
    94       }
    95       /* If the parent has is an empty box, double check the children */
    96       if ((box.height == 0) || (box.width == 0)) {
    97         if (checkChildren && node.childNodes.length > 0) {
    98           for(let i=0; i < node.childNodes.length; i++) {
    99             if (node.childNodes[i].nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
   100               /* For performance reasons, we only go down one level */
   101               /* of children */
   102               if (isVisible(node.childNodes[i], false)) {
   103                 return true;
   104               }
   105             }
   106           }
   107         }
   108         return false
   109       }
   110       return true;
   111     }
   113     /* Create objects for the microformat nodes and put them into the microformats */
   114     /* array */
   115     for (let i = 0; i < microformatNodes.length; i++) {
   116       /* If showHidden undefined or false, don't add microformats to the list that aren't visible */
   117       if (!options || !options.hasOwnProperty("showHidden") || !options.showHidden) {
   118         if (microformatNodes[i].ownerDocument) {
   119           if (!isVisible(microformatNodes[i], true)) {
   120             continue;
   121           }
   122         }
   123       }
   124       try {
   125         if (options && options.debug) {
   126           /* Don't validate in the debug case so that we don't get errors thrown */
   127           /* in the debug case, we want all microformats, even if they are invalid */
   128           targetArray.push(new Microformats[name].mfObject(microformatNodes[i], false));
   129         } else {
   130           targetArray.push(new Microformats[name].mfObject(microformatNodes[i], true));
   131         }
   132       } catch (ex) {
   133         /* Creation of individual object probably failed because it is invalid. */
   134         /* This isn't a problem, because the page might have invalid microformats */
   135       }
   136     }
   137     return targetArray;
   138   },
   139   /**
   140    * Counts microformats objects of the given type from a document
   141    * 
   142    * @param  name          The name of the microformat (required)
   143    * @param  rootElement   The DOM element at which to start searching (required)
   144    * @param  options       Literal object with the following options:
   145    *                       recurseExternalFrames - Whether or not to search child frames
   146    *                       that reference external pages (with a src attribute)
   147    *                       for microformats (optional - defaults to true)
   148    *                       showHidden -  Whether or not to add hidden microformat
   149    *                       (optional - defaults to false)
   150    *                       debug - Whether or not we are in debug mode (optional
   151    *                       - defaults to false)
   152    * @return The new count
   153    */
   154   count: function(name, rootElement, options) {
   155     var mfArray = Microformats.get(name, rootElement, options);
   156     if (mfArray) {
   157       return mfArray.length;
   158     }
   159     return 0;
   160   },
   161   /**
   162    * Returns true if the passed in node is a microformat. Does NOT return true
   163    * if the passed in node is a child of a microformat.
   164    *
   165    * @param  node          DOM node to check
   166    * @return true if the node is a microformat, false if it is not
   167    */
   168   isMicroformat: function(node) {
   169     for (let i in Microformats)
   170     {
   171       if (Microformats[i].className) {
   172         if (Microformats.matchClass(node, Microformats[i].className)) {
   173             return true;
   174         }
   175       } else {
   176         var attribute;
   177         if (attribute = node.getAttribute(Microformats[i].attributeName)) {
   178           var attributeList = Microformats[i].attributeValues.split(" ");
   179           for (let j=0; j < attributeList.length; j++) {
   180             if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) {
   181               return true;
   182             }
   183           }
   184         }
   185       }
   186     }
   187     return false;
   188   },
   189   /**
   190    * This function searches a given nodes ancestors looking for a microformat
   191    * and if it finds it, returns it. It does NOT include self, so if the passed
   192    * in node is a microformat, it will still search ancestors for a microformat.
   193    *
   194    * @param  node          DOM node to check
   195    * @return If the node is contained in a microformat, it returns the parent
   196    *         DOM node, otherwise returns null
   197    */
   198   getParent: function(node) {
   199     var xpathExpression;
   200     var xpathResult;
   202     xpathExpression = "ancestor::*[";
   203     for (let i=0; i < Microformats.list.length; i++) {
   204       var mfname = Microformats.list[i];
   205       if (i != 0) {
   206         xpathExpression += " or ";
   207       }
   208       if (Microformats[mfname].className) {
   209         xpathExpression += "contains(concat(' ', @class, ' '), ' " + Microformats[mfname].className + " ')";
   210       } else {
   211         var attributeList = Microformats[mfname].attributeValues.split(" ");
   212         for (let j=0; j < attributeList.length; j++) {
   213           if (j != 0) {
   214             xpathExpression += " or ";
   215           }
   216           xpathExpression += "contains(concat(' ', @" + Microformats[mfname].attributeName + ", ' '), ' " + attributeList[j] + " ')";
   217         }
   218       }
   219     }
   220     xpathExpression += "][1]";
   221     xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
   222     if (xpathResult.singleNodeValue) {
   223       xpathResult.singleNodeValue.microformat = mfname;
   224       return xpathResult.singleNodeValue;
   225     }
   226     return null;
   227   },
   228   /**
   229    * If the passed in node is a microformat, this function returns a space 
   230    * separated list of the microformat names that correspond to this node
   231    *
   232    * @param  node          DOM node to check
   233    * @return If the node is a microformat, a space separated list of microformat
   234    *         names, otherwise returns nothing
   235    */
   236   getNamesFromNode: function(node) {
   237     var microformatNames = [];
   238     var xpathExpression;
   239     var xpathResult;
   240     for (let i in Microformats)
   241     {
   242       if (Microformats[i]) {
   243         if (Microformats[i].className) {
   244           if (Microformats.matchClass(node, Microformats[i].className)) {
   245             microformatNames.push(i);
   246             continue;
   247           }
   248         } else if (Microformats[i].attributeValues) {
   249           var attribute;
   250           if (attribute = node.getAttribute(Microformats[i].attributeName)) {
   251             var attributeList = Microformats[i].attributeValues.split(" ");
   252             for (let j=0; j < attributeList.length; j++) {
   253               /* If we match any attribute, we've got a microformat */
   254               if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) {
   255                 microformatNames.push(i);
   256                 break;
   257               }
   258             }
   259           }
   260         }
   261       }
   262     }
   263     return microformatNames.join(" ");
   264   },
   265   /**
   266    * Outputs the contents of a microformat object for debug purposes.
   267    *
   268    * @param  microformatObject JavaScript object that represents a microformat
   269    * @return string containing a visual representation of the contents of the microformat
   270    */
   271   debug: function debug(microformatObject) {
   272     function dumpObject(item, indent)
   273     {
   274       if (!indent) {
   275         indent = "";
   276       }
   277       var toreturn = "";
   278       var testArray = [];
   280       for (let i in item)
   281       {
   282         if (testArray[i]) {
   283           continue;
   284         }
   285         if (typeof item[i] == "object") {
   286           if ((i != "node") && (i != "resolvedNode")) {
   287             if (item[i] && item[i].semanticType) {
   288               toreturn += indent + item[i].semanticType + " [" + i + "] { \n";
   289             } else {
   290               toreturn += indent + "object " + i + " { \n";
   291             }
   292             toreturn += dumpObject(item[i], indent + "\t");
   293             toreturn += indent + "}\n";
   294           }
   295         } else if ((typeof item[i] != "function") && (i != "semanticType")) {
   296           if (item[i]) {
   297             toreturn += indent + i + "=" + item[i] + "\n";
   298           }
   299         }
   300       }
   301       if (!toreturn && item) {
   302         toreturn = item.toString();
   303       }
   304       return toreturn;
   305     }
   306     return dumpObject(microformatObject);
   307   },
   308   add: function add(microformat, microformatDefinition) {
   309     /* We always replace an existing definition with the new one */
   310     if (!Microformats[microformat]) {
   311       Microformats.list.push(microformat);
   312     }
   313     Microformats[microformat] = microformatDefinition;
   314     microformatDefinition.mfObject.prototype.debug =
   315       function(microformatObject) {
   316         return Microformats.debug(microformatObject)
   317       };
   318   },
   319   remove: function remove(microformat) {
   320     if (Microformats[microformat]) {
   321       var list = Microformats.list;
   322       var index = list.indexOf(microformat, 1);
   323       if (index != -1) {
   324         list.splice(index, 1);
   325       }
   326       delete Microformats[microformat];
   327     }
   328   },
   329   /* All parser specific functions are contained in this object */
   330   parser: {
   331     /**
   332      * Uses the microformat patterns to decide what the correct text for a
   333      * given microformat property is. This includes looking at things like
   334      * abbr, img/alt, area/alt and value excerpting.
   335      *
   336      * @param  propnode   The DOMNode to check
   337      * @param  parentnode The parent node of the property. If it is a subproperty,
   338      *                    this is the parent property node. If it is not, this is the
   339      *                    microformat node.
   340      & @param  datatype   HTML/text - whether to use innerHTML or innerText - defaults to text
   341      * @return A string with the value of the property
   342      */
   343     defaultGetter: function(propnode, parentnode, datatype) {
   344       function collapseWhitespace(instring) {
   345         /* Remove new lines, carriage returns and tabs */
   346         outstring = instring.replace(/[\n\r\t]/gi, ' ');
   347         /* Replace any double spaces with single spaces */
   348         outstring = outstring.replace(/\s{2,}/gi, ' ');
   349         /* Remove any double spaces that are left */
   350         outstring = outstring.replace(/\s{2,}/gi, '');
   351         /* Remove any spaces at the beginning */
   352         outstring = outstring.replace(/^\s+/, '');
   353         /* Remove any spaces at the end */
   354         outstring = outstring.replace(/\s+$/, '');
   355         return outstring;
   356       }
   359       if (((((propnode.localName.toLowerCase() == "abbr") || (propnode.localName.toLowerCase() == "html:abbr")) && !propnode.namespaceURI) || 
   360          ((propnode.localName.toLowerCase() == "abbr") && (propnode.namespaceURI == "http://www.w3.org/1999/xhtml"))) && (propnode.hasAttribute("title"))) {
   361         return propnode.getAttribute("title");
   362       } else if ((propnode.nodeName.toLowerCase() == "img") && (propnode.hasAttribute("alt"))) {
   363         return propnode.getAttribute("alt");
   364       } else if ((propnode.nodeName.toLowerCase() == "area") && (propnode.hasAttribute("alt"))) {
   365         return propnode.getAttribute("alt");
   366       } else if ((propnode.nodeName.toLowerCase() == "textarea") ||
   367                  (propnode.nodeName.toLowerCase() == "select") ||
   368                  (propnode.nodeName.toLowerCase() == "input")) {
   369         return propnode.value;
   370       } else {
   371         var values = Microformats.getElementsByClassName(propnode, "value");
   372         /* Verify that values are children of the propnode */
   373         for (let i = values.length-1; i >= 0; i--) {
   374           if (values[i].parentNode != propnode) {
   375             values.splice(i,1);
   376           }
   377         }
   378         if (values.length > 0) {
   379           var value = "";
   380           for (let j=0;j<values.length;j++) {
   381             value += Microformats.parser.defaultGetter(values[j], propnode, datatype);
   382           }
   383           return collapseWhitespace(value);
   384         }
   385         var s;
   386         if (datatype == "HTML") {
   387           s = propnode.innerHTML;
   388         } else {
   389           if (propnode.innerText) {
   390             s = propnode.innerText;
   391           } else {
   392             s = propnode.textContent;
   393           }
   394         }
   395         /* If we are processing a value node, don't remove whitespace now */
   396         /* (we'll do it later) */
   397         if (!Microformats.matchClass(propnode, "value")) {
   398           s = collapseWhitespace(s);
   399         }
   400         if (s.length > 0) {
   401           return s;
   402         }
   403       }
   404     },
   405     /**
   406      * Used to specifically retrieve a date in a microformat node.
   407      * After getting the default text, it normalizes it to an ISO8601 date.
   408      *
   409      * @param  propnode   The DOMNode to check
   410      * @param  parentnode The parent node of the property. If it is a subproperty,
   411      *                    this is the parent property node. If it is not, this is the
   412      *                    microformat node.
   413      * @return A string with the normalized date.
   414      */
   415     dateTimeGetter: function(propnode, parentnode) {
   416       var date = Microformats.parser.textGetter(propnode, parentnode);
   417       if (date) {
   418         return Microformats.parser.normalizeISO8601(date);
   419       }
   420     },
   421     /**
   422      * Used to specifically retrieve a URI in a microformat node. This includes
   423      * looking at an href/img/object/area to get the fully qualified URI.
   424      *
   425      * @param  propnode   The DOMNode to check
   426      * @param  parentnode The parent node of the property. If it is a subproperty,
   427      *                    this is the parent property node. If it is not, this is the
   428      *                    microformat node.
   429      * @return A string with the fully qualified URI.
   430      */
   431     uriGetter: function(propnode, parentnode) {
   432       var pairs = {"a":"href", "img":"src", "object":"data", "area":"href"};
   433       var name = propnode.nodeName.toLowerCase();
   434       if (pairs.hasOwnProperty(name)) {
   435         return propnode[pairs[name]];
   436       }
   437       return Microformats.parser.textGetter(propnode, parentnode);
   438     },
   439     /**
   440      * Used to specifically retrieve a telephone number in a microformat node.
   441      * Basically this is to handle the face that telephone numbers use value
   442      * as the name as one of their subproperties, but value is also used for
   443      * value excerpting (http://microformats.org/wiki/hcard#Value_excerpting)
   445      * @param  propnode   The DOMNode to check
   446      * @param  parentnode The parent node of the property. If it is a subproperty,
   447      *                    this is the parent property node. If it is not, this is the
   448      *                    microformat node.
   449      * @return A string with the telephone number
   450      */
   451     telGetter: function(propnode, parentnode) {
   452       var pairs = {"a":"href", "object":"data", "area":"href"};
   453       var name = propnode.nodeName.toLowerCase();
   454       if (pairs.hasOwnProperty(name)) {
   455         var protocol;
   456         if (propnode[pairs[name]].indexOf("tel:") == 0) {
   457           protocol = "tel:";
   458         }
   459         if (propnode[pairs[name]].indexOf("fax:") == 0) {
   460           protocol = "fax:";
   461         }
   462         if (propnode[pairs[name]].indexOf("modem:") == 0) {
   463           protocol = "modem:";
   464         }
   465         if (protocol) {
   466           if (propnode[pairs[name]].indexOf('?') > 0) {
   467             return unescape(propnode[pairs[name]].substring(protocol.length, propnode[pairs[name]].indexOf('?')));
   468           } else {
   469             return unescape(propnode[pairs[name]].substring(protocol.length));
   470           }
   471         }
   472       }
   473      /* Special case - if this node is a value, use the parent node to get all the values */
   474       if (Microformats.matchClass(propnode, "value")) {
   475         return Microformats.parser.textGetter(parentnode, parentnode);
   476       } else {
   477         /* Virtual case */
   478         if (!parentnode && (Microformats.getElementsByClassName(propnode, "type").length > 0)) {
   479           var tempNode = propnode.cloneNode(true);
   480           var typeNodes = Microformats.getElementsByClassName(tempNode, "type");
   481           for (let i=0; i < typeNodes.length; i++) {
   482             typeNodes[i].parentNode.removeChild(typeNodes[i]);
   483           }
   484           return Microformats.parser.textGetter(tempNode);
   485         }
   486         return Microformats.parser.textGetter(propnode, parentnode);
   487       }
   488     },
   489     /**
   490      * Used to specifically retrieve an email address in a microformat node.
   491      * This includes at an href, as well as removing subject if specified and
   492      * the mailto prefix.
   493      *
   494      * @param  propnode   The DOMNode to check
   495      * @param  parentnode The parent node of the property. If it is a subproperty,
   496      *                    this is the parent property node. If it is not, this is the
   497      *                    microformat node.
   498      * @return A string with the email address.
   499      */
   500     emailGetter: function(propnode, parentnode) {
   501       if ((propnode.nodeName.toLowerCase() == "a") || (propnode.nodeName.toLowerCase() == "area")) {
   502         var mailto = propnode.href;
   503         /* IO Service won't fully parse mailto, so we do it manually */
   504         if (mailto.indexOf('?') > 0) {
   505           return unescape(mailto.substring("mailto:".length, mailto.indexOf('?')));
   506         } else {
   507           return unescape(mailto.substring("mailto:".length));
   508         }
   509       } else {
   510         /* Special case - if this node is a value, use the parent node to get all the values */
   511         /* If this case gets executed, per the value design pattern, the result */
   512         /* will be the EXACT email address with no extra parsing required */
   513         if (Microformats.matchClass(propnode, "value")) {
   514           return Microformats.parser.textGetter(parentnode, parentnode);
   515         } else {
   516           /* Virtual case */
   517           if (!parentnode && (Microformats.getElementsByClassName(propnode, "type").length > 0)) {
   518             var tempNode = propnode.cloneNode(true);
   519             var typeNodes = Microformats.getElementsByClassName(tempNode, "type");
   520             for (let i=0; i < typeNodes.length; i++) {
   521               typeNodes[i].parentNode.removeChild(typeNodes[i]);
   522             }
   523             return Microformats.parser.textGetter(tempNode);
   524           }
   525           return Microformats.parser.textGetter(propnode, parentnode);
   526         }
   527       }
   528     },
   529     /**
   530      * Used when a caller needs the text inside a particular DOM node.
   531      * It calls defaultGetter to handle all the subtleties of getting
   532      * text from a microformat.
   533      *
   534      * @param  propnode   The DOMNode to check
   535      * @param  parentnode The parent node of the property. If it is a subproperty,
   536      *                    this is the parent property node. If it is not, this is the
   537      *                    microformat node.
   538      * @return A string with just the text including all tags.
   539      */
   540     textGetter: function(propnode, parentnode) {
   541       return Microformats.parser.defaultGetter(propnode, parentnode, "text");
   542     },
   543     /**
   544      * Used when a caller needs the HTML inside a particular DOM node.
   545      *
   546      * @param  propnode   The DOMNode to check
   547      * @param  parentnode The parent node of the property. If it is a subproperty,
   548      *                    this is the parent property node. If it is not, this is the
   549      *                    microformat node.
   550      * @return An emulated string object that also has a new function called toHTML
   551      */
   552     HTMLGetter: function(propnode, parentnode) {
   553       /* This is so we can have a string that behaves like a string */
   554       /* but also has a new function that can return the HTML that corresponds */
   555       /* to the string. */
   556       function mfHTML(value) {
   557         this.valueOf = function() {return value ? value.valueOf() : "";}
   558         this.toString = function() {return value ? value.toString() : "";}
   559       }
   560       mfHTML.prototype = new String;
   561       mfHTML.prototype.toHTML = function() {
   562         return Microformats.parser.defaultGetter(propnode, parentnode, "HTML");
   563       }
   564       return new mfHTML(Microformats.parser.defaultGetter(propnode, parentnode, "text"));
   565     },
   566     /**
   567      * Internal parser API used to determine which getter to call based on the
   568      * datatype specified in the microformat definition.
   569      *
   570      * @param  prop       The microformat property in the definition
   571      * @param  propnode   The DOMNode to check
   572      * @param  parentnode The parent node of the property. If it is a subproperty,
   573      *                    this is the parent property node. If it is not, this is the
   574      *                    microformat node.
   575      * @return A string with the property value.
   576      */
   577     datatypeHelper: function(prop, node, parentnode) {
   578       var result;
   579       var datatype = prop.datatype;
   580       switch (datatype) {
   581         case "dateTime":
   582           result = Microformats.parser.dateTimeGetter(node, parentnode);
   583           break;
   584         case "anyURI":
   585           result = Microformats.parser.uriGetter(node, parentnode);
   586           break;
   587         case "email":
   588           result = Microformats.parser.emailGetter(node, parentnode);
   589           break;
   590         case "tel":
   591           result = Microformats.parser.telGetter(node, parentnode);
   592           break;
   593         case "HTML":
   594           result = Microformats.parser.HTMLGetter(node, parentnode);
   595           break;
   596         case "float":
   597           var asText = Microformats.parser.textGetter(node, parentnode);
   598           if (!isNaN(asText)) {
   599             result = parseFloat(asText);
   600           }
   601           break;
   602         case "custom":
   603           result = prop.customGetter(node, parentnode);
   604           break;
   605         case "microformat":
   606           try {
   607             result = new Microformats[prop.microformat].mfObject(node, true);
   608           } catch (ex) {
   609             /* There are two reasons we get here, one because the node is not */
   610             /* a microformat and two because the node is a microformat and */
   611             /* creation failed. If the node is not a microformat, we just fall */
   612             /* through and use the default getter since there are some cases */
   613             /* (location in hCalendar) where a property can be either a microformat */
   614             /* or a string. If creation failed, we break and simply don't add the */
   615             /* microformat property to the parent microformat */
   616             if (ex != "Node is not a microformat (" + prop.microformat + ")") {
   617               break;
   618             }
   619           }
   620           if (result != undefined) {
   621             if (prop.microformat_property) {
   622               result = result[prop.microformat_property];
   623             }
   624             break;
   625           }
   626         default:
   627           result = Microformats.parser.textGetter(node, parentnode);
   628           break;
   629       }
   630       /* This handles the case where one property implies another property */
   631       /* For instance, org by itself is actually org.organization-name */
   632       if (prop.values && (result != undefined)) {
   633         var validType = false;
   634         for (let value in prop.values) {
   635           if (result.toLowerCase() == prop.values[value]) {
   636             result = result.toLowerCase();
   637             validType = true;
   638             break;
   639           }
   640         }
   641         if (!validType) {
   642           return;
   643         }
   644       }
   645       return result;
   646     },
   647     newMicroformat: function(object, in_node, microformat, validate) {
   648       /* check to see if we are even valid */
   649       if (!Microformats[microformat]) {
   650         throw("Invalid microformat - " + microformat);
   651       }
   652       if (in_node.ownerDocument) {
   653         if (Microformats[microformat].attributeName) {
   654           if (!(in_node.hasAttribute(Microformats[microformat].attributeName))) {
   655             throw("Node is not a microformat (" + microformat + ")");
   656           }
   657         } else {
   658           if (!Microformats.matchClass(in_node, Microformats[microformat].className)) {
   659             throw("Node is not a microformat (" + microformat + ")");
   660           }
   661         }
   662       }
   663       var node = in_node;
   664       if ((Microformats[microformat].className) && in_node.ownerDocument) {
   665         node = Microformats.parser.preProcessMicroformat(in_node);
   666       }
   668       for (let i in Microformats[microformat].properties) {
   669         object.__defineGetter__(i, Microformats.parser.getMicroformatPropertyGenerator(node, microformat, i, object));
   670       }
   672       /* The node in the object should be the original node */
   673       object.node = in_node;
   674       /* we also store the node that has been "resolved" */
   675       object.resolvedNode = node; 
   676       object.semanticType = microformat;
   677       if (validate) {
   678         Microformats.parser.validate(node, microformat);
   679       }
   680     },
   681     getMicroformatPropertyGenerator: function getMicroformatPropertyGenerator(node, name, property, microformat)
   682     {
   683       return function() {
   684         var result = Microformats.parser.getMicroformatProperty(node, name, property);
   685 //        delete microformat[property];
   686 //        microformat[property] = result; 
   687         return result;
   688       };
   689     },
   690     getPropertyInternal: function getPropertyInternal(propnode, parentnode, propobj, propname, mfnode) {
   691       var result;
   692       if (propobj.subproperties) {
   693         for (let subpropname in propobj.subproperties) {
   694           var subpropnodes;
   695           var subpropobj = propobj.subproperties[subpropname];
   696           if (subpropobj.rel == true) {
   697             subpropnodes = Microformats.getElementsByAttribute(propnode, "rel", subpropname);
   698           } else {
   699             subpropnodes = Microformats.getElementsByClassName(propnode, subpropname);
   700           }
   701           var resultArray = [];
   702           var subresult;
   703           for (let i = 0; i < subpropnodes.length; i++) {
   704             subresult = Microformats.parser.getPropertyInternal(subpropnodes[i], propnode,
   705                                                                 subpropobj,
   706                                                                 subpropname, mfnode);
   707             if (subresult != undefined) {
   708               resultArray.push(subresult);
   709               /* If we're not a plural property, don't bother getting more */
   710               if (!subpropobj.plural) {
   711                 break;
   712               }
   713             }
   714           }
   715           if (resultArray.length == 0) {
   716             subresult = Microformats.parser.getPropertyInternal(propnode, null,
   717                                                                 subpropobj,
   718                                                                 subpropname, mfnode);
   719             if (subresult != undefined) {
   720               resultArray.push(subresult);
   721             }
   722           }
   723           if (resultArray.length > 0) {
   724             result = result || {};
   725             if (subpropobj.plural) {
   726               result[subpropname] = resultArray;
   727             } else {
   728               result[subpropname] = resultArray[0];
   729             }
   730           }
   731         }
   732       }
   733       if (!parentnode || (!result && propobj.subproperties)) {
   734         if (propobj.virtual) {
   735           if (propobj.virtualGetter) {
   736             result = propobj.virtualGetter(mfnode || propnode);
   737           } else {
   738             result = Microformats.parser.datatypeHelper(propobj, propnode);
   739           }
   740         }
   741       } else if (!result) {
   742         result = Microformats.parser.datatypeHelper(propobj, propnode, parentnode);
   743       }
   744       return result;
   745     },
   746     getMicroformatProperty: function getMicroformatProperty(in_mfnode, mfname, propname) {
   747       var mfnode = in_mfnode;
   748       /* If the node has not been preprocessed, the requested microformat */
   749       /* is a class based microformat and the passed in node is not the */
   750       /* entire document, preprocess it. Preprocessing the node involves */
   751       /* creating a duplicate of the node and taking care of things like */
   752       /* the include and header design patterns */
   753       if (!in_mfnode.origNode && Microformats[mfname].className && in_mfnode.ownerDocument) {
   754         mfnode = Microformats.parser.preProcessMicroformat(in_mfnode);
   755       }
   756       /* propobj is the corresponding property object in the microformat */
   757       var propobj;
   758       /* If there is a corresponding property in the microformat, use it */
   759       if (Microformats[mfname].properties[propname]) {
   760         propobj = Microformats[mfname].properties[propname];
   761       } else {
   762         /* If we didn't get a property, bail */
   763         return;
   764       }
   765       /* Query the correct set of nodes (rel or class) based on the setting */
   766       /* in the property */
   767       var propnodes;
   768       if (propobj.rel == true) {
   769         propnodes = Microformats.getElementsByAttribute(mfnode, "rel", propname);
   770       } else {
   771         propnodes = Microformats.getElementsByClassName(mfnode, propname);
   772       }
   773       for (let i=propnodes.length-1; i >= 0; i--) {
   774         /* The reason getParent is not used here is because this code does */
   775         /* not apply to attribute based microformats, plus adr and geo */
   776         /* when contained in hCard are a special case */
   777         var parentnode;
   778         var node = propnodes[i];
   779         var xpathExpression = "";
   780         for (let j=0; j < Microformats.list.length; j++) {
   781           /* Don't treat adr or geo in an hCard as a microformat in this case */
   782           if ((mfname == "hCard") && ((Microformats.list[j] == "adr") || (Microformats.list[j] == "geo"))) {
   783             continue;
   784           }
   785           if (Microformats[Microformats.list[j]].className) {
   786             if (xpathExpression.length == 0) {
   787               xpathExpression = "ancestor::*[";
   788             } else {
   789               xpathExpression += " or ";
   790             }
   791             xpathExpression += "contains(concat(' ', @class, ' '), ' " + Microformats[Microformats.list[j]].className + " ')";
   792           }
   793         }
   794         xpathExpression += "][1]";
   795         var xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
   796         if (xpathResult.singleNodeValue) {
   797           xpathResult.singleNodeValue.microformat = mfname;
   798           parentnode = xpathResult.singleNodeValue;
   799         }
   800         /* If the propnode is not a child of the microformat, and */
   801         /* the property belongs to the parent microformat as well, */
   802         /* remove it. */
   803         if (parentnode != mfnode) {
   804           var mfNameString = Microformats.getNamesFromNode(parentnode);
   805           var mfNames = mfNameString.split(" ");
   806           var j;
   807           for (j=0; j < mfNames.length; j++) {
   808             /* If this property is in the parent microformat, remove the node  */
   809             if (Microformats[mfNames[j]].properties[propname]) {
   810               propnodes.splice(i,1);
   811               break;
   812             }
   813           }
   814         }
   815       }
   816       if (propnodes.length > 0) {
   817         var resultArray = [];
   818         for (let i = 0; i < propnodes.length; i++) {
   819           var subresult = Microformats.parser.getPropertyInternal(propnodes[i],
   820                                                                   mfnode,
   821                                                                   propobj,
   822                                                                   propname);
   823           if (subresult != undefined) {
   824             resultArray.push(subresult);
   825             /* If we're not a plural property, don't bother getting more */
   826             if (!propobj.plural) {
   827               return resultArray[0];
   828             }
   829           }
   830         }
   831         if (resultArray.length > 0) {
   832           return resultArray;
   833         }
   834       } else {
   835         /* If we didn't find any class nodes, check to see if this property */
   836         /* is virtual and if so, call getPropertyInternal again */
   837         if (propobj.virtual) {
   838           return Microformats.parser.getPropertyInternal(mfnode, null,
   839                                                          propobj, propname);
   840         }
   841       }
   842       return;
   843     },
   844     /**
   845      * Internal parser API used to resolve includes and headers. Includes are
   846      * resolved by simply cloning the node and replacing it in a clone of the
   847      * original DOM node. Headers are resolved by creating a span and then copying
   848      * the innerHTML and the class name.
   849      *
   850      * @param  in_mfnode The node to preProcess.
   851      * @return If the node had includes or headers, a cloned node otherwise
   852      *         the original node. You can check to see if the node was cloned
   853      *         by looking for .origNode in the new node.
   854      */
   855     preProcessMicroformat: function preProcessMicroformat(in_mfnode) {
   856       var mfnode;
   857       if ((in_mfnode.nodeName.toLowerCase() == "td") && (in_mfnode.hasAttribute("headers"))) {
   858         mfnode = in_mfnode.cloneNode(true);
   859         mfnode.origNode = in_mfnode;
   860         var headers = in_mfnode.getAttribute("headers").split(" ");
   861         for (let i = 0; i < headers.length; i++) {
   862           var tempNode = in_mfnode.ownerDocument.createElement("span");
   863           var headerNode = in_mfnode.ownerDocument.getElementById(headers[i]);
   864           if (headerNode) {
   865             tempNode.innerHTML = headerNode.innerHTML;
   866             tempNode.className = headerNode.className;
   867             mfnode.appendChild(tempNode);
   868           }
   869         }
   870       } else {
   871         mfnode = in_mfnode;
   872       }
   873       var includes = Microformats.getElementsByClassName(mfnode, "include");
   874       if (includes.length > 0) {
   875         /* If we didn't clone, clone now */
   876         if (!mfnode.origNode) {
   877           mfnode = in_mfnode.cloneNode(true);
   878           mfnode.origNode = in_mfnode;
   879         }
   880         includes = Microformats.getElementsByClassName(mfnode, "include");
   881         var includeId;
   882         var include_length = includes.length;
   883         for (let i = include_length -1; i >= 0; i--) {
   884           if (includes[i].nodeName.toLowerCase() == "a") {
   885             includeId = includes[i].getAttribute("href").substr(1);
   886           }
   887           if (includes[i].nodeName.toLowerCase() == "object") {
   888             includeId = includes[i].getAttribute("data").substr(1);
   889           }
   890           if (in_mfnode.ownerDocument.getElementById(includeId)) {
   891             includes[i].parentNode.replaceChild(in_mfnode.ownerDocument.getElementById(includeId).cloneNode(true), includes[i]);
   892           }
   893         }
   894       }
   895       return mfnode;
   896     },
   897     validate: function validate(mfnode, mfname) {
   898       var error = "";
   899       if (Microformats[mfname].validate) {
   900         return Microformats[mfname].validate(mfnode);
   901       } else if (Microformats[mfname].required) {
   902         for (let i=0;i<Microformats[mfname].required.length;i++) {
   903           if (!Microformats.parser.getMicroformatProperty(mfnode, mfname, Microformats[mfname].required[i])) {
   904             error += "Required property " + Microformats[mfname].required[i] + " not specified\n";
   905           }
   906         }
   907         if (error.length > 0) {
   908           throw(error);
   909         }
   910         return true;
   911       }
   912     },
   913     /* This function normalizes an ISO8601 date by adding punctuation and */
   914     /* ensuring that hours and seconds have values */
   915     normalizeISO8601: function normalizeISO8601(string)
   916     {
   917       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))?)?)?)?)?)?/);
   919       var dateString;
   920       var tzOffset = 0;
   921       if (!dateArray) {
   922         return;
   923       }
   924       if (dateArray[1]) {
   925         dateString = dateArray[1];
   926         if (dateArray[2]) {
   927           dateString += "-" + dateArray[2];
   928           if (dateArray[3]) {
   929             dateString += "-" + dateArray[3];
   930             if (dateArray[4]) {
   931               dateString += "T" + dateArray[4];
   932               if (dateArray[5]) {
   933                 dateString += ":" + dateArray[5];
   934               } else {
   935                 dateString += ":" + "00";
   936               }
   937               if (dateArray[6]) {
   938                 dateString += ":" + dateArray[6];
   939               } else {
   940                 dateString += ":" + "00";
   941               }
   942               if (dateArray[7]) {
   943                 dateString += "." + dateArray[7];
   944               }
   945               if (dateArray[8]) {
   946                 dateString += dateArray[8];
   947                 if ((dateArray[8] == "+") || (dateArray[8] == "-")) {
   948                   if (dateArray[9]) {
   949                     dateString += dateArray[9];
   950                     if (dateArray[10]) {
   951                       dateString += dateArray[10];
   952                     }
   953                   }
   954                 }
   955               }
   956             }
   957           }
   958         }
   959       }
   960       return dateString;
   961     }
   962   },
   963   /**
   964    * Converts an ISO8601 date into a JavaScript date object, honoring the TZ
   965    * offset and Z if present to convert the date to local time
   966    * NOTE: I'm using an extra parameter on the date object for this function.
   967    * I set date.time to true if there is a date, otherwise date.time is false.
   968    * 
   969    * @param  string ISO8601 formatted date
   970    * @return JavaScript date object that represents the ISO date. 
   971    */
   972   dateFromISO8601: function dateFromISO8601(string) {
   973     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))?)?)?)?)?)?/);
   975     var date = new Date(dateArray[1], 0, 1);
   976     date.time = false;
   978     if (dateArray[2]) {
   979       date.setMonth(dateArray[2] - 1);
   980     }
   981     if (dateArray[3]) {
   982       date.setDate(dateArray[3]);
   983     }
   984     if (dateArray[4]) {
   985       date.setHours(dateArray[4]);
   986       date.time = true;
   987       if (dateArray[5]) {
   988         date.setMinutes(dateArray[5]);
   989         if (dateArray[6]) {
   990           date.setSeconds(dateArray[6]);
   991           if (dateArray[7]) {
   992             date.setMilliseconds(Number("0." + dateArray[7]) * 1000);
   993           }
   994         }
   995       }
   996     }
   997     if (dateArray[8]) {
   998       if (dateArray[8] == "-") {
   999         if (dateArray[9] && dateArray[10]) {
  1000           date.setHours(date.getHours() + parseInt(dateArray[9], 10));
  1001           date.setMinutes(date.getMinutes() + parseInt(dateArray[10], 10));
  1003       } else if (dateArray[8] == "+") {
  1004         if (dateArray[9] && dateArray[10]) {
  1005           date.setHours(date.getHours() - parseInt(dateArray[9], 10));
  1006           date.setMinutes(date.getMinutes() - parseInt(dateArray[10], 10));
  1009       /* at this point we have the time in gmt */
  1010       /* convert to local if we had a Z - or + */
  1011       if (dateArray[8]) {
  1012         var tzOffset = date.getTimezoneOffset();
  1013         if (tzOffset < 0) {
  1014           date.setMinutes(date.getMinutes() + tzOffset); 
  1015         } else if (tzOffset > 0) {
  1016           date.setMinutes(date.getMinutes() - tzOffset); 
  1020     return date;
  1021   },
  1022   /**
  1023    * Converts a Javascript date object into an ISO 8601 formatted date
  1024    * NOTE: I'm using an extra parameter on the date object for this function.
  1025    * If date.time is NOT true, this function only outputs the date.
  1027    * @param  date        Javascript Date object
  1028    * @param  punctuation true if the date should have -/:
  1029    * @return string with the ISO date. 
  1030    */
  1031   iso8601FromDate: function iso8601FromDate(date, punctuation) {
  1032     var string = date.getFullYear().toString();
  1033     if (punctuation) {
  1034       string += "-";
  1036     string += (date.getMonth() + 1).toString().replace(/\b(\d)\b/g, '0$1');
  1037     if (punctuation) {
  1038       string += "-";
  1040     string += date.getDate().toString().replace(/\b(\d)\b/g, '0$1');
  1041     if (date.time) {
  1042       string += "T";
  1043       string += date.getHours().toString().replace(/\b(\d)\b/g, '0$1');
  1044       if (punctuation) {
  1045         string += ":";
  1047       string += date.getMinutes().toString().replace(/\b(\d)\b/g, '0$1');
  1048       if (punctuation) {
  1049         string += ":";
  1051       string += date.getSeconds().toString().replace(/\b(\d)\b/g, '0$1');
  1052       if (date.getMilliseconds() > 0) {
  1053         if (punctuation) {
  1054           string += ".";
  1056         string += date.getMilliseconds().toString();
  1059     return string;
  1060   },
  1061   simpleEscape: function simpleEscape(s)
  1063     s = s.replace(/\&/g, '%26');
  1064     s = s.replace(/\#/g, '%23');
  1065     s = s.replace(/\+/g, '%2B');
  1066     s = s.replace(/\-/g, '%2D');
  1067     s = s.replace(/\=/g, '%3D');
  1068     s = s.replace(/\'/g, '%27');
  1069     s = s.replace(/\,/g, '%2C');
  1070 //    s = s.replace(/\r/g, '%0D');
  1071 //    s = s.replace(/\n/g, '%0A');
  1072     s = s.replace(/ /g, '+');
  1073     return s;
  1074   },
  1075   /**
  1076    * Not intended for external consumption. Microformat implementations might use it.
  1078    * Retrieve elements matching all classes listed in a space-separated string.
  1079    * I had to implement my own because I need an Array, not an nsIDomNodeList
  1081    * @param  rootElement      The DOM element at which to start searching (optional)
  1082    * @param  className        A space separated list of classenames
  1083    * @return microformatNodes An array of DOM Nodes, each representing a
  1084                               microformat in the document.
  1085    */
  1086   getElementsByClassName: function getElementsByClassName(rootNode, className)
  1088     var returnElements = [];
  1090     if ((rootNode.ownerDocument || rootNode).getElementsByClassName) {
  1091     /* Firefox 3 - native getElementsByClassName */
  1092       var col = rootNode.getElementsByClassName(className);
  1093       for (let i = 0; i < col.length; i++) {
  1094         returnElements[i] = col[i];
  1096     } else if ((rootNode.ownerDocument || rootNode).evaluate) {
  1097     /* Firefox 2 and below - XPath */
  1098       var xpathExpression;
  1099       xpathExpression = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
  1100       var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null);
  1102       var node;
  1103       while (node = xpathResult.iterateNext()) {
  1104         returnElements.push(node);
  1106     } else {
  1107     /* Slow fallback for testing */
  1108       className = className.replace(/\-/g, "\\-");
  1109       var elements = rootNode.getElementsByTagName("*");
  1110       for (let i=0;i<elements.length;i++) {
  1111         if (elements[i].className.match("(^|\\s)" + className + "(\\s|$)")) {
  1112           returnElements.push(elements[i]);
  1116     return returnElements;
  1117   },
  1118   /**
  1119    * Not intended for external consumption. Microformat implementations might use it.
  1121    * Retrieve elements matching an attribute and an attribute list in a space-separated string.
  1123    * @param  rootElement      The DOM element at which to start searching (optional)
  1124    * @param  atributeName     The attribute name to match against
  1125    * @param  attributeValues  A space separated list of attribute values
  1126    * @return microformatNodes An array of DOM Nodes, each representing a
  1127                               microformat in the document.
  1128    */
  1129   getElementsByAttribute: function getElementsByAttribute(rootNode, attributeName, attributeValues)
  1131     var attributeList = attributeValues.split(" ");
  1133     var returnElements = [];
  1135     if ((rootNode.ownerDocument || rootNode).evaluate) {
  1136     /* Firefox 3 and below - XPath */
  1137       /* Create an XPath expression based on the attribute list */
  1138       var xpathExpression = ".//*[";
  1139       for (let i = 0; i < attributeList.length; i++) {
  1140         if (i != 0) {
  1141           xpathExpression += " or ";
  1143         xpathExpression += "contains(concat(' ', @" + attributeName + ", ' '), ' " + attributeList[i] + " ')";
  1145       xpathExpression += "]"; 
  1147       var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null);
  1149       var node;
  1150       while (node = xpathResult.iterateNext()) {
  1151         returnElements.push(node);
  1153     } else {
  1154     /* Need Slow fallback for testing */
  1156     return returnElements;
  1157   },
  1158   matchClass: function matchClass(node, className) {
  1159     var classValue = node.getAttribute("class");
  1160     return (classValue && classValue.match("(^|\\s)" + className + "(\\s|$)"));
  1162 };
  1164 /* MICROFORMAT DEFINITIONS BEGIN HERE */
  1166 this.adr = function adr(node, validate) {
  1167   if (node) {
  1168     Microformats.parser.newMicroformat(this, node, "adr", validate);
  1172 adr.prototype.toString = function() {
  1173   var address_text = "";
  1174   var start_parens = false;
  1175   if (this["street-address"]) {
  1176     address_text += this["street-address"][0];
  1177   } else if (this["extended-address"]) {
  1178     address_text += this["extended-address"];
  1180   if (this["locality"]) {
  1181     if (this["street-address"] || this["extended-address"]) {
  1182       address_text += " (";
  1183       start_parens = true;
  1185     address_text += this["locality"];
  1187   if (this["region"]) {
  1188     if ((this["street-address"] || this["extended-address"]) && (!start_parens)) {
  1189       address_text += " (";
  1190       start_parens = true;
  1191     } else if (this["locality"]) {
  1192       address_text += ", ";
  1194     address_text += this["region"];
  1196   if (this["country-name"]) {
  1197     if ((this["street-address"] || this["extended-address"]) && (!start_parens)) {
  1198       address_text += " (";
  1199       start_parens = true;
  1200       address_text += this["country-name"];
  1201     } else if ((!this["locality"]) && (!this["region"])) {
  1202       address_text += this["country-name"];
  1203     } else if (((!this["locality"]) && (this["region"])) || ((this["locality"]) && (!this["region"]))) {
  1204       address_text += ", ";
  1205       address_text += this["country-name"];
  1208   if (start_parens) {
  1209     address_text += ")";
  1211   return address_text;
  1214 var adr_definition = {
  1215   mfObject: adr,
  1216   className: "adr",
  1217   properties: {
  1218     "type" : {
  1219       plural: true,
  1220       values: ["work", "home", "pref", "postal", "dom", "intl", "parcel"]
  1221     },
  1222     "post-office-box" : {
  1223     },
  1224     "street-address" : {
  1225       plural: true
  1226     },
  1227     "extended-address" : {
  1228     },
  1229     "locality" : {
  1230     },
  1231     "region" : {
  1232     },
  1233     "postal-code" : {
  1234     },
  1235     "country-name" : {
  1237   },
  1238   validate: function(node) {
  1239     var xpathExpression = "count(descendant::*[" +
  1240                                               "contains(concat(' ', @class, ' '), ' post-office-box ')" +
  1241                                               " or contains(concat(' ', @class, ' '), ' street-address ')" +
  1242                                               " or contains(concat(' ', @class, ' '), ' extended-address ')" +
  1243                                               " or contains(concat(' ', @class, ' '), ' locality ')" +
  1244                                               " or contains(concat(' ', @class, ' '), ' region ')" +
  1245                                               " or contains(concat(' ', @class, ' '), ' postal-code ')" +
  1246                                               " or contains(concat(' ', @class, ' '), ' country-name')" +
  1247                                               "])";
  1248     var xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null).numberValue;
  1249     if (xpathResult == 0) {
  1250       throw("Unable to create microformat");
  1252     return true;
  1254 };
  1256 Microformats.add("adr", adr_definition);
  1258 this.hCard = function hCard(node, validate) {
  1259   if (node) {
  1260     Microformats.parser.newMicroformat(this, node, "hCard", validate);
  1263 hCard.prototype.toString = function() {
  1264   if (this.resolvedNode) {
  1265     /* If this microformat has an include pattern, put the */
  1266     /* organization-name in parenthesis after the fn to differentiate */
  1267     /* them. */
  1268     var fns = Microformats.getElementsByClassName(this.node, "fn");
  1269     if (fns.length === 0) {
  1270       if (this.fn) {
  1271         if (this.org && this.org[0]["organization-name"] && (this.fn != this.org[0]["organization-name"])) {
  1272           return this.fn + " (" + this.org[0]["organization-name"] + ")";
  1277   return this.fn;
  1280 var hCard_definition = {
  1281   mfObject: hCard,
  1282   className: "vcard",
  1283   required: ["fn"],
  1284   properties: {
  1285     "adr" : {
  1286       plural: true,
  1287       datatype: "microformat",
  1288       microformat: "adr"
  1289     },
  1290     "agent" : {
  1291       plural: true,
  1292       datatype: "microformat",
  1293       microformat: "hCard"
  1294     },
  1295     "bday" : {
  1296       datatype: "dateTime"
  1297     },
  1298     "class" : {
  1299     },
  1300     "category" : {
  1301       plural: true,
  1302       datatype: "microformat",
  1303       microformat: "tag",
  1304       microformat_property: "tag"
  1305     },
  1306     "email" : {
  1307       subproperties: {
  1308         "type" : {
  1309           plural: true,
  1310           values: ["internet", "x400", "pref"]
  1311         },
  1312         "value" : {
  1313           datatype: "email",
  1314           virtual: true
  1316       },
  1317       plural: true   
  1318     },
  1319     "fn" : {
  1320       required: true
  1321     },
  1322     "geo" : {
  1323       datatype: "microformat",
  1324       microformat: "geo"
  1325     },
  1326     "key" : {
  1327       plural: true
  1328     },
  1329     "label" : {
  1330       plural: true
  1331     },
  1332     "logo" : {
  1333       plural: true,
  1334       datatype: "anyURI"
  1335     },
  1336     "mailer" : {
  1337       plural: true
  1338     },
  1339     "n" : {
  1340       subproperties: {
  1341         "honorific-prefix" : {
  1342           plural: true
  1343         },
  1344         "given-name" : {
  1345           plural: true
  1346         },
  1347         "additional-name" : {
  1348           plural: true
  1349         },
  1350         "family-name" : {
  1351           plural: true
  1352         },
  1353         "honorific-suffix" : {
  1354           plural: true
  1356       },
  1357       virtual: true,
  1358       /*  Implied "n" Optimization */
  1359       /* http://microformats.org/wiki/hcard#Implied_.22n.22_Optimization */
  1360       virtualGetter: function(mfnode) {
  1361         var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn");
  1362         var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org");
  1363         var given_name = [];
  1364         var family_name = [];
  1365         if (fn && (!orgs || (orgs.length > 1) || (fn != orgs[0]["organization-name"]))) {
  1366           var fns = fn.split(" ");
  1367           if (fns.length === 2) {
  1368             if (fns[0].charAt(fns[0].length-1) == ',') {
  1369               given_name[0] = fns[1];
  1370               family_name[0] = fns[0].substr(0, fns[0].length-1);
  1371             } else if (fns[1].length == 1) {
  1372               given_name[0] = fns[1];
  1373               family_name[0] = fns[0];
  1374             } else if ((fns[1].length == 2) && (fns[1].charAt(fns[1].length-1) == '.')) {
  1375               given_name[0] = fns[1];
  1376               family_name[0] = fns[0];
  1377             } else {
  1378               given_name[0] = fns[0];
  1379               family_name[0] = fns[1];
  1381             return {"given-name" : given_name, "family-name" : family_name};
  1385     },
  1386     "nickname" : {
  1387       plural: true,
  1388       virtual: true,
  1389       /* Implied "nickname" Optimization */
  1390       /* http://microformats.org/wiki/hcard#Implied_.22nickname.22_Optimization */
  1391       virtualGetter: function(mfnode) {
  1392         var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn");
  1393         var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org");
  1394         var given_name;
  1395         var family_name;
  1396         if (fn && (!orgs || (orgs.length) > 1 || (fn != orgs[0]["organization-name"]))) {
  1397           var fns = fn.split(" ");
  1398           if (fns.length === 1) {
  1399             return [fns[0]];
  1402         return;
  1404     },
  1405     "note" : {
  1406       plural: true,
  1407       datatype: "HTML"
  1408     },
  1409     "org" : {
  1410       subproperties: {
  1411         "organization-name" : {
  1412           virtual: true
  1413         },
  1414         "organization-unit" : {
  1415           plural: true
  1417       },
  1418       plural: true
  1419     },
  1420     "photo" : {
  1421       plural: true,
  1422       datatype: "anyURI"
  1423     },
  1424     "rev" : {
  1425       datatype: "dateTime"
  1426     },
  1427     "role" : {
  1428       plural: true
  1429     },
  1430     "sequence" : {
  1431     },
  1432     "sort-string" : {
  1433     },
  1434     "sound" : {
  1435       plural: true
  1436     },
  1437     "title" : {
  1438       plural: true
  1439     },
  1440     "tel" : {
  1441       subproperties: {
  1442         "type" : {
  1443           plural: true,
  1444           values: ["msg", "home", "work", "pref", "voice", "fax", "cell", "video", "pager", "bbs", "car", "isdn", "pcs"]
  1445         },
  1446         "value" : {
  1447           datatype: "tel",
  1448           virtual: true
  1450       },
  1451       plural: true
  1452     },
  1453     "tz" : {
  1454     },
  1455     "uid" : {
  1456       datatype: "anyURI"
  1457     },
  1458     "url" : {
  1459       plural: true,
  1460       datatype: "anyURI"
  1463 };
  1465 Microformats.add("hCard", hCard_definition);
  1467 this.hCalendar = function hCalendar(node, validate) {
  1468   if (node) {
  1469     Microformats.parser.newMicroformat(this, node, "hCalendar", validate);
  1472 hCalendar.prototype.toString = function() {
  1473   if (this.resolvedNode) {
  1474     /* If this microformat has an include pattern, put the */
  1475     /* dtstart in parenthesis after the summary to differentiate */
  1476     /* them. */
  1477     var summaries = Microformats.getElementsByClassName(this.node, "summary");
  1478     if (summaries.length === 0) {
  1479       if (this.summary) {
  1480         if (this.dtstart) {
  1481           return this.summary + " (" + Microformats.dateFromISO8601(this.dtstart).toLocaleString() + ")";
  1486   if (this.dtstart) {
  1487     return this.summary;
  1489   return;
  1492 var hCalendar_definition = {
  1493   mfObject: hCalendar,
  1494   className: "vevent",
  1495   required: ["summary", "dtstart"],
  1496   properties: {
  1497     "category" : {
  1498       plural: true,
  1499       datatype: "microformat",
  1500       microformat: "tag",
  1501       microformat_property: "tag"
  1502     },
  1503     "class" : {
  1504       values: ["public", "private", "confidential"]
  1505     },
  1506     "description" : {
  1507       datatype: "HTML"
  1508     },
  1509     "dtstart" : {
  1510       datatype: "dateTime"
  1511     },
  1512     "dtend" : {
  1513       datatype: "dateTime"
  1514     },
  1515     "dtstamp" : {
  1516       datatype: "dateTime"
  1517     },
  1518     "duration" : {
  1519     },
  1520     "geo" : {
  1521       datatype: "microformat",
  1522       microformat: "geo"
  1523     },
  1524     "location" : {
  1525       datatype: "microformat",
  1526       microformat: "hCard"
  1527     },
  1528     "status" : {
  1529       values: ["tentative", "confirmed", "cancelled"]
  1530     },
  1531     "summary" : {},
  1532     "transp" : {
  1533       values: ["opaque", "transparent"]
  1534     },
  1535     "uid" : {
  1536       datatype: "anyURI"
  1537     },
  1538     "url" : {
  1539       datatype: "anyURI"
  1540     },
  1541     "last-modified" : {
  1542       datatype: "dateTime"
  1543     },
  1544     "rrule" : {
  1545       subproperties: {
  1546         "interval" : {
  1547           virtual: true,
  1548           /* This will only be called in the virtual case */
  1549           virtualGetter: function(mfnode) {
  1550             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "interval");
  1552         },
  1553         "freq" : {
  1554           virtual: true,
  1555           /* This will only be called in the virtual case */
  1556           virtualGetter: function(mfnode) {
  1557             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "freq");
  1559         },
  1560         "bysecond" : {
  1561           virtual: true,
  1562           /* This will only be called in the virtual case */
  1563           virtualGetter: function(mfnode) {
  1564             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bysecond");
  1566         },
  1567         "byminute" : {
  1568           virtual: true,
  1569           /* This will only be called in the virtual case */
  1570           virtualGetter: function(mfnode) {
  1571             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byminute");
  1573         },
  1574         "byhour" : {
  1575           virtual: true,
  1576           /* This will only be called in the virtual case */
  1577           virtualGetter: function(mfnode) {
  1578             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byhour");
  1580         },
  1581         "bymonthday" : {
  1582           virtual: true,
  1583           /* This will only be called in the virtual case */
  1584           virtualGetter: function(mfnode) {
  1585             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonthday");
  1587         },
  1588         "byyearday" : {
  1589           virtual: true,
  1590           /* This will only be called in the virtual case */
  1591           virtualGetter: function(mfnode) {
  1592             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byyearday");
  1594         },
  1595         "byweekno" : {
  1596           virtual: true,
  1597           /* This will only be called in the virtual case */
  1598           virtualGetter: function(mfnode) {
  1599             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byweekno");
  1601         },
  1602         "bymonth" : {
  1603           virtual: true,
  1604           /* This will only be called in the virtual case */
  1605           virtualGetter: function(mfnode) {
  1606             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonth");
  1608         },
  1609         "byday" : {
  1610           virtual: true,
  1611           /* This will only be called in the virtual case */
  1612           virtualGetter: function(mfnode) {
  1613             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byday");
  1615         },
  1616         "until" : {
  1617           virtual: true,
  1618           /* This will only be called in the virtual case */
  1619           virtualGetter: function(mfnode) {
  1620             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "until");
  1622         },
  1623         "count" : {
  1624           virtual: true,
  1625           /* This will only be called in the virtual case */
  1626           virtualGetter: function(mfnode) {
  1627             return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "count");
  1630       },
  1631       retrieve: function(mfnode, property) {
  1632         var value = Microformats.parser.textGetter(mfnode);
  1633         var rrule;
  1634         rrule = value.split(';');
  1635         for (let i=0; i < rrule.length; i++) {
  1636           if (rrule[i].match(property)) {
  1637             return rrule[i].split('=')[1];
  1643 };
  1645 Microformats.add("hCalendar", hCalendar_definition);
  1647 this.geo = function geo(node, validate) {
  1648   if (node) {
  1649     Microformats.parser.newMicroformat(this, node, "geo", validate);
  1652 geo.prototype.toString = function() {
  1653   if (this.latitude != undefined) {
  1654     if (!isFinite(this.latitude) || (this.latitude > 360) || (this.latitude < -360)) {
  1655       return;
  1658   if (this.longitude != undefined) {
  1659     if (!isFinite(this.longitude) || (this.longitude > 360) || (this.longitude < -360)) {
  1660       return;
  1664   if ((this.latitude != undefined) && (this.longitude != undefined)) {
  1665     var s;
  1666     if ((this.node.localName.toLowerCase() == "abbr") || (this.node.localName.toLowerCase() == "html:abbr")) {
  1667       s = this.node.textContent;
  1670     if (s) {
  1671       return s;
  1674     /* check if geo is contained in a vcard */
  1675     var xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vcard ')]";
  1676     var xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
  1677     if (xpathResult.singleNodeValue) {
  1678       var hcard = new hCard(xpathResult.singleNodeValue);
  1679       if (hcard.fn) {
  1680         return hcard.fn;
  1683     /* check if geo is contained in a vevent */
  1684     xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vevent ')]";
  1685     xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, xpathResult);
  1686     if (xpathResult.singleNodeValue) {
  1687       var hcal = new hCalendar(xpathResult.singleNodeValue);
  1688       if (hcal.summary) {
  1689         return hcal.summary;
  1692     if (s) {
  1693       return s;
  1694     } else {
  1695       return this.latitude + ", " + this.longitude;
  1700 var geo_definition = {
  1701   mfObject: geo,
  1702   className: "geo",
  1703   required: ["latitude","longitude"],
  1704   properties: {
  1705     "latitude" : {
  1706       datatype: "float",
  1707       virtual: true,
  1708       /* This will only be called in the virtual case */
  1709       virtualGetter: function(mfnode) {
  1710         var value = Microformats.parser.textGetter(mfnode);
  1711         var latlong;
  1712         if (value.match(';')) {
  1713           latlong = value.split(';');
  1714           if (latlong[0]) {
  1715             if (!isNaN(latlong[0])) {
  1716               return parseFloat(latlong[0]);
  1721     },
  1722     "longitude" : {
  1723       datatype: "float",
  1724       virtual: true,
  1725       /* This will only be called in the virtual case */
  1726       virtualGetter: function(mfnode) {
  1727         var value = Microformats.parser.textGetter(mfnode);
  1728         var latlong;
  1729         if (value.match(';')) {
  1730           latlong = value.split(';');
  1731           if (latlong[1]) {
  1732             if (!isNaN(latlong[1])) {
  1733               return parseFloat(latlong[1]);
  1739   },
  1740   validate: function(node) {
  1741     var latitude = Microformats.parser.getMicroformatProperty(node, "geo", "latitude");
  1742     var longitude = Microformats.parser.getMicroformatProperty(node, "geo", "longitude");
  1743     if (latitude != undefined) {
  1744       if (!isFinite(latitude) || (latitude > 360) || (latitude < -360)) {
  1745         throw("Invalid latitude");
  1747     } else {
  1748       throw("No latitude specified");
  1750     if (longitude != undefined) {
  1751       if (!isFinite(longitude) || (longitude > 360) || (longitude < -360)) {
  1752         throw("Invalid longitude");
  1754     } else {
  1755       throw("No longitude specified");
  1757     return true;
  1759 };
  1761 Microformats.add("geo", geo_definition);
  1763 this.tag = function tag(node, validate) {
  1764   if (node) {
  1765     Microformats.parser.newMicroformat(this, node, "tag", validate);
  1768 tag.prototype.toString = function() {
  1769   return this.tag;
  1772 var tag_definition = {
  1773   mfObject: tag,
  1774   attributeName: "rel",
  1775   attributeValues: "tag",
  1776   properties: {
  1777     "tag" : {
  1778       virtual: true,
  1779       virtualGetter: function(mfnode) {
  1780         if (mfnode.href) {
  1781           var ioService = Components.classes["@mozilla.org/network/io-service;1"].
  1782                                      getService(Components.interfaces.nsIIOService);
  1783           var uri = ioService.newURI(mfnode.href, null, null);
  1784           var url_array = uri.path.split("/");
  1785           for(let i=url_array.length-1; i > 0; i--) {
  1786             if (url_array[i] !== "") {
  1787               var tag
  1788               if (tag = Microformats.tag.validTagName(url_array[i].replace(/\+/g, ' '))) {
  1789                 try {
  1790                   return decodeURIComponent(tag);
  1791                 } catch (ex) {
  1792                   return unescape(tag);
  1798         return null;
  1800     },
  1801     "link" : {
  1802       virtual: true,
  1803       datatype: "anyURI"
  1804     },
  1805     "text" : {
  1806       virtual: true
  1808   },
  1809   validTagName: function(tag)
  1811     var returnTag = tag;
  1812     if (tag.indexOf('?') != -1) {
  1813       if (tag.indexOf('?') === 0) {
  1814         return false;
  1815       } else {
  1816         returnTag = tag.substr(0, tag.indexOf('?'));
  1819     if (tag.indexOf('#') != -1) {
  1820       if (tag.indexOf('#') === 0) {
  1821         return false;
  1822       } else {
  1823         returnTag = tag.substr(0, tag.indexOf('#'));
  1826     if (tag.indexOf('.html') != -1) {
  1827       if (tag.indexOf('.html') == tag.length - 5) {
  1828         return false;
  1831     return returnTag;
  1832   },
  1833   validate: function(node) {
  1834     var tag = Microformats.parser.getMicroformatProperty(node, "tag", "tag");
  1835     if (!tag) {
  1836       if (node.href) {
  1837         var url_array = node.getAttribute("href").split("/");
  1838         for(let i=url_array.length-1; i > 0; i--) {
  1839           if (url_array[i] !== "") {
  1840             throw("Invalid tag name (" + url_array[i] + ")");
  1843       } else {
  1844         throw("No href specified on tag");
  1847     return true;
  1849 };
  1851 Microformats.add("tag", tag_definition);

mercurial