toolkit/components/microformats/Microformats.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/microformats/Microformats.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1851 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +this.EXPORTED_SYMBOLS = ["Microformats", "adr", "tag", "hCard", "hCalendar", "geo"];
     1.9 +
    1.10 +this.Microformats = {
    1.11 +  /* When a microformat is added, the name is placed in this list */
    1.12 +  list: [],
    1.13 +  /* Custom iterator so that microformats can be enumerated as */
    1.14 +  /* for (i in Microformats) */
    1.15 +  __iterator__: function () {
    1.16 +    for (let i=0; i < this.list.length; i++) {
    1.17 +      yield this.list[i];
    1.18 +    }
    1.19 +  },
    1.20 +  /**
    1.21 +   * Retrieves microformats objects of the given type from a document
    1.22 +   * 
    1.23 +   * @param  name          The name of the microformat (required)
    1.24 +   * @param  rootElement   The DOM element at which to start searching (required)
    1.25 +   * @param  options       Literal object with the following options:
    1.26 +   *                       recurseExternalFrames - Whether or not to search child frames
    1.27 +   *                       that reference external pages (with a src attribute)
    1.28 +   *                       for microformats (optional - defaults to true)
    1.29 +   *                       showHidden -  Whether or not to add hidden microformat
    1.30 +   *                       (optional - defaults to false)
    1.31 +   *                       debug - Whether or not we are in debug mode (optional
    1.32 +   *                       - defaults to false)
    1.33 +   * @param  targetArray  An array of microformat objects to which is added the results (optional)
    1.34 +   * @return A new array of microformat objects or the passed in microformat 
    1.35 +   *         object array with the new objects added
    1.36 +   */
    1.37 +  get: function(name, rootElement, options, targetArray) {
    1.38 +    function isAncestor(haystack, needle) {
    1.39 +      var parent = needle;
    1.40 +      while (parent = parent.parentNode) {
    1.41 +        /* We need to check parentNode because defaultView.frames[i].frameElement */
    1.42 +        /* isn't a real DOM node */
    1.43 +        if (parent == needle.parentNode) {
    1.44 +          return true;
    1.45 +        }
    1.46 +      }
    1.47 +      return false;
    1.48 +    }
    1.49 +    if (!Microformats[name] || !rootElement) {
    1.50 +      return;
    1.51 +    }
    1.52 +    targetArray = targetArray || [];
    1.53 +
    1.54 +    /* Root element might not be the document - we need the document's default view */
    1.55 +    /* to get frames and to check their ancestry */
    1.56 +    var defaultView = rootElement.defaultView || rootElement.ownerDocument.defaultView;
    1.57 +    var rootDocument = rootElement.ownerDocument || rootElement;
    1.58 +
    1.59 +    /* If recurseExternalFrames is undefined or true, look through all child frames for microformats */
    1.60 +    if (!options || !options.hasOwnProperty("recurseExternalFrames") || options.recurseExternalFrames) {
    1.61 +      if (defaultView && defaultView.frames.length > 0) {
    1.62 +        for (let i=0; i < defaultView.frames.length; i++) {
    1.63 +          if (isAncestor(rootDocument, defaultView.frames[i].frameElement)) {
    1.64 +            Microformats.get(name, defaultView.frames[i].document, options, targetArray);
    1.65 +          }
    1.66 +        }
    1.67 +      }
    1.68 +    }
    1.69 +
    1.70 +    /* Get the microformat nodes for the document */
    1.71 +    var microformatNodes = [];
    1.72 +    if (Microformats[name].className) {
    1.73 +      microformatNodes = Microformats.getElementsByClassName(rootElement,
    1.74 +                                        Microformats[name].className);
    1.75 +      /* alternateClassName is for cases where a parent microformat is inferred by the children */
    1.76 +      /* If we find alternateClassName, the entire document becomes the microformat */
    1.77 +      if ((microformatNodes.length == 0) && Microformats[name].alternateClassName) {
    1.78 +        var altClass = Microformats.getElementsByClassName(rootElement, Microformats[name].alternateClassName);
    1.79 +        if (altClass.length > 0) {
    1.80 +          microformatNodes.push(rootElement); 
    1.81 +        }
    1.82 +      }
    1.83 +    } else if (Microformats[name].attributeValues) {
    1.84 +      microformatNodes =
    1.85 +        Microformats.getElementsByAttribute(rootElement,
    1.86 +                                            Microformats[name].attributeName,
    1.87 +                                            Microformats[name].attributeValues);
    1.88 +      
    1.89 +    }
    1.90 +    
    1.91 +
    1.92 +    function isVisible(node, checkChildren) {
    1.93 +      if (node.getBoundingClientRect) {
    1.94 +        var box = node.getBoundingClientRect();
    1.95 +      } else {
    1.96 +        var box = node.ownerDocument.getBoxObjectFor(node);
    1.97 +      }
    1.98 +      /* If the parent has is an empty box, double check the children */
    1.99 +      if ((box.height == 0) || (box.width == 0)) {
   1.100 +        if (checkChildren && node.childNodes.length > 0) {
   1.101 +          for(let i=0; i < node.childNodes.length; i++) {
   1.102 +            if (node.childNodes[i].nodeType == Components.interfaces.nsIDOMNode.ELEMENT_NODE) {
   1.103 +              /* For performance reasons, we only go down one level */
   1.104 +              /* of children */
   1.105 +              if (isVisible(node.childNodes[i], false)) {
   1.106 +                return true;
   1.107 +              }
   1.108 +            }
   1.109 +          }
   1.110 +        }
   1.111 +        return false
   1.112 +      }
   1.113 +      return true;
   1.114 +    }
   1.115 +    
   1.116 +    /* Create objects for the microformat nodes and put them into the microformats */
   1.117 +    /* array */
   1.118 +    for (let i = 0; i < microformatNodes.length; i++) {
   1.119 +      /* If showHidden undefined or false, don't add microformats to the list that aren't visible */
   1.120 +      if (!options || !options.hasOwnProperty("showHidden") || !options.showHidden) {
   1.121 +        if (microformatNodes[i].ownerDocument) {
   1.122 +          if (!isVisible(microformatNodes[i], true)) {
   1.123 +            continue;
   1.124 +          }
   1.125 +        }
   1.126 +      }
   1.127 +      try {
   1.128 +        if (options && options.debug) {
   1.129 +          /* Don't validate in the debug case so that we don't get errors thrown */
   1.130 +          /* in the debug case, we want all microformats, even if they are invalid */
   1.131 +          targetArray.push(new Microformats[name].mfObject(microformatNodes[i], false));
   1.132 +        } else {
   1.133 +          targetArray.push(new Microformats[name].mfObject(microformatNodes[i], true));
   1.134 +        }
   1.135 +      } catch (ex) {
   1.136 +        /* Creation of individual object probably failed because it is invalid. */
   1.137 +        /* This isn't a problem, because the page might have invalid microformats */
   1.138 +      }
   1.139 +    }
   1.140 +    return targetArray;
   1.141 +  },
   1.142 +  /**
   1.143 +   * Counts microformats objects of the given type from a document
   1.144 +   * 
   1.145 +   * @param  name          The name of the microformat (required)
   1.146 +   * @param  rootElement   The DOM element at which to start searching (required)
   1.147 +   * @param  options       Literal object with the following options:
   1.148 +   *                       recurseExternalFrames - Whether or not to search child frames
   1.149 +   *                       that reference external pages (with a src attribute)
   1.150 +   *                       for microformats (optional - defaults to true)
   1.151 +   *                       showHidden -  Whether or not to add hidden microformat
   1.152 +   *                       (optional - defaults to false)
   1.153 +   *                       debug - Whether or not we are in debug mode (optional
   1.154 +   *                       - defaults to false)
   1.155 +   * @return The new count
   1.156 +   */
   1.157 +  count: function(name, rootElement, options) {
   1.158 +    var mfArray = Microformats.get(name, rootElement, options);
   1.159 +    if (mfArray) {
   1.160 +      return mfArray.length;
   1.161 +    }
   1.162 +    return 0;
   1.163 +  },
   1.164 +  /**
   1.165 +   * Returns true if the passed in node is a microformat. Does NOT return true
   1.166 +   * if the passed in node is a child of a microformat.
   1.167 +   *
   1.168 +   * @param  node          DOM node to check
   1.169 +   * @return true if the node is a microformat, false if it is not
   1.170 +   */
   1.171 +  isMicroformat: function(node) {
   1.172 +    for (let i in Microformats)
   1.173 +    {
   1.174 +      if (Microformats[i].className) {
   1.175 +        if (Microformats.matchClass(node, Microformats[i].className)) {
   1.176 +            return true;
   1.177 +        }
   1.178 +      } else {
   1.179 +        var attribute;
   1.180 +        if (attribute = node.getAttribute(Microformats[i].attributeName)) {
   1.181 +          var attributeList = Microformats[i].attributeValues.split(" ");
   1.182 +          for (let j=0; j < attributeList.length; j++) {
   1.183 +            if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) {
   1.184 +              return true;
   1.185 +            }
   1.186 +          }
   1.187 +        }
   1.188 +      }
   1.189 +    }
   1.190 +    return false;
   1.191 +  },
   1.192 +  /**
   1.193 +   * This function searches a given nodes ancestors looking for a microformat
   1.194 +   * and if it finds it, returns it. It does NOT include self, so if the passed
   1.195 +   * in node is a microformat, it will still search ancestors for a microformat.
   1.196 +   *
   1.197 +   * @param  node          DOM node to check
   1.198 +   * @return If the node is contained in a microformat, it returns the parent
   1.199 +   *         DOM node, otherwise returns null
   1.200 +   */
   1.201 +  getParent: function(node) {
   1.202 +    var xpathExpression;
   1.203 +    var xpathResult;
   1.204 +
   1.205 +    xpathExpression = "ancestor::*[";
   1.206 +    for (let i=0; i < Microformats.list.length; i++) {
   1.207 +      var mfname = Microformats.list[i];
   1.208 +      if (i != 0) {
   1.209 +        xpathExpression += " or ";
   1.210 +      }
   1.211 +      if (Microformats[mfname].className) {
   1.212 +        xpathExpression += "contains(concat(' ', @class, ' '), ' " + Microformats[mfname].className + " ')";
   1.213 +      } else {
   1.214 +        var attributeList = Microformats[mfname].attributeValues.split(" ");
   1.215 +        for (let j=0; j < attributeList.length; j++) {
   1.216 +          if (j != 0) {
   1.217 +            xpathExpression += " or ";
   1.218 +          }
   1.219 +          xpathExpression += "contains(concat(' ', @" + Microformats[mfname].attributeName + ", ' '), ' " + attributeList[j] + " ')";
   1.220 +        }
   1.221 +      }
   1.222 +    }
   1.223 +    xpathExpression += "][1]";
   1.224 +    xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
   1.225 +    if (xpathResult.singleNodeValue) {
   1.226 +      xpathResult.singleNodeValue.microformat = mfname;
   1.227 +      return xpathResult.singleNodeValue;
   1.228 +    }
   1.229 +    return null;
   1.230 +  },
   1.231 +  /**
   1.232 +   * If the passed in node is a microformat, this function returns a space 
   1.233 +   * separated list of the microformat names that correspond to this node
   1.234 +   *
   1.235 +   * @param  node          DOM node to check
   1.236 +   * @return If the node is a microformat, a space separated list of microformat
   1.237 +   *         names, otherwise returns nothing
   1.238 +   */
   1.239 +  getNamesFromNode: function(node) {
   1.240 +    var microformatNames = [];
   1.241 +    var xpathExpression;
   1.242 +    var xpathResult;
   1.243 +    for (let i in Microformats)
   1.244 +    {
   1.245 +      if (Microformats[i]) {
   1.246 +        if (Microformats[i].className) {
   1.247 +          if (Microformats.matchClass(node, Microformats[i].className)) {
   1.248 +            microformatNames.push(i);
   1.249 +            continue;
   1.250 +          }
   1.251 +        } else if (Microformats[i].attributeValues) {
   1.252 +          var attribute;
   1.253 +          if (attribute = node.getAttribute(Microformats[i].attributeName)) {
   1.254 +            var attributeList = Microformats[i].attributeValues.split(" ");
   1.255 +            for (let j=0; j < attributeList.length; j++) {
   1.256 +              /* If we match any attribute, we've got a microformat */
   1.257 +              if (attribute.match("(^|\\s)" + attributeList[j] + "(\\s|$)")) {
   1.258 +                microformatNames.push(i);
   1.259 +                break;
   1.260 +              }
   1.261 +            }
   1.262 +          }
   1.263 +        }
   1.264 +      }
   1.265 +    }
   1.266 +    return microformatNames.join(" ");
   1.267 +  },
   1.268 +  /**
   1.269 +   * Outputs the contents of a microformat object for debug purposes.
   1.270 +   *
   1.271 +   * @param  microformatObject JavaScript object that represents a microformat
   1.272 +   * @return string containing a visual representation of the contents of the microformat
   1.273 +   */
   1.274 +  debug: function debug(microformatObject) {
   1.275 +    function dumpObject(item, indent)
   1.276 +    {
   1.277 +      if (!indent) {
   1.278 +        indent = "";
   1.279 +      }
   1.280 +      var toreturn = "";
   1.281 +      var testArray = [];
   1.282 +      
   1.283 +      for (let i in item)
   1.284 +      {
   1.285 +        if (testArray[i]) {
   1.286 +          continue;
   1.287 +        }
   1.288 +        if (typeof item[i] == "object") {
   1.289 +          if ((i != "node") && (i != "resolvedNode")) {
   1.290 +            if (item[i] && item[i].semanticType) {
   1.291 +              toreturn += indent + item[i].semanticType + " [" + i + "] { \n";
   1.292 +            } else {
   1.293 +              toreturn += indent + "object " + i + " { \n";
   1.294 +            }
   1.295 +            toreturn += dumpObject(item[i], indent + "\t");
   1.296 +            toreturn += indent + "}\n";
   1.297 +          }
   1.298 +        } else if ((typeof item[i] != "function") && (i != "semanticType")) {
   1.299 +          if (item[i]) {
   1.300 +            toreturn += indent + i + "=" + item[i] + "\n";
   1.301 +          }
   1.302 +        }
   1.303 +      }
   1.304 +      if (!toreturn && item) {
   1.305 +        toreturn = item.toString();
   1.306 +      }
   1.307 +      return toreturn;
   1.308 +    }
   1.309 +    return dumpObject(microformatObject);
   1.310 +  },
   1.311 +  add: function add(microformat, microformatDefinition) {
   1.312 +    /* We always replace an existing definition with the new one */
   1.313 +    if (!Microformats[microformat]) {
   1.314 +      Microformats.list.push(microformat);
   1.315 +    }
   1.316 +    Microformats[microformat] = microformatDefinition;
   1.317 +    microformatDefinition.mfObject.prototype.debug =
   1.318 +      function(microformatObject) {
   1.319 +        return Microformats.debug(microformatObject)
   1.320 +      };
   1.321 +  },
   1.322 +  remove: function remove(microformat) {
   1.323 +    if (Microformats[microformat]) {
   1.324 +      var list = Microformats.list;
   1.325 +      var index = list.indexOf(microformat, 1);
   1.326 +      if (index != -1) {
   1.327 +        list.splice(index, 1);
   1.328 +      }
   1.329 +      delete Microformats[microformat];
   1.330 +    }
   1.331 +  },
   1.332 +  /* All parser specific functions are contained in this object */
   1.333 +  parser: {
   1.334 +    /**
   1.335 +     * Uses the microformat patterns to decide what the correct text for a
   1.336 +     * given microformat property is. This includes looking at things like
   1.337 +     * abbr, img/alt, area/alt and value excerpting.
   1.338 +     *
   1.339 +     * @param  propnode   The DOMNode to check
   1.340 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.341 +     *                    this is the parent property node. If it is not, this is the
   1.342 +     *                    microformat node.
   1.343 +     & @param  datatype   HTML/text - whether to use innerHTML or innerText - defaults to text
   1.344 +     * @return A string with the value of the property
   1.345 +     */
   1.346 +    defaultGetter: function(propnode, parentnode, datatype) {
   1.347 +      function collapseWhitespace(instring) {
   1.348 +        /* Remove new lines, carriage returns and tabs */
   1.349 +        outstring = instring.replace(/[\n\r\t]/gi, ' ');
   1.350 +        /* Replace any double spaces with single spaces */
   1.351 +        outstring = outstring.replace(/\s{2,}/gi, ' ');
   1.352 +        /* Remove any double spaces that are left */
   1.353 +        outstring = outstring.replace(/\s{2,}/gi, '');
   1.354 +        /* Remove any spaces at the beginning */
   1.355 +        outstring = outstring.replace(/^\s+/, '');
   1.356 +        /* Remove any spaces at the end */
   1.357 +        outstring = outstring.replace(/\s+$/, '');
   1.358 +        return outstring;
   1.359 +      }
   1.360 +      
   1.361 +      
   1.362 +      if (((((propnode.localName.toLowerCase() == "abbr") || (propnode.localName.toLowerCase() == "html:abbr")) && !propnode.namespaceURI) || 
   1.363 +         ((propnode.localName.toLowerCase() == "abbr") && (propnode.namespaceURI == "http://www.w3.org/1999/xhtml"))) && (propnode.hasAttribute("title"))) {
   1.364 +        return propnode.getAttribute("title");
   1.365 +      } else if ((propnode.nodeName.toLowerCase() == "img") && (propnode.hasAttribute("alt"))) {
   1.366 +        return propnode.getAttribute("alt");
   1.367 +      } else if ((propnode.nodeName.toLowerCase() == "area") && (propnode.hasAttribute("alt"))) {
   1.368 +        return propnode.getAttribute("alt");
   1.369 +      } else if ((propnode.nodeName.toLowerCase() == "textarea") ||
   1.370 +                 (propnode.nodeName.toLowerCase() == "select") ||
   1.371 +                 (propnode.nodeName.toLowerCase() == "input")) {
   1.372 +        return propnode.value;
   1.373 +      } else {
   1.374 +        var values = Microformats.getElementsByClassName(propnode, "value");
   1.375 +        /* Verify that values are children of the propnode */
   1.376 +        for (let i = values.length-1; i >= 0; i--) {
   1.377 +          if (values[i].parentNode != propnode) {
   1.378 +            values.splice(i,1);
   1.379 +          }
   1.380 +        }
   1.381 +        if (values.length > 0) {
   1.382 +          var value = "";
   1.383 +          for (let j=0;j<values.length;j++) {
   1.384 +            value += Microformats.parser.defaultGetter(values[j], propnode, datatype);
   1.385 +          }
   1.386 +          return collapseWhitespace(value);
   1.387 +        }
   1.388 +        var s;
   1.389 +        if (datatype == "HTML") {
   1.390 +          s = propnode.innerHTML;
   1.391 +        } else {
   1.392 +          if (propnode.innerText) {
   1.393 +            s = propnode.innerText;
   1.394 +          } else {
   1.395 +            s = propnode.textContent;
   1.396 +          }
   1.397 +        }
   1.398 +        /* If we are processing a value node, don't remove whitespace now */
   1.399 +        /* (we'll do it later) */
   1.400 +        if (!Microformats.matchClass(propnode, "value")) {
   1.401 +          s = collapseWhitespace(s);
   1.402 +        }
   1.403 +        if (s.length > 0) {
   1.404 +          return s;
   1.405 +        }
   1.406 +      }
   1.407 +    },
   1.408 +    /**
   1.409 +     * Used to specifically retrieve a date in a microformat node.
   1.410 +     * After getting the default text, it normalizes it to an ISO8601 date.
   1.411 +     *
   1.412 +     * @param  propnode   The DOMNode to check
   1.413 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.414 +     *                    this is the parent property node. If it is not, this is the
   1.415 +     *                    microformat node.
   1.416 +     * @return A string with the normalized date.
   1.417 +     */
   1.418 +    dateTimeGetter: function(propnode, parentnode) {
   1.419 +      var date = Microformats.parser.textGetter(propnode, parentnode);
   1.420 +      if (date) {
   1.421 +        return Microformats.parser.normalizeISO8601(date);
   1.422 +      }
   1.423 +    },
   1.424 +    /**
   1.425 +     * Used to specifically retrieve a URI in a microformat node. This includes
   1.426 +     * looking at an href/img/object/area to get the fully qualified URI.
   1.427 +     *
   1.428 +     * @param  propnode   The DOMNode to check
   1.429 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.430 +     *                    this is the parent property node. If it is not, this is the
   1.431 +     *                    microformat node.
   1.432 +     * @return A string with the fully qualified URI.
   1.433 +     */
   1.434 +    uriGetter: function(propnode, parentnode) {
   1.435 +      var pairs = {"a":"href", "img":"src", "object":"data", "area":"href"};
   1.436 +      var name = propnode.nodeName.toLowerCase();
   1.437 +      if (pairs.hasOwnProperty(name)) {
   1.438 +        return propnode[pairs[name]];
   1.439 +      }
   1.440 +      return Microformats.parser.textGetter(propnode, parentnode);
   1.441 +    },
   1.442 +    /**
   1.443 +     * Used to specifically retrieve a telephone number in a microformat node.
   1.444 +     * Basically this is to handle the face that telephone numbers use value
   1.445 +     * as the name as one of their subproperties, but value is also used for
   1.446 +     * value excerpting (http://microformats.org/wiki/hcard#Value_excerpting)
   1.447 +     
   1.448 +     * @param  propnode   The DOMNode to check
   1.449 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.450 +     *                    this is the parent property node. If it is not, this is the
   1.451 +     *                    microformat node.
   1.452 +     * @return A string with the telephone number
   1.453 +     */
   1.454 +    telGetter: function(propnode, parentnode) {
   1.455 +      var pairs = {"a":"href", "object":"data", "area":"href"};
   1.456 +      var name = propnode.nodeName.toLowerCase();
   1.457 +      if (pairs.hasOwnProperty(name)) {
   1.458 +        var protocol;
   1.459 +        if (propnode[pairs[name]].indexOf("tel:") == 0) {
   1.460 +          protocol = "tel:";
   1.461 +        }
   1.462 +        if (propnode[pairs[name]].indexOf("fax:") == 0) {
   1.463 +          protocol = "fax:";
   1.464 +        }
   1.465 +        if (propnode[pairs[name]].indexOf("modem:") == 0) {
   1.466 +          protocol = "modem:";
   1.467 +        }
   1.468 +        if (protocol) {
   1.469 +          if (propnode[pairs[name]].indexOf('?') > 0) {
   1.470 +            return unescape(propnode[pairs[name]].substring(protocol.length, propnode[pairs[name]].indexOf('?')));
   1.471 +          } else {
   1.472 +            return unescape(propnode[pairs[name]].substring(protocol.length));
   1.473 +          }
   1.474 +        }
   1.475 +      }
   1.476 +     /* Special case - if this node is a value, use the parent node to get all the values */
   1.477 +      if (Microformats.matchClass(propnode, "value")) {
   1.478 +        return Microformats.parser.textGetter(parentnode, parentnode);
   1.479 +      } else {
   1.480 +        /* Virtual case */
   1.481 +        if (!parentnode && (Microformats.getElementsByClassName(propnode, "type").length > 0)) {
   1.482 +          var tempNode = propnode.cloneNode(true);
   1.483 +          var typeNodes = Microformats.getElementsByClassName(tempNode, "type");
   1.484 +          for (let i=0; i < typeNodes.length; i++) {
   1.485 +            typeNodes[i].parentNode.removeChild(typeNodes[i]);
   1.486 +          }
   1.487 +          return Microformats.parser.textGetter(tempNode);
   1.488 +        }
   1.489 +        return Microformats.parser.textGetter(propnode, parentnode);
   1.490 +      }
   1.491 +    },
   1.492 +    /**
   1.493 +     * Used to specifically retrieve an email address in a microformat node.
   1.494 +     * This includes at an href, as well as removing subject if specified and
   1.495 +     * the mailto prefix.
   1.496 +     *
   1.497 +     * @param  propnode   The DOMNode to check
   1.498 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.499 +     *                    this is the parent property node. If it is not, this is the
   1.500 +     *                    microformat node.
   1.501 +     * @return A string with the email address.
   1.502 +     */
   1.503 +    emailGetter: function(propnode, parentnode) {
   1.504 +      if ((propnode.nodeName.toLowerCase() == "a") || (propnode.nodeName.toLowerCase() == "area")) {
   1.505 +        var mailto = propnode.href;
   1.506 +        /* IO Service won't fully parse mailto, so we do it manually */
   1.507 +        if (mailto.indexOf('?') > 0) {
   1.508 +          return unescape(mailto.substring("mailto:".length, mailto.indexOf('?')));
   1.509 +        } else {
   1.510 +          return unescape(mailto.substring("mailto:".length));
   1.511 +        }
   1.512 +      } else {
   1.513 +        /* Special case - if this node is a value, use the parent node to get all the values */
   1.514 +        /* If this case gets executed, per the value design pattern, the result */
   1.515 +        /* will be the EXACT email address with no extra parsing required */
   1.516 +        if (Microformats.matchClass(propnode, "value")) {
   1.517 +          return Microformats.parser.textGetter(parentnode, parentnode);
   1.518 +        } else {
   1.519 +          /* Virtual case */
   1.520 +          if (!parentnode && (Microformats.getElementsByClassName(propnode, "type").length > 0)) {
   1.521 +            var tempNode = propnode.cloneNode(true);
   1.522 +            var typeNodes = Microformats.getElementsByClassName(tempNode, "type");
   1.523 +            for (let i=0; i < typeNodes.length; i++) {
   1.524 +              typeNodes[i].parentNode.removeChild(typeNodes[i]);
   1.525 +            }
   1.526 +            return Microformats.parser.textGetter(tempNode);
   1.527 +          }
   1.528 +          return Microformats.parser.textGetter(propnode, parentnode);
   1.529 +        }
   1.530 +      }
   1.531 +    },
   1.532 +    /**
   1.533 +     * Used when a caller needs the text inside a particular DOM node.
   1.534 +     * It calls defaultGetter to handle all the subtleties of getting
   1.535 +     * text from a microformat.
   1.536 +     *
   1.537 +     * @param  propnode   The DOMNode to check
   1.538 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.539 +     *                    this is the parent property node. If it is not, this is the
   1.540 +     *                    microformat node.
   1.541 +     * @return A string with just the text including all tags.
   1.542 +     */
   1.543 +    textGetter: function(propnode, parentnode) {
   1.544 +      return Microformats.parser.defaultGetter(propnode, parentnode, "text");
   1.545 +    },
   1.546 +    /**
   1.547 +     * Used when a caller needs the HTML inside a particular DOM node.
   1.548 +     *
   1.549 +     * @param  propnode   The DOMNode to check
   1.550 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.551 +     *                    this is the parent property node. If it is not, this is the
   1.552 +     *                    microformat node.
   1.553 +     * @return An emulated string object that also has a new function called toHTML
   1.554 +     */
   1.555 +    HTMLGetter: function(propnode, parentnode) {
   1.556 +      /* This is so we can have a string that behaves like a string */
   1.557 +      /* but also has a new function that can return the HTML that corresponds */
   1.558 +      /* to the string. */
   1.559 +      function mfHTML(value) {
   1.560 +        this.valueOf = function() {return value ? value.valueOf() : "";}
   1.561 +        this.toString = function() {return value ? value.toString() : "";}
   1.562 +      }
   1.563 +      mfHTML.prototype = new String;
   1.564 +      mfHTML.prototype.toHTML = function() {
   1.565 +        return Microformats.parser.defaultGetter(propnode, parentnode, "HTML");
   1.566 +      }
   1.567 +      return new mfHTML(Microformats.parser.defaultGetter(propnode, parentnode, "text"));
   1.568 +    },
   1.569 +    /**
   1.570 +     * Internal parser API used to determine which getter to call based on the
   1.571 +     * datatype specified in the microformat definition.
   1.572 +     *
   1.573 +     * @param  prop       The microformat property in the definition
   1.574 +     * @param  propnode   The DOMNode to check
   1.575 +     * @param  parentnode The parent node of the property. If it is a subproperty,
   1.576 +     *                    this is the parent property node. If it is not, this is the
   1.577 +     *                    microformat node.
   1.578 +     * @return A string with the property value.
   1.579 +     */
   1.580 +    datatypeHelper: function(prop, node, parentnode) {
   1.581 +      var result;
   1.582 +      var datatype = prop.datatype;
   1.583 +      switch (datatype) {
   1.584 +        case "dateTime":
   1.585 +          result = Microformats.parser.dateTimeGetter(node, parentnode);
   1.586 +          break;
   1.587 +        case "anyURI":
   1.588 +          result = Microformats.parser.uriGetter(node, parentnode);
   1.589 +          break;
   1.590 +        case "email":
   1.591 +          result = Microformats.parser.emailGetter(node, parentnode);
   1.592 +          break;
   1.593 +        case "tel":
   1.594 +          result = Microformats.parser.telGetter(node, parentnode);
   1.595 +          break;
   1.596 +        case "HTML":
   1.597 +          result = Microformats.parser.HTMLGetter(node, parentnode);
   1.598 +          break;
   1.599 +        case "float":
   1.600 +          var asText = Microformats.parser.textGetter(node, parentnode);
   1.601 +          if (!isNaN(asText)) {
   1.602 +            result = parseFloat(asText);
   1.603 +          }
   1.604 +          break;
   1.605 +        case "custom":
   1.606 +          result = prop.customGetter(node, parentnode);
   1.607 +          break;
   1.608 +        case "microformat":
   1.609 +          try {
   1.610 +            result = new Microformats[prop.microformat].mfObject(node, true);
   1.611 +          } catch (ex) {
   1.612 +            /* There are two reasons we get here, one because the node is not */
   1.613 +            /* a microformat and two because the node is a microformat and */
   1.614 +            /* creation failed. If the node is not a microformat, we just fall */
   1.615 +            /* through and use the default getter since there are some cases */
   1.616 +            /* (location in hCalendar) where a property can be either a microformat */
   1.617 +            /* or a string. If creation failed, we break and simply don't add the */
   1.618 +            /* microformat property to the parent microformat */
   1.619 +            if (ex != "Node is not a microformat (" + prop.microformat + ")") {
   1.620 +              break;
   1.621 +            }
   1.622 +          }
   1.623 +          if (result != undefined) {
   1.624 +            if (prop.microformat_property) {
   1.625 +              result = result[prop.microformat_property];
   1.626 +            }
   1.627 +            break;
   1.628 +          }
   1.629 +        default:
   1.630 +          result = Microformats.parser.textGetter(node, parentnode);
   1.631 +          break;
   1.632 +      }
   1.633 +      /* This handles the case where one property implies another property */
   1.634 +      /* For instance, org by itself is actually org.organization-name */
   1.635 +      if (prop.values && (result != undefined)) {
   1.636 +        var validType = false;
   1.637 +        for (let value in prop.values) {
   1.638 +          if (result.toLowerCase() == prop.values[value]) {
   1.639 +            result = result.toLowerCase();
   1.640 +            validType = true;
   1.641 +            break;
   1.642 +          }
   1.643 +        }
   1.644 +        if (!validType) {
   1.645 +          return;
   1.646 +        }
   1.647 +      }
   1.648 +      return result;
   1.649 +    },
   1.650 +    newMicroformat: function(object, in_node, microformat, validate) {
   1.651 +      /* check to see if we are even valid */
   1.652 +      if (!Microformats[microformat]) {
   1.653 +        throw("Invalid microformat - " + microformat);
   1.654 +      }
   1.655 +      if (in_node.ownerDocument) {
   1.656 +        if (Microformats[microformat].attributeName) {
   1.657 +          if (!(in_node.hasAttribute(Microformats[microformat].attributeName))) {
   1.658 +            throw("Node is not a microformat (" + microformat + ")");
   1.659 +          }
   1.660 +        } else {
   1.661 +          if (!Microformats.matchClass(in_node, Microformats[microformat].className)) {
   1.662 +            throw("Node is not a microformat (" + microformat + ")");
   1.663 +          }
   1.664 +        }
   1.665 +      }
   1.666 +      var node = in_node;
   1.667 +      if ((Microformats[microformat].className) && in_node.ownerDocument) {
   1.668 +        node = Microformats.parser.preProcessMicroformat(in_node);
   1.669 +      }
   1.670 +
   1.671 +      for (let i in Microformats[microformat].properties) {
   1.672 +        object.__defineGetter__(i, Microformats.parser.getMicroformatPropertyGenerator(node, microformat, i, object));
   1.673 +      }
   1.674 +      
   1.675 +      /* The node in the object should be the original node */
   1.676 +      object.node = in_node;
   1.677 +      /* we also store the node that has been "resolved" */
   1.678 +      object.resolvedNode = node; 
   1.679 +      object.semanticType = microformat;
   1.680 +      if (validate) {
   1.681 +        Microformats.parser.validate(node, microformat);
   1.682 +      }
   1.683 +    },
   1.684 +    getMicroformatPropertyGenerator: function getMicroformatPropertyGenerator(node, name, property, microformat)
   1.685 +    {
   1.686 +      return function() {
   1.687 +        var result = Microformats.parser.getMicroformatProperty(node, name, property);
   1.688 +//        delete microformat[property];
   1.689 +//        microformat[property] = result; 
   1.690 +        return result;
   1.691 +      };
   1.692 +    },
   1.693 +    getPropertyInternal: function getPropertyInternal(propnode, parentnode, propobj, propname, mfnode) {
   1.694 +      var result;
   1.695 +      if (propobj.subproperties) {
   1.696 +        for (let subpropname in propobj.subproperties) {
   1.697 +          var subpropnodes;
   1.698 +          var subpropobj = propobj.subproperties[subpropname];
   1.699 +          if (subpropobj.rel == true) {
   1.700 +            subpropnodes = Microformats.getElementsByAttribute(propnode, "rel", subpropname);
   1.701 +          } else {
   1.702 +            subpropnodes = Microformats.getElementsByClassName(propnode, subpropname);
   1.703 +          }
   1.704 +          var resultArray = [];
   1.705 +          var subresult;
   1.706 +          for (let i = 0; i < subpropnodes.length; i++) {
   1.707 +            subresult = Microformats.parser.getPropertyInternal(subpropnodes[i], propnode,
   1.708 +                                                                subpropobj,
   1.709 +                                                                subpropname, mfnode);
   1.710 +            if (subresult != undefined) {
   1.711 +              resultArray.push(subresult);
   1.712 +              /* If we're not a plural property, don't bother getting more */
   1.713 +              if (!subpropobj.plural) {
   1.714 +                break;
   1.715 +              }
   1.716 +            }
   1.717 +          }
   1.718 +          if (resultArray.length == 0) {
   1.719 +            subresult = Microformats.parser.getPropertyInternal(propnode, null,
   1.720 +                                                                subpropobj,
   1.721 +                                                                subpropname, mfnode);
   1.722 +            if (subresult != undefined) {
   1.723 +              resultArray.push(subresult);
   1.724 +            }
   1.725 +          }
   1.726 +          if (resultArray.length > 0) {
   1.727 +            result = result || {};
   1.728 +            if (subpropobj.plural) {
   1.729 +              result[subpropname] = resultArray;
   1.730 +            } else {
   1.731 +              result[subpropname] = resultArray[0];
   1.732 +            }
   1.733 +          }
   1.734 +        }
   1.735 +      }
   1.736 +      if (!parentnode || (!result && propobj.subproperties)) {
   1.737 +        if (propobj.virtual) {
   1.738 +          if (propobj.virtualGetter) {
   1.739 +            result = propobj.virtualGetter(mfnode || propnode);
   1.740 +          } else {
   1.741 +            result = Microformats.parser.datatypeHelper(propobj, propnode);
   1.742 +          }
   1.743 +        }
   1.744 +      } else if (!result) {
   1.745 +        result = Microformats.parser.datatypeHelper(propobj, propnode, parentnode);
   1.746 +      }
   1.747 +      return result;
   1.748 +    },
   1.749 +    getMicroformatProperty: function getMicroformatProperty(in_mfnode, mfname, propname) {
   1.750 +      var mfnode = in_mfnode;
   1.751 +      /* If the node has not been preprocessed, the requested microformat */
   1.752 +      /* is a class based microformat and the passed in node is not the */
   1.753 +      /* entire document, preprocess it. Preprocessing the node involves */
   1.754 +      /* creating a duplicate of the node and taking care of things like */
   1.755 +      /* the include and header design patterns */
   1.756 +      if (!in_mfnode.origNode && Microformats[mfname].className && in_mfnode.ownerDocument) {
   1.757 +        mfnode = Microformats.parser.preProcessMicroformat(in_mfnode);
   1.758 +      }
   1.759 +      /* propobj is the corresponding property object in the microformat */
   1.760 +      var propobj;
   1.761 +      /* If there is a corresponding property in the microformat, use it */
   1.762 +      if (Microformats[mfname].properties[propname]) {
   1.763 +        propobj = Microformats[mfname].properties[propname];
   1.764 +      } else {
   1.765 +        /* If we didn't get a property, bail */
   1.766 +        return;
   1.767 +      }
   1.768 +      /* Query the correct set of nodes (rel or class) based on the setting */
   1.769 +      /* in the property */
   1.770 +      var propnodes;
   1.771 +      if (propobj.rel == true) {
   1.772 +        propnodes = Microformats.getElementsByAttribute(mfnode, "rel", propname);
   1.773 +      } else {
   1.774 +        propnodes = Microformats.getElementsByClassName(mfnode, propname);
   1.775 +      }
   1.776 +      for (let i=propnodes.length-1; i >= 0; i--) {
   1.777 +        /* The reason getParent is not used here is because this code does */
   1.778 +        /* not apply to attribute based microformats, plus adr and geo */
   1.779 +        /* when contained in hCard are a special case */
   1.780 +        var parentnode;
   1.781 +        var node = propnodes[i];
   1.782 +        var xpathExpression = "";
   1.783 +        for (let j=0; j < Microformats.list.length; j++) {
   1.784 +          /* Don't treat adr or geo in an hCard as a microformat in this case */
   1.785 +          if ((mfname == "hCard") && ((Microformats.list[j] == "adr") || (Microformats.list[j] == "geo"))) {
   1.786 +            continue;
   1.787 +          }
   1.788 +          if (Microformats[Microformats.list[j]].className) {
   1.789 +            if (xpathExpression.length == 0) {
   1.790 +              xpathExpression = "ancestor::*[";
   1.791 +            } else {
   1.792 +              xpathExpression += " or ";
   1.793 +            }
   1.794 +            xpathExpression += "contains(concat(' ', @class, ' '), ' " + Microformats[Microformats.list[j]].className + " ')";
   1.795 +          }
   1.796 +        }
   1.797 +        xpathExpression += "][1]";
   1.798 +        var xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
   1.799 +        if (xpathResult.singleNodeValue) {
   1.800 +          xpathResult.singleNodeValue.microformat = mfname;
   1.801 +          parentnode = xpathResult.singleNodeValue;
   1.802 +        }
   1.803 +        /* If the propnode is not a child of the microformat, and */
   1.804 +        /* the property belongs to the parent microformat as well, */
   1.805 +        /* remove it. */
   1.806 +        if (parentnode != mfnode) {
   1.807 +          var mfNameString = Microformats.getNamesFromNode(parentnode);
   1.808 +          var mfNames = mfNameString.split(" ");
   1.809 +          var j;
   1.810 +          for (j=0; j < mfNames.length; j++) {
   1.811 +            /* If this property is in the parent microformat, remove the node  */
   1.812 +            if (Microformats[mfNames[j]].properties[propname]) {
   1.813 +              propnodes.splice(i,1);
   1.814 +              break;
   1.815 +            }
   1.816 +          }
   1.817 +        }
   1.818 +      }
   1.819 +      if (propnodes.length > 0) {
   1.820 +        var resultArray = [];
   1.821 +        for (let i = 0; i < propnodes.length; i++) {
   1.822 +          var subresult = Microformats.parser.getPropertyInternal(propnodes[i],
   1.823 +                                                                  mfnode,
   1.824 +                                                                  propobj,
   1.825 +                                                                  propname);
   1.826 +          if (subresult != undefined) {
   1.827 +            resultArray.push(subresult);
   1.828 +            /* If we're not a plural property, don't bother getting more */
   1.829 +            if (!propobj.plural) {
   1.830 +              return resultArray[0];
   1.831 +            }
   1.832 +          }
   1.833 +        }
   1.834 +        if (resultArray.length > 0) {
   1.835 +          return resultArray;
   1.836 +        }
   1.837 +      } else {
   1.838 +        /* If we didn't find any class nodes, check to see if this property */
   1.839 +        /* is virtual and if so, call getPropertyInternal again */
   1.840 +        if (propobj.virtual) {
   1.841 +          return Microformats.parser.getPropertyInternal(mfnode, null,
   1.842 +                                                         propobj, propname);
   1.843 +        }
   1.844 +      }
   1.845 +      return;
   1.846 +    },
   1.847 +    /**
   1.848 +     * Internal parser API used to resolve includes and headers. Includes are
   1.849 +     * resolved by simply cloning the node and replacing it in a clone of the
   1.850 +     * original DOM node. Headers are resolved by creating a span and then copying
   1.851 +     * the innerHTML and the class name.
   1.852 +     *
   1.853 +     * @param  in_mfnode The node to preProcess.
   1.854 +     * @return If the node had includes or headers, a cloned node otherwise
   1.855 +     *         the original node. You can check to see if the node was cloned
   1.856 +     *         by looking for .origNode in the new node.
   1.857 +     */
   1.858 +    preProcessMicroformat: function preProcessMicroformat(in_mfnode) {
   1.859 +      var mfnode;
   1.860 +      if ((in_mfnode.nodeName.toLowerCase() == "td") && (in_mfnode.hasAttribute("headers"))) {
   1.861 +        mfnode = in_mfnode.cloneNode(true);
   1.862 +        mfnode.origNode = in_mfnode;
   1.863 +        var headers = in_mfnode.getAttribute("headers").split(" ");
   1.864 +        for (let i = 0; i < headers.length; i++) {
   1.865 +          var tempNode = in_mfnode.ownerDocument.createElement("span");
   1.866 +          var headerNode = in_mfnode.ownerDocument.getElementById(headers[i]);
   1.867 +          if (headerNode) {
   1.868 +            tempNode.innerHTML = headerNode.innerHTML;
   1.869 +            tempNode.className = headerNode.className;
   1.870 +            mfnode.appendChild(tempNode);
   1.871 +          }
   1.872 +        }
   1.873 +      } else {
   1.874 +        mfnode = in_mfnode;
   1.875 +      }
   1.876 +      var includes = Microformats.getElementsByClassName(mfnode, "include");
   1.877 +      if (includes.length > 0) {
   1.878 +        /* If we didn't clone, clone now */
   1.879 +        if (!mfnode.origNode) {
   1.880 +          mfnode = in_mfnode.cloneNode(true);
   1.881 +          mfnode.origNode = in_mfnode;
   1.882 +        }
   1.883 +        includes = Microformats.getElementsByClassName(mfnode, "include");
   1.884 +        var includeId;
   1.885 +        var include_length = includes.length;
   1.886 +        for (let i = include_length -1; i >= 0; i--) {
   1.887 +          if (includes[i].nodeName.toLowerCase() == "a") {
   1.888 +            includeId = includes[i].getAttribute("href").substr(1);
   1.889 +          }
   1.890 +          if (includes[i].nodeName.toLowerCase() == "object") {
   1.891 +            includeId = includes[i].getAttribute("data").substr(1);
   1.892 +          }
   1.893 +          if (in_mfnode.ownerDocument.getElementById(includeId)) {
   1.894 +            includes[i].parentNode.replaceChild(in_mfnode.ownerDocument.getElementById(includeId).cloneNode(true), includes[i]);
   1.895 +          }
   1.896 +        }
   1.897 +      }
   1.898 +      return mfnode;
   1.899 +    },
   1.900 +    validate: function validate(mfnode, mfname) {
   1.901 +      var error = "";
   1.902 +      if (Microformats[mfname].validate) {
   1.903 +        return Microformats[mfname].validate(mfnode);
   1.904 +      } else if (Microformats[mfname].required) {
   1.905 +        for (let i=0;i<Microformats[mfname].required.length;i++) {
   1.906 +          if (!Microformats.parser.getMicroformatProperty(mfnode, mfname, Microformats[mfname].required[i])) {
   1.907 +            error += "Required property " + Microformats[mfname].required[i] + " not specified\n";
   1.908 +          }
   1.909 +        }
   1.910 +        if (error.length > 0) {
   1.911 +          throw(error);
   1.912 +        }
   1.913 +        return true;
   1.914 +      }
   1.915 +    },
   1.916 +    /* This function normalizes an ISO8601 date by adding punctuation and */
   1.917 +    /* ensuring that hours and seconds have values */
   1.918 +    normalizeISO8601: function normalizeISO8601(string)
   1.919 +    {
   1.920 +      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))?)?)?)?)?)?/);
   1.921 +  
   1.922 +      var dateString;
   1.923 +      var tzOffset = 0;
   1.924 +      if (!dateArray) {
   1.925 +        return;
   1.926 +      }
   1.927 +      if (dateArray[1]) {
   1.928 +        dateString = dateArray[1];
   1.929 +        if (dateArray[2]) {
   1.930 +          dateString += "-" + dateArray[2];
   1.931 +          if (dateArray[3]) {
   1.932 +            dateString += "-" + dateArray[3];
   1.933 +            if (dateArray[4]) {
   1.934 +              dateString += "T" + dateArray[4];
   1.935 +              if (dateArray[5]) {
   1.936 +                dateString += ":" + dateArray[5];
   1.937 +              } else {
   1.938 +                dateString += ":" + "00";
   1.939 +              }
   1.940 +              if (dateArray[6]) {
   1.941 +                dateString += ":" + dateArray[6];
   1.942 +              } else {
   1.943 +                dateString += ":" + "00";
   1.944 +              }
   1.945 +              if (dateArray[7]) {
   1.946 +                dateString += "." + dateArray[7];
   1.947 +              }
   1.948 +              if (dateArray[8]) {
   1.949 +                dateString += dateArray[8];
   1.950 +                if ((dateArray[8] == "+") || (dateArray[8] == "-")) {
   1.951 +                  if (dateArray[9]) {
   1.952 +                    dateString += dateArray[9];
   1.953 +                    if (dateArray[10]) {
   1.954 +                      dateString += dateArray[10];
   1.955 +                    }
   1.956 +                  }
   1.957 +                }
   1.958 +              }
   1.959 +            }
   1.960 +          }
   1.961 +        }
   1.962 +      }
   1.963 +      return dateString;
   1.964 +    }
   1.965 +  },
   1.966 +  /**
   1.967 +   * Converts an ISO8601 date into a JavaScript date object, honoring the TZ
   1.968 +   * offset and Z if present to convert the date to local time
   1.969 +   * NOTE: I'm using an extra parameter on the date object for this function.
   1.970 +   * I set date.time to true if there is a date, otherwise date.time is false.
   1.971 +   * 
   1.972 +   * @param  string ISO8601 formatted date
   1.973 +   * @return JavaScript date object that represents the ISO date. 
   1.974 +   */
   1.975 +  dateFromISO8601: function dateFromISO8601(string) {
   1.976 +    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))?)?)?)?)?)?/);
   1.977 +  
   1.978 +    var date = new Date(dateArray[1], 0, 1);
   1.979 +    date.time = false;
   1.980 +
   1.981 +    if (dateArray[2]) {
   1.982 +      date.setMonth(dateArray[2] - 1);
   1.983 +    }
   1.984 +    if (dateArray[3]) {
   1.985 +      date.setDate(dateArray[3]);
   1.986 +    }
   1.987 +    if (dateArray[4]) {
   1.988 +      date.setHours(dateArray[4]);
   1.989 +      date.time = true;
   1.990 +      if (dateArray[5]) {
   1.991 +        date.setMinutes(dateArray[5]);
   1.992 +        if (dateArray[6]) {
   1.993 +          date.setSeconds(dateArray[6]);
   1.994 +          if (dateArray[7]) {
   1.995 +            date.setMilliseconds(Number("0." + dateArray[7]) * 1000);
   1.996 +          }
   1.997 +        }
   1.998 +      }
   1.999 +    }
  1.1000 +    if (dateArray[8]) {
  1.1001 +      if (dateArray[8] == "-") {
  1.1002 +        if (dateArray[9] && dateArray[10]) {
  1.1003 +          date.setHours(date.getHours() + parseInt(dateArray[9], 10));
  1.1004 +          date.setMinutes(date.getMinutes() + parseInt(dateArray[10], 10));
  1.1005 +        }
  1.1006 +      } else if (dateArray[8] == "+") {
  1.1007 +        if (dateArray[9] && dateArray[10]) {
  1.1008 +          date.setHours(date.getHours() - parseInt(dateArray[9], 10));
  1.1009 +          date.setMinutes(date.getMinutes() - parseInt(dateArray[10], 10));
  1.1010 +        }
  1.1011 +      }
  1.1012 +      /* at this point we have the time in gmt */
  1.1013 +      /* convert to local if we had a Z - or + */
  1.1014 +      if (dateArray[8]) {
  1.1015 +        var tzOffset = date.getTimezoneOffset();
  1.1016 +        if (tzOffset < 0) {
  1.1017 +          date.setMinutes(date.getMinutes() + tzOffset); 
  1.1018 +        } else if (tzOffset > 0) {
  1.1019 +          date.setMinutes(date.getMinutes() - tzOffset); 
  1.1020 +        }
  1.1021 +      }
  1.1022 +    }
  1.1023 +    return date;
  1.1024 +  },
  1.1025 +  /**
  1.1026 +   * Converts a Javascript date object into an ISO 8601 formatted date
  1.1027 +   * NOTE: I'm using an extra parameter on the date object for this function.
  1.1028 +   * If date.time is NOT true, this function only outputs the date.
  1.1029 +   * 
  1.1030 +   * @param  date        Javascript Date object
  1.1031 +   * @param  punctuation true if the date should have -/:
  1.1032 +   * @return string with the ISO date. 
  1.1033 +   */
  1.1034 +  iso8601FromDate: function iso8601FromDate(date, punctuation) {
  1.1035 +    var string = date.getFullYear().toString();
  1.1036 +    if (punctuation) {
  1.1037 +      string += "-";
  1.1038 +    }
  1.1039 +    string += (date.getMonth() + 1).toString().replace(/\b(\d)\b/g, '0$1');
  1.1040 +    if (punctuation) {
  1.1041 +      string += "-";
  1.1042 +    }
  1.1043 +    string += date.getDate().toString().replace(/\b(\d)\b/g, '0$1');
  1.1044 +    if (date.time) {
  1.1045 +      string += "T";
  1.1046 +      string += date.getHours().toString().replace(/\b(\d)\b/g, '0$1');
  1.1047 +      if (punctuation) {
  1.1048 +        string += ":";
  1.1049 +      }
  1.1050 +      string += date.getMinutes().toString().replace(/\b(\d)\b/g, '0$1');
  1.1051 +      if (punctuation) {
  1.1052 +        string += ":";
  1.1053 +      }
  1.1054 +      string += date.getSeconds().toString().replace(/\b(\d)\b/g, '0$1');
  1.1055 +      if (date.getMilliseconds() > 0) {
  1.1056 +        if (punctuation) {
  1.1057 +          string += ".";
  1.1058 +        }
  1.1059 +        string += date.getMilliseconds().toString();
  1.1060 +      }
  1.1061 +    }
  1.1062 +    return string;
  1.1063 +  },
  1.1064 +  simpleEscape: function simpleEscape(s)
  1.1065 +  {
  1.1066 +    s = s.replace(/\&/g, '%26');
  1.1067 +    s = s.replace(/\#/g, '%23');
  1.1068 +    s = s.replace(/\+/g, '%2B');
  1.1069 +    s = s.replace(/\-/g, '%2D');
  1.1070 +    s = s.replace(/\=/g, '%3D');
  1.1071 +    s = s.replace(/\'/g, '%27');
  1.1072 +    s = s.replace(/\,/g, '%2C');
  1.1073 +//    s = s.replace(/\r/g, '%0D');
  1.1074 +//    s = s.replace(/\n/g, '%0A');
  1.1075 +    s = s.replace(/ /g, '+');
  1.1076 +    return s;
  1.1077 +  },
  1.1078 +  /**
  1.1079 +   * Not intended for external consumption. Microformat implementations might use it.
  1.1080 +   *
  1.1081 +   * Retrieve elements matching all classes listed in a space-separated string.
  1.1082 +   * I had to implement my own because I need an Array, not an nsIDomNodeList
  1.1083 +   * 
  1.1084 +   * @param  rootElement      The DOM element at which to start searching (optional)
  1.1085 +   * @param  className        A space separated list of classenames
  1.1086 +   * @return microformatNodes An array of DOM Nodes, each representing a
  1.1087 +                              microformat in the document.
  1.1088 +   */
  1.1089 +  getElementsByClassName: function getElementsByClassName(rootNode, className)
  1.1090 +  {
  1.1091 +    var returnElements = [];
  1.1092 +
  1.1093 +    if ((rootNode.ownerDocument || rootNode).getElementsByClassName) {
  1.1094 +    /* Firefox 3 - native getElementsByClassName */
  1.1095 +      var col = rootNode.getElementsByClassName(className);
  1.1096 +      for (let i = 0; i < col.length; i++) {
  1.1097 +        returnElements[i] = col[i];
  1.1098 +      }
  1.1099 +    } else if ((rootNode.ownerDocument || rootNode).evaluate) {
  1.1100 +    /* Firefox 2 and below - XPath */
  1.1101 +      var xpathExpression;
  1.1102 +      xpathExpression = ".//*[contains(concat(' ', @class, ' '), ' " + className + " ')]";
  1.1103 +      var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null);
  1.1104 +
  1.1105 +      var node;
  1.1106 +      while (node = xpathResult.iterateNext()) {
  1.1107 +        returnElements.push(node);
  1.1108 +      }
  1.1109 +    } else {
  1.1110 +    /* Slow fallback for testing */
  1.1111 +      className = className.replace(/\-/g, "\\-");
  1.1112 +      var elements = rootNode.getElementsByTagName("*");
  1.1113 +      for (let i=0;i<elements.length;i++) {
  1.1114 +        if (elements[i].className.match("(^|\\s)" + className + "(\\s|$)")) {
  1.1115 +          returnElements.push(elements[i]);
  1.1116 +        }
  1.1117 +      }
  1.1118 +    }
  1.1119 +    return returnElements;
  1.1120 +  },
  1.1121 +  /**
  1.1122 +   * Not intended for external consumption. Microformat implementations might use it.
  1.1123 +   *
  1.1124 +   * Retrieve elements matching an attribute and an attribute list in a space-separated string.
  1.1125 +   * 
  1.1126 +   * @param  rootElement      The DOM element at which to start searching (optional)
  1.1127 +   * @param  atributeName     The attribute name to match against
  1.1128 +   * @param  attributeValues  A space separated list of attribute values
  1.1129 +   * @return microformatNodes An array of DOM Nodes, each representing a
  1.1130 +                              microformat in the document.
  1.1131 +   */
  1.1132 +  getElementsByAttribute: function getElementsByAttribute(rootNode, attributeName, attributeValues)
  1.1133 +  {
  1.1134 +    var attributeList = attributeValues.split(" ");
  1.1135 +
  1.1136 +    var returnElements = [];
  1.1137 +
  1.1138 +    if ((rootNode.ownerDocument || rootNode).evaluate) {
  1.1139 +    /* Firefox 3 and below - XPath */
  1.1140 +      /* Create an XPath expression based on the attribute list */
  1.1141 +      var xpathExpression = ".//*[";
  1.1142 +      for (let i = 0; i < attributeList.length; i++) {
  1.1143 +        if (i != 0) {
  1.1144 +          xpathExpression += " or ";
  1.1145 +        }
  1.1146 +        xpathExpression += "contains(concat(' ', @" + attributeName + ", ' '), ' " + attributeList[i] + " ')";
  1.1147 +      }
  1.1148 +      xpathExpression += "]"; 
  1.1149 +
  1.1150 +      var xpathResult = (rootNode.ownerDocument || rootNode).evaluate(xpathExpression, rootNode, null, 0, null);
  1.1151 +
  1.1152 +      var node;
  1.1153 +      while (node = xpathResult.iterateNext()) {
  1.1154 +        returnElements.push(node);
  1.1155 +      }
  1.1156 +    } else {
  1.1157 +    /* Need Slow fallback for testing */
  1.1158 +    }
  1.1159 +    return returnElements;
  1.1160 +  },
  1.1161 +  matchClass: function matchClass(node, className) {
  1.1162 +    var classValue = node.getAttribute("class");
  1.1163 +    return (classValue && classValue.match("(^|\\s)" + className + "(\\s|$)"));
  1.1164 +  }
  1.1165 +};
  1.1166 +
  1.1167 +/* MICROFORMAT DEFINITIONS BEGIN HERE */
  1.1168 +
  1.1169 +this.adr = function adr(node, validate) {
  1.1170 +  if (node) {
  1.1171 +    Microformats.parser.newMicroformat(this, node, "adr", validate);
  1.1172 +  }
  1.1173 +}
  1.1174 +
  1.1175 +adr.prototype.toString = function() {
  1.1176 +  var address_text = "";
  1.1177 +  var start_parens = false;
  1.1178 +  if (this["street-address"]) {
  1.1179 +    address_text += this["street-address"][0];
  1.1180 +  } else if (this["extended-address"]) {
  1.1181 +    address_text += this["extended-address"];
  1.1182 +  }
  1.1183 +  if (this["locality"]) {
  1.1184 +    if (this["street-address"] || this["extended-address"]) {
  1.1185 +      address_text += " (";
  1.1186 +      start_parens = true;
  1.1187 +    }
  1.1188 +    address_text += this["locality"];
  1.1189 +  }
  1.1190 +  if (this["region"]) {
  1.1191 +    if ((this["street-address"] || this["extended-address"]) && (!start_parens)) {
  1.1192 +      address_text += " (";
  1.1193 +      start_parens = true;
  1.1194 +    } else if (this["locality"]) {
  1.1195 +      address_text += ", ";
  1.1196 +    }
  1.1197 +    address_text += this["region"];
  1.1198 +  }
  1.1199 +  if (this["country-name"]) {
  1.1200 +    if ((this["street-address"] || this["extended-address"]) && (!start_parens)) {
  1.1201 +      address_text += " (";
  1.1202 +      start_parens = true;
  1.1203 +      address_text += this["country-name"];
  1.1204 +    } else if ((!this["locality"]) && (!this["region"])) {
  1.1205 +      address_text += this["country-name"];
  1.1206 +    } else if (((!this["locality"]) && (this["region"])) || ((this["locality"]) && (!this["region"]))) {
  1.1207 +      address_text += ", ";
  1.1208 +      address_text += this["country-name"];
  1.1209 +    }
  1.1210 +  }
  1.1211 +  if (start_parens) {
  1.1212 +    address_text += ")";
  1.1213 +  }
  1.1214 +  return address_text;
  1.1215 +}
  1.1216 +
  1.1217 +var adr_definition = {
  1.1218 +  mfObject: adr,
  1.1219 +  className: "adr",
  1.1220 +  properties: {
  1.1221 +    "type" : {
  1.1222 +      plural: true,
  1.1223 +      values: ["work", "home", "pref", "postal", "dom", "intl", "parcel"]
  1.1224 +    },
  1.1225 +    "post-office-box" : {
  1.1226 +    },
  1.1227 +    "street-address" : {
  1.1228 +      plural: true
  1.1229 +    },
  1.1230 +    "extended-address" : {
  1.1231 +    },
  1.1232 +    "locality" : {
  1.1233 +    },
  1.1234 +    "region" : {
  1.1235 +    },
  1.1236 +    "postal-code" : {
  1.1237 +    },
  1.1238 +    "country-name" : {
  1.1239 +    }
  1.1240 +  },
  1.1241 +  validate: function(node) {
  1.1242 +    var xpathExpression = "count(descendant::*[" +
  1.1243 +                                              "contains(concat(' ', @class, ' '), ' post-office-box ')" +
  1.1244 +                                              " or contains(concat(' ', @class, ' '), ' street-address ')" +
  1.1245 +                                              " or contains(concat(' ', @class, ' '), ' extended-address ')" +
  1.1246 +                                              " or contains(concat(' ', @class, ' '), ' locality ')" +
  1.1247 +                                              " or contains(concat(' ', @class, ' '), ' region ')" +
  1.1248 +                                              " or contains(concat(' ', @class, ' '), ' postal-code ')" +
  1.1249 +                                              " or contains(concat(' ', @class, ' '), ' country-name')" +
  1.1250 +                                              "])";
  1.1251 +    var xpathResult = (node.ownerDocument || node).evaluate(xpathExpression, node, null,  Components.interfaces.nsIDOMXPathResult.ANY_TYPE, null).numberValue;
  1.1252 +    if (xpathResult == 0) {
  1.1253 +      throw("Unable to create microformat");
  1.1254 +    }
  1.1255 +    return true;
  1.1256 +  }
  1.1257 +};
  1.1258 +
  1.1259 +Microformats.add("adr", adr_definition);
  1.1260 +
  1.1261 +this.hCard = function hCard(node, validate) {
  1.1262 +  if (node) {
  1.1263 +    Microformats.parser.newMicroformat(this, node, "hCard", validate);
  1.1264 +  }
  1.1265 +}
  1.1266 +hCard.prototype.toString = function() {
  1.1267 +  if (this.resolvedNode) {
  1.1268 +    /* If this microformat has an include pattern, put the */
  1.1269 +    /* organization-name in parenthesis after the fn to differentiate */
  1.1270 +    /* them. */
  1.1271 +    var fns = Microformats.getElementsByClassName(this.node, "fn");
  1.1272 +    if (fns.length === 0) {
  1.1273 +      if (this.fn) {
  1.1274 +        if (this.org && this.org[0]["organization-name"] && (this.fn != this.org[0]["organization-name"])) {
  1.1275 +          return this.fn + " (" + this.org[0]["organization-name"] + ")";
  1.1276 +        }
  1.1277 +      }
  1.1278 +    }
  1.1279 +  }
  1.1280 +  return this.fn;
  1.1281 +}
  1.1282 +
  1.1283 +var hCard_definition = {
  1.1284 +  mfObject: hCard,
  1.1285 +  className: "vcard",
  1.1286 +  required: ["fn"],
  1.1287 +  properties: {
  1.1288 +    "adr" : {
  1.1289 +      plural: true,
  1.1290 +      datatype: "microformat",
  1.1291 +      microformat: "adr"
  1.1292 +    },
  1.1293 +    "agent" : {
  1.1294 +      plural: true,
  1.1295 +      datatype: "microformat",
  1.1296 +      microformat: "hCard"
  1.1297 +    },
  1.1298 +    "bday" : {
  1.1299 +      datatype: "dateTime"
  1.1300 +    },
  1.1301 +    "class" : {
  1.1302 +    },
  1.1303 +    "category" : {
  1.1304 +      plural: true,
  1.1305 +      datatype: "microformat",
  1.1306 +      microformat: "tag",
  1.1307 +      microformat_property: "tag"
  1.1308 +    },
  1.1309 +    "email" : {
  1.1310 +      subproperties: {
  1.1311 +        "type" : {
  1.1312 +          plural: true,
  1.1313 +          values: ["internet", "x400", "pref"]
  1.1314 +        },
  1.1315 +        "value" : {
  1.1316 +          datatype: "email",
  1.1317 +          virtual: true
  1.1318 +        }
  1.1319 +      },
  1.1320 +      plural: true   
  1.1321 +    },
  1.1322 +    "fn" : {
  1.1323 +      required: true
  1.1324 +    },
  1.1325 +    "geo" : {
  1.1326 +      datatype: "microformat",
  1.1327 +      microformat: "geo"
  1.1328 +    },
  1.1329 +    "key" : {
  1.1330 +      plural: true
  1.1331 +    },
  1.1332 +    "label" : {
  1.1333 +      plural: true
  1.1334 +    },
  1.1335 +    "logo" : {
  1.1336 +      plural: true,
  1.1337 +      datatype: "anyURI"
  1.1338 +    },
  1.1339 +    "mailer" : {
  1.1340 +      plural: true
  1.1341 +    },
  1.1342 +    "n" : {
  1.1343 +      subproperties: {
  1.1344 +        "honorific-prefix" : {
  1.1345 +          plural: true
  1.1346 +        },
  1.1347 +        "given-name" : {
  1.1348 +          plural: true
  1.1349 +        },
  1.1350 +        "additional-name" : {
  1.1351 +          plural: true
  1.1352 +        },
  1.1353 +        "family-name" : {
  1.1354 +          plural: true
  1.1355 +        },
  1.1356 +        "honorific-suffix" : {
  1.1357 +          plural: true
  1.1358 +        }
  1.1359 +      },
  1.1360 +      virtual: true,
  1.1361 +      /*  Implied "n" Optimization */
  1.1362 +      /* http://microformats.org/wiki/hcard#Implied_.22n.22_Optimization */
  1.1363 +      virtualGetter: function(mfnode) {
  1.1364 +        var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn");
  1.1365 +        var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org");
  1.1366 +        var given_name = [];
  1.1367 +        var family_name = [];
  1.1368 +        if (fn && (!orgs || (orgs.length > 1) || (fn != orgs[0]["organization-name"]))) {
  1.1369 +          var fns = fn.split(" ");
  1.1370 +          if (fns.length === 2) {
  1.1371 +            if (fns[0].charAt(fns[0].length-1) == ',') {
  1.1372 +              given_name[0] = fns[1];
  1.1373 +              family_name[0] = fns[0].substr(0, fns[0].length-1);
  1.1374 +            } else if (fns[1].length == 1) {
  1.1375 +              given_name[0] = fns[1];
  1.1376 +              family_name[0] = fns[0];
  1.1377 +            } else if ((fns[1].length == 2) && (fns[1].charAt(fns[1].length-1) == '.')) {
  1.1378 +              given_name[0] = fns[1];
  1.1379 +              family_name[0] = fns[0];
  1.1380 +            } else {
  1.1381 +              given_name[0] = fns[0];
  1.1382 +              family_name[0] = fns[1];
  1.1383 +            }
  1.1384 +            return {"given-name" : given_name, "family-name" : family_name};
  1.1385 +          }
  1.1386 +        }
  1.1387 +      }
  1.1388 +    },
  1.1389 +    "nickname" : {
  1.1390 +      plural: true,
  1.1391 +      virtual: true,
  1.1392 +      /* Implied "nickname" Optimization */
  1.1393 +      /* http://microformats.org/wiki/hcard#Implied_.22nickname.22_Optimization */
  1.1394 +      virtualGetter: function(mfnode) {
  1.1395 +        var fn = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "fn");
  1.1396 +        var orgs = Microformats.parser.getMicroformatProperty(mfnode, "hCard", "org");
  1.1397 +        var given_name;
  1.1398 +        var family_name;
  1.1399 +        if (fn && (!orgs || (orgs.length) > 1 || (fn != orgs[0]["organization-name"]))) {
  1.1400 +          var fns = fn.split(" ");
  1.1401 +          if (fns.length === 1) {
  1.1402 +            return [fns[0]];
  1.1403 +          }
  1.1404 +        }
  1.1405 +        return;
  1.1406 +      }
  1.1407 +    },
  1.1408 +    "note" : {
  1.1409 +      plural: true,
  1.1410 +      datatype: "HTML"
  1.1411 +    },
  1.1412 +    "org" : {
  1.1413 +      subproperties: {
  1.1414 +        "organization-name" : {
  1.1415 +          virtual: true
  1.1416 +        },
  1.1417 +        "organization-unit" : {
  1.1418 +          plural: true
  1.1419 +        }
  1.1420 +      },
  1.1421 +      plural: true
  1.1422 +    },
  1.1423 +    "photo" : {
  1.1424 +      plural: true,
  1.1425 +      datatype: "anyURI"
  1.1426 +    },
  1.1427 +    "rev" : {
  1.1428 +      datatype: "dateTime"
  1.1429 +    },
  1.1430 +    "role" : {
  1.1431 +      plural: true
  1.1432 +    },
  1.1433 +    "sequence" : {
  1.1434 +    },
  1.1435 +    "sort-string" : {
  1.1436 +    },
  1.1437 +    "sound" : {
  1.1438 +      plural: true
  1.1439 +    },
  1.1440 +    "title" : {
  1.1441 +      plural: true
  1.1442 +    },
  1.1443 +    "tel" : {
  1.1444 +      subproperties: {
  1.1445 +        "type" : {
  1.1446 +          plural: true,
  1.1447 +          values: ["msg", "home", "work", "pref", "voice", "fax", "cell", "video", "pager", "bbs", "car", "isdn", "pcs"]
  1.1448 +        },
  1.1449 +        "value" : {
  1.1450 +          datatype: "tel",
  1.1451 +          virtual: true
  1.1452 +        }
  1.1453 +      },
  1.1454 +      plural: true
  1.1455 +    },
  1.1456 +    "tz" : {
  1.1457 +    },
  1.1458 +    "uid" : {
  1.1459 +      datatype: "anyURI"
  1.1460 +    },
  1.1461 +    "url" : {
  1.1462 +      plural: true,
  1.1463 +      datatype: "anyURI"
  1.1464 +    }
  1.1465 +  }
  1.1466 +};
  1.1467 +
  1.1468 +Microformats.add("hCard", hCard_definition);
  1.1469 +
  1.1470 +this.hCalendar = function hCalendar(node, validate) {
  1.1471 +  if (node) {
  1.1472 +    Microformats.parser.newMicroformat(this, node, "hCalendar", validate);
  1.1473 +  }
  1.1474 +}
  1.1475 +hCalendar.prototype.toString = function() {
  1.1476 +  if (this.resolvedNode) {
  1.1477 +    /* If this microformat has an include pattern, put the */
  1.1478 +    /* dtstart in parenthesis after the summary to differentiate */
  1.1479 +    /* them. */
  1.1480 +    var summaries = Microformats.getElementsByClassName(this.node, "summary");
  1.1481 +    if (summaries.length === 0) {
  1.1482 +      if (this.summary) {
  1.1483 +        if (this.dtstart) {
  1.1484 +          return this.summary + " (" + Microformats.dateFromISO8601(this.dtstart).toLocaleString() + ")";
  1.1485 +        }
  1.1486 +      }
  1.1487 +    }
  1.1488 +  }
  1.1489 +  if (this.dtstart) {
  1.1490 +    return this.summary;
  1.1491 +  }
  1.1492 +  return;
  1.1493 +}
  1.1494 +
  1.1495 +var hCalendar_definition = {
  1.1496 +  mfObject: hCalendar,
  1.1497 +  className: "vevent",
  1.1498 +  required: ["summary", "dtstart"],
  1.1499 +  properties: {
  1.1500 +    "category" : {
  1.1501 +      plural: true,
  1.1502 +      datatype: "microformat",
  1.1503 +      microformat: "tag",
  1.1504 +      microformat_property: "tag"
  1.1505 +    },
  1.1506 +    "class" : {
  1.1507 +      values: ["public", "private", "confidential"]
  1.1508 +    },
  1.1509 +    "description" : {
  1.1510 +      datatype: "HTML"
  1.1511 +    },
  1.1512 +    "dtstart" : {
  1.1513 +      datatype: "dateTime"
  1.1514 +    },
  1.1515 +    "dtend" : {
  1.1516 +      datatype: "dateTime"
  1.1517 +    },
  1.1518 +    "dtstamp" : {
  1.1519 +      datatype: "dateTime"
  1.1520 +    },
  1.1521 +    "duration" : {
  1.1522 +    },
  1.1523 +    "geo" : {
  1.1524 +      datatype: "microformat",
  1.1525 +      microformat: "geo"
  1.1526 +    },
  1.1527 +    "location" : {
  1.1528 +      datatype: "microformat",
  1.1529 +      microformat: "hCard"
  1.1530 +    },
  1.1531 +    "status" : {
  1.1532 +      values: ["tentative", "confirmed", "cancelled"]
  1.1533 +    },
  1.1534 +    "summary" : {},
  1.1535 +    "transp" : {
  1.1536 +      values: ["opaque", "transparent"]
  1.1537 +    },
  1.1538 +    "uid" : {
  1.1539 +      datatype: "anyURI"
  1.1540 +    },
  1.1541 +    "url" : {
  1.1542 +      datatype: "anyURI"
  1.1543 +    },
  1.1544 +    "last-modified" : {
  1.1545 +      datatype: "dateTime"
  1.1546 +    },
  1.1547 +    "rrule" : {
  1.1548 +      subproperties: {
  1.1549 +        "interval" : {
  1.1550 +          virtual: true,
  1.1551 +          /* This will only be called in the virtual case */
  1.1552 +          virtualGetter: function(mfnode) {
  1.1553 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "interval");
  1.1554 +          }
  1.1555 +        },
  1.1556 +        "freq" : {
  1.1557 +          virtual: true,
  1.1558 +          /* This will only be called in the virtual case */
  1.1559 +          virtualGetter: function(mfnode) {
  1.1560 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "freq");
  1.1561 +          }
  1.1562 +        },
  1.1563 +        "bysecond" : {
  1.1564 +          virtual: true,
  1.1565 +          /* This will only be called in the virtual case */
  1.1566 +          virtualGetter: function(mfnode) {
  1.1567 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bysecond");
  1.1568 +          }
  1.1569 +        },
  1.1570 +        "byminute" : {
  1.1571 +          virtual: true,
  1.1572 +          /* This will only be called in the virtual case */
  1.1573 +          virtualGetter: function(mfnode) {
  1.1574 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byminute");
  1.1575 +          }
  1.1576 +        },
  1.1577 +        "byhour" : {
  1.1578 +          virtual: true,
  1.1579 +          /* This will only be called in the virtual case */
  1.1580 +          virtualGetter: function(mfnode) {
  1.1581 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byhour");
  1.1582 +          }
  1.1583 +        },
  1.1584 +        "bymonthday" : {
  1.1585 +          virtual: true,
  1.1586 +          /* This will only be called in the virtual case */
  1.1587 +          virtualGetter: function(mfnode) {
  1.1588 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonthday");
  1.1589 +          }
  1.1590 +        },
  1.1591 +        "byyearday" : {
  1.1592 +          virtual: true,
  1.1593 +          /* This will only be called in the virtual case */
  1.1594 +          virtualGetter: function(mfnode) {
  1.1595 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byyearday");
  1.1596 +          }
  1.1597 +        },
  1.1598 +        "byweekno" : {
  1.1599 +          virtual: true,
  1.1600 +          /* This will only be called in the virtual case */
  1.1601 +          virtualGetter: function(mfnode) {
  1.1602 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byweekno");
  1.1603 +          }
  1.1604 +        },
  1.1605 +        "bymonth" : {
  1.1606 +          virtual: true,
  1.1607 +          /* This will only be called in the virtual case */
  1.1608 +          virtualGetter: function(mfnode) {
  1.1609 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "bymonth");
  1.1610 +          }
  1.1611 +        },
  1.1612 +        "byday" : {
  1.1613 +          virtual: true,
  1.1614 +          /* This will only be called in the virtual case */
  1.1615 +          virtualGetter: function(mfnode) {
  1.1616 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "byday");
  1.1617 +          }
  1.1618 +        },
  1.1619 +        "until" : {
  1.1620 +          virtual: true,
  1.1621 +          /* This will only be called in the virtual case */
  1.1622 +          virtualGetter: function(mfnode) {
  1.1623 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "until");
  1.1624 +          }
  1.1625 +        },
  1.1626 +        "count" : {
  1.1627 +          virtual: true,
  1.1628 +          /* This will only be called in the virtual case */
  1.1629 +          virtualGetter: function(mfnode) {
  1.1630 +            return Microformats.hCalendar.properties.rrule.retrieve(mfnode, "count");
  1.1631 +          }
  1.1632 +        }
  1.1633 +      },
  1.1634 +      retrieve: function(mfnode, property) {
  1.1635 +        var value = Microformats.parser.textGetter(mfnode);
  1.1636 +        var rrule;
  1.1637 +        rrule = value.split(';');
  1.1638 +        for (let i=0; i < rrule.length; i++) {
  1.1639 +          if (rrule[i].match(property)) {
  1.1640 +            return rrule[i].split('=')[1];
  1.1641 +          }
  1.1642 +        }
  1.1643 +      }
  1.1644 +    }
  1.1645 +  }
  1.1646 +};
  1.1647 +
  1.1648 +Microformats.add("hCalendar", hCalendar_definition);
  1.1649 +
  1.1650 +this.geo = function geo(node, validate) {
  1.1651 +  if (node) {
  1.1652 +    Microformats.parser.newMicroformat(this, node, "geo", validate);
  1.1653 +  }
  1.1654 +}
  1.1655 +geo.prototype.toString = function() {
  1.1656 +  if (this.latitude != undefined) {
  1.1657 +    if (!isFinite(this.latitude) || (this.latitude > 360) || (this.latitude < -360)) {
  1.1658 +      return;
  1.1659 +    }
  1.1660 +  }
  1.1661 +  if (this.longitude != undefined) {
  1.1662 +    if (!isFinite(this.longitude) || (this.longitude > 360) || (this.longitude < -360)) {
  1.1663 +      return;
  1.1664 +    }
  1.1665 +  }
  1.1666 +
  1.1667 +  if ((this.latitude != undefined) && (this.longitude != undefined)) {
  1.1668 +    var s;
  1.1669 +    if ((this.node.localName.toLowerCase() == "abbr") || (this.node.localName.toLowerCase() == "html:abbr")) {
  1.1670 +      s = this.node.textContent;
  1.1671 +    }
  1.1672 +
  1.1673 +    if (s) {
  1.1674 +      return s;
  1.1675 +    }
  1.1676 +
  1.1677 +    /* check if geo is contained in a vcard */
  1.1678 +    var xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vcard ')]";
  1.1679 +    var xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, null);
  1.1680 +    if (xpathResult.singleNodeValue) {
  1.1681 +      var hcard = new hCard(xpathResult.singleNodeValue);
  1.1682 +      if (hcard.fn) {
  1.1683 +        return hcard.fn;
  1.1684 +      }
  1.1685 +    }
  1.1686 +    /* check if geo is contained in a vevent */
  1.1687 +    xpathExpression = "ancestor::*[contains(concat(' ', @class, ' '), ' vevent ')]";
  1.1688 +    xpathResult = this.node.ownerDocument.evaluate(xpathExpression, this.node, null,  Components.interfaces.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE, xpathResult);
  1.1689 +    if (xpathResult.singleNodeValue) {
  1.1690 +      var hcal = new hCalendar(xpathResult.singleNodeValue);
  1.1691 +      if (hcal.summary) {
  1.1692 +        return hcal.summary;
  1.1693 +      }
  1.1694 +    }
  1.1695 +    if (s) {
  1.1696 +      return s;
  1.1697 +    } else {
  1.1698 +      return this.latitude + ", " + this.longitude;
  1.1699 +    }
  1.1700 +  }
  1.1701 +}
  1.1702 +
  1.1703 +var geo_definition = {
  1.1704 +  mfObject: geo,
  1.1705 +  className: "geo",
  1.1706 +  required: ["latitude","longitude"],
  1.1707 +  properties: {
  1.1708 +    "latitude" : {
  1.1709 +      datatype: "float",
  1.1710 +      virtual: true,
  1.1711 +      /* This will only be called in the virtual case */
  1.1712 +      virtualGetter: function(mfnode) {
  1.1713 +        var value = Microformats.parser.textGetter(mfnode);
  1.1714 +        var latlong;
  1.1715 +        if (value.match(';')) {
  1.1716 +          latlong = value.split(';');
  1.1717 +          if (latlong[0]) {
  1.1718 +            if (!isNaN(latlong[0])) {
  1.1719 +              return parseFloat(latlong[0]);
  1.1720 +            }
  1.1721 +          }
  1.1722 +        }
  1.1723 +      }
  1.1724 +    },
  1.1725 +    "longitude" : {
  1.1726 +      datatype: "float",
  1.1727 +      virtual: true,
  1.1728 +      /* This will only be called in the virtual case */
  1.1729 +      virtualGetter: function(mfnode) {
  1.1730 +        var value = Microformats.parser.textGetter(mfnode);
  1.1731 +        var latlong;
  1.1732 +        if (value.match(';')) {
  1.1733 +          latlong = value.split(';');
  1.1734 +          if (latlong[1]) {
  1.1735 +            if (!isNaN(latlong[1])) {
  1.1736 +              return parseFloat(latlong[1]);
  1.1737 +            }
  1.1738 +          }
  1.1739 +        }
  1.1740 +      }
  1.1741 +    }
  1.1742 +  },
  1.1743 +  validate: function(node) {
  1.1744 +    var latitude = Microformats.parser.getMicroformatProperty(node, "geo", "latitude");
  1.1745 +    var longitude = Microformats.parser.getMicroformatProperty(node, "geo", "longitude");
  1.1746 +    if (latitude != undefined) {
  1.1747 +      if (!isFinite(latitude) || (latitude > 360) || (latitude < -360)) {
  1.1748 +        throw("Invalid latitude");
  1.1749 +      }
  1.1750 +    } else {
  1.1751 +      throw("No latitude specified");
  1.1752 +    }
  1.1753 +    if (longitude != undefined) {
  1.1754 +      if (!isFinite(longitude) || (longitude > 360) || (longitude < -360)) {
  1.1755 +        throw("Invalid longitude");
  1.1756 +      }
  1.1757 +    } else {
  1.1758 +      throw("No longitude specified");
  1.1759 +    }
  1.1760 +    return true;
  1.1761 +  }
  1.1762 +};
  1.1763 +
  1.1764 +Microformats.add("geo", geo_definition);
  1.1765 +
  1.1766 +this.tag = function tag(node, validate) {
  1.1767 +  if (node) {
  1.1768 +    Microformats.parser.newMicroformat(this, node, "tag", validate);
  1.1769 +  }
  1.1770 +}
  1.1771 +tag.prototype.toString = function() {
  1.1772 +  return this.tag;
  1.1773 +}
  1.1774 +
  1.1775 +var tag_definition = {
  1.1776 +  mfObject: tag,
  1.1777 +  attributeName: "rel",
  1.1778 +  attributeValues: "tag",
  1.1779 +  properties: {
  1.1780 +    "tag" : {
  1.1781 +      virtual: true,
  1.1782 +      virtualGetter: function(mfnode) {
  1.1783 +        if (mfnode.href) {
  1.1784 +          var ioService = Components.classes["@mozilla.org/network/io-service;1"].
  1.1785 +                                     getService(Components.interfaces.nsIIOService);
  1.1786 +          var uri = ioService.newURI(mfnode.href, null, null);
  1.1787 +          var url_array = uri.path.split("/");
  1.1788 +          for(let i=url_array.length-1; i > 0; i--) {
  1.1789 +            if (url_array[i] !== "") {
  1.1790 +              var tag
  1.1791 +              if (tag = Microformats.tag.validTagName(url_array[i].replace(/\+/g, ' '))) {
  1.1792 +                try {
  1.1793 +                  return decodeURIComponent(tag);
  1.1794 +                } catch (ex) {
  1.1795 +                  return unescape(tag);
  1.1796 +                }
  1.1797 +              }
  1.1798 +            }
  1.1799 +          }
  1.1800 +        }
  1.1801 +        return null;
  1.1802 +      }
  1.1803 +    },
  1.1804 +    "link" : {
  1.1805 +      virtual: true,
  1.1806 +      datatype: "anyURI"
  1.1807 +    },
  1.1808 +    "text" : {
  1.1809 +      virtual: true
  1.1810 +    }
  1.1811 +  },
  1.1812 +  validTagName: function(tag)
  1.1813 +  {
  1.1814 +    var returnTag = tag;
  1.1815 +    if (tag.indexOf('?') != -1) {
  1.1816 +      if (tag.indexOf('?') === 0) {
  1.1817 +        return false;
  1.1818 +      } else {
  1.1819 +        returnTag = tag.substr(0, tag.indexOf('?'));
  1.1820 +      }
  1.1821 +    }
  1.1822 +    if (tag.indexOf('#') != -1) {
  1.1823 +      if (tag.indexOf('#') === 0) {
  1.1824 +        return false;
  1.1825 +      } else {
  1.1826 +        returnTag = tag.substr(0, tag.indexOf('#'));
  1.1827 +      }
  1.1828 +    }
  1.1829 +    if (tag.indexOf('.html') != -1) {
  1.1830 +      if (tag.indexOf('.html') == tag.length - 5) {
  1.1831 +        return false;
  1.1832 +      }
  1.1833 +    }
  1.1834 +    return returnTag;
  1.1835 +  },
  1.1836 +  validate: function(node) {
  1.1837 +    var tag = Microformats.parser.getMicroformatProperty(node, "tag", "tag");
  1.1838 +    if (!tag) {
  1.1839 +      if (node.href) {
  1.1840 +        var url_array = node.getAttribute("href").split("/");
  1.1841 +        for(let i=url_array.length-1; i > 0; i--) {
  1.1842 +          if (url_array[i] !== "") {
  1.1843 +            throw("Invalid tag name (" + url_array[i] + ")");
  1.1844 +          }
  1.1845 +        }
  1.1846 +      } else {
  1.1847 +        throw("No href specified on tag");
  1.1848 +      }
  1.1849 +    }
  1.1850 +    return true;
  1.1851 +  }
  1.1852 +};
  1.1853 +
  1.1854 +Microformats.add("tag", tag_definition);

mercurial