Chromium Code Reviews

Side by Side Diff: pkg/third_party/html5lib/lib/src/treebuilder.dart

Issue 22375011: move html5lib code into dart svn repo (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: change location of html5lib to pkg/third_party/html5lib Created 7 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | | Annotate | Revision Log
OLDNEW
(Empty)
1 /** Internals to the tree builders. */
2 library treebuilder;
3
4 import 'dart:collection';
5 import 'package:html5lib/dom.dart';
6 import 'package:source_maps/span.dart' show FileSpan;
7 import 'constants.dart';
8 import 'list_proxy.dart';
9 import 'token.dart';
10 import 'utils.dart';
11
12 // The scope markers are inserted when entering object elements,
13 // marquees, table cells, and table captions, and are used to prevent formatting
14 // from "leaking" into tables, object elements, and marquees.
15 final Node Marker = null;
16
17 // TODO(jmesserly): this should extend ListBase<Node>, but my simple attempt
18 // didn't work.
19 class ActiveFormattingElements extends ListProxy<Node> {
20 ActiveFormattingElements() : super();
21
22 // Override the "add" method.
23 // TODO(jmesserly): I'd rather not override this; can we do this in the
24 // calling code instead?
25 void add(Node node) {
26 int equalCount = 0;
27 if (node != Marker) {
28 for (Node element in reversed) {
29 if (element == Marker) {
30 break;
31 }
32 if (_nodesEqual(element, node)) {
33 equalCount += 1;
34 }
35 if (equalCount == 3) {
36 remove(element);
37 break;
38 }
39 }
40 }
41 super.add(node);
42 }
43 }
44
45 // TODO(jmesserly): this should exist in corelib...
46 bool _mapEquals(Map a, Map b) {
47 if (a.length != b.length) return false;
48 if (a.length == 0) return true;
49
50 for (var keyA in a.keys) {
51 var valB = b[keyA];
52 if (valB == null && !b.containsKey(keyA)) {
53 return false;
54 }
55
56 if (a[keyA] != valB) {
57 return false;
58 }
59 }
60 return true;
61 }
62
63
64 bool _nodesEqual(Node node1, Node node2) {
65 return node1.nameTuple == node2.nameTuple &&
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 openElements = <Node>[];
76
77 final activeFormattingElements = new ActiveFormattingElements();
78
79 Node headPointer;
80
81 Node formPointer;
82
83 /**
84 * Switch the function used to insert an element from the
85 * normal one to the misnested table one and back again
86 */
87 bool insertFromTable;
88
89 TreeBuilder(bool namespaceHTMLElements)
90 : defaultNamespace = namespaceHTMLElements ? Namespaces.html : null {
91 reset();
92 }
93
94 void reset() {
95 openElements.clear();
96 activeFormattingElements.clear();
97
98 //XXX - rename these to headElement, formElement
99 headPointer = null;
100 formPointer = null;
101
102 insertFromTable = false;
103
104 document = new Document();
105 }
106
107 bool elementInScope(target, {String variant}) {
108 //If we pass a node in we match that. if we pass a string
109 //match any node with that name
110 bool exactNode = target is Node && target.nameTuple != null;
111
112 List listElements1 = scopingElements;
113 List listElements2 = const [];
114 bool invert = false;
115 if (variant != null) {
116 switch (variant) {
117 case "button":
118 listElements2 = const [const Pair(Namespaces.html, "button")];
119 break;
120 case "list":
121 listElements2 = const [const Pair(Namespaces.html, "ol"),
122 const Pair(Namespaces.html, "ul")];
123 break;
124 case "table":
125 listElements1 = const [const Pair(Namespaces.html, "html"),
126 const Pair(Namespaces.html, "table")];
127 break;
128 case "select":
129 listElements1 = const [const Pair(Namespaces.html, "optgroup"),
130 const Pair(Namespaces.html, "option")];
131 invert = true;
132 break;
133 default: assert(false); break;
134 }
135 }
136
137 for (Node node in openElements.reversed) {
138 if (node.tagName == target && !exactNode ||
139 node == target && exactNode) {
140 return true;
141 } else if (invert !=
142 (listElements1.contains(node.nameTuple) ||
143 listElements2.contains(node.nameTuple))) {
144 return false;
145 }
146 }
147
148 assert(false); // 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.tagName,
190 namespace: entry.namespace,
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 /**
215 * Check if an element exists between the end of the active
216 * formatting elements and the last marker. If it does, return it, else
217 * return null.
218 */
219 Node elementInActiveFormattingElements(String name) {
220 for (Node item in activeFormattingElements.reversed) {
221 // Check for Marker first because if it's a Marker it doesn't have a
222 // name attribute.
223 if (item == Marker) {
224 break;
225 } else if (item.tagName == name) {
226 return item;
227 }
228 }
229 return null;
230 }
231
232 void insertRoot(Token token) {
233 var element = createElement(token);
234 openElements.add(element);
235 document.nodes.add(element);
236 }
237
238 void insertDoctype(DoctypeToken token) {
239 var doctype = new DocumentType(token.name, token.publicId, token.systemId)
240 ..sourceSpan = token.span;
241 document.nodes.add(doctype);
242 }
243
244 void insertComment(StringToken token, [Node parent]) {
245 if (parent == null) {
246 parent = openElements.last;
247 }
248 parent.nodes.add(new Comment(token.data)..sourceSpan = token.span);
249 }
250
251 /** Create an element but don't insert it anywhere */
252 Element createElement(StartTagToken token) {
253 var name = token.name;
254 var namespace = token.namespace;
255 if (namespace == null) namespace = defaultNamespace;
256 var element = new Element(name, namespace)
257 ..attributes = token.data
258 ..sourceSpan = token.span;
259 return element;
260 }
261
262 Element insertElement(StartTagToken token) {
263 if (insertFromTable) return insertElementTable(token);
264 return insertElementNormal(token);
265 }
266
267 Element insertElementNormal(StartTagToken token) {
268 var name = token.name;
269 var namespace = token.namespace;
270 if (namespace == null) namespace = defaultNamespace;
271 var element = new Element(name, namespace)
272 ..attributes = token.data
273 ..sourceSpan = token.span;
274 openElements.last.nodes.add(element);
275 openElements.add(element);
276 return element;
277 }
278
279 Element insertElementTable(token) {
280 /** Create an element and insert it into the tree */
281 var element = createElement(token);
282 if (!tableInsertModeElements.contains(openElements.last.tagName)) {
283 return insertElementNormal(token);
284 } else {
285 // We should be in the InTable mode. This means we want to do
286 // special magic element rearranging
287 var nodePos = getTableMisnestedNodePosition();
288 if (nodePos[1] == null) {
289 // TODO(jmesserly): I don't think this is reachable. If insertFromTable
290 // is true, there will be a <table> element open, and it always has a
291 // parent pointer.
292 nodePos[0].nodes.add(element);
293 } else {
294 nodePos[0].insertBefore(element, nodePos[1]);
295 }
296 openElements.add(element);
297 }
298 return element;
299 }
300
301 /** Insert text data. */
302 void insertText(String data, FileSpan span) {
303 var parent = openElements.last;
304
305 if (!insertFromTable || insertFromTable &&
306 !tableInsertModeElements.contains(openElements.last.tagName)) {
307 _insertText(parent, data, span);
308 } else {
309 // We should be in the InTable mode. This means we want to do
310 // special magic element rearranging
311 var nodePos = getTableMisnestedNodePosition();
312 _insertText(nodePos[0], data, span, nodePos[1]);
313 }
314 }
315
316 /**
317 * Insert [data] as text in the current node, positioned before the
318 * start of node [refNode] or to the end of the node's text.
319 */
320 static void _insertText(Node parent, String data, FileSpan span,
321 [Element refNode]) {
322 var nodes = parent.nodes;
323 if (refNode == null) {
324 if (nodes.length > 0 && nodes.last is Text) {
325 Text last = nodes.last;
326 last.value = '${last.value}$data';
327
328 if (span != null) {
329 last.sourceSpan = span.file.span(last.sourceSpan.start.offset,
330 span.end.offset);
331 }
332 } else {
333 nodes.add(new Text(data)..sourceSpan = span);
334 }
335 } else {
336 int index = nodes.indexOf(refNode);
337 if (index > 0 && nodes[index - 1] is Text) {
338 Text last = nodes[index - 1];
339 last.value = '${last.value}$data';
340 } else {
341 nodes.insert(index, new Text(data)..sourceSpan = span);
342 }
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 */
350 List<Node> getTableMisnestedNodePosition() {
351 // The foster parent element is the one which comes before the most
352 // recently opened table element
353 // XXX - this is really inelegant
354 Node lastTable = null;
355 Node fosterParent = null;
356 var insertBefore = null;
357 for (Node elm in openElements.reversed) {
358 if (elm.tagName == "table") {
359 lastTable = elm;
360 break;
361 }
362 }
363 if (lastTable != null) {
364 // XXX - we should really check that this parent is actually a
365 // node here
366 if (lastTable.parent != null) {
367 fosterParent = lastTable.parent;
368 insertBefore = lastTable;
369 } else {
370 fosterParent = openElements[openElements.indexOf(lastTable) - 1];
371 }
372 } else {
373 fosterParent = openElements[0];
374 }
375 return [fosterParent, insertBefore];
376 }
377
378 void generateImpliedEndTags([String exclude]) {
379 var name = openElements.last.tagName;
380 // XXX td, th and tr are not actually needed
381 if (name != exclude && const ["dd", "dt", "li", "option", "optgroup", "p",
382 "rp", "rt"].contains(name)) {
383 openElements.removeLast();
384 // XXX This is not entirely what the specification says. We should
385 // investigate it more closely.
386 generateImpliedEndTags(exclude);
387 }
388 }
389
390 /** Return the final tree. */
391 Document getDocument() => document;
392
393 /** Return the final fragment. */
394 DocumentFragment getFragment() {
395 //XXX assert innerHTML
396 var fragment = new DocumentFragment();
397 openElements[0].reparentChildren(fragment);
398 return fragment;
399 }
400 }
OLDNEW

Powered by Google App Engine