blob: 3435a812817aa07be30716f0fdfd2a8820b6a924 [file] [log] [blame]
/// This library contains extra APIs that aren't in the DOM, but are useful
/// when interacting with the parse tree.
library dom_parsing;
import 'dom.dart';
import 'src/constants.dart' show rcdataElements;
/// A simple tree visitor for the DOM nodes.
class TreeVisitor {
visit(Node node) {
switch (node.nodeType) {
case Node.ELEMENT_NODE:
return visitElement(node);
case Node.TEXT_NODE:
return visitText(node);
case Node.COMMENT_NODE:
return visitComment(node);
case Node.DOCUMENT_FRAGMENT_NODE:
return visitDocumentFragment(node);
case Node.DOCUMENT_NODE:
return visitDocument(node);
case Node.DOCUMENT_TYPE_NODE:
return visitDocumentType(node);
default:
throw new UnsupportedError('DOM node type ${node.nodeType}');
}
}
visitChildren(Node node) {
// Allow for mutations (remove works) while iterating.
for (var child in node.nodes.toList()) visit(child);
}
/// The fallback handler if the more specific visit method hasn't been
/// overriden. Only use this from a subclass of [TreeVisitor], otherwise
/// call [visit] instead.
visitNodeFallback(Node node) => visitChildren(node);
visitDocument(Document node) => visitNodeFallback(node);
visitDocumentType(DocumentType node) => visitNodeFallback(node);
visitText(Text node) => visitNodeFallback(node);
// TODO(jmesserly): visit attributes.
visitElement(Element node) => visitNodeFallback(node);
visitComment(Comment node) => visitNodeFallback(node);
visitDocumentFragment(DocumentFragment node) => visitNodeFallback(node);
}
/// Converts the DOM tree into an HTML string with code markup suitable for
/// displaying the HTML's source code with CSS colors for different parts of the
/// markup. See also [CodeMarkupVisitor].
String htmlToCodeMarkup(Node node) {
return (new CodeMarkupVisitor()..visit(node)).toString();
}
/// Converts the DOM tree into an HTML string with code markup suitable for
/// displaying the HTML's source code with CSS colors for different parts of the
/// markup. See also [htmlToCodeMarkup].
class CodeMarkupVisitor extends TreeVisitor {
final StringBuffer _str;
CodeMarkupVisitor() : _str = new StringBuffer();
String toString() => _str.toString();
visitDocument(Document node) {
_str.write("<pre>");
visitChildren(node);
_str.write("</pre>");
}
visitDocumentType(DocumentType node) {
_str.write('<code class="markup doctype">&lt;!DOCTYPE ${node.name}>'
'</code>');
}
visitText(Text node) {
writeTextNodeAsHtml(_str, node);
}
visitElement(Element node) {
final tag = node.localName;
_str.write('&lt;<code class="markup element-name">$tag</code>');
if (node.attributes.isNotEmpty) {
node.attributes.forEach((key, v) {
v = htmlSerializeEscape(v, attributeMode: true);
_str.write(' <code class="markup attribute-name">$key</code>'
'=<code class="markup attribute-value">"$v"</code>');
});
}
if (node.nodes.isNotEmpty) {
_str.write(">");
visitChildren(node);
} else if (isVoidElement(tag)) {
_str.write(">");
return;
}
_str.write('&lt;/<code class="markup element-name">$tag</code>>');
}
visitComment(Comment node) {
var data = htmlSerializeEscape(node.data);
_str.write('<code class="markup comment">&lt;!--${data}--></code>');
}
}
// TODO(jmesserly): reconcile this with dart:web htmlEscape.
// This one might be more useful, as it is HTML5 spec compliant.
/// Escapes [text] for use in the
/// [HTML fragment serialization algorithm][1]. In particular, as described
/// in the [specification][2]:
///
/// - Replace any occurrence of the `&` character by the string `&amp;`.
/// - Replace any occurrences of the U+00A0 NO-BREAK SPACE character by the
/// string `&nbsp;`.
/// - If the algorithm was invoked in [attributeMode], replace any occurrences
/// of the `"` character by the string `&quot;`.
/// - If the algorithm was not invoked in [attributeMode], replace any
/// occurrences of the `<` character by the string `&lt;`, and any occurrences
/// of the `>` character by the string `&gt;`.
///
/// [1]: http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#serializing-html-fragments
/// [2]: http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#escapingString
String htmlSerializeEscape(String text, {bool attributeMode: false}) {
// TODO(jmesserly): is it faster to build up a list of codepoints?
// StringBuffer seems cleaner assuming Dart can unbox 1-char strings.
StringBuffer result;
for (int i = 0; i < text.length; i++) {
var ch = text[i];
String replace;
switch (ch) {
case '&':
replace = '&amp;';
break;
case '\u00A0' /*NO-BREAK SPACE*/ :
replace = '&nbsp;';
break;
case '"':
if (attributeMode) replace = '&quot;';
break;
case '<':
if (!attributeMode) replace = '&lt;';
break;
case '>':
if (!attributeMode) replace = '&gt;';
break;
}
if (replace != null) {
if (result == null) result = new StringBuffer(text.substring(0, i));
result.write(replace);
} else if (result != null) {
result.write(ch);
}
}
return result != null ? result.toString() : text;
}
/// Returns true if this tag name is a void element.
/// This method is useful to a pretty printer, because void elements must not
/// have an end tag.
/// See also: <http://dev.w3.org/html5/markup/syntax.html#void-elements>.
bool isVoidElement(String tagName) {
switch (tagName) {
case "area":
case "base":
case "br":
case "col":
case "command":
case "embed":
case "hr":
case "img":
case "input":
case "keygen":
case "link":
case "meta":
case "param":
case "source":
case "track":
case "wbr":
return true;
}
return false;
}
/// Serialize text node according to:
/// <http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#html-fragment-serialization-algorithm>
void writeTextNodeAsHtml(StringBuffer str, Text node) {
// Don't escape text for certain elements, notably <script>.
final parent = node.parentNode;
if (parent is Element) {
var tag = parent.localName;
if (rcdataElements.contains(tag) || tag == 'plaintext') {
str.write(node.data);
return;
}
}
str.write(htmlSerializeEscape(node.data));
}