itemref partially working, but deleting nodes not properly handled
[microdatajs:microdatajs.git] / microdata.js
1 // emulate Object.defineProperty
2 if (!Object.defineProperty && document.__defineGetter__) {
3     Object.defineProperty = function(obj, prop, desc) {
4         if (desc.get)
5             obj.__defineGetter__(prop, desc.get);
6         if (desc.set)
7             obj.__defineSetter__(prop, desc.set);
8     };
9 }
10
11 // emulate Element.textContent
12 if (typeof document.documentElement.textContent == 'undefined') {
13     Object.defineProperty(Element.prototype, 'textContent',
14         {
15             get: function() {
16                 return this.innerText;
17             },
18             set: function(val) {
19                 this.innerText = val;
20             }
21         });
22 }
23
24 // utility functions
25
26 function splitTokens(s) {
27     if (s && /\S/.test(s)) return s.split(/\s+/);
28     return [];
29 }
30
31 function inList(item, list) {
32     for (var i = 0; i < list.length; i++) {
33         if (item == list[i])
34             return true;
35     }
36     return false;
37 }
38
39 // http://ejohn.org/blog/comparing-document-position/
40 function contains(a, b){
41     return a.contains ?
42         a != b && a.contains(b) :
43         !!(a.compareDocumentPosition(b) & 16);
44 }
45
46 function fakeCollection(rootElem, incFilter, recFilter) {
47     var elems = [];
48     elems.item = function(idx){return this[idx];};
49
50     function update(_incFilter, _recFilter) {
51         while (elems.length)
52             elems.pop();
53         function pushElements(elem) {
54             if (_incFilter(elem))
55                 elems.push(elem);
56             if (!_recFilter || _recFilter(elem)) {
57                 for (var child = elem.firstChild; child; child = child.nextSibling) {
58                     if (child.nodeType == 1) {
59                         pushElements(child);
60                     }
61                 }
62             }
63         }
64         pushElements(rootElem);
65     }
66
67     function updateSimple() {
68         update(incFilter, recFilter);
69     }
70
71     function afterUpdate() {
72         if (typeof elems.__onchange__ == 'function') {
73             elems.__onchange__();
74         }
75     }
76
77     function updateHandler() {
78         updateSimple();
79         afterUpdate();
80     }
81
82     // keep collection up to date if possible
83     if (rootElem.addEventListener) {
84         rootElem.addEventListener('DOMAttrModified', updateHandler, false);
85         rootElem.addEventListener('DOMNodeInserted', updateHandler, false);
86         rootElem.addEventListener('DOMNodeRemoved',
87             function(ev) {
88                 update(function(e){return e != ev.target && incFilter(e);},
89                        function(e){return e != ev.target;});
90                 afterUpdate();
91             }, false);
92     }
93
94     updateSimple();
95
96     return elems;
97 }
98
99 function map(list, func) {
100     var ret = [];
101     for (var i = 0; i < list.length; i++) {
102         ret.push(func(list[i]));
103     }
104     return ret;
105 }
106
107 function filter(list, pred) {
108     var ret = [];
109     for (var i = 0; i < list.length; i++) {
110         if (pred(list[i]))
111             ret.push(list[i]);
112     }
113     return ret;
114 }
115
116 // Element attribute<->property reflection
117
118 function reflectBoolean(attr, prop) {
119     Object.defineProperty(Element.prototype, prop,
120         { get: function () { return this.hasAttribute(attr); },
121           set: function (val) { if (val) this.setAttribute(attr, attr);
122                                 else this.removeAttribute(attr); } });
123 }
124
125 function reflectString(attr, prop) {
126     Object.defineProperty(Element.prototype, prop,
127         { get: function () { return this.getAttribute(attr) || ""; },
128           set: function (val) { this.setAttribute(attr, val); }});
129 }
130
131 reflectBoolean('itemscope', 'itemScope');
132 reflectString('itemtype', 'itemType');
133 reflectString('itemid', 'itemId');
134 reflectString('itemprop', 'itemProp');
135 reflectString('itemref', 'itemRef');
136
137 function getItemValueProperty(e) {
138     if (e.tagName == 'META')
139         return 'content';
140     if (e.tagName == 'AUDIO' || e.tagName == 'EMBED' ||
141         e.tagName == 'IFRAME' || e.tagName == 'IMG' ||
142         e.tagName == 'SOURCE' || e.tagName == 'VIDEO')
143         return 'src';
144     if (e.tagName == 'A' || e.tagName == 'AREA' || e.tagName == 'LINK')
145         return 'href';
146     if (e.tagName == 'OBJECT')
147         return 'data';
148     if (e.tagName == 'TIME' && e.hasAttribute('datetime'))
149         return 'datetime';
150     return 'textContent';
151 }
152
153 Object.defineProperty(Element.prototype, 'itemValue',
154 {
155     get: function() {
156         if (!this.hasAttribute('itemprop'))
157             return null;
158         if (this.hasAttribute('itemscope'))
159             return this;
160         return this[getItemValueProperty(this)];
161     },
162     set: function(val) {
163         if (!this.hasAttribute('itemprop') || this.hasAttribute('itemscope'))
164             throw new Error('INVALID_ACCESS_ERR');
165         this[getItemValueProperty(this)] = val;
166     }
167 });
168
169 // Element.properties
170
171 Object.defineProperty(Element.prototype, 'properties', { get:
172 function() {
173     // http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-properties-of-an-item
174     var itemElem = this;
175
176     var props = [];
177     function updateProperties() {
178         var root = itemElem;
179         var pending = [];
180         function pushChildren(e) {
181             for (var child = e.lastChild; child; child = child.previousSibling) {
182                 if (child.nodeType == 1) {
183                     pending.push(child);
184                 }
185             }
186         }
187         pushChildren(root);
188
189         props.length = 0;
190
191         function getScopeNode(e) {
192             var scope = e.parentNode;
193             while (scope && !scope.itemScope)
194                 scope = scope.parentNode;
195             return scope;
196         }
197         var refIds = splitTokens(root.itemRef);
198         idloop: for (var i=0; i<refIds.length; i++) {
199             var candidate = document.getElementById(refIds[i]);
200             if (!candidate) continue;
201             var scope = getScopeNode(candidate);
202             for (var j=0; j<pending.length; j++) {
203                 if (candidate == pending[j])
204                     continue idloop;
205                 if (contains(pending[j], candidate) &&
206                     (pending[j] == scope ||
207                      getScopeNode(pending[j]) == scope))
208                     continue idloop;
209             }
210             pending.push(candidate);
211         }
212
213         // from http://www.quirksmode.org/dom/getElementsByTagNames.html
214         pending.sort(function (a,b){return (a.compareDocumentPosition(b)&6)-3;});
215
216         while (pending.length) {
217             var current = pending.pop();
218             if (current.hasAttribute('itemprop'))
219                 props.push(current);
220             if (!current.hasAttribute('itemscope'))
221                 pushChildren(current);
222         }
223     }
224
225     props.names = [];
226     function updateNames() {
227         while (props.names.length)
228             props.names.pop();
229         for (var i = 0; i < props.length; i++) {
230             var propNames = splitTokens(props[i].getAttribute('itemprop'));
231             for (var j = 0; j < propNames.length; j++) {
232                 if (!inList(propNames[j], props.names))
233                     props.names.push(propNames[j]);
234             }
235         }
236     }
237
238     function updatePropertyNodeList(pnl, name) {
239         while (pnl.length)
240             pnl.pop();
241         while (pnl.values.length)
242             pnl.values.pop();
243         for (var i=0; i<props.length; i++) {
244             if (inList(name, splitTokens(props[i].getAttribute('itemprop')))) {
245                 pnl.push(props[i]);
246                 pnl.values.push(props[i].itemValue);
247             }
248         }
249     }
250
251     props.item = function(idx){return this[idx];};
252
253     var pnlCache = {};
254     props.namedItem = function (name) {
255         if (!pnlCache[name]) {
256             // fake PropertyNodeList
257             var pnl = [];
258             pnl.item = function(idx){return this[idx];};
259             pnl.values = [];
260             updatePropertyNodeList(pnl, name);
261             pnlCache[name] = pnl;
262         }
263         return pnlCache[name];
264     };
265
266     function updateHandler() {
267         updateProperties();
268         updateNames();
269         for (name in pnlCache)
270             updatePropertyNodeList(pnlCache[name], name);
271     }
272
273     // keep collection up to date if possible
274     if (document.documentElement.addEventListener) {
275         document.documentElement.addEventListener('DOMAttrModified', updateHandler, false);
276         document.documentElement.addEventListener('DOMNodeInserted', updateHandler, false);
277         document.documentElement.addEventListener('DOMNodeRemoved', updateHandler, false);
278     }
279
280 /*
281           function(ev) {
282               update(function(e){return e != ev.target && incFilter(e);},
283                      function(e){return e != ev.target;});
284           }
285 */
286
287     updateHandler();
288     return props;
289 }});
290
291 // document.getItems
292
293 document.getItems = function(typeNames) {
294     var types = splitTokens(typeNames);
295
296     function isTopLevelItem(e) {
297         return e.hasAttribute('itemscope') &&
298             !e.hasAttribute('itemprop') &&
299             (types.length == 0 ||
300              inList(e.getAttribute('itemtype'), types));
301     }
302
303     return fakeCollection(this.documentElement, isTopLevelItem);
304 };