| OLD | NEW |
| (Empty) |
| 1 /** | |
| 2 * A simple tree API that results from parsing html. Intended to be compatible | |
| 3 * with dart:html, but right now it resembles the classic JS DOM. | |
| 4 */ | |
| 5 #library('simpletree'); | |
| 6 | |
| 7 #import('../lib/constants.dart'); | |
| 8 #import('../lib/utils.dart'); | |
| 9 #import('base.dart'); | |
| 10 | |
| 11 class Span { | |
| 12 /** The line of this span. 1-based. */ | |
| 13 final int line; | |
| 14 | |
| 15 /** The column of this span. */ | |
| 16 final int column; | |
| 17 | |
| 18 Span(this.line, this.column); | |
| 19 } | |
| 20 | |
| 21 // TODO(jmesserly): I added this class to replace the tuple usage in Python. | |
| 22 // How does this fit in to dart:html? | |
| 23 class AttributeName implements Hashable, Comparable { | |
| 24 /** The namespace prefix, e.g. `xlink`. */ | |
| 25 final String prefix; | |
| 26 | |
| 27 /** The attribute name, e.g. `title`. */ | |
| 28 final String name; | |
| 29 | |
| 30 /** The namespace url, e.g. `http://www.w3.org/1999/xlink` */ | |
| 31 final String namespace; | |
| 32 | |
| 33 const AttributeName(this.prefix, this.name, this.namespace); | |
| 34 | |
| 35 int hashCode() { | |
| 36 int h = prefix.hashCode(); | |
| 37 h = 37 * (h & 0x1FFFFF) + name.hashCode(); | |
| 38 h = 37 * (h & 0x1FFFFF) + namespace.hashCode(); | |
| 39 return h & 0x3FFFFFFF; | |
| 40 } | |
| 41 | |
| 42 int compareTo(other) { | |
| 43 // Not sure about this sort order | |
| 44 if (other is! AttributeName) return 1; | |
| 45 int cmp = (prefix != null ? prefix : "").compareTo( | |
| 46 (other.prefix != null ? other.prefix : "")); | |
| 47 if (cmp != 0) return cmp; | |
| 48 cmp = name.compareTo(other.name); | |
| 49 if (cmp != 0) return cmp; | |
| 50 return namespace.compareTo(other.namespace); | |
| 51 } | |
| 52 | |
| 53 bool operator ==(x) { | |
| 54 if (x is! AttributeName) return false; | |
| 55 return prefix == x.prefix && name == x.name && namespace == x.namespace; | |
| 56 } | |
| 57 } | |
| 58 | |
| 59 // TODO(jmesserly): move code away from $dom methods | |
| 60 /** Really basic implementation of a DOM-core like Node. */ | |
| 61 abstract class Node implements Hashable { | |
| 62 static const int ATTRIBUTE_NODE = 2; | |
| 63 static const int CDATA_SECTION_NODE = 4; | |
| 64 static const int COMMENT_NODE = 8; | |
| 65 static const int DOCUMENT_FRAGMENT_NODE = 11; | |
| 66 static const int DOCUMENT_NODE = 9; | |
| 67 static const int DOCUMENT_TYPE_NODE = 10; | |
| 68 static const int ELEMENT_NODE = 1; | |
| 69 static const int ENTITY_NODE = 6; | |
| 70 static const int ENTITY_REFERENCE_NODE = 5; | |
| 71 static const int NOTATION_NODE = 12; | |
| 72 static const int PROCESSING_INSTRUCTION_NODE = 7; | |
| 73 static const int TEXT_NODE = 3; | |
| 74 | |
| 75 static int _lastHashCode = 0; | |
| 76 final int _hashCode; | |
| 77 | |
| 78 // TODO(jmesserly): this should be on Element | |
| 79 /** The tag name associated with the node. */ | |
| 80 final String tagName; | |
| 81 | |
| 82 /** The parent of the current node (or null for the document node). */ | |
| 83 Node parent; | |
| 84 | |
| 85 /** A map holding name, value pairs for attributes of the node. */ | |
| 86 Map attributes; | |
| 87 | |
| 88 // TODO(jmesserly): this collection needs to handle addition and removal of | |
| 89 // items and automatically fix the parent pointer, like dart:html does. | |
| 90 /** | |
| 91 * A list of child nodes of the current node. This must | |
| 92 * include all elements but not necessarily other node types. | |
| 93 */ | |
| 94 final List<Node> nodes; | |
| 95 | |
| 96 Node(this.tagName) | |
| 97 : attributes = {}, | |
| 98 nodes = <Node>[], | |
| 99 _hashCode = ++_lastHashCode; | |
| 100 | |
| 101 /** | |
| 102 * Return a shallow copy of the current node i.e. a node with the same | |
| 103 * name and attributes but with no parent or child nodes. | |
| 104 */ | |
| 105 abstract Node clone(); | |
| 106 | |
| 107 String get id { | |
| 108 var result = attributes['id']; | |
| 109 return result != null ? result : ''; | |
| 110 } | |
| 111 | |
| 112 String get namespace => null; | |
| 113 | |
| 114 // TODO(jmesserly): do we need this here? | |
| 115 /** The value of the current node (applies to text nodes and comments). */ | |
| 116 String get value => null; | |
| 117 | |
| 118 // TODO(jmesserly): this is a workaround for http://dartbug.com/4754 | |
| 119 int get $dom_nodeType => nodeType; | |
| 120 | |
| 121 abstract int get nodeType; | |
| 122 | |
| 123 String get outerHTML => _addOuterHtml(new StringBuffer()).toString(); | |
| 124 | |
| 125 String get innerHTML => _addInnerHtml(new StringBuffer()).toString(); | |
| 126 | |
| 127 abstract StringBuffer _addOuterHtml(StringBuffer str); | |
| 128 | |
| 129 StringBuffer _addInnerHtml(StringBuffer str) { | |
| 130 for (Node child in nodes) child._addOuterHtml(str); | |
| 131 return str; | |
| 132 } | |
| 133 | |
| 134 String toString() => tagName; | |
| 135 | |
| 136 int hashCode() => _hashCode; | |
| 137 | |
| 138 /** | |
| 139 * Insert [node] as a child of the current node | |
| 140 */ | |
| 141 void $dom_appendChild(Node node) { | |
| 142 if (node is Text && nodes.length > 0 && | |
| 143 nodes.last() is Text) { | |
| 144 Text last = nodes.last(); | |
| 145 last.value = '${last.value}${node.value}'; | |
| 146 } else { | |
| 147 nodes.add(node); | |
| 148 } | |
| 149 node.parent = this; | |
| 150 } | |
| 151 | |
| 152 /** | |
| 153 * Insert [data] as text in the current node, positioned before the | |
| 154 * start of node [refNode] or to the end of the node's text. | |
| 155 */ | |
| 156 void insertText(String data, [Node refNode]) { | |
| 157 if (refNode == null) { | |
| 158 $dom_appendChild(new Text(data)); | |
| 159 } else { | |
| 160 insertBefore(new Text(data), refNode); | |
| 161 } | |
| 162 } | |
| 163 | |
| 164 /** | |
| 165 * Insert [node] as a child of the current node, before [refNode] in the | |
| 166 * list of child nodes. Raises [UnsupportedOperationException] if [refNode] | |
| 167 * is not a child of the current node. | |
| 168 */ | |
| 169 void insertBefore(Node node, Node refNode) { | |
| 170 int index = nodes.indexOf(refNode); | |
| 171 if (node is Text && index > 0 && | |
| 172 nodes[index - 1] is Text) { | |
| 173 Text last = nodes[index - 1]; | |
| 174 last.value = '${last.value}${node.value}'; | |
| 175 } else { | |
| 176 nodes.insertRange(index, 1, node); | |
| 177 } | |
| 178 node.parent = this; | |
| 179 } | |
| 180 | |
| 181 /** | |
| 182 * Remove [node] from the children of the current node | |
| 183 */ | |
| 184 void $dom_removeChild(Node node) { | |
| 185 removeFromList(nodes, node); | |
| 186 node.parent = null; | |
| 187 } | |
| 188 | |
| 189 // TODO(jmesserly): should this be a property? | |
| 190 /** Return true if the node has children or text. */ | |
| 191 bool hasContent() => nodes.length > 0; | |
| 192 | |
| 193 Pair<String, String> get nameTuple { | |
| 194 var ns = namespace != null ? namespace : Namespaces.html; | |
| 195 return new Pair(ns, tagName); | |
| 196 } | |
| 197 | |
| 198 /** | |
| 199 * Move all the children of the current node to [newParent]. | |
| 200 * This is needed so that trees that don't store text as nodes move the | |
| 201 * text in the correct way. | |
| 202 */ | |
| 203 void reparentChildren(Node newParent) { | |
| 204 //XXX - should this method be made more general? | |
| 205 for (var child in nodes) { | |
| 206 newParent.$dom_appendChild(child); | |
| 207 } | |
| 208 nodes.clear(); | |
| 209 } | |
| 210 | |
| 211 /** | |
| 212 * Seaches for the first descendant node matching the given selectors, using a | |
| 213 * preorder traversal. NOTE: right now, this supports only a single type | |
| 214 * selectors, e.g. `node.query('div')`. | |
| 215 */ | |
| 216 Element query(String selectors) => _queryType(_typeSelector(selectors)); | |
| 217 | |
| 218 /** | |
| 219 * Retursn all descendant nodes matching the given selectors, using a | |
| 220 * preorder traversal. NOTE: right now, this supports only a single type | |
| 221 * selectors, e.g. `node.queryAll('div')`. | |
| 222 */ | |
| 223 List<Element> queryAll(String selectors) { | |
| 224 var results = new List<Element>(); | |
| 225 _queryAllType(_typeSelector(selectors), results); | |
| 226 return results; | |
| 227 } | |
| 228 | |
| 229 String _typeSelector(String selectors) { | |
| 230 selectors = selectors.trim(); | |
| 231 if (!selectors.splitChars().every(isLetter)) { | |
| 232 throw new NotImplementedException('only type selectors are implemented'); | |
| 233 } | |
| 234 return selectors; | |
| 235 } | |
| 236 | |
| 237 Element _queryType(String tag) { | |
| 238 for (var node in nodes) { | |
| 239 if (node is! Element) continue; | |
| 240 if (node.tagName == tag) return node; | |
| 241 var result = node._queryType(tag); | |
| 242 if (result != null) return result; | |
| 243 } | |
| 244 return null; | |
| 245 } | |
| 246 | |
| 247 void _queryAllType(String tag, List<Element> results) { | |
| 248 for (var node in nodes) { | |
| 249 if (node is! Element) continue; | |
| 250 if (node.tagName == tag) results.add(node); | |
| 251 node._queryAllType(tag, results); | |
| 252 } | |
| 253 } | |
| 254 } | |
| 255 | |
| 256 class Document extends Node { | |
| 257 Document() : super(null); | |
| 258 | |
| 259 int get nodeType => Node.DOCUMENT_NODE; | |
| 260 | |
| 261 Element get body { | |
| 262 for (var node in nodes) { | |
| 263 if (node.tagName != 'html') continue; | |
| 264 for (var node2 in node.nodes) { | |
| 265 if (node2.tagName != 'body') continue; | |
| 266 return node2; | |
| 267 } | |
| 268 } | |
| 269 return null; | |
| 270 } | |
| 271 | |
| 272 String toString() => "#document"; | |
| 273 | |
| 274 StringBuffer _addOuterHtml(StringBuffer str) => _addInnerHtml(str); | |
| 275 | |
| 276 Document clone() => new Document(); | |
| 277 } | |
| 278 | |
| 279 class DocumentFragment extends Document { | |
| 280 int get nodeType => Node.DOCUMENT_FRAGMENT_NODE; | |
| 281 | |
| 282 String toString() => "#document-fragment"; | |
| 283 | |
| 284 DocumentFragment clone() => new DocumentFragment(); | |
| 285 } | |
| 286 | |
| 287 class DocumentType extends Node { | |
| 288 final String publicId; | |
| 289 final String systemId; | |
| 290 | |
| 291 DocumentType(String name, this.publicId, this.systemId) : super(name); | |
| 292 | |
| 293 int get nodeType => Node.DOCUMENT_TYPE_NODE; | |
| 294 | |
| 295 String toString() { | |
| 296 if (publicId != null || systemId != null) { | |
| 297 var pid = publicId != null ? publicId : ''; | |
| 298 var sid = systemId != null ? systemId : ''; | |
| 299 return '<!DOCTYPE $tagName "$pid" "$sid">'; | |
| 300 } else { | |
| 301 return '<!DOCTYPE $tagName>'; | |
| 302 } | |
| 303 } | |
| 304 | |
| 305 | |
| 306 StringBuffer _addOuterHtml(StringBuffer str) => str.add(toString()); | |
| 307 | |
| 308 DocumentType clone() => new DocumentType(tagName, publicId, systemId); | |
| 309 } | |
| 310 | |
| 311 class Text extends Node { | |
| 312 String value; | |
| 313 | |
| 314 Text(this.value) : super(null); | |
| 315 | |
| 316 int get nodeType => Node.TEXT_NODE; | |
| 317 | |
| 318 String toString() => '"$value"'; | |
| 319 | |
| 320 StringBuffer _addOuterHtml(StringBuffer str) => | |
| 321 str.add(htmlEscapeMinimal(value)); | |
| 322 | |
| 323 Text clone() => new Text(value); | |
| 324 } | |
| 325 | |
| 326 class Element extends Node { | |
| 327 final String namespace; | |
| 328 | |
| 329 Element(String name, [this.namespace]) : super(name); | |
| 330 | |
| 331 int get nodeType => Node.ELEMENT_NODE; | |
| 332 | |
| 333 String toString() { | |
| 334 if (namespace == null) return "<$tagName>"; | |
| 335 return "<${Namespaces.getPrefix(namespace)} $tagName>"; | |
| 336 } | |
| 337 | |
| 338 StringBuffer _addOuterHtml(StringBuffer str) { | |
| 339 str.add('<$tagName'); | |
| 340 if (attributes.length > 0) { | |
| 341 attributes.forEach((key, v) { | |
| 342 v = htmlEscapeMinimal(v, {'"': """}); | |
| 343 str.add(' $key="$v"'); | |
| 344 }); | |
| 345 } | |
| 346 str.add('>'); | |
| 347 if (nodes.length > 0) { | |
| 348 _addInnerHtml(str); | |
| 349 } | |
| 350 // void elements must not have an end tag | |
| 351 // http://dev.w3.org/html5/markup/syntax.html#void-elements | |
| 352 if (voidElements.indexOf(tagName) < 0) { | |
| 353 str.add('</$tagName>'); | |
| 354 } | |
| 355 return str; | |
| 356 } | |
| 357 | |
| 358 Element clone() => | |
| 359 new Element(tagName, namespace)..attributes = new Map.from(attributes); | |
| 360 } | |
| 361 | |
| 362 class Comment extends Node { | |
| 363 final String data; | |
| 364 | |
| 365 Comment(this.data) : super(null); | |
| 366 | |
| 367 int get nodeType => Node.COMMENT_NODE; | |
| 368 | |
| 369 String toString() => "<!-- $data -->"; | |
| 370 | |
| 371 StringBuffer _addOuterHtml(StringBuffer str) => str.add("<!--$data-->"); | |
| 372 | |
| 373 Comment clone() => new Comment(data); | |
| 374 } | |
| 375 | |
| 376 /** A simple tree visitor for the DOM nodes. */ | |
| 377 class TreeVisitor { | |
| 378 visit(Node node) { | |
| 379 switch (node.nodeType) { | |
| 380 case Node.ELEMENT_NODE: return visitElement(node); | |
| 381 case Node.TEXT_NODE: return visitText(node); | |
| 382 case Node.COMMENT_NODE: return visitComment(node); | |
| 383 case Node.DOCUMENT_FRAGMENT_NODE: return visitDocumentFragment(node); | |
| 384 case Node.DOCUMENT_NODE: return visitDocument(node); | |
| 385 case Node.DOCUMENT_TYPE_NODE: return visitDocumentType(node); | |
| 386 default: throw new UnsupportedOperationException( | |
| 387 'DOM node type ${node.nodeType}'); | |
| 388 } | |
| 389 } | |
| 390 | |
| 391 visitChildren(Node node) { | |
| 392 for (var child in node.nodes) visit(child); | |
| 393 } | |
| 394 | |
| 395 /** | |
| 396 * The fallback handler if the more specific visit method hasn't been | |
| 397 * overriden. Only use this from a subclass of [TreeVisitor], otherwise | |
| 398 * call [visit] instead. | |
| 399 */ | |
| 400 visitNodeFallback(Node node) => visitChildren(node); | |
| 401 | |
| 402 visitDocument(Document node) => visitNodeFallback(node); | |
| 403 | |
| 404 visitDocumentType(DocumentType node) => visitNodeFallback(node); | |
| 405 | |
| 406 visitText(Text node) => visitNodeFallback(node); | |
| 407 | |
| 408 // TODO(jmesserly): visit attributes. | |
| 409 visitElement(Element node) => visitNodeFallback(node); | |
| 410 | |
| 411 visitComment(Comment node) => visitNodeFallback(node); | |
| 412 | |
| 413 // Note: visits document by default because DocumentFragment is a Document. | |
| 414 visitDocumentFragment(DocumentFragment node) => visitDocument(node); | |
| 415 } | |
| 416 | |
| 417 /** | |
| 418 * Converts the DOM tree into an HTML string with code markup suitable for | |
| 419 * displaying the HTML's source code with CSS colors for different parts of the | |
| 420 * markup. See also [CodeMarkupVisitor]. | |
| 421 */ | |
| 422 String htmlToCodeMarkup(Node node) { | |
| 423 return (new CodeMarkupVisitor()..visit(node)).toString(); | |
| 424 } | |
| 425 | |
| 426 /** | |
| 427 * Converts the DOM tree into an HTML string with code markup suitable for | |
| 428 * displaying the HTML's source code with CSS colors for different parts of the | |
| 429 * markup. See also [htmlToCodeMarkup]. | |
| 430 */ | |
| 431 class CodeMarkupVisitor extends TreeVisitor { | |
| 432 final StringBuffer _str; | |
| 433 | |
| 434 CodeMarkupVisitor() : _str = new StringBuffer(); | |
| 435 | |
| 436 String toString() => _str.toString(); | |
| 437 | |
| 438 visitDocument(Document node) { | |
| 439 _str.add("<pre>"); | |
| 440 visitChildren(node); | |
| 441 _str.add("</pre>"); | |
| 442 } | |
| 443 | |
| 444 visitDocumentType(DocumentType node) { | |
| 445 _str.add('<code class="markup doctype"><!DOCTYPE ${node.tagName}></code>'
); | |
| 446 } | |
| 447 | |
| 448 visitText(Text node) { | |
| 449 node._addOuterHtml(_str); | |
| 450 } | |
| 451 | |
| 452 visitElement(Element node) { | |
| 453 _str.add('<<code class="markup element-name">${node.tagName}</code>'); | |
| 454 if (node.attributes.length > 0) { | |
| 455 node.attributes.forEach((key, v) { | |
| 456 v = htmlEscapeMinimal(v, {'"': """}); | |
| 457 _str.add(' <code class="markup attribute-name">$key</code>' | |
| 458 '=<code class="markup attribute-value">"$v"</code>'); | |
| 459 }); | |
| 460 } | |
| 461 if (node.nodes.length > 0) { | |
| 462 _str.add(">"); | |
| 463 visitChildren(node); | |
| 464 } else if (voidElements.indexOf(node.tagName) >= 0) { | |
| 465 _str.add(">"); | |
| 466 return; | |
| 467 } | |
| 468 _str.add('</<code class="markup element-name">${node.tagName}</code>>'); | |
| 469 } | |
| 470 | |
| 471 visitComment(Comment node) { | |
| 472 var data = htmlEscapeMinimal(node.data); | |
| 473 _str.add('<code class="markup comment"><!--${data}--></code>'); | |
| 474 } | |
| 475 } | |
| OLD | NEW |