Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(378)

Side by Side Diff: html/lib/src/treebuilder.dart

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « html/lib/src/tokenizer.dart ('k') | html/lib/src/utils.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 }
OLDNEW
« no previous file with comments | « html/lib/src/tokenizer.dart ('k') | html/lib/src/utils.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698