Add <track> as a URL property element with itemprop reflecting src
[microdatajs:evo42-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() {
85     return this.attr('itemscope') != undefined;
86   }
87
88   function itemType() {
89     return this.attr('itemtype') || '';
90   }
91
92   function resolve(url) {
93     if (!url)
94       return '';
95     var img = document.createElement('img');
96     img.setAttribute('src', url);
97     return img.src;
98   }
99
100   function itemId() {
101     return resolve(this.attr('itemid'));
102   }
103
104   function tokenList(attr) {
105     return function() {
106       var tokens = [];
107       jQuery.each(splitTokens(this.attr(attr)), function(i, token) {
108         if (jQuery.inArray(token, tokens) == -1)
109           tokens.push(token);
110       });
111       return jQuery(tokens);
112     };
113   }
114
115   function itemValue() {
116     var elm = this.get(0);
117     if (this.attr('itemprop') === undefined)
118       return null;
119     if (this.itemScope()) {
120       return elm; // or a new jQuery object?
121     }
122     switch (elm.tagName.toUpperCase()) {
123     case 'META':
124       return this.attr('content') || '';
125     case 'AUDIO':
126     case 'EMBED':
127     case 'IFRAME':
128     case 'IMG':
129     case 'SOURCE':
130     case 'TRACK':
131     case 'VIDEO':
132       return resolve(this.attr('src'));
133     case 'A':
134     case 'AREA':
135     case 'LINK':
136       return resolve(this.attr('href'));
137     case 'OBJECT':
138       return resolve(this.attr('data'));
139     case 'TIME':
140       var datetime = this.attr('datetime');
141       if (!(datetime === undefined))
142         return datetime;
143     default:
144       return this.text();
145     }
146   }
147
148   function properties(name) {
149     var props = [];
150     // visitItem adds properties or checks for itemref loops,
151     // depending on if a stack of visited items is given.
152     function visitItem(item, visited) {
153       // traverse tree for property nodes
154       function traverse(node) {
155         var $node = jQuery(node);
156         var $names = $node.itemProp();
157         if ($names.length > 0) {
158           // this is a property node
159           if (visited) {
160             // only look for itemref loops; don't add properties
161             if ($node.itemScope()) {
162               switch (jQuery.inArray(node, visited)) {
163               case -1:
164                 // no loop (yet)
165                 visitItem(node, visited.concat([node]));
166                 break;
167               case 0:
168                 // self-referring item/property
169                 throw prop;
170               }
171             }
172           } else {
173             // add property if name matches and it is not self-referring
174             if (!name || jQuery.inArray(name, $names.toArray()) != -1) {
175               if ($node.itemScope()) {
176                 try {
177                   visitItem(node, [item]);
178                 } catch (ex) {
179                   // skip this self-referring property
180                   return;
181                 }
182               }
183               props.push(node);
184             }
185           }
186         }
187         // don't traverse into subitems
188         if (!$node.itemScope()) {
189           $node.children().each(function() {
190             traverse(this);
191           });
192         }
193       }
194       var $item = jQuery(item);
195       $item.children().each(function() {
196         traverse(this);
197       });
198       $item.itemRef().each(function(i, id) {
199         var $ref = jQuery('#'+id);
200         if ($ref.length == 1)
201           traverse($ref.get(0));
202       });
203     }
204
205     this.each(function(i, node) {
206       if (jQuery(node).itemScope())
207         visitItem(node);
208     });
209     // make results unique and sorted in document order
210     return jQuery(jQuery.unique(props));
211   }
212
213   jQuery.fn.extend({
214     items     : getItems,
215     itemScope : itemScope,
216     itemType  : itemType,
217     itemId    : itemId,
218     itemProp  : tokenList('itemprop'),
219     itemRef   : tokenList('itemref'),
220     itemValue : itemValue,
221     properties: properties
222   });
223 })();