Make URL resolving work relative to the element
[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   // find the furthest ancestor (usually Document)
90   function ancestor(node) {
91     while (node.parentNode)
92       node = node.parentNode;
93     return node;
94   }
95
96   function resolve(elm, attr) {
97     // in order to handle <base> and attributes which aren't properly
98     // reflected as URLs, insert a temporary <img> element just before
99     // elm and resolve using its src attribute. the <img> element must
100     // be created using the parent document due IE security policy.
101     var url = elm.getAttribute(attr);
102     if (!url)
103       return '';
104     var a = ancestor(elm);
105     var p = elm.parentNode;
106     var img = (a.createElement ? a : document).createElement('img');
107     img.setAttribute('src', url);
108     if (p)
109       p.insertBefore(img, elm);
110     url = img.src;
111     if (p)
112       p.removeChild(img);
113     return url;
114   }
115
116   function tokenList(attr) {
117     return function() {
118       return $(splitTokens(this.attr(attr)));
119     };
120   }
121
122   function itemValue() {
123     var elm = this[0];
124     if (this.attr('itemprop') === undefined)
125       return null;
126     if (this.itemScope()) {
127       return elm; // or a new jQuery object?
128     }
129     switch (elm.tagName.toUpperCase()) {
130     case 'META':
131       return this.attr('content') || '';
132     case 'AUDIO':
133     case 'EMBED':
134     case 'IFRAME':
135     case 'IMG':
136     case 'SOURCE':
137     case 'TRACK':
138     case 'VIDEO':
139       return resolve(elm, 'src');
140     case 'A':
141     case 'AREA':
142     case 'LINK':
143       return resolve(elm, 'href');
144     case 'OBJECT':
145       return resolve(elm, 'data');
146     case 'TIME':
147       var datetime = this.attr('datetime');
148       if (!(datetime === undefined))
149         return datetime;
150     default:
151       return this.text();
152     }
153   }
154
155   function properties(name) {
156     // Find all elements that add properties to the item, optionally
157     // filtered by a property name. Look in the subtrees rooted at the
158     // item itself and any itemref'd elements. An item can never have
159     // itself as a property, but circular reference is possible.
160
161     var props = [];
162
163     function crawl(root) {
164       var toTraverse = [root];
165
166       function traverse(node) {
167         for (var i = 0; i < toTraverse.length; i++) {
168           if (toTraverse[i] == node)
169             toTraverse.splice(i--, 1);
170         }
171         var $node = $(node);
172         if (node != root) {
173           var $names = $node.itemProp();
174           if ($names.length) {
175             if (!name || $.inArray(name, $names.toArray()) != -1)
176               props.push(node);
177           }
178           if ($node.itemScope())
179             return;
180         }
181         $node.children().each(function() {
182           traverse(this);
183         });
184       }
185
186       var context = ancestor(root);
187       $(root).itemRef().each(function(i, id) {
188         var $ref = $('#'+id, context);
189         if ($ref.length)
190           toTraverse.push($ref[0]);
191       });
192       $.unique(toTraverse);
193
194       while (toTraverse.length) {
195         traverse(toTraverse[0]);
196       }
197     }
198
199     if (this.itemScope())
200       crawl(this[0]);
201
202     // properties are already sorted in tree order
203     return $(props);
204   }
205
206   // feature detection to use native support where available
207   var t = $('<div itemscope itemtype="type" itemid="id" itemprop="prop" itemref="ref">')[0];
208
209   $.fn.extend({
210     items: getItems,
211     itemScope: t.itemScope ? function() {
212       return this[0].itemScope;
213     } : function () {
214       return this.attr('itemscope') != undefined;
215     },
216     itemType: t.itemType ? function() {
217       return this[0].itemType;
218     } : function () {
219       return this.attr('itemtype') || '';
220     },
221     itemId: t.itemId ? function() {
222       return this[0].itemId;
223     } : function () {
224       return resolve(this[0], 'itemid');
225     },
226     itemProp: t.itemProp && t.itemProp.length ? function() {
227       return $(this[0].itemProp);
228     } : tokenList('itemprop'),
229     itemRef: t.itemRef && t.itemRef.length ? function() {
230       return $(this[0].itemRef);
231     } : tokenList('itemref'),
232     itemValue: t.itemValue ? function() {
233       return this[0].itemValue;
234     } : itemValue,
235     properties: t.properties && t.properties.namedItem ? function(name) {
236       return $(name ? this[0].properties.namedItem(name) : this[0].properties);
237     } : properties
238   });
239 })();