/* -*- mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */ 'use strict'; (function(){ var $ = jQuery; $.microdata = {}; // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string function validTimeStringLength(s) { var m = /^(\d\d):(\d\d)(:(\d\d)(\.\d+)?)?/.exec(s); if (m && m[1]<=23 && m[2]<=59 && (!m[4] || m[4]<=59)) return m[0].length; return 0; } function isValidTimeString(s) { return s && validTimeStringLength(s) == s.length; } // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#number-of-days-in-month-month-of-year-year function daysInMonth(year, month) { if (month==1 || month==3 || month==5 || month==7 || month==8 || month==10 || month==12) { return 31; } else if (month==4 || month==6 || month==9 || month==11) { return 30; } else if (month == 2 && (year%400==0 || (year%4==0 && year%100!=0))) { return 29; } else { return 28; } } // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string function validDateStringLength(s) { var m = /^(\d{4,})-(\d\d)-(\d\d)/.exec(s); if (m && m[1]>=1 && m[2]>=1 && m[2]<=12 && m[3]>=1 && m[3]<=daysInMonth(m[1],m[2])) return m[0].length; return 0; } function isValidDateString(s) { return s && validDateStringLength(s) == s.length; } // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-global-date-and-time-string function isValidGlobalDateAndTimeString(s) { var skip = validDateStringLength(s); if (skip && s[skip] == 'T') { s = s.substr(skip+1); skip = validTimeStringLength(s); if (skip) { s = s.substr(skip); if (s == 'Z') return true; var m = /^[+-](\d\d):(\d\d)$/.exec(s); if (m && m[1]<=23 && m[2]<=59) return true; } } return false; } $.microdata.isValidGlobalDateAndTimeString = isValidGlobalDateAndTimeString; $.microdata.isValidDateString = isValidDateString; function splitTokens(s) { if (s && /\S/.test(s)) return s.replace(/^\s+|\s+$/g,'').split(/\s+/); return []; } function getItems(types) { var selector = $.map(splitTokens(types), function(t) { return '[itemtype~="'+t.replace(/"/g, '\\"')+'"]'; }).join('') || '*'; // filter results to only match top-level items. // because [attr] selector doesn't work in IE we have to // filter the elements. http://dev.jquery.com/ticket/5637 return $(selector, this).filter(function() { return (this.getAttribute('itemscope') != null && this.getAttribute('itemprop') == null); }); } // find the furthest ancestor (usually Document) function ancestor(node) { while (node.parentNode) node = node.parentNode; return node; } function resolve(elm, attr) { // in order to handle and attributes which aren't properly // reflected as URLs, insert a temporary element just before // elm and resolve using its href property. the element must // be created using the parent document due IE security policy. var url = elm.getAttribute(attr); if (!url) return ''; var a = ancestor(elm); var p = elm.parentNode; var div = (a.createElement ? a : document).createElement('div'); try { // Setting the href attribute/property on an element // directly doesn't trigger resolution against the base URL in // IE, but creating an element with innerHTML does. We have // to do some acrobatics to avoid having to HTML-escape the URL. // See http://stackoverflow.com/a/22918332/250798 div.innerHTML = ''; div.firstChild.href = url; div.innerHTML = div.innerHTML; if (p) p.insertBefore(div, elm); url = div.firstChild.href; if (p) p.removeChild(div); } catch (e) { // IE>6 throws "TypeError: Access is denied." for mailto: // URLs. This is annoying, but harmless to ignore. } return url; } function tokenList(attr) { return function() { var list = splitTokens(this.attr(attr)); list.contains = function(token) { return $.inArray(token, this) != -1; }; return list; }; } function itemValue() { var elm = this[0]; if (elm.getAttribute('itemprop') == null) return null; if (this.itemScope()) { return elm; // or a new jQuery object? } switch (elm.tagName.toUpperCase()) { case 'META': return this.attr('content') || ''; case 'AUDIO': case 'EMBED': case 'IFRAME': case 'IMG': case 'SOURCE': case 'TRACK': case 'VIDEO': return resolve(elm, 'src'); case 'A': case 'AREA': case 'LINK': return resolve(elm, 'href'); case 'OBJECT': return resolve(elm, 'data'); case 'DATA': return this.attr('value') || ''; case 'TIME': var datetime = elm.getAttribute('datetime'); if (datetime != null) return datetime; default: return this.text(); } } function properties(name) { // Find all elements that add properties to the item, optionally // filtered by a property name. Look in the subtrees rooted at the // item itself and any itemref'd elements. An item can never have // itself as a property, but circular reference is possible. var props = []; function crawl(root) { var toTraverse = [root]; function traverse(node) { for (var i = 0; i < toTraverse.length; i++) { if (toTraverse[i] == node) toTraverse.splice(i--, 1); } var $node = $(node); if (node != root) { var names = $node.itemProp(); if (names.length) { if (!name || names.contains(name)) props.push(node); } if ($node.itemScope()) return; } $node.children().each(function() { traverse(this); }); } var context = ancestor(root); $.each($(root).itemRef(), function(i, id) { var $ref = $('#'+id, context); if ($ref.length) toTraverse.push($ref[0]); }); $.unique(toTraverse); while (toTraverse.length) { traverse(toTraverse[0]); } } if (this.itemScope()) crawl(this[0]); // properties are already sorted in tree order return $(props); } $.fn.extend({ items: getItems, itemScope: function () { return this[0].getAttribute('itemscope') != null; }, itemType: tokenList('itemtype'), itemId: function () { return resolve(this[0], 'itemid'); }, itemProp: tokenList('itemprop'), itemRef: tokenList('itemref'), itemValue: itemValue, properties: properties }); })();