OLD | NEW |
| (Empty) |
1 /// Internals to the tree builders. | |
2 library treebuilder; | |
3 | |
4 import 'dart:collection'; | |
5 import 'package:html/dom.dart'; | |
6 import 'package:html/parser.dart' show getElementNameTuple; | |
7 import 'package:source_span/source_span.dart'; | |
8 import 'constants.dart'; | |
9 import 'list_proxy.dart'; | |
10 import 'token.dart'; | |
11 import 'utils.dart'; | |
12 | |
13 // The scope markers are inserted when entering object elements, | |
14 // marquees, table cells, and table captions, and are used to prevent formatting | |
15 // from "leaking" into tables, object elements, and marquees. | |
16 const Node Marker = null; | |
17 | |
18 // TODO(jmesserly): this should extend ListBase<Element>, but my simple attempt | |
19 // didn't work. | |
20 class ActiveFormattingElements extends ListProxy<Element> { | |
21 ActiveFormattingElements() : super(); | |
22 | |
23 // Override the "add" method. | |
24 // TODO(jmesserly): I'd rather not override this; can we do this in the | |
25 // calling code instead? | |
26 void add(Element node) { | |
27 int equalCount = 0; | |
28 if (node != Marker) { | |
29 for (var element in reversed) { | |
30 if (element == Marker) { | |
31 break; | |
32 } | |
33 if (_nodesEqual(element, node)) { | |
34 equalCount += 1; | |
35 } | |
36 if (equalCount == 3) { | |
37 remove(element); | |
38 break; | |
39 } | |
40 } | |
41 } | |
42 super.add(node); | |
43 } | |
44 } | |
45 | |
46 // TODO(jmesserly): this should exist in corelib... | |
47 bool _mapEquals(Map a, Map b) { | |
48 if (a.length != b.length) return false; | |
49 if (a.length == 0) return true; | |
50 | |
51 for (var keyA in a.keys) { | |
52 var valB = b[keyA]; | |
53 if (valB == null && !b.containsKey(keyA)) { | |
54 return false; | |
55 } | |
56 | |
57 if (a[keyA] != valB) { | |
58 return false; | |
59 } | |
60 } | |
61 return true; | |
62 } | |
63 | |
64 bool _nodesEqual(Element node1, Element node2) { | |
65 return getElementNameTuple(node1) == getElementNameTuple(node2) && | |
66 _mapEquals(node1.attributes, node2.attributes); | |
67 } | |
68 | |
69 /// Basic treebuilder implementation. | |
70 class TreeBuilder { | |
71 final String defaultNamespace; | |
72 | |
73 Document document; | |
74 | |
75 final List<Element> openElements = <Element>[]; | |
76 | |
77 final activeFormattingElements = new ActiveFormattingElements(); | |
78 | |
79 Node headPointer; | |
80 | |
81 Element formPointer; | |
82 | |
83 /// Switch the function used to insert an element from the | |
84 /// normal one to the misnested table one and back again | |
85 bool insertFromTable; | |
86 | |
87 TreeBuilder(bool namespaceHTMLElements) | |
88 : defaultNamespace = namespaceHTMLElements ? Namespaces.html : null { | |
89 reset(); | |
90 } | |
91 | |
92 void reset() { | |
93 openElements.clear(); | |
94 activeFormattingElements.clear(); | |
95 | |
96 //XXX - rename these to headElement, formElement | |
97 headPointer = null; | |
98 formPointer = null; | |
99 | |
100 insertFromTable = false; | |
101 | |
102 document = new Document(); | |
103 } | |
104 | |
105 bool elementInScope(target, {String variant}) { | |
106 //If we pass a node in we match that. if we pass a string | |
107 //match any node with that name | |
108 bool exactNode = target is Node; | |
109 | |
110 List listElements1 = scopingElements; | |
111 List listElements2 = const []; | |
112 bool invert = false; | |
113 if (variant != null) { | |
114 switch (variant) { | |
115 case "button": | |
116 listElements2 = const [const Pair(Namespaces.html, "button")]; | |
117 break; | |
118 case "list": | |
119 listElements2 = const [ | |
120 const Pair(Namespaces.html, "ol"), | |
121 const Pair(Namespaces.html, "ul") | |
122 ]; | |
123 break; | |
124 case "table": | |
125 listElements1 = const [ | |
126 const Pair(Namespaces.html, "html"), | |
127 const Pair(Namespaces.html, "table") | |
128 ]; | |
129 break; | |
130 case "select": | |
131 listElements1 = const [ | |
132 const Pair(Namespaces.html, "optgroup"), | |
133 const Pair(Namespaces.html, "option") | |
134 ]; | |
135 invert = true; | |
136 break; | |
137 default: | |
138 throw new StateError('We should never reach this point'); | |
139 } | |
140 } | |
141 | |
142 for (var node in openElements.reversed) { | |
143 if (!exactNode && node.localName == target || | |
144 exactNode && node == target) { | |
145 return true; | |
146 } else if (invert != | |
147 (listElements1.contains(getElementNameTuple(node)) || | |
148 listElements2.contains(getElementNameTuple(node)))) { | |
149 return false; | |
150 } | |
151 } | |
152 | |
153 throw new StateError('We should never reach this point'); | |
154 } | |
155 | |
156 void reconstructActiveFormattingElements() { | |
157 // Within this algorithm the order of steps described in the | |
158 // specification is not quite the same as the order of steps in the | |
159 // code. It should still do the same though. | |
160 | |
161 // Step 1: stop the algorithm when there's nothing to do. | |
162 if (activeFormattingElements.length == 0) { | |
163 return; | |
164 } | |
165 | |
166 // Step 2 and step 3: we start with the last element. So i is -1. | |
167 int i = activeFormattingElements.length - 1; | |
168 var entry = activeFormattingElements[i]; | |
169 if (entry == Marker || openElements.contains(entry)) { | |
170 return; | |
171 } | |
172 | |
173 // Step 6 | |
174 while (entry != Marker && !openElements.contains(entry)) { | |
175 if (i == 0) { | |
176 //This will be reset to 0 below | |
177 i = -1; | |
178 break; | |
179 } | |
180 i -= 1; | |
181 // Step 5: let entry be one earlier in the list. | |
182 entry = activeFormattingElements[i]; | |
183 } | |
184 | |
185 while (true) { | |
186 // Step 7 | |
187 i += 1; | |
188 | |
189 // Step 8 | |
190 entry = activeFormattingElements[i]; | |
191 | |
192 // TODO(jmesserly): optimize this. No need to create a token. | |
193 var cloneToken = new StartTagToken(entry.localName, | |
194 namespace: entry.namespaceUri, | |
195 data: new LinkedHashMap.from(entry.attributes)) | |
196 ..span = entry.sourceSpan; | |
197 | |
198 // Step 9 | |
199 var element = insertElement(cloneToken); | |
200 | |
201 // Step 10 | |
202 activeFormattingElements[i] = element; | |
203 | |
204 // Step 11 | |
205 if (element == activeFormattingElements.last) { | |
206 break; | |
207 } | |
208 } | |
209 } | |
210 | |
211 void clearActiveFormattingElements() { | |
212 var entry = activeFormattingElements.removeLast(); | |
213 while (activeFormattingElements.length > 0 && entry != Marker) { | |
214 entry = activeFormattingElements.removeLast(); | |
215 } | |
216 } | |
217 | |
218 /// Check if an element exists between the end of the active | |
219 /// formatting elements and the last marker. If it does, return it, else | |
220 /// return null. | |
221 Element elementInActiveFormattingElements(String name) { | |
222 for (var item in activeFormattingElements.reversed) { | |
223 // Check for Marker first because if it's a Marker it doesn't have a | |
224 // name attribute. | |
225 if (item == Marker) { | |
226 break; | |
227 } else if (item.localName == name) { | |
228 return item; | |
229 } | |
230 } | |
231 return null; | |
232 } | |
233 | |
234 void insertRoot(Token token) { | |
235 var element = createElement(token); | |
236 openElements.add(element); | |
237 document.nodes.add(element); | |
238 } | |
239 | |
240 void insertDoctype(DoctypeToken token) { | |
241 var doctype = new DocumentType(token.name, token.publicId, token.systemId) | |
242 ..sourceSpan = token.span; | |
243 document.nodes.add(doctype); | |
244 } | |
245 | |
246 void insertComment(StringToken token, [Node parent]) { | |
247 if (parent == null) { | |
248 parent = openElements.last; | |
249 } | |
250 parent.nodes.add(new Comment(token.data)..sourceSpan = token.span); | |
251 } | |
252 | |
253 /// Create an element but don't insert it anywhere | |
254 Element createElement(StartTagToken token) { | |
255 var name = token.name; | |
256 var namespace = token.namespace; | |
257 if (namespace == null) namespace = defaultNamespace; | |
258 var element = document.createElementNS(namespace, name) | |
259 ..attributes = token.data | |
260 ..sourceSpan = token.span; | |
261 return element; | |
262 } | |
263 | |
264 Element insertElement(StartTagToken token) { | |
265 if (insertFromTable) return insertElementTable(token); | |
266 return insertElementNormal(token); | |
267 } | |
268 | |
269 Element insertElementNormal(StartTagToken token) { | |
270 var name = token.name; | |
271 var namespace = token.namespace; | |
272 if (namespace == null) namespace = defaultNamespace; | |
273 var element = document.createElementNS(namespace, name) | |
274 ..attributes = token.data | |
275 ..sourceSpan = token.span; | |
276 openElements.last.nodes.add(element); | |
277 openElements.add(element); | |
278 return element; | |
279 } | |
280 | |
281 Element insertElementTable(token) { | |
282 /// Create an element and insert it into the tree | |
283 var element = createElement(token); | |
284 if (!tableInsertModeElements.contains(openElements.last.localName)) { | |
285 return insertElementNormal(token); | |
286 } else { | |
287 // We should be in the InTable mode. This means we want to do | |
288 // special magic element rearranging | |
289 var nodePos = getTableMisnestedNodePosition(); | |
290 if (nodePos[1] == null) { | |
291 // TODO(jmesserly): I don't think this is reachable. If insertFromTable | |
292 // is true, there will be a <table> element open, and it always has a | |
293 // parent pointer. | |
294 nodePos[0].nodes.add(element); | |
295 } else { | |
296 nodePos[0].insertBefore(element, nodePos[1]); | |
297 } | |
298 openElements.add(element); | |
299 } | |
300 return element; | |
301 } | |
302 | |
303 /// Insert text data. | |
304 void insertText(String data, FileSpan span) { | |
305 var parent = openElements.last; | |
306 | |
307 if (!insertFromTable || | |
308 insertFromTable && | |
309 !tableInsertModeElements.contains(openElements.last.localName)) { | |
310 _insertText(parent, data, span); | |
311 } else { | |
312 // We should be in the InTable mode. This means we want to do | |
313 // special magic element rearranging | |
314 var nodePos = getTableMisnestedNodePosition(); | |
315 _insertText(nodePos[0], data, span, nodePos[1]); | |
316 } | |
317 } | |
318 | |
319 /// Insert [data] as text in the current node, positioned before the | |
320 /// start of node [refNode] or to the end of the node's text. | |
321 static void _insertText(Node parent, String data, FileSpan span, | |
322 [Element refNode]) { | |
323 var nodes = parent.nodes; | |
324 if (refNode == null) { | |
325 if (nodes.length > 0 && nodes.last is Text) { | |
326 Text last = nodes.last; | |
327 last.appendData(data); | |
328 | |
329 if (span != null) { | |
330 last.sourceSpan = | |
331 span.file.span(last.sourceSpan.start.offset, span.end.offset); | |
332 } | |
333 } else { | |
334 nodes.add(new Text(data)..sourceSpan = span); | |
335 } | |
336 } else { | |
337 int index = nodes.indexOf(refNode); | |
338 if (index > 0 && nodes[index - 1] is Text) { | |
339 Text last = nodes[index - 1]; | |
340 last.appendData(data); | |
341 } else { | |
342 nodes.insert(index, new Text(data)..sourceSpan = span); | |
343 } | |
344 } | |
345 } | |
346 | |
347 /// Get the foster parent element, and sibling to insert before | |
348 /// (or null) when inserting a misnested table node | |
349 List<Node> getTableMisnestedNodePosition() { | |
350 // The foster parent element is the one which comes before the most | |
351 // recently opened table element | |
352 // XXX - this is really inelegant | |
353 Node lastTable = null; | |
354 Node fosterParent = null; | |
355 var insertBefore = null; | |
356 for (var elm in openElements.reversed) { | |
357 if (elm.localName == "table") { | |
358 lastTable = elm; | |
359 break; | |
360 } | |
361 } | |
362 if (lastTable != null) { | |
363 // XXX - we should really check that this parent is actually a | |
364 // node here | |
365 if (lastTable.parentNode != null) { | |
366 fosterParent = lastTable.parentNode; | |
367 insertBefore = lastTable; | |
368 } else { | |
369 fosterParent = openElements[openElements.indexOf(lastTable) - 1]; | |
370 } | |
371 } else { | |
372 fosterParent = openElements[0]; | |
373 } | |
374 return [fosterParent, insertBefore]; | |
375 } | |
376 | |
377 void generateImpliedEndTags([String exclude]) { | |
378 var name = openElements.last.localName; | |
379 // XXX td, th and tr are not actually needed | |
380 if (name != exclude && | |
381 const [ | |
382 "dd", | |
383 "dt", | |
384 "li", | |
385 "option", | |
386 "optgroup", | |
387 "p", | |
388 "rp", | |
389 "rt" | |
390 ].contains(name)) { | |
391 openElements.removeLast(); | |
392 // XXX This is not entirely what the specification says. We should | |
393 // investigate it more closely. | |
394 generateImpliedEndTags(exclude); | |
395 } | |
396 } | |
397 | |
398 /// Return the final tree. | |
399 Document getDocument() => document; | |
400 | |
401 /// Return the final fragment. | |
402 DocumentFragment getFragment() { | |
403 //XXX assert innerHTML | |
404 var fragment = new DocumentFragment(); | |
405 openElements[0].reparentChildren(fragment); | |
406 return fragment; | |
407 } | |
408 } | |
OLD | NEW |