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