Sync vCard itemref loop handling with spec
[microdatajs:microdatajs.git] / jquery.microdata.js
1 /* -*- mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
2
3 (function(){
4   var $ = jQuery;
5
6   $.microdata = {};
7
8   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string
9   function validTimeStringLength(s) {
10     var m = /^(\d\d):(\d\d)(:(\d\d)(\.\d+)?)?/.exec(s);
11     if (m && m[1]<=23 && m[2]<=59 && (!m[4] || m[4]<=59))
12       return m[0].length;
13     return 0;
14   }
15
16   function isValidTimeString(s) {
17     return s && validTimeStringLength(s) == s.length;
18   }
19
20   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#number-of-days-in-month-month-of-year-year
21   function daysInMonth(year, month) {
22     if (month==1 || month==3 || month==5 || month==7 ||
23         month==8 || month==10 || month==12) {
24       return 31;
25     } else if (month==4 || month==6 || month==9 || month==11) {
26       return 30;
27     } else if (month == 2 && (year%400==0 || (year%4==0 && year%100!=0))) {
28       return 29;
29     } else {
30       return 28;
31     }
32   }
33
34   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string
35   function validDateStringLength(s) {
36     var m = /^(\d{4,})-(\d\d)-(\d\d)/.exec(s);
37     if (m && m[1]>=1 && m[2]>=1 && m[2]<=12 && m[3]>=1 && m[3]<=daysInMonth(m[1],m[2]))
38       return m[0].length;
39     return 0;
40   }
41
42   function isValidDateString(s) {
43     return s && validDateStringLength(s) == s.length;
44   }
45
46   // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-global-date-and-time-string
47   function isValidGlobalDateAndTimeString(s) {
48     var skip = validDateStringLength(s);
49     if (skip && s[skip] == 'T') {
50       s = s.substr(skip+1);
51       skip = validTimeStringLength(s);
52       if (skip) {
53         s = s.substr(skip);
54         if (s == 'Z')
55           return true;
56         var m = /^[+-](\d\d):(\d\d)$/.exec(s);
57         if (m && m[1]<=23 && m[2]<=59)
58           return true;
59       }
60     }
61     return false;
62   }
63
64   $.microdata.isValidGlobalDateAndTimeString = isValidGlobalDateAndTimeString;
65   $.microdata.isValidDateString = isValidDateString;
66
67   function splitTokens(s) {
68     if (s && /\S/.test(s))
69       return s.replace(/^\s+|\s+$/g,'').split(/\s+/);
70     return [];
71   }
72
73   function getItems(types) {
74     var doc = this[0];
75     if (doc.getItems)
76       return $(types ? doc.getItems(types) : doc.getItems());
77     var selector = $.map(splitTokens(types), function(t) {
78       return '[itemtype~="'+t.replace(/"/g, '\\"')+'"]';
79     }).join(',') || '*';
80     // filter results to only match top-level items
81     // because [attr] selector doesn't work in IE we have to
82     // filter the elements. http://dev.jquery.com/ticket/5637
83     return $(selector, this).filter(function() {
84       return (this.getAttribute('itemscope') != null &&
85               this.getAttribute('itemprop') == null);
86     });
87   }
88
89   function resolve(url) {
90     if (!url)
91       return '';
92     var img = document.createElement('img');
93     img.setAttribute('src', url);
94     return img.src;
95   }
96
97   function tokenList(attr) {
98     return function() {
99       return $(splitTokens(this.attr(attr)));
100     };
101   }
102
103   function itemValue() {
104     var elm = this[0];
105     if (this.attr('itemprop') === undefined)
106       return null;
107     if (this.itemScope()) {
108       return elm; // or a new jQuery object?
109     }
110     switch (elm.tagName.toUpperCase()) {
111     case 'META':
112       return this.attr('content') || '';
113     case 'AUDIO':
114     case 'EMBED':
115     case 'IFRAME':
116     case 'IMG':
117     case 'SOURCE':
118     case 'TRACK':
119     case 'VIDEO':
120       return resolve(this.attr('src'));
121     case 'A':
122     case 'AREA':
123     case 'LINK':
124       return resolve(this.attr('href'));
125     case 'OBJECT':
126       return resolve(this.attr('data'));
127     case 'TIME':
128       var datetime = this.attr('datetime');
129       if (!(datetime === undefined))
130         return datetime;
131     default:
132       return this.text();
133     }
134   }
135
136   function properties(name) {
137     // Find all elements that add properties to the item, optionally
138     // filtered by a property name. Look in the subtrees rooted at the
139     // item itself and any itemref'd elements. An item can never have
140     // itself as a property, but circular reference is possible.
141
142     var props = [];
143
144     function crawl(root) {
145       var toTraverse = [root];
146
147       function traverse(node) {
148         for (var i = 0; i < toTraverse.length; i++) {
149           if (toTraverse[i] == node)
150             toTraverse.splice(i--, 1);
151         }
152         var $node = $(node);
153         if (node != root) {
154           var $names = $node.itemProp();
155           if ($names.length) {
156             if (!name || $names.toArray().indexOf(name) != -1)
157               props.push(node);
158           }
159           if ($node.itemScope())
160             return;
161         }
162         $node.children().each(function() {
163           traverse(this);
164         });
165       }
166
167       var context = root;
168       while (context.parentNode)
169         context = context.parentNode;
170       $(root).itemRef().each(function(i, id) {
171         var $ref = $('#'+id, context);
172         if ($ref.length)
173           toTraverse.push($ref[0]);
174       });
175       $.unique(toTraverse);
176
177       while (toTraverse.length) {
178         traverse(toTraverse[0]);
179       }
180     }
181
182     if (this.itemScope())
183       crawl(this[0]);
184
185     // properties are already sorted in tree order
186     return $(props);
187   }
188
189   // feature detection to use native support where available
190   var t = $('<div itemscope itemtype="type" itemid="id" itemprop="prop" itemref="ref">')[0];
191
192   $.fn.extend({
193     items: getItems,
194     itemScope: t.itemScope ? function() {
195       return this[0].itemScope;
196     } : function () {
197       return this.attr('itemscope') != undefined;
198     },
199     itemType: t.itemType ? function() {
200       return this[0].itemType;
201     } : function () {
202       return this.attr('itemtype') || '';
203     },
204     itemId: t.itemId ? function() {
205       return this[0].itemId;
206     } : function () {
207       return resolve(this.attr('itemid'));
208     },
209     itemProp: t.itemProp && t.itemProp.length ? function() {
210       return $(this[0].itemProp);
211     } : tokenList('itemprop'),
212     itemRef: t.itemRef && t.itemRef.length ? function() {
213       return $(this[0].itemRef);
214     } : tokenList('itemref'),
215     itemValue: t.itemValue ? function() {
216       return this[0].itemValue;
217     } : itemValue,
218     properties: t.properties && t.properties.namedItem ? function(name) {
219       return $(name ? this[0].properties.namedItem(name) : this[0].properties);
220     } : properties
221   });
222 })();