home *** CD-ROM | disk | FTP | other *** search
/ PC Advisor 2010 April / PCA177.iso / ESSENTIALS / Firefox Setup.exe / nonlocalized / modules / Microformats.js < prev    next >
Encoding:
Text File  |  2009-07-15  |  64.9 KB  |  1,838 lines

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