make .properties() more tolerant
[microdatajs:microdatajs.git] / jquery.microdata.js
1 /* -*- mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil -*- */
2
3 (function(){
4   jQuery.microdata = {};
5
6   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string
7   function validTimeStringLength(s) {
8     var m = /^(\d\d):(\d\d)(:(\d\d)(\.\d+)?)?/.exec(s);
9     if (m && m[1]<=23 && m[2]<=59 && (!m[4] || m[4]<=59))
10       return m[0].length;
11     return 0;
12   }
13
14   function isValidTimeString(s) {
15     return s && validTimeStringLength(s) == s.length;
16   }
17
18   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#number-of-days-in-month-month-of-year-year
19   function daysInMonth(year, month) {
20     if (month==1 || month==3 || month==5 || month==7 ||
21         month==8 || month==10 || month==12) {
22       return 31;
23     } else if (month==4 || month==6 || month==9 || month==11) {
24       return 30;
25     } else if (month == 2 && (year%400==0 || (year%4==0 && year%100!=0))) {
26       return 29;
27     } else {
28       return 28;
29     }
30   }
31
32   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string
33   function validDateStringLength(s) {
34     var m = /^(\d{4,})-(\d\d)-(\d\d)/.exec(s);
35     if (m && m[1]>=1 && m[2]>=1 && m[2]<=12 && m[3]>=1 && m[3]<=daysInMonth(m[1],m[2]))
36       return m[0].length;
37     return 0;
38   }
39
40   function isValidDateString(s) {
41     return s && validDateStringLength(s) == s.length;
42   }
43
44   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-global-date-and-time-string
45   function isValidGlobalDateAndTimeString(s) {
46     var skip = validDateStringLength(s);
47     if (skip && s[skip] == 'T') {
48       s = s.substr(skip+1);
49       skip = validTimeStringLength(s);
50       if (skip) {
51         s = s.substr(skip);
52         if (s == 'Z')
53           return true;
54         var m = /^[+-](\d\d):(\d\d)$/.exec(s);
55         if (m && m[1]<=23 && m[2]<=59)
56           return true;
57       }
58     }
59     return false;
60   }
61
62   jQuery.microdata.isValidGlobalDateAndTimeString = isValidGlobalDateAndTimeString;
63   jQuery.microdata.isValidDateString = isValidDateString;
64
65   function splitTokens(s) {
66     if (s && /\S/.test(s))
67       return s.replace(/^\s+|\s+$/g,'').split(/\s+/);
68     return [];
69   }
70
71   function getItems(types) {
72     var selector = jQuery.map(splitTokens(types), function(t) {
73       return '[itemtype~="'+t.replace(/"/g, '\\"')+'"]';
74     }).join(',') || '*';
75     // filter results to only match top-level items
76     // because [attr] selector doesn't work in IE we have to
77     // filter the elements. http://dev.jquery.com/ticket/5637
78     return jQuery(selector, this).filter(function() {
79       return (this.getAttribute('itemscope') != null &&
80               this.getAttribute('itemprop') == null);
81     });
82   };
83
84   function itemScope(val) {
85     if (arguments.length == 0) {
86       return this.attr('itemscope') != undefined;
87     } else {
88       if (val) {
89         this.attr('itemscope', 'itemscope');
90       } else {
91         this.removeAttr('itemscope');
92       }
93     }
94     return this;
95   }
96
97   function itemType(val) {
98     if (arguments.length == 0) {
99       return this.attr('itemtype') || '';
100     } else {
101       this.attr('itemtype', val);
102     }
103     return this;
104   }
105
106   function resolve(url) {
107     if (!url)
108       return '';
109     var img = document.createElement('img');
110     img.setAttribute('src', url);
111     return img.src;
112   }
113
114   function itemId(val) {
115     if (arguments.length == 0) {
116       return resolve(this.attr('itemid'));
117     } else {
118       this.attr('itemid', val);
119     }
120     return this;
121   }
122
123   function tokenList(attr) {
124     return function() {
125       var tokens = [];
126       jQuery.each(splitTokens(this.attr(attr)), function(i, token) {
127         if (jQuery.inArray(token, tokens) == -1)
128           tokens.push(token);
129       });
130       return jQuery(tokens);
131     };
132   }
133
134   function itemValue(val) {
135     var elm = this.get(0);
136     var tag = elm.tagName.toUpperCase();
137     if (arguments.length == 0) {
138       if (this.attr('itemprop') === undefined)
139         return null;
140       if (this.itemScope()) {
141         return elm; // or a new jQuery object?
142       }
143       switch (tag) {
144       case 'META':
145         return this.attr('content') || '';
146       case 'AUDIO':
147       case 'EMBED':
148       case 'IFRAME':
149       case 'IMG':
150       case 'SOURCE':
151       case 'VIDEO':
152         return resolve(this.attr('src'));
153       case 'A':
154       case 'AREA':
155       case 'LINK':
156         return resolve(this.attr('href'));
157       case 'OBJECT':
158         return resolve(this.attr('data'));
159       case 'TIME':
160         var datetime = this.attr('datetime');
161         if (!(datetime === undefined))
162           return datetime;
163       default:
164         return this.text();
165       }
166     } else {
167       switch (tag) {
168       case 'META':
169         return this.attr('content', val);
170       case 'AUDIO':
171       case 'EMBED':
172       case 'IFRAME':
173       case 'IMG':
174       case 'SOURCE':
175       case 'VIDEO':
176         return this.attr('src', val);
177       case 'A':
178       case 'AREA':
179       case 'LINK':
180         return this.attr('href', val);
181       case 'OBJECT':
182         return this.attr('data', val);
183       case 'TIME':
184         if (!(this.attr('datetime') === undefined))
185           return this.attr('datetime', val);
186       default:
187         return this.text(val);
188       }
189     }
190   }
191
192   function properties(name) {
193     var props = [];
194     // visitItem adds properties or checks for itemref loops,
195     // depending on if a stack of visited items is given.
196     function visitItem(item, visited) {
197       // traverse tree for property nodes
198       function traverse(node) {
199         var $node = jQuery(node);
200         var $names = $node.itemProp();
201         if ($names.length > 0) {
202           // this is a property node
203           if (visited) {
204             // only look for itemref loops; don't add properties
205             if ($node.itemScope()) {
206               switch (jQuery.inArray(node, visited)) {
207               case -1:
208                 // no loop (yet)
209                 visitItem(node, visited.concat([node]));
210                 break;
211               case 0:
212                 // self-referring item/property
213                 throw prop;
214               }
215             }
216           } else {
217             // add property if name matches and it is not self-referring
218             if (!name || jQuery.inArray(name, $names.toArray()) != -1) {
219               if ($node.itemScope()) {
220                 try {
221                   visitItem(node, [item]);
222                 } catch (ex) {
223                   // skip this self-referring property
224                   return;
225                 }
226               }
227               props.push(node);
228             }
229           }
230         }
231         // don't traverse into subitems
232         if (!$node.itemScope()) {
233           $node.children().each(function() {
234             traverse(this);
235           });
236         }
237       }
238       var $item = jQuery(item);
239       $item.children().each(function() {
240         traverse(this);
241       });
242       $item.itemRef().each(function(i, id) {
243         var $ref = jQuery('#'+id);
244         if ($ref.length == 1)
245           traverse($ref.get(0));
246       });
247     }
248
249     this.each(function(i, node) {
250       if (jQuery(node).itemScope())
251         visitItem(node);
252     });
253     // make results unique and sorted in document order
254     return jQuery(jQuery.unique(props));
255   }
256
257   jQuery.fn.extend({
258     items     : getItems,
259     itemScope : itemScope,
260     itemType  : itemType,
261     itemId    : itemId,
262     itemProp  : tokenList('itemprop'),
263     itemRef   : tokenList('itemref'),
264     itemValue : itemValue,
265     properties: properties
266   });
267 })();