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