Mercurial > caja-test
comparison js/ext/html-emitter.js @ 0:633c9cb05555
Origination.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Sun, 07 Jun 2009 19:29:10 -0700 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:633c9cb05555 |
---|---|
1 // Copyright (C) 2008 Google Inc. | |
2 // | |
3 // Licensed under the Apache License, Version 2.0 (the "License"); | |
4 // you may not use this file except in compliance with the License. | |
5 // You may obtain a copy of the License at | |
6 // | |
7 // http://www.apache.org/licenses/LICENSE-2.0 | |
8 // | |
9 // Unless required by applicable law or agreed to in writing, software | |
10 // distributed under the License is distributed on an "AS IS" BASIS, | |
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
12 // See the License for the specific language governing permissions and | |
13 // limitations under the License. | |
14 | |
15 /** | |
16 * @fileoverview | |
17 * JavaScript support for TemplateCompiler.java. | |
18 * <p> | |
19 * This handles the problem of making sure that only the bits of a Gadget's | |
20 * static HTML which should be visible to a script are visible, and provides | |
21 * mechanisms to reliably find elements using dynamically generated unique IDs | |
22 * in the face of DOM modifications by untrusted scripts. | |
23 * | |
24 * @author mikesamuel@gmail.com | |
25 */ | |
26 function HtmlEmitter(base, opt_tameDocument) { | |
27 if (!base) { throw new Error(); } | |
28 | |
29 /** | |
30 * Contiguous pairs of ex-descendants of base, and their ex-parent. | |
31 * The detached elements (even indices) are ordered depth-first. | |
32 */ | |
33 var detached = null; | |
34 /** Makes sure IDs are accessible within removed detached nodes. */ | |
35 var idMap = null; | |
36 | |
37 var arraySplice = Array.prototype.splice; | |
38 | |
39 function buildIdMap() { | |
40 idMap = {}; | |
41 var descs = base.getElementsByTagName('*'); | |
42 for (var i = 0, desc; (desc = descs[i]); ++i) { | |
43 if (desc.id) { idMap[desc.id] = desc; } | |
44 } | |
45 } | |
46 /** | |
47 * Returns the element with the given ID under the base node. | |
48 * @param id an auto-generated ID since we cannot rely on user supplied IDs | |
49 * to be unique. | |
50 * @return {Element|null} null if no such element exists. | |
51 */ | |
52 function byId(id) { | |
53 if (!idMap) { buildIdMap(); } | |
54 var node = idMap[id]; | |
55 if (node) { return node; } | |
56 for (; (node = base.ownerDocument.getElementById(id));) { | |
57 if (base.contains | |
58 ? base.contains(node) | |
59 : (base.compareDocumentPosition(node) & 0x10)) { | |
60 idMap[id] = node; | |
61 return node; | |
62 } else { | |
63 node.id = ''; | |
64 } | |
65 } | |
66 return null; | |
67 } | |
68 | |
69 // Below we define the attach, unwrap, and finish operations. | |
70 // These obey the conventions that: | |
71 // (1) All detached nodes, along with their ex-parents are in detached, | |
72 // and they are ordered depth-first. | |
73 // (2) When a node is specified by an ID, after the operation is performed, | |
74 // it is in the tree. | |
75 // (3) Each node is attached to the same parent regardless of what the | |
76 // script does. Even if a node is removed from the DOM by a script, | |
77 // any of its children that appear after the script, will be added. | |
78 // As an example, consider this HTML which has the end-tags removed since | |
79 // they don't correspond to actual nodes. | |
80 // <table> | |
81 // <script> | |
82 // <tr> | |
83 // <td>Foo<script>Bar | |
84 // <th>Baz | |
85 // <script> | |
86 // <p>The-End | |
87 // There are two script elements, and we need to make sure that each only | |
88 // sees the bits of the DOM that it is supposed to be aware of. | |
89 // | |
90 // To make sure that things work when javascript is off, we emit the whole | |
91 // HTML tree, and then detach everything that shouldn't be present. | |
92 // We represent the removed bits as pairs of (removedNode, parentItWasPartOf). | |
93 // Including both makes us robust against changes scripts make to the DOM. | |
94 // In this case, the detach operation results in the tree | |
95 // <table> | |
96 // and the detached list | |
97 // [<tr><td>FooBar<th>Baz in <table>, <p>The-End in (base)] | |
98 | |
99 // After the first script executes, we reattach the bits needed by the second | |
100 // script, which gives us the DOM | |
101 // <table><tr><td>Foo | |
102 // and the detached list | |
103 // ['Bar' in <td>, <th>Baz in <tr>, <p>The-End in (base)] | |
104 // Note that we did not simply remove items from the old detached list. Since | |
105 // the second script was deeper than the first, we had to add only a portion | |
106 // of the <tr>'s content which required doing a separate mini-detach operation | |
107 // and push its operation on to the front of the detached list. | |
108 | |
109 // After the second script executes, we reattach the bits needed by the third | |
110 // script, which gives us the DOM | |
111 // <table><tr><td>FooBar<th>Baz | |
112 // and the detached list | |
113 // [<p>The-End in (base)] | |
114 | |
115 // After the third script executes, we reattached the rest of the detached | |
116 // nodes, and we're done. | |
117 | |
118 // To perform a detach or reattach operation, we impose a depth-first ordering | |
119 // on HTML start tags, and text nodes: | |
120 // [0: <table>, 1: <tr>, 2: <td>, 3: 'Foo', 4: 'Bar', 5: <th>, 6: 'Baz', | |
121 // 7: <p>, 8: 'The-End'] | |
122 // Then the detach operation simply removes the minimal number of nodes from | |
123 // the DOM to make sure that only a prefix of those nodes are present. | |
124 // In the case above, we are detaching everything after item 0. | |
125 // Then the reattach operation advances the number. In the example above, we | |
126 // advance the index from 0 to 3, and then from 3 to 6. | |
127 // The finish operation simply reattaches the rest, advancing the counter from | |
128 // 6 to the end. | |
129 | |
130 // The minimal detached list from the node with DFS index I is the ordered | |
131 // list such that a (node, parent) pair (N, P) is on the list if | |
132 // dfs-index(N) > I and there is no pair (P, GP) on the list. | |
133 | |
134 // To calculate the minimal detached list given a node representing a point in | |
135 // that ordering, we rely on the following observations: | |
136 // The minimal detached list after a node, is the concatenation of | |
137 // (1) that node's children in order | |
138 // (2) the next sibling of that node and its later siblings, | |
139 // the next sibling of that node's parent and its later siblings, | |
140 // the next sibling of that node's grandparent and its later siblings, | |
141 // etc., until base is reached. | |
142 | |
143 function detachOnto(limit, out) { | |
144 // Set detached to be the minimal set of nodes that have to be removed | |
145 // to make sure that limit is the last attached node in DFS order as | |
146 // specified above. | |
147 | |
148 // First, store all the children. | |
149 for (var child = limit.firstChild, next; child; child = next) { | |
150 next = child.nextSibling; // removeChild kills nextSibling. | |
151 out.push(child, limit); | |
152 limit.removeChild(child); | |
153 } | |
154 | |
155 // Second, store your ancestor's next siblings and recurse. | |
156 for (var anc = limit, greatAnc; anc && anc !== base; anc = greatAnc) { | |
157 greatAnc = anc.parentNode; | |
158 for (var sibling = anc.nextSibling, next; sibling; sibling = next) { | |
159 next = sibling.nextSibling; | |
160 out.push(sibling, greatAnc); | |
161 greatAnc.removeChild(sibling); | |
162 } | |
163 } | |
164 } | |
165 /** | |
166 * Make sure that everything up to and including the node with the given ID | |
167 * is attached, and that nothing that follows the node is attached. | |
168 */ | |
169 function attach(id) { | |
170 var limit = byId(id); | |
171 if (detached) { | |
172 // Build an array of arguments to splice so we can replace the reattached | |
173 // nodes with the nodes detached from limit. | |
174 var newDetached = [0, 0]; | |
175 // Since limit has no parent, detachOnto will bottom out at its sibling. | |
176 detachOnto(limit, newDetached); | |
177 // Find the node containing limit that appears on detached. | |
178 for (var limitAnc = limit, parent; (parent = limitAnc.parentNode);) { | |
179 limitAnc = parent; | |
180 } | |
181 // Reattach up to and including limit ancestor. | |
182 var nConsumed = 0; | |
183 while (true) { | |
184 var toReattach = detached[nConsumed]; | |
185 (detached[nConsumed + 1] /* the parent */).appendChild(toReattach); | |
186 nConsumed += 2; | |
187 if (toReattach === limitAnc) { break; } | |
188 } | |
189 // Replace the reattached bits with the ones detached from limit. | |
190 newDetached[1] = nConsumed; // splice's second arg is the number removed | |
191 arraySplice.apply(detached, newDetached); | |
192 } else { | |
193 // The first time attach is called, the limit is actually part of the DOM. | |
194 // There's no point removing anything when all scripts are deferred. | |
195 detached = []; | |
196 detachOnto(limit, detached); | |
197 } | |
198 return limit; | |
199 } | |
200 /** | |
201 * Removes a wrapper from a textNode | |
202 * When a text node immediately precedes a script block, the limit will be | |
203 * a text node. Text nodes can't be addressed by ID, so the TemplateCompiler | |
204 * wraps them in a <span> which must be removed to be semantics preserving. | |
205 */ | |
206 function unwrap(wrapper) { | |
207 // Text nodes must have exactly one child, so it must be first on the | |
208 // detached list, since children are earlier than siblings by DFS order. | |
209 var text = detached[0]; | |
210 // If this is not true, the TemplateCompiler must be generating unwrap calls | |
211 // out of order. | |
212 // An untrusted script block should not be able to nuke the wrapper before | |
213 // it's removed so there should be a parentNode. | |
214 wrapper.parentNode.replaceChild(text, wrapper); | |
215 detached.splice(0, 2); | |
216 } | |
217 /** | |
218 * Reattach any remaining detached bits, free resources, and fire a document | |
219 * loaded event. | |
220 */ | |
221 function finish() { | |
222 if (detached) { | |
223 for (var i = 0, n = detached.length; i < n; i += 2) { | |
224 detached[i + 1].appendChild(detached[i]); | |
225 } | |
226 } | |
227 // Release references so nodes can be garbage collected. | |
228 idMap = detached = base = null; | |
229 | |
230 // Signals the close of the document and fires any window.onload event | |
231 // handlers. | |
232 var doc = opt_tameDocument; | |
233 if (doc) { doc.signalLoaded___(); } | |
234 return this; | |
235 } | |
236 | |
237 this.byId = byId; | |
238 this.attach = attach; | |
239 this.unwrap = unwrap; | |
240 this.finish = finish; | |
241 this.setAttr = bridal.setAttribute; | |
242 } |