diff options
Diffstat (limited to 'server/src/org/jsoup/nodes')
-rw-r--r-- | server/src/org/jsoup/nodes/Attribute.java | 131 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/Attributes.java | 249 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/Comment.java | 46 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/DataNode.java | 62 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/Document.java | 350 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/DocumentType.java | 46 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/Element.java | 1119 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/Entities.java | 184 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/Node.java | 615 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/TextNode.java | 175 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/XmlDeclaration.java | 48 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/entities-base.properties | 106 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/entities-full.properties | 2032 | ||||
-rw-r--r-- | server/src/org/jsoup/nodes/package-info.java | 4 |
14 files changed, 5167 insertions, 0 deletions
diff --git a/server/src/org/jsoup/nodes/Attribute.java b/server/src/org/jsoup/nodes/Attribute.java new file mode 100644 index 0000000000..02eb29db83 --- /dev/null +++ b/server/src/org/jsoup/nodes/Attribute.java @@ -0,0 +1,131 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.Validate; + +import java.util.Map; + +/** + A single key + value attribute. Keys are trimmed and normalised to lower-case. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Attribute implements Map.Entry<String, String>, Cloneable { + private String key; + private String value; + + /** + * Create a new attribute from unencoded (raw) key and value. + * @param key attribute key + * @param value attribute value + * @see #createFromEncoded + */ + public Attribute(String key, String value) { + Validate.notEmpty(key); + Validate.notNull(value); + this.key = key.trim().toLowerCase(); + this.value = value; + } + + /** + Get the attribute key. + @return the attribute key + */ + public String getKey() { + return key; + } + + /** + Set the attribute key. Gets normalised as per the constructor method. + @param key the new key; must not be null + */ + public void setKey(String key) { + Validate.notEmpty(key); + this.key = key.trim().toLowerCase(); + } + + /** + Get the attribute value. + @return the attribute value + */ + public String getValue() { + return value; + } + + /** + Set the attribute value. + @param value the new attribute value; must not be null + */ + public String setValue(String value) { + Validate.notNull(value); + String old = this.value; + this.value = value; + return old; + } + + /** + Get the HTML representation of this attribute; e.g. {@code href="index.html"}. + @return HTML + */ + public String html() { + return key + "=\"" + Entities.escape(value, (new Document("")).outputSettings()) + "\""; + } + + protected void html(StringBuilder accum, Document.OutputSettings out) { + accum + .append(key) + .append("=\"") + .append(Entities.escape(value, out)) + .append("\""); + } + + /** + Get the string representation of this attribute, implemented as {@link #html()}. + @return string + */ + public String toString() { + return html(); + } + + /** + * Create a new Attribute from an unencoded key and a HTML attribute encoded value. + * @param unencodedKey assumes the key is not encoded, as can be only run of simple \w chars. + * @param encodedValue HTML attribute encoded value + * @return attribute + */ + public static Attribute createFromEncoded(String unencodedKey, String encodedValue) { + String value = Entities.unescape(encodedValue, true); + return new Attribute(unencodedKey, value); + } + + protected boolean isDataAttribute() { + return key.startsWith(Attributes.dataPrefix) && key.length() > Attributes.dataPrefix.length(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Attribute)) return false; + + Attribute attribute = (Attribute) o; + + if (key != null ? !key.equals(attribute.key) : attribute.key != null) return false; + if (value != null ? !value.equals(attribute.value) : attribute.value != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = key != null ? key.hashCode() : 0; + result = 31 * result + (value != null ? value.hashCode() : 0); + return result; + } + + @Override + public Attribute clone() { + try { + return (Attribute) super.clone(); // only fields are immutable strings key and value, so no more deep copy required + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/server/src/org/jsoup/nodes/Attributes.java b/server/src/org/jsoup/nodes/Attributes.java new file mode 100644 index 0000000000..9436750fc9 --- /dev/null +++ b/server/src/org/jsoup/nodes/Attributes.java @@ -0,0 +1,249 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.Validate; + +import java.util.*; + +/** + * The attributes of an Element. + * <p/> + * Attributes are treated as a map: there can be only one value associated with an attribute key. + * <p/> + * Attribute key and value comparisons are done case insensitively, and keys are normalised to + * lower-case. + * + * @author Jonathan Hedley, jonathan@hedley.net + */ +public class Attributes implements Iterable<Attribute>, Cloneable { + protected static final String dataPrefix = "data-"; + + private LinkedHashMap<String, Attribute> attributes = null; + // linked hash map to preserve insertion order. + // null be default as so many elements have no attributes -- saves a good chunk of memory + + /** + Get an attribute value by key. + @param key the attribute key + @return the attribute value if set; or empty string if not set. + @see #hasKey(String) + */ + public String get(String key) { + Validate.notEmpty(key); + + if (attributes == null) + return ""; + + Attribute attr = attributes.get(key.toLowerCase()); + return attr != null ? attr.getValue() : ""; + } + + /** + Set a new attribute, or replace an existing one by key. + @param key attribute key + @param value attribute value + */ + public void put(String key, String value) { + Attribute attr = new Attribute(key, value); + put(attr); + } + + /** + Set a new attribute, or replace an existing one by key. + @param attribute attribute + */ + public void put(Attribute attribute) { + Validate.notNull(attribute); + if (attributes == null) + attributes = new LinkedHashMap<String, Attribute>(2); + attributes.put(attribute.getKey(), attribute); + } + + /** + Remove an attribute by key. + @param key attribute key to remove + */ + public void remove(String key) { + Validate.notEmpty(key); + if (attributes == null) + return; + attributes.remove(key.toLowerCase()); + } + + /** + Tests if these attributes contain an attribute with this key. + @param key key to check for + @return true if key exists, false otherwise + */ + public boolean hasKey(String key) { + return attributes != null && attributes.containsKey(key.toLowerCase()); + } + + /** + Get the number of attributes in this set. + @return size + */ + public int size() { + if (attributes == null) + return 0; + return attributes.size(); + } + + /** + Add all the attributes from the incoming set to this set. + @param incoming attributes to add to these attributes. + */ + public void addAll(Attributes incoming) { + if (incoming.size() == 0) + return; + if (attributes == null) + attributes = new LinkedHashMap<String, Attribute>(incoming.size()); + attributes.putAll(incoming.attributes); + } + + public Iterator<Attribute> iterator() { + return asList().iterator(); + } + + /** + Get the attributes as a List, for iteration. Do not modify the keys of the attributes via this view, as changes + to keys will not be recognised in the containing set. + @return an view of the attributes as a List. + */ + public List<Attribute> asList() { + if (attributes == null) + return Collections.emptyList(); + + List<Attribute> list = new ArrayList<Attribute>(attributes.size()); + for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { + list.add(entry.getValue()); + } + return Collections.unmodifiableList(list); + } + + /** + * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is, attributes with keys + * starting with {@code data-}. + * @return map of custom data attributes. + */ + public Map<String, String> dataset() { + return new Dataset(); + } + + /** + Get the HTML representation of these attributes. + @return HTML + */ + public String html() { + StringBuilder accum = new StringBuilder(); + html(accum, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used + return accum.toString(); + } + + void html(StringBuilder accum, Document.OutputSettings out) { + if (attributes == null) + return; + + for (Map.Entry<String, Attribute> entry : attributes.entrySet()) { + Attribute attribute = entry.getValue(); + accum.append(" "); + attribute.html(accum, out); + } + } + + public String toString() { + return html(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Attributes)) return false; + + Attributes that = (Attributes) o; + + if (attributes != null ? !attributes.equals(that.attributes) : that.attributes != null) return false; + + return true; + } + + @Override + public int hashCode() { + return attributes != null ? attributes.hashCode() : 0; + } + + @Override + public Attributes clone() { + if (attributes == null) + return new Attributes(); + + Attributes clone; + try { + clone = (Attributes) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + clone.attributes = new LinkedHashMap<String, Attribute>(attributes.size()); + for (Attribute attribute: this) + clone.attributes.put(attribute.getKey(), attribute.clone()); + return clone; + } + + private class Dataset extends AbstractMap<String, String> { + + private Dataset() { + if (attributes == null) + attributes = new LinkedHashMap<String, Attribute>(2); + } + + public Set<Entry<String, String>> entrySet() { + return new EntrySet(); + } + + @Override + public String put(String key, String value) { + String dataKey = dataKey(key); + String oldValue = hasKey(dataKey) ? attributes.get(dataKey).getValue() : null; + Attribute attr = new Attribute(dataKey, value); + attributes.put(dataKey, attr); + return oldValue; + } + + private class EntrySet extends AbstractSet<Map.Entry<String, String>> { + public Iterator<Map.Entry<String, String>> iterator() { + return new DatasetIterator(); + } + + public int size() { + int count = 0; + Iterator iter = new DatasetIterator(); + while (iter.hasNext()) + count++; + return count; + } + } + + private class DatasetIterator implements Iterator<Map.Entry<String, String>> { + private Iterator<Attribute> attrIter = attributes.values().iterator(); + private Attribute attr; + public boolean hasNext() { + while (attrIter.hasNext()) { + attr = attrIter.next(); + if (attr.isDataAttribute()) return true; + } + return false; + } + + public Entry<String, String> next() { + return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue()); + } + + public void remove() { + attributes.remove(attr.getKey()); + } + } + } + + private static String dataKey(String key) { + return dataPrefix + key; + } +} diff --git a/server/src/org/jsoup/nodes/Comment.java b/server/src/org/jsoup/nodes/Comment.java new file mode 100644 index 0000000000..37fd4368fa --- /dev/null +++ b/server/src/org/jsoup/nodes/Comment.java @@ -0,0 +1,46 @@ +package org.jsoup.nodes; + +/** + A comment node. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Comment extends Node { + private static final String COMMENT_KEY = "comment"; + + /** + Create a new comment node. + @param data The contents of the comment + @param baseUri base URI + */ + public Comment(String data, String baseUri) { + super(baseUri); + attributes.put(COMMENT_KEY, data); + } + + public String nodeName() { + return "#comment"; + } + + /** + Get the contents of the comment. + @return comment content + */ + public String getData() { + return attributes.get(COMMENT_KEY); + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + if (out.prettyPrint()) + indent(accum, depth, out); + accum + .append("<!--") + .append(getData()) + .append("-->"); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } +} diff --git a/server/src/org/jsoup/nodes/DataNode.java b/server/src/org/jsoup/nodes/DataNode.java new file mode 100644 index 0000000000..a64f56f0a4 --- /dev/null +++ b/server/src/org/jsoup/nodes/DataNode.java @@ -0,0 +1,62 @@ +package org.jsoup.nodes; + +/** + A data node, for contents of style, script tags etc, where contents should not show in text(). + + @author Jonathan Hedley, jonathan@hedley.net */ +public class DataNode extends Node{ + private static final String DATA_KEY = "data"; + + /** + Create a new DataNode. + @param data data contents + @param baseUri base URI + */ + public DataNode(String data, String baseUri) { + super(baseUri); + attributes.put(DATA_KEY, data); + } + + public String nodeName() { + return "#data"; + } + + /** + Get the data contents of this node. Will be unescaped and with original new lines, space etc. + @return data + */ + public String getWholeData() { + return attributes.get(DATA_KEY); + } + + /** + * Set the data contents of this node. + * @param data unencoded data + * @return this node, for chaining + */ + public DataNode setWholeData(String data) { + attributes.put(DATA_KEY, data); + return this; + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + accum.append(getWholeData()); // data is not escaped in return from data nodes, so " in script, style is plain + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } + + /** + Create a new DataNode from HTML encoded data. + @param encodedData encoded data + @param baseUri bass URI + @return new DataNode + */ + public static DataNode createFromEncoded(String encodedData, String baseUri) { + String data = Entities.unescape(encodedData); + return new DataNode(data, baseUri); + } +} diff --git a/server/src/org/jsoup/nodes/Document.java b/server/src/org/jsoup/nodes/Document.java new file mode 100644 index 0000000000..adb371ce14 --- /dev/null +++ b/server/src/org/jsoup/nodes/Document.java @@ -0,0 +1,350 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.Validate; +import org.jsoup.parser.Tag; +import org.jsoup.select.Elements; + +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.ArrayList; +import java.util.List; + +/** + A HTML Document. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class Document extends Element { + private OutputSettings outputSettings = new OutputSettings(); + private QuirksMode quirksMode = QuirksMode.noQuirks; + + /** + Create a new, empty Document. + @param baseUri base URI of document + @see org.jsoup.Jsoup#parse + @see #createShell + */ + public Document(String baseUri) { + super(Tag.valueOf("#root"), baseUri); + } + + /** + Create a valid, empty shell of a document, suitable for adding more elements to. + @param baseUri baseUri of document + @return document with html, head, and body elements. + */ + static public Document createShell(String baseUri) { + Validate.notNull(baseUri); + + Document doc = new Document(baseUri); + Element html = doc.appendElement("html"); + html.appendElement("head"); + html.appendElement("body"); + + return doc; + } + + /** + Accessor to the document's {@code head} element. + @return {@code head} + */ + public Element head() { + return findFirstElementByTagName("head", this); + } + + /** + Accessor to the document's {@code body} element. + @return {@code body} + */ + public Element body() { + return findFirstElementByTagName("body", this); + } + + /** + Get the string contents of the document's {@code title} element. + @return Trimmed title, or empty string if none set. + */ + public String title() { + Element titleEl = getElementsByTag("title").first(); + return titleEl != null ? titleEl.text().trim() : ""; + } + + /** + Set the document's {@code title} element. Updates the existing element, or adds {@code title} to {@code head} if + not present + @param title string to set as title + */ + public void title(String title) { + Validate.notNull(title); + Element titleEl = getElementsByTag("title").first(); + if (titleEl == null) { // add to head + head().appendElement("title").text(title); + } else { + titleEl.text(title); + } + } + + /** + Create a new Element, with this document's base uri. Does not make the new element a child of this document. + @param tagName element tag name (e.g. {@code a}) + @return new element + */ + public Element createElement(String tagName) { + return new Element(Tag.valueOf(tagName), this.baseUri()); + } + + /** + Normalise the document. This happens after the parse phase so generally does not need to be called. + Moves any text content that is not in the body element into the body. + @return this document after normalisation + */ + public Document normalise() { + Element htmlEl = findFirstElementByTagName("html", this); + if (htmlEl == null) + htmlEl = appendElement("html"); + if (head() == null) + htmlEl.prependElement("head"); + if (body() == null) + htmlEl.appendElement("body"); + + // pull text nodes out of root, html, and head els, and push into body. non-text nodes are already taken care + // of. do in inverse order to maintain text order. + normaliseTextNodes(head()); + normaliseTextNodes(htmlEl); + normaliseTextNodes(this); + + normaliseStructure("head", htmlEl); + normaliseStructure("body", htmlEl); + + return this; + } + + // does not recurse. + private void normaliseTextNodes(Element element) { + List<Node> toMove = new ArrayList<Node>(); + for (Node node: element.childNodes) { + if (node instanceof TextNode) { + TextNode tn = (TextNode) node; + if (!tn.isBlank()) + toMove.add(tn); + } + } + + for (int i = toMove.size()-1; i >= 0; i--) { + Node node = toMove.get(i); + element.removeChild(node); + body().prependChild(new TextNode(" ", "")); + body().prependChild(node); + } + } + + // merge multiple <head> or <body> contents into one, delete the remainder, and ensure they are owned by <html> + private void normaliseStructure(String tag, Element htmlEl) { + Elements elements = this.getElementsByTag(tag); + Element master = elements.first(); // will always be available as created above if not existent + if (elements.size() > 1) { // dupes, move contents to master + List<Node> toMove = new ArrayList<Node>(); + for (int i = 1; i < elements.size(); i++) { + Node dupe = elements.get(i); + for (Node node : dupe.childNodes) + toMove.add(node); + dupe.remove(); + } + + for (Node dupe : toMove) + master.appendChild(dupe); + } + // ensure parented by <html> + if (!master.parent().equals(htmlEl)) { + htmlEl.appendChild(master); // includes remove() + } + } + + // fast method to get first by tag name, used for html, head, body finders + private Element findFirstElementByTagName(String tag, Node node) { + if (node.nodeName().equals(tag)) + return (Element) node; + else { + for (Node child: node.childNodes) { + Element found = findFirstElementByTagName(tag, child); + if (found != null) + return found; + } + } + return null; + } + + @Override + public String outerHtml() { + return super.html(); // no outer wrapper tag + } + + /** + Set the text of the {@code body} of this document. Any existing nodes within the body will be cleared. + @param text unencoded text + @return this document + */ + @Override + public Element text(String text) { + body().text(text); // overridden to not nuke doc structure + return this; + } + + @Override + public String nodeName() { + return "#document"; + } + + @Override + public Document clone() { + Document clone = (Document) super.clone(); + clone.outputSettings = this.outputSettings.clone(); + return clone; + } + + /** + * A Document's output settings control the form of the text() and html() methods. + */ + public static class OutputSettings implements Cloneable { + private Entities.EscapeMode escapeMode = Entities.EscapeMode.base; + private Charset charset = Charset.forName("UTF-8"); + private CharsetEncoder charsetEncoder = charset.newEncoder(); + private boolean prettyPrint = true; + private int indentAmount = 1; + + public OutputSettings() {} + + /** + * Get the document's current HTML escape mode: <code>base</code>, which provides a limited set of named HTML + * entities and escapes other characters as numbered entities for maximum compatibility; or <code>extended</code>, + * which uses the complete set of HTML named entities. + * <p> + * The default escape mode is <code>base</code>. + * @return the document's current escape mode + */ + public Entities.EscapeMode escapeMode() { + return escapeMode; + } + + /** + * Set the document's escape mode + * @param escapeMode the new escape mode to use + * @return the document's output settings, for chaining + */ + public OutputSettings escapeMode(Entities.EscapeMode escapeMode) { + this.escapeMode = escapeMode; + return this; + } + + /** + * Get the document's current output charset, which is used to control which characters are escaped when + * generating HTML (via the <code>html()</code> methods), and which are kept intact. + * <p> + * Where possible (when parsing from a URL or File), the document's output charset is automatically set to the + * input charset. Otherwise, it defaults to UTF-8. + * @return the document's current charset. + */ + public Charset charset() { + return charset; + } + + /** + * Update the document's output charset. + * @param charset the new charset to use. + * @return the document's output settings, for chaining + */ + public OutputSettings charset(Charset charset) { + // todo: this should probably update the doc's meta charset + this.charset = charset; + charsetEncoder = charset.newEncoder(); + return this; + } + + /** + * Update the document's output charset. + * @param charset the new charset (by name) to use. + * @return the document's output settings, for chaining + */ + public OutputSettings charset(String charset) { + charset(Charset.forName(charset)); + return this; + } + + CharsetEncoder encoder() { + return charsetEncoder; + } + + /** + * Get if pretty printing is enabled. Default is true. If disabled, the HTML output methods will not re-format + * the output, and the output will generally look like the input. + * @return if pretty printing is enabled. + */ + public boolean prettyPrint() { + return prettyPrint; + } + + /** + * Enable or disable pretty printing. + * @param pretty new pretty print setting + * @return this, for chaining + */ + public OutputSettings prettyPrint(boolean pretty) { + prettyPrint = pretty; + return this; + } + + /** + * Get the current tag indent amount, used when pretty printing. + * @return the current indent amount + */ + public int indentAmount() { + return indentAmount; + } + + /** + * Set the indent amount for pretty printing + * @param indentAmount number of spaces to use for indenting each level. Must be >= 0. + * @return this, for chaining + */ + public OutputSettings indentAmount(int indentAmount) { + Validate.isTrue(indentAmount >= 0); + this.indentAmount = indentAmount; + return this; + } + + @Override + public OutputSettings clone() { + OutputSettings clone; + try { + clone = (OutputSettings) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + clone.charset(charset.name()); // new charset and charset encoder + clone.escapeMode = Entities.EscapeMode.valueOf(escapeMode.name()); + // indentAmount, prettyPrint are primitives so object.clone() will handle + return clone; + } + } + + /** + * Get the document's current output settings. + * @return the document's current output settings. + */ + public OutputSettings outputSettings() { + return outputSettings; + } + + public enum QuirksMode { + noQuirks, quirks, limitedQuirks; + } + + public QuirksMode quirksMode() { + return quirksMode; + } + + public Document quirksMode(QuirksMode quirksMode) { + this.quirksMode = quirksMode; + return this; + } +} + diff --git a/server/src/org/jsoup/nodes/DocumentType.java b/server/src/org/jsoup/nodes/DocumentType.java new file mode 100644 index 0000000000..f8c79f0d18 --- /dev/null +++ b/server/src/org/jsoup/nodes/DocumentType.java @@ -0,0 +1,46 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; + +/** + * A {@code <!DOCTPYE>} node. + */ +public class DocumentType extends Node { + // todo: quirk mode from publicId and systemId + + /** + * Create a new doctype element. + * @param name the doctype's name + * @param publicId the doctype's public ID + * @param systemId the doctype's system ID + * @param baseUri the doctype's base URI + */ + public DocumentType(String name, String publicId, String systemId, String baseUri) { + super(baseUri); + + Validate.notEmpty(name); + attr("name", name); + attr("publicId", publicId); + attr("systemId", systemId); + } + + @Override + public String nodeName() { + return "#doctype"; + } + + @Override + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + accum.append("<!DOCTYPE ").append(attr("name")); + if (!StringUtil.isBlank(attr("publicId"))) + accum.append(" PUBLIC \"").append(attr("publicId")).append("\""); + if (!StringUtil.isBlank(attr("systemId"))) + accum.append(" \"").append(attr("systemId")).append("\""); + accum.append('>'); + } + + @Override + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) { + } +} diff --git a/server/src/org/jsoup/nodes/Element.java b/server/src/org/jsoup/nodes/Element.java new file mode 100644 index 0000000000..5c1894c934 --- /dev/null +++ b/server/src/org/jsoup/nodes/Element.java @@ -0,0 +1,1119 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.parser.Parser; +import org.jsoup.parser.Tag; +import org.jsoup.select.Collector; +import org.jsoup.select.Elements; +import org.jsoup.select.Evaluator; +import org.jsoup.select.Selector; + +import java.util.*; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A HTML element consists of a tag name, attributes, and child nodes (including text nodes and + * other elements). + * + * From an Element, you can extract data, traverse the node graph, and manipulate the HTML. + * + * @author Jonathan Hedley, jonathan@hedley.net + */ +public class Element extends Node { + private Tag tag; + private Set<String> classNames; + + /** + * Create a new, standalone Element. (Standalone in that is has no parent.) + * + * @param tag tag of this element + * @param baseUri the base URI + * @param attributes initial attributes + * @see #appendChild(Node) + * @see #appendElement(String) + */ + public Element(Tag tag, String baseUri, Attributes attributes) { + super(baseUri, attributes); + + Validate.notNull(tag); + this.tag = tag; + } + + /** + * Create a new Element from a tag and a base URI. + * + * @param tag element tag + * @param baseUri the base URI of this element. It is acceptable for the base URI to be an empty + * string, but not null. + * @see Tag#valueOf(String) + */ + public Element(Tag tag, String baseUri) { + this(tag, baseUri, new Attributes()); + } + + @Override + public String nodeName() { + return tag.getName(); + } + + /** + * Get the name of the tag for this element. E.g. {@code div} + * + * @return the tag name + */ + public String tagName() { + return tag.getName(); + } + + /** + * Change the tag of this element. For example, convert a {@code <span>} to a {@code <div>} with + * {@code el.tagName("div");}. + * + * @param tagName new tag name for this element + * @return this element, for chaining + */ + public Element tagName(String tagName) { + Validate.notEmpty(tagName, "Tag name must not be empty."); + tag = Tag.valueOf(tagName); + return this; + } + + /** + * Get the Tag for this element. + * + * @return the tag object + */ + public Tag tag() { + return tag; + } + + /** + * Test if this element is a block-level element. (E.g. {@code <div> == true} or an inline element + * {@code <p> == false}). + * + * @return true if block, false if not (and thus inline) + */ + public boolean isBlock() { + return tag.isBlock(); + } + + /** + * Get the {@code id} attribute of this element. + * + * @return The id attribute, if present, or an empty string if not. + */ + public String id() { + String id = attr("id"); + return id == null ? "" : id; + } + + /** + * Set an attribute value on this element. If this element already has an attribute with the + * key, its value is updated; otherwise, a new attribute is added. + * + * @return this element + */ + public Element attr(String attributeKey, String attributeValue) { + super.attr(attributeKey, attributeValue); + return this; + } + + /** + * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key + * starting with "data-" is included the dataset. + * <p> + * E.g., the element {@code <div data-package="jsoup" data-language="Java" class="group">...} has the dataset + * {@code package=jsoup, language=java}. + * <p> + * This map is a filtered view of the element's attribute map. Changes to one map (add, remove, update) are reflected + * in the other map. + * <p> + * You can find elements that have data attributes using the {@code [^data-]} attribute key prefix selector. + * @return a map of {@code key=value} custom data attributes. + */ + public Map<String, String> dataset() { + return attributes.dataset(); + } + + @Override + public final Element parent() { + return (Element) parentNode; + } + + /** + * Get this element's parent and ancestors, up to the document root. + * @return this element's stack of parents, closest first. + */ + public Elements parents() { + Elements parents = new Elements(); + accumulateParents(this, parents); + return parents; + } + + private static void accumulateParents(Element el, Elements parents) { + Element parent = el.parent(); + if (parent != null && !parent.tagName().equals("#root")) { + parents.add(parent); + accumulateParents(parent, parents); + } + } + + /** + * Get a child element of this element, by its 0-based index number. + * <p/> + * Note that an element can have both mixed Nodes and Elements as children. This method inspects + * a filtered list of children that are elements, and the index is based on that filtered list. + * + * @param index the index number of the element to retrieve + * @return the child element, if it exists, or {@code null} if absent. + * @see #childNode(int) + */ + public Element child(int index) { + return children().get(index); + } + + /** + * Get this element's child elements. + * <p/> + * This is effectively a filter on {@link #childNodes()} to get Element nodes. + * @return child elements. If this element has no children, returns an + * empty list. + * @see #childNodes() + */ + public Elements children() { + // create on the fly rather than maintaining two lists. if gets slow, memoize, and mark dirty on change + List<Element> elements = new ArrayList<Element>(); + for (Node node : childNodes) { + if (node instanceof Element) + elements.add((Element) node); + } + return new Elements(elements); + } + + /** + * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated. + * <p/> + * This is effectively a filter on {@link #childNodes()} to get Text nodes. + * @return child text nodes. If this element has no text nodes, returns an + * empty list. + * <p/> + * For example, with the input HTML: {@code <p>One <span>Two</span> Three <br> Four</p>} with the {@code p} element selected: + * <ul> + * <li>{@code p.text()} = {@code "One Two Three Four"}</li> + * <li>{@code p.ownText()} = {@code "One Three Four"}</li> + * <li>{@code p.children()} = {@code Elements[<span>, <br>]}</li> + * <li>{@code p.childNodes()} = {@code List<Node>["One ", <span>, " Three ", <br>, " Four"]}</li> + * <li>{@code p.textNodes()} = {@code List<TextNode>["One ", " Three ", " Four"]}</li> + * </ul> + */ + public List<TextNode> textNodes() { + List<TextNode> textNodes = new ArrayList<TextNode>(); + for (Node node : childNodes) { + if (node instanceof TextNode) + textNodes.add((TextNode) node); + } + return Collections.unmodifiableList(textNodes); + } + + /** + * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated. + * <p/> + * This is effectively a filter on {@link #childNodes()} to get Data nodes. + * @return child data nodes. If this element has no data nodes, returns an + * empty list. + * @see #data() + */ + public List<DataNode> dataNodes() { + List<DataNode> dataNodes = new ArrayList<DataNode>(); + for (Node node : childNodes) { + if (node instanceof DataNode) + dataNodes.add((DataNode) node); + } + return Collections.unmodifiableList(dataNodes); + } + + /** + * Find elements that match the {@link Selector} CSS query, with this element as the starting context. Matched elements + * may include this element, or any of its children. + * <p/> + * This method is generally more powerful to use than the DOM-type {@code getElementBy*} methods, because + * multiple filters can be combined, e.g.: + * <ul> + * <li>{@code el.select("a[href]")} - finds links ({@code a} tags with {@code href} attributes) + * <li>{@code el.select("a[href*=example.com]")} - finds links pointing to example.com (loosely) + * </ul> + * <p/> + * See the query syntax documentation in {@link org.jsoup.select.Selector}. + * + * @param cssQuery a {@link Selector} CSS-like query + * @return elements that match the query (empty if none match) + * @see org.jsoup.select.Selector + */ + public Elements select(String cssQuery) { + return Selector.select(cssQuery, this); + } + + /** + * Add a node child node to this element. + * + * @param child node to add. Must not already have a parent. + * @return this element, so that you can add more child nodes or elements. + */ + public Element appendChild(Node child) { + Validate.notNull(child); + + addChildren(child); + return this; + } + + /** + * Add a node to the start of this element's children. + * + * @param child node to add. Must not already have a parent. + * @return this element, so that you can add more child nodes or elements. + */ + public Element prependChild(Node child) { + Validate.notNull(child); + + addChildren(0, child); + return this; + } + + /** + * Create a new element by tag name, and add it as the last child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.appendElement("h1").attr("id", "header").text("Welcome");} + */ + public Element appendElement(String tagName) { + Element child = new Element(Tag.valueOf(tagName), baseUri()); + appendChild(child); + return child; + } + + /** + * Create a new element by tag name, and add it as the first child. + * + * @param tagName the name of the tag (e.g. {@code div}). + * @return the new element, to allow you to add content to it, e.g.: + * {@code parent.prependElement("h1").attr("id", "header").text("Welcome");} + */ + public Element prependElement(String tagName) { + Element child = new Element(Tag.valueOf(tagName), baseUri()); + prependChild(child); + return child; + } + + /** + * Create and append a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + public Element appendText(String text) { + TextNode node = new TextNode(text, baseUri()); + appendChild(node); + return this; + } + + /** + * Create and prepend a new TextNode to this element. + * + * @param text the unencoded text to add + * @return this element + */ + public Element prependText(String text) { + TextNode node = new TextNode(text, baseUri()); + prependChild(node); + return this; + } + + /** + * Add inner HTML to this element. The supplied HTML will be parsed, and each node appended to the end of the children. + * @param html HTML to add inside this element, after the existing HTML + * @return this element + * @see #html(String) + */ + public Element append(String html) { + Validate.notNull(html); + + List<Node> nodes = Parser.parseFragment(html, this, baseUri()); + addChildren(nodes.toArray(new Node[nodes.size()])); + return this; + } + + /** + * Add inner HTML into this element. The supplied HTML will be parsed, and each node prepended to the start of the element's children. + * @param html HTML to add inside this element, before the existing HTML + * @return this element + * @see #html(String) + */ + public Element prepend(String html) { + Validate.notNull(html); + + List<Node> nodes = Parser.parseFragment(html, this, baseUri()); + addChildren(0, nodes.toArray(new Node[nodes.size()])); + return this; + } + + /** + * Insert the specified HTML into the DOM before this element (i.e. as a preceding sibling). + * + * @param html HTML to add before this element + * @return this element, for chaining + * @see #after(String) + */ + @Override + public Element before(String html) { + return (Element) super.before(html); + } + + /** + * Insert the specified node into the DOM before this node (i.e. as a preceding sibling). + * @param node to add before this element + * @return this Element, for chaining + * @see #after(Node) + */ + @Override + public Element before(Node node) { + return (Element) super.before(node); + } + + /** + * Insert the specified HTML into the DOM after this element (i.e. as a following sibling). + * + * @param html HTML to add after this element + * @return this element, for chaining + * @see #before(String) + */ + @Override + public Element after(String html) { + return (Element) super.after(html); + } + + /** + * Insert the specified node into the DOM after this node (i.e. as a following sibling). + * @param node to add after this element + * @return this element, for chaining + * @see #before(Node) + */ + @Override + public Element after(Node node) { + return (Element) super.after(node); + } + + /** + * Remove all of the element's child nodes. Any attributes are left as-is. + * @return this element + */ + public Element empty() { + childNodes.clear(); + return this; + } + + /** + * Wrap the supplied HTML around this element. + * + * @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. + * @return this element, for chaining. + */ + @Override + public Element wrap(String html) { + return (Element) super.wrap(html); + } + + /** + * Get sibling elements. If the element has no sibling elements, returns an empty list. An element is not a sibling + * of itself, so will not be included in the returned list. + * @return sibling elements + */ + public Elements siblingElements() { + if (parentNode == null) + return new Elements(0); + + List<Element> elements = parent().children(); + Elements siblings = new Elements(elements.size() - 1); + for (Element el: elements) + if (el != this) + siblings.add(el); + return siblings; + } + + /** + * Gets the next sibling element of this element. E.g., if a {@code div} contains two {@code p}s, + * the {@code nextElementSibling} of the first {@code p} is the second {@code p}. + * <p/> + * This is similar to {@link #nextSibling()}, but specifically finds only Elements + * @return the next element, or null if there is no next element + * @see #previousElementSibling() + */ + public Element nextElementSibling() { + if (parentNode == null) return null; + List<Element> siblings = parent().children(); + Integer index = indexInList(this, siblings); + Validate.notNull(index); + if (siblings.size() > index+1) + return siblings.get(index+1); + else + return null; + } + + /** + * Gets the previous element sibling of this element. + * @return the previous element, or null if there is no previous element + * @see #nextElementSibling() + */ + public Element previousElementSibling() { + if (parentNode == null) return null; + List<Element> siblings = parent().children(); + Integer index = indexInList(this, siblings); + Validate.notNull(index); + if (index > 0) + return siblings.get(index-1); + else + return null; + } + + /** + * Gets the first element sibling of this element. + * @return the first sibling that is an element (aka the parent's first element child) + */ + public Element firstElementSibling() { + // todo: should firstSibling() exclude this? + List<Element> siblings = parent().children(); + return siblings.size() > 1 ? siblings.get(0) : null; + } + + /** + * Get the list index of this element in its element sibling list. I.e. if this is the first element + * sibling, returns 0. + * @return position in element sibling list + */ + public Integer elementSiblingIndex() { + if (parent() == null) return 0; + return indexInList(this, parent().children()); + } + + /** + * Gets the last element sibling of this element + * @return the last sibling that is an element (aka the parent's last element child) + */ + public Element lastElementSibling() { + List<Element> siblings = parent().children(); + return siblings.size() > 1 ? siblings.get(siblings.size() - 1) : null; + } + + private static <E extends Element> Integer indexInList(Element search, List<E> elements) { + Validate.notNull(search); + Validate.notNull(elements); + + for (int i = 0; i < elements.size(); i++) { + E element = elements.get(i); + if (element.equals(search)) + return i; + } + return null; + } + + // DOM type methods + + /** + * Finds elements, including and recursively under this element, with the specified tag name. + * @param tagName The tag name to search for (case insensitively). + * @return a matching unmodifiable list of elements. Will be empty if this element and none of its children match. + */ + public Elements getElementsByTag(String tagName) { + Validate.notEmpty(tagName); + tagName = tagName.toLowerCase().trim(); + + return Collector.collect(new Evaluator.Tag(tagName), this); + } + + /** + * Find an element by ID, including or under this element. + * <p> + * Note that this finds the first matching ID, starting with this element. If you search down from a different + * starting point, it is possible to find a different element by ID. For unique element by ID within a Document, + * use {@link Document#getElementById(String)} + * @param id The ID to search for. + * @return The first matching element by ID, starting with this element, or null if none found. + */ + public Element getElementById(String id) { + Validate.notEmpty(id); + + Elements elements = Collector.collect(new Evaluator.Id(id), this); + if (elements.size() > 0) + return elements.get(0); + else + return null; + } + + /** + * Find elements that have this class, including or under this element. Case insensitive. + * <p> + * Elements can have multiple classes (e.g. {@code <div class="header round first">}. This method + * checks each class, so you can find the above with {@code el.getElementsByClass("header");}. + * + * @param className the name of the class to search for. + * @return elements with the supplied class name, empty if none + * @see #hasClass(String) + * @see #classNames() + */ + public Elements getElementsByClass(String className) { + Validate.notEmpty(className); + + return Collector.collect(new Evaluator.Class(className), this); + } + + /** + * Find elements that have a named attribute set. Case insensitive. + * + * @param key name of the attribute, e.g. {@code href} + * @return elements that have this attribute, empty if none + */ + public Elements getElementsByAttribute(String key) { + Validate.notEmpty(key); + key = key.trim().toLowerCase(); + + return Collector.collect(new Evaluator.Attribute(key), this); + } + + /** + * Find elements that have an attribute name starting with the supplied prefix. Use {@code data-} to find elements + * that have HTML5 datasets. + * @param keyPrefix name prefix of the attribute e.g. {@code data-} + * @return elements that have attribute names that start with with the prefix, empty if none. + */ + public Elements getElementsByAttributeStarting(String keyPrefix) { + Validate.notEmpty(keyPrefix); + keyPrefix = keyPrefix.trim().toLowerCase(); + + return Collector.collect(new Evaluator.AttributeStarting(keyPrefix), this); + } + + /** + * Find elements that have an attribute with the specific value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that have this attribute with this value, empty if none + */ + public Elements getElementsByAttributeValue(String key, String value) { + return Collector.collect(new Evaluator.AttributeWithValue(key, value), this); + } + + /** + * Find elements that either do not have this attribute, or have it with a different value. Case insensitive. + * + * @param key name of the attribute + * @param value value of the attribute + * @return elements that do not have a matching attribute + */ + public Elements getElementsByAttributeValueNot(String key, String value) { + return Collector.collect(new Evaluator.AttributeWithValueNot(key, value), this); + } + + /** + * Find elements that have attributes that start with the value prefix. Case insensitive. + * + * @param key name of the attribute + * @param valuePrefix start of attribute value + * @return elements that have attributes that start with the value prefix + */ + public Elements getElementsByAttributeValueStarting(String key, String valuePrefix) { + return Collector.collect(new Evaluator.AttributeWithValueStarting(key, valuePrefix), this); + } + + /** + * Find elements that have attributes that end with the value suffix. Case insensitive. + * + * @param key name of the attribute + * @param valueSuffix end of the attribute value + * @return elements that have attributes that end with the value suffix + */ + public Elements getElementsByAttributeValueEnding(String key, String valueSuffix) { + return Collector.collect(new Evaluator.AttributeWithValueEnding(key, valueSuffix), this); + } + + /** + * Find elements that have attributes whose value contains the match string. Case insensitive. + * + * @param key name of the attribute + * @param match substring of value to search for + * @return elements that have attributes containing this text + */ + public Elements getElementsByAttributeValueContaining(String key, String match) { + return Collector.collect(new Evaluator.AttributeWithValueContaining(key, match), this); + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param pattern compiled regular expression to match against attribute values + * @return elements that have attributes matching this regular expression + */ + public Elements getElementsByAttributeValueMatching(String key, Pattern pattern) { + return Collector.collect(new Evaluator.AttributeWithValueMatching(key, pattern), this); + + } + + /** + * Find elements that have attributes whose values match the supplied regular expression. + * @param key name of the attribute + * @param regex regular expression to match against attribute values. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options. + * @return elements that have attributes matching this regular expression + */ + public Elements getElementsByAttributeValueMatching(String key, String regex) { + Pattern pattern; + try { + pattern = Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Pattern syntax error: " + regex, e); + } + return getElementsByAttributeValueMatching(key, pattern); + } + + /** + * Find elements whose sibling index is less than the supplied index. + * @param index 0-based index + * @return elements less than index + */ + public Elements getElementsByIndexLessThan(int index) { + return Collector.collect(new Evaluator.IndexLessThan(index), this); + } + + /** + * Find elements whose sibling index is greater than the supplied index. + * @param index 0-based index + * @return elements greater than index + */ + public Elements getElementsByIndexGreaterThan(int index) { + return Collector.collect(new Evaluator.IndexGreaterThan(index), this); + } + + /** + * Find elements whose sibling index is equal to the supplied index. + * @param index 0-based index + * @return elements equal to index + */ + public Elements getElementsByIndexEquals(int index) { + return Collector.collect(new Evaluator.IndexEquals(index), this); + } + + /** + * Find elements that contain the specified string. The search is case insensitive. The text may appear directly + * in the element, or in any of its descendants. + * @param searchText to look for in the element's text + * @return elements that contain the string, case insensitive. + * @see Element#text() + */ + public Elements getElementsContainingText(String searchText) { + return Collector.collect(new Evaluator.ContainsText(searchText), this); + } + + /** + * Find elements that directly contain the specified string. The search is case insensitive. The text must appear directly + * in the element, not in any of its descendants. + * @param searchText to look for in the element's own text + * @return elements that contain the string, case insensitive. + * @see Element#ownText() + */ + public Elements getElementsContainingOwnText(String searchText) { + return Collector.collect(new Evaluator.ContainsOwnText(searchText), this); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see Element#text() + */ + public Elements getElementsMatchingText(Pattern pattern) { + return Collector.collect(new Evaluator.Matches(pattern), this); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see Element#text() + */ + public Elements getElementsMatchingText(String regex) { + Pattern pattern; + try { + pattern = Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Pattern syntax error: " + regex, e); + } + return getElementsMatchingText(pattern); + } + + /** + * Find elements whose own text matches the supplied regular expression. + * @param pattern regular expression to match text against + * @return elements matching the supplied regular expression. + * @see Element#ownText() + */ + public Elements getElementsMatchingOwnText(Pattern pattern) { + return Collector.collect(new Evaluator.MatchesOwn(pattern), this); + } + + /** + * Find elements whose text matches the supplied regular expression. + * @param regex regular expression to match text against. You can use <a href="http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded">embedded flags</a> (such as (?i) and (?m) to control regex options. + * @return elements matching the supplied regular expression. + * @see Element#ownText() + */ + public Elements getElementsMatchingOwnText(String regex) { + Pattern pattern; + try { + pattern = Pattern.compile(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException("Pattern syntax error: " + regex, e); + } + return getElementsMatchingOwnText(pattern); + } + + /** + * Find all elements under this element (including self, and children of children). + * + * @return all elements + */ + public Elements getAllElements() { + return Collector.collect(new Evaluator.AllElements(), this); + } + + /** + * Gets the combined text of this element and all its children. + * <p> + * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.text()} returns {@code "Hello there now!"} + * + * @return unencoded text, or empty string if none. + * @see #ownText() + * @see #textNodes() + */ + public String text() { + StringBuilder sb = new StringBuilder(); + text(sb); + return sb.toString().trim(); + } + + private void text(StringBuilder accum) { + appendWhitespaceIfBr(this, accum); + + for (Node child : childNodes) { + if (child instanceof TextNode) { + TextNode textNode = (TextNode) child; + appendNormalisedText(accum, textNode); + } else if (child instanceof Element) { + Element element = (Element) child; + if (accum.length() > 0 && element.isBlock() && !TextNode.lastCharIsWhitespace(accum)) + accum.append(" "); + element.text(accum); + } + } + } + + /** + * Gets the text owned by this element only; does not get the combined text of all children. + * <p> + * For example, given HTML {@code <p>Hello <b>there</b> now!</p>}, {@code p.ownText()} returns {@code "Hello now!"}, + * whereas {@code p.text()} returns {@code "Hello there now!"}. + * Note that the text within the {@code b} element is not returned, as it is not a direct child of the {@code p} element. + * + * @return unencoded text, or empty string if none. + * @see #text() + * @see #textNodes() + */ + public String ownText() { + StringBuilder sb = new StringBuilder(); + ownText(sb); + return sb.toString().trim(); + } + + private void ownText(StringBuilder accum) { + for (Node child : childNodes) { + if (child instanceof TextNode) { + TextNode textNode = (TextNode) child; + appendNormalisedText(accum, textNode); + } else if (child instanceof Element) { + appendWhitespaceIfBr((Element) child, accum); + } + } + } + + private void appendNormalisedText(StringBuilder accum, TextNode textNode) { + String text = textNode.getWholeText(); + + if (!preserveWhitespace()) { + text = TextNode.normaliseWhitespace(text); + if (TextNode.lastCharIsWhitespace(accum)) + text = TextNode.stripLeadingWhitespace(text); + } + accum.append(text); + } + + private static void appendWhitespaceIfBr(Element element, StringBuilder accum) { + if (element.tag.getName().equals("br") && !TextNode.lastCharIsWhitespace(accum)) + accum.append(" "); + } + + boolean preserveWhitespace() { + return tag.preserveWhitespace() || parent() != null && parent().preserveWhitespace(); + } + + /** + * Set the text of this element. Any existing contents (text or elements) will be cleared + * @param text unencoded text + * @return this element + */ + public Element text(String text) { + Validate.notNull(text); + + empty(); + TextNode textNode = new TextNode(text, baseUri); + appendChild(textNode); + + return this; + } + + /** + Test if this element has any text content (that is not just whitespace). + @return true if element has non-blank text content. + */ + public boolean hasText() { + for (Node child: childNodes) { + if (child instanceof TextNode) { + TextNode textNode = (TextNode) child; + if (!textNode.isBlank()) + return true; + } else if (child instanceof Element) { + Element el = (Element) child; + if (el.hasText()) + return true; + } + } + return false; + } + + /** + * Get the combined data of this element. Data is e.g. the inside of a {@code script} tag. + * @return the data, or empty string if none + * + * @see #dataNodes() + */ + public String data() { + StringBuilder sb = new StringBuilder(); + + for (Node childNode : childNodes) { + if (childNode instanceof DataNode) { + DataNode data = (DataNode) childNode; + sb.append(data.getWholeData()); + } else if (childNode instanceof Element) { + Element element = (Element) childNode; + String elementData = element.data(); + sb.append(elementData); + } + } + return sb.toString(); + } + + /** + * Gets the literal value of this element's "class" attribute, which may include multiple class names, space + * separated. (E.g. on <code><div class="header gray"></code> returns, "<code>header gray</code>") + * @return The literal class attribute, or <b>empty string</b> if no class attribute set. + */ + public String className() { + return attr("class"); + } + + /** + * Get all of the element's class names. E.g. on element {@code <div class="header gray"}>}, + * returns a set of two elements {@code "header", "gray"}. Note that modifications to this set are not pushed to + * the backing {@code class} attribute; use the {@link #classNames(java.util.Set)} method to persist them. + * @return set of classnames, empty if no class attribute + */ + public Set<String> classNames() { + if (classNames == null) { + String[] names = className().split("\\s+"); + classNames = new LinkedHashSet<String>(Arrays.asList(names)); + } + return classNames; + } + + /** + Set the element's {@code class} attribute to the supplied class names. + @param classNames set of classes + @return this element, for chaining + */ + public Element classNames(Set<String> classNames) { + Validate.notNull(classNames); + attributes.put("class", StringUtil.join(classNames, " ")); + return this; + } + + /** + * Tests if this element has a class. Case insensitive. + * @param className name of class to check for + * @return true if it does, false if not + */ + public boolean hasClass(String className) { + Set<String> classNames = classNames(); + for (String name : classNames) { + if (className.equalsIgnoreCase(name)) + return true; + } + return false; + } + + /** + Add a class name to this element's {@code class} attribute. + @param className class name to add + @return this element + */ + public Element addClass(String className) { + Validate.notNull(className); + + Set<String> classes = classNames(); + classes.add(className); + classNames(classes); + + return this; + } + + /** + Remove a class name from this element's {@code class} attribute. + @param className class name to remove + @return this element + */ + public Element removeClass(String className) { + Validate.notNull(className); + + Set<String> classes = classNames(); + classes.remove(className); + classNames(classes); + + return this; + } + + /** + Toggle a class name on this element's {@code class} attribute: if present, remove it; otherwise add it. + @param className class name to toggle + @return this element + */ + public Element toggleClass(String className) { + Validate.notNull(className); + + Set<String> classes = classNames(); + if (classes.contains(className)) + classes.remove(className); + else + classes.add(className); + classNames(classes); + + return this; + } + + /** + * Get the value of a form element (input, textarea, etc). + * @return the value of the form element, or empty string if not set. + */ + public String val() { + if (tagName().equals("textarea")) + return text(); + else + return attr("value"); + } + + /** + * Set the value of a form element (input, textarea, etc). + * @param value value to set + * @return this element (for chaining) + */ + public Element val(String value) { + if (tagName().equals("textarea")) + text(value); + else + attr("value", value); + return this; + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + if (accum.length() > 0 && out.prettyPrint() && (tag.formatAsBlock() || (parent() != null && parent().tag().formatAsBlock()))) + indent(accum, depth, out); + accum + .append("<") + .append(tagName()); + attributes.html(accum, out); + + if (childNodes.isEmpty() && tag.isSelfClosing()) + accum.append(" />"); + else + accum.append(">"); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) { + if (!(childNodes.isEmpty() && tag.isSelfClosing())) { + if (out.prettyPrint() && !childNodes.isEmpty() && tag.formatAsBlock()) + indent(accum, depth, out); + accum.append("</").append(tagName()).append(">"); + } + } + + /** + * Retrieves the element's inner HTML. E.g. on a {@code <div>} with one empty {@code <p>}, would return + * {@code <p></p>}. (Whereas {@link #outerHtml()} would return {@code <div><p></p></div>}.) + * + * @return String of HTML. + * @see #outerHtml() + */ + public String html() { + StringBuilder accum = new StringBuilder(); + html(accum); + return accum.toString().trim(); + } + + private void html(StringBuilder accum) { + for (Node node : childNodes) + node.outerHtml(accum); + } + + /** + * Set this element's inner HTML. Clears the existing HTML first. + * @param html HTML to parse and set into this element + * @return this element + * @see #append(String) + */ + public Element html(String html) { + empty(); + append(html); + return this; + } + + public String toString() { + return outerHtml(); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + // todo: fixup, not very useful + int result = super.hashCode(); + result = 31 * result + (tag != null ? tag.hashCode() : 0); + return result; + } + + @Override + public Element clone() { + Element clone = (Element) super.clone(); + clone.classNames(); // creates linked set of class names from class attribute + return clone; + } +} diff --git a/server/src/org/jsoup/nodes/Entities.java b/server/src/org/jsoup/nodes/Entities.java new file mode 100644 index 0000000000..0ae83e1fc0 --- /dev/null +++ b/server/src/org/jsoup/nodes/Entities.java @@ -0,0 +1,184 @@ +package org.jsoup.nodes; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.CharsetEncoder; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HTML entities, and escape routines. + * Source: <a href="http://www.w3.org/TR/html5/named-character-references.html#named-character-references">W3C HTML + * named character references</a>. + */ +public class Entities { + public enum EscapeMode { + /** Restricted entities suitable for XHTML output: lt, gt, amp, apos, and quot only. */ + xhtml(xhtmlByVal), + /** Default HTML output entities. */ + base(baseByVal), + /** Complete HTML entities. */ + extended(fullByVal); + + private Map<Character, String> map; + + EscapeMode(Map<Character, String> map) { + this.map = map; + } + + public Map<Character, String> getMap() { + return map; + } + } + + private static final Map<String, Character> full; + private static final Map<Character, String> xhtmlByVal; + private static final Map<Character, String> baseByVal; + private static final Map<Character, String> fullByVal; + private static final Pattern unescapePattern = Pattern.compile("&(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]+\\d*);?"); + private static final Pattern strictUnescapePattern = Pattern.compile("&(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]+\\d*);"); + + private Entities() {} + + /** + * Check if the input is a known named entity + * @param name the possible entity name (e.g. "lt" or "amp" + * @return true if a known named entity + */ + public static boolean isNamedEntity(String name) { + return full.containsKey(name); + } + + /** + * Get the Character value of the named entity + * @param name named entity (e.g. "lt" or "amp") + * @return the Character value of the named entity (e.g. '<' or '&') + */ + public static Character getCharacterByName(String name) { + return full.get(name); + } + + static String escape(String string, Document.OutputSettings out) { + return escape(string, out.encoder(), out.escapeMode()); + } + + static String escape(String string, CharsetEncoder encoder, EscapeMode escapeMode) { + StringBuilder accum = new StringBuilder(string.length() * 2); + Map<Character, String> map = escapeMode.getMap(); + + for (int pos = 0; pos < string.length(); pos++) { + Character c = string.charAt(pos); + if (map.containsKey(c)) + accum.append('&').append(map.get(c)).append(';'); + else if (encoder.canEncode(c)) + accum.append(c.charValue()); + else + accum.append("&#").append((int) c).append(';'); + } + + return accum.toString(); + } + + static String unescape(String string) { + return unescape(string, false); + } + + /** + * Unescape the input string. + * @param string + * @param strict if "strict" (that is, requires trailing ';' char, otherwise that's optional) + * @return + */ + static String unescape(String string, boolean strict) { + // todo: change this method to use Tokeniser.consumeCharacterReference + if (!string.contains("&")) + return string; + + Matcher m = strict? strictUnescapePattern.matcher(string) : unescapePattern.matcher(string); // &(#(x|X)?([0-9a-fA-F]+)|[a-zA-Z]\\d*);? + StringBuffer accum = new StringBuffer(string.length()); // pity matcher can't use stringbuilder, avoid syncs + // todo: replace m.appendReplacement with own impl, so StringBuilder and quoteReplacement not required + + while (m.find()) { + int charval = -1; + String num = m.group(3); + if (num != null) { + try { + int base = m.group(2) != null ? 16 : 10; // 2 is hex indicator + charval = Integer.valueOf(num, base); + } catch (NumberFormatException e) { + } // skip + } else { + String name = m.group(1); + if (full.containsKey(name)) + charval = full.get(name); + } + + if (charval != -1 || charval > 0xFFFF) { // out of range + String c = Character.toString((char) charval); + m.appendReplacement(accum, Matcher.quoteReplacement(c)); + } else { + m.appendReplacement(accum, Matcher.quoteReplacement(m.group(0))); // replace with original string + } + } + m.appendTail(accum); + return accum.toString(); + } + + // xhtml has restricted entities + private static final Object[][] xhtmlArray = { + {"quot", 0x00022}, + {"amp", 0x00026}, + {"apos", 0x00027}, + {"lt", 0x0003C}, + {"gt", 0x0003E} + }; + + static { + xhtmlByVal = new HashMap<Character, String>(); + baseByVal = toCharacterKey(loadEntities("entities-base.properties")); // most common / default + full = loadEntities("entities-full.properties"); // extended and overblown. + fullByVal = toCharacterKey(full); + + for (Object[] entity : xhtmlArray) { + Character c = Character.valueOf((char) ((Integer) entity[1]).intValue()); + xhtmlByVal.put(c, ((String) entity[0])); + } + } + + private static Map<String, Character> loadEntities(String filename) { + Properties properties = new Properties(); + Map<String, Character> entities = new HashMap<String, Character>(); + try { + InputStream in = Entities.class.getResourceAsStream(filename); + properties.load(in); + in.close(); + } catch (IOException e) { + throw new MissingResourceException("Error loading entities resource: " + e.getMessage(), "Entities", filename); + } + + for (Map.Entry entry: properties.entrySet()) { + Character val = Character.valueOf((char) Integer.parseInt((String) entry.getValue(), 16)); + String name = (String) entry.getKey(); + entities.put(name, val); + } + return entities; + } + + private static Map<Character, String> toCharacterKey(Map<String, Character> inMap) { + Map<Character, String> outMap = new HashMap<Character, String>(); + for (Map.Entry<String, Character> entry: inMap.entrySet()) { + Character character = entry.getValue(); + String name = entry.getKey(); + + if (outMap.containsKey(character)) { + // dupe, prefer the lower case version + if (name.toLowerCase().equals(name)) + outMap.put(character, name); + } else { + outMap.put(character, name); + } + } + return outMap; + } +} diff --git a/server/src/org/jsoup/nodes/Node.java b/server/src/org/jsoup/nodes/Node.java new file mode 100644 index 0000000000..eb2b40ee73 --- /dev/null +++ b/server/src/org/jsoup/nodes/Node.java @@ -0,0 +1,615 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; +import org.jsoup.parser.Parser; +import org.jsoup.select.NodeTraversor; +import org.jsoup.select.NodeVisitor; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + The base, abstract Node model. Elements, Documents, Comments etc are all Node instances. + + @author Jonathan Hedley, jonathan@hedley.net */ +public abstract class Node implements Cloneable { + Node parentNode; + List<Node> childNodes; + Attributes attributes; + String baseUri; + int siblingIndex; + + /** + Create a new Node. + @param baseUri base URI + @param attributes attributes (not null, but may be empty) + */ + protected Node(String baseUri, Attributes attributes) { + Validate.notNull(baseUri); + Validate.notNull(attributes); + + childNodes = new ArrayList<Node>(4); + this.baseUri = baseUri.trim(); + this.attributes = attributes; + } + + protected Node(String baseUri) { + this(baseUri, new Attributes()); + } + + /** + * Default constructor. Doesn't setup base uri, children, or attributes; use with caution. + */ + protected Node() { + childNodes = Collections.emptyList(); + attributes = null; + } + + /** + Get the node name of this node. Use for debugging purposes and not logic switching (for that, use instanceof). + @return node name + */ + public abstract String nodeName(); + + /** + * Get an attribute's value by its key. + * <p/> + * To get an absolute URL from an attribute that may be a relative URL, prefix the key with <code><b>abs</b></code>, + * which is a shortcut to the {@link #absUrl} method. + * E.g.: <blockquote><code>String url = a.attr("abs:href");</code></blockquote> + * @param attributeKey The attribute key. + * @return The attribute, or empty string if not present (to avoid nulls). + * @see #attributes() + * @see #hasAttr(String) + * @see #absUrl(String) + */ + public String attr(String attributeKey) { + Validate.notNull(attributeKey); + + if (attributes.hasKey(attributeKey)) + return attributes.get(attributeKey); + else if (attributeKey.toLowerCase().startsWith("abs:")) + return absUrl(attributeKey.substring("abs:".length())); + else return ""; + } + + /** + * Get all of the element's attributes. + * @return attributes (which implements iterable, in same order as presented in original HTML). + */ + public Attributes attributes() { + return attributes; + } + + /** + * Set an attribute (key=value). If the attribute already exists, it is replaced. + * @param attributeKey The attribute key. + * @param attributeValue The attribute value. + * @return this (for chaining) + */ + public Node attr(String attributeKey, String attributeValue) { + attributes.put(attributeKey, attributeValue); + return this; + } + + /** + * Test if this element has an attribute. + * @param attributeKey The attribute key to check. + * @return true if the attribute exists, false if not. + */ + public boolean hasAttr(String attributeKey) { + Validate.notNull(attributeKey); + + if (attributeKey.toLowerCase().startsWith("abs:")) { + String key = attributeKey.substring("abs:".length()); + if (attributes.hasKey(key) && !absUrl(key).equals("")) + return true; + } + return attributes.hasKey(attributeKey); + } + + /** + * Remove an attribute from this element. + * @param attributeKey The attribute to remove. + * @return this (for chaining) + */ + public Node removeAttr(String attributeKey) { + Validate.notNull(attributeKey); + attributes.remove(attributeKey); + return this; + } + + /** + Get the base URI of this node. + @return base URI + */ + public String baseUri() { + return baseUri; + } + + /** + Update the base URI of this node and all of its descendants. + @param baseUri base URI to set + */ + public void setBaseUri(final String baseUri) { + Validate.notNull(baseUri); + + traverse(new NodeVisitor() { + public void head(Node node, int depth) { + node.baseUri = baseUri; + } + + public void tail(Node node, int depth) { + } + }); + } + + /** + * Get an absolute URL from a URL attribute that may be relative (i.e. an <code><a href></code> or + * <code><img src></code>). + * <p/> + * E.g.: <code>String absUrl = linkEl.absUrl("href");</code> + * <p/> + * If the attribute value is already absolute (i.e. it starts with a protocol, like + * <code>http://</code> or <code>https://</code> etc), and it successfully parses as a URL, the attribute is + * returned directly. Otherwise, it is treated as a URL relative to the element's {@link #baseUri}, and made + * absolute using that. + * <p/> + * As an alternate, you can use the {@link #attr} method with the <code>abs:</code> prefix, e.g.: + * <code>String absUrl = linkEl.attr("abs:href");</code> + * + * @param attributeKey The attribute key + * @return An absolute URL if one could be made, or an empty string (not null) if the attribute was missing or + * could not be made successfully into a URL. + * @see #attr + * @see java.net.URL#URL(java.net.URL, String) + */ + public String absUrl(String attributeKey) { + Validate.notEmpty(attributeKey); + + String relUrl = attr(attributeKey); + if (!hasAttr(attributeKey)) { + return ""; // nothing to make absolute with + } else { + URL base; + try { + try { + base = new URL(baseUri); + } catch (MalformedURLException e) { + // the base is unsuitable, but the attribute may be abs on its own, so try that + URL abs = new URL(relUrl); + return abs.toExternalForm(); + } + // workaround: java resolves '//path/file + ?foo' to '//path/?foo', not '//path/file?foo' as desired + if (relUrl.startsWith("?")) + relUrl = base.getPath() + relUrl; + URL abs = new URL(base, relUrl); + return abs.toExternalForm(); + } catch (MalformedURLException e) { + return ""; + } + } + } + + /** + Get a child node by index + @param index index of child node + @return the child node at this index. + */ + public Node childNode(int index) { + return childNodes.get(index); + } + + /** + Get this node's children. Presented as an unmodifiable list: new children can not be added, but the child nodes + themselves can be manipulated. + @return list of children. If no children, returns an empty list. + */ + public List<Node> childNodes() { + return Collections.unmodifiableList(childNodes); + } + + protected Node[] childNodesAsArray() { + return childNodes.toArray(new Node[childNodes().size()]); + } + + /** + Gets this node's parent node. + @return parent node; or null if no parent. + */ + public Node parent() { + return parentNode; + } + + /** + * Gets the Document associated with this Node. + * @return the Document associated with this Node, or null if there is no such Document. + */ + public Document ownerDocument() { + if (this instanceof Document) + return (Document) this; + else if (parentNode == null) + return null; + else + return parentNode.ownerDocument(); + } + + /** + * Remove (delete) this node from the DOM tree. If this node has children, they are also removed. + */ + public void remove() { + Validate.notNull(parentNode); + parentNode.removeChild(this); + } + + /** + * Insert the specified HTML into the DOM before this node (i.e. as a preceding sibling). + * @param html HTML to add before this node + * @return this node, for chaining + * @see #after(String) + */ + public Node before(String html) { + addSiblingHtml(siblingIndex(), html); + return this; + } + + /** + * Insert the specified node into the DOM before this node (i.e. as a preceding sibling). + * @param node to add before this node + * @return this node, for chaining + * @see #after(Node) + */ + public Node before(Node node) { + Validate.notNull(node); + Validate.notNull(parentNode); + + parentNode.addChildren(siblingIndex(), node); + return this; + } + + /** + * Insert the specified HTML into the DOM after this node (i.e. as a following sibling). + * @param html HTML to add after this node + * @return this node, for chaining + * @see #before(String) + */ + public Node after(String html) { + addSiblingHtml(siblingIndex()+1, html); + return this; + } + + /** + * Insert the specified node into the DOM after this node (i.e. as a following sibling). + * @param node to add after this node + * @return this node, for chaining + * @see #before(Node) + */ + public Node after(Node node) { + Validate.notNull(node); + Validate.notNull(parentNode); + + parentNode.addChildren(siblingIndex()+1, node); + return this; + } + + private void addSiblingHtml(int index, String html) { + Validate.notNull(html); + Validate.notNull(parentNode); + + Element context = parent() instanceof Element ? (Element) parent() : null; + List<Node> nodes = Parser.parseFragment(html, context, baseUri()); + parentNode.addChildren(index, nodes.toArray(new Node[nodes.size()])); + } + + /** + Wrap the supplied HTML around this node. + @param html HTML to wrap around this element, e.g. {@code <div class="head"></div>}. Can be arbitrarily deep. + @return this node, for chaining. + */ + public Node wrap(String html) { + Validate.notEmpty(html); + + Element context = parent() instanceof Element ? (Element) parent() : null; + List<Node> wrapChildren = Parser.parseFragment(html, context, baseUri()); + Node wrapNode = wrapChildren.get(0); + if (wrapNode == null || !(wrapNode instanceof Element)) // nothing to wrap with; noop + return null; + + Element wrap = (Element) wrapNode; + Element deepest = getDeepChild(wrap); + parentNode.replaceChild(this, wrap); + deepest.addChildren(this); + + // remainder (unbalanced wrap, like <div></div><p></p> -- The <p> is remainder + if (wrapChildren.size() > 0) { + for (int i = 0; i < wrapChildren.size(); i++) { + Node remainder = wrapChildren.get(i); + remainder.parentNode.removeChild(remainder); + wrap.appendChild(remainder); + } + } + return this; + } + + /** + * Removes this node from the DOM, and moves its children up into the node's parent. This has the effect of dropping + * the node but keeping its children. + * <p/> + * For example, with the input html:<br/> + * {@code <div>One <span>Two <b>Three</b></span></div>}<br/> + * Calling {@code element.unwrap()} on the {@code span} element will result in the html:<br/> + * {@code <div>One Two <b>Three</b></div>}<br/> + * and the {@code "Two "} {@link TextNode} being returned. + * @return the first child of this node, after the node has been unwrapped. Null if the node had no children. + * @see #remove() + * @see #wrap(String) + */ + public Node unwrap() { + Validate.notNull(parentNode); + + int index = siblingIndex; + Node firstChild = childNodes.size() > 0 ? childNodes.get(0) : null; + parentNode.addChildren(index, this.childNodesAsArray()); + this.remove(); + + return firstChild; + } + + private Element getDeepChild(Element el) { + List<Element> children = el.children(); + if (children.size() > 0) + return getDeepChild(children.get(0)); + else + return el; + } + + /** + * Replace this node in the DOM with the supplied node. + * @param in the node that will will replace the existing node. + */ + public void replaceWith(Node in) { + Validate.notNull(in); + Validate.notNull(parentNode); + parentNode.replaceChild(this, in); + } + + protected void setParentNode(Node parentNode) { + if (this.parentNode != null) + this.parentNode.removeChild(this); + this.parentNode = parentNode; + } + + protected void replaceChild(Node out, Node in) { + Validate.isTrue(out.parentNode == this); + Validate.notNull(in); + if (in.parentNode != null) + in.parentNode.removeChild(in); + + Integer index = out.siblingIndex(); + childNodes.set(index, in); + in.parentNode = this; + in.setSiblingIndex(index); + out.parentNode = null; + } + + protected void removeChild(Node out) { + Validate.isTrue(out.parentNode == this); + int index = out.siblingIndex(); + childNodes.remove(index); + reindexChildren(); + out.parentNode = null; + } + + protected void addChildren(Node... children) { + //most used. short circuit addChildren(int), which hits reindex children and array copy + for (Node child: children) { + reparentChild(child); + childNodes.add(child); + child.setSiblingIndex(childNodes.size()-1); + } + } + + protected void addChildren(int index, Node... children) { + Validate.noNullElements(children); + for (int i = children.length - 1; i >= 0; i--) { + Node in = children[i]; + reparentChild(in); + childNodes.add(index, in); + } + reindexChildren(); + } + + private void reparentChild(Node child) { + if (child.parentNode != null) + child.parentNode.removeChild(child); + child.setParentNode(this); + } + + private void reindexChildren() { + for (int i = 0; i < childNodes.size(); i++) { + childNodes.get(i).setSiblingIndex(i); + } + } + + /** + Retrieves this node's sibling nodes. Similar to {@link #childNodes() node.parent.childNodes()}, but does not + include this node (a node is not a sibling of itself). + @return node siblings. If the node has no parent, returns an empty list. + */ + public List<Node> siblingNodes() { + if (parentNode == null) + return Collections.emptyList(); + + List<Node> nodes = parentNode.childNodes; + List<Node> siblings = new ArrayList<Node>(nodes.size() - 1); + for (Node node: nodes) + if (node != this) + siblings.add(node); + return siblings; + } + + /** + Get this node's next sibling. + @return next sibling, or null if this is the last sibling + */ + public Node nextSibling() { + if (parentNode == null) + return null; // root + + List<Node> siblings = parentNode.childNodes; + Integer index = siblingIndex(); + Validate.notNull(index); + if (siblings.size() > index+1) + return siblings.get(index+1); + else + return null; + } + + /** + Get this node's previous sibling. + @return the previous sibling, or null if this is the first sibling + */ + public Node previousSibling() { + if (parentNode == null) + return null; // root + + List<Node> siblings = parentNode.childNodes; + Integer index = siblingIndex(); + Validate.notNull(index); + if (index > 0) + return siblings.get(index-1); + else + return null; + } + + /** + * Get the list index of this node in its node sibling list. I.e. if this is the first node + * sibling, returns 0. + * @return position in node sibling list + * @see org.jsoup.nodes.Element#elementSiblingIndex() + */ + public int siblingIndex() { + return siblingIndex; + } + + protected void setSiblingIndex(int siblingIndex) { + this.siblingIndex = siblingIndex; + } + + /** + * Perform a depth-first traversal through this node and its descendants. + * @param nodeVisitor the visitor callbacks to perform on each node + * @return this node, for chaining + */ + public Node traverse(NodeVisitor nodeVisitor) { + Validate.notNull(nodeVisitor); + NodeTraversor traversor = new NodeTraversor(nodeVisitor); + traversor.traverse(this); + return this; + } + + /** + Get the outer HTML of this node. + @return HTML + */ + public String outerHtml() { + StringBuilder accum = new StringBuilder(128); + outerHtml(accum); + return accum.toString(); + } + + protected void outerHtml(StringBuilder accum) { + new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this); + } + + // if this node has no document (or parent), retrieve the default output settings + private Document.OutputSettings getOutputSettings() { + return ownerDocument() != null ? ownerDocument().outputSettings() : (new Document("")).outputSettings(); + } + + /** + Get the outer HTML of this node. + @param accum accumulator to place HTML into + */ + abstract void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out); + + abstract void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out); + + public String toString() { + return outerHtml(); + } + + protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) { + accum.append("\n").append(StringUtil.padding(depth * out.indentAmount())); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + // todo: have nodes hold a child index, compare against that and parent (not children) + return false; + } + + @Override + public int hashCode() { + int result = parentNode != null ? parentNode.hashCode() : 0; + // not children, or will block stack as they go back up to parent) + result = 31 * result + (attributes != null ? attributes.hashCode() : 0); + return result; + } + + /** + * Create a stand-alone, deep copy of this node, and all of its children. The cloned node will have no siblings or + * parent node. As a stand-alone object, any changes made to the clone or any of its children will not impact the + * original node. + * <p> + * The cloned node may be adopted into another Document or node structure using {@link Element#appendChild(Node)}. + * @return stand-alone cloned node + */ + @Override + public Node clone() { + return doClone(null); // splits for orphan + } + + protected Node doClone(Node parent) { + Node clone; + try { + clone = (Node) super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException(e); + } + + clone.parentNode = parent; // can be null, to create an orphan split + clone.siblingIndex = parent == null ? 0 : siblingIndex; + clone.attributes = attributes != null ? attributes.clone() : null; + clone.baseUri = baseUri; + clone.childNodes = new ArrayList<Node>(childNodes.size()); + for (Node child: childNodes) + clone.childNodes.add(child.doClone(clone)); // clone() creates orphans, doClone() keeps parent + + return clone; + } + + private static class OuterHtmlVisitor implements NodeVisitor { + private StringBuilder accum; + private Document.OutputSettings out; + + OuterHtmlVisitor(StringBuilder accum, Document.OutputSettings out) { + this.accum = accum; + this.out = out; + } + + public void head(Node node, int depth) { + node.outerHtmlHead(accum, depth, out); + } + + public void tail(Node node, int depth) { + if (!node.nodeName().equals("#text")) // saves a void hit. + node.outerHtmlTail(accum, depth, out); + } + } +} diff --git a/server/src/org/jsoup/nodes/TextNode.java b/server/src/org/jsoup/nodes/TextNode.java new file mode 100644 index 0000000000..9fd0feac8f --- /dev/null +++ b/server/src/org/jsoup/nodes/TextNode.java @@ -0,0 +1,175 @@ +package org.jsoup.nodes; + +import org.jsoup.helper.StringUtil; +import org.jsoup.helper.Validate; + +/** + A text node. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class TextNode extends Node { + /* + TextNode is a node, and so by default comes with attributes and children. The attributes are seldom used, but use + memory, and the child nodes are never used. So we don't have them, and override accessors to attributes to create + them as needed on the fly. + */ + private static final String TEXT_KEY = "text"; + String text; + + /** + Create a new TextNode representing the supplied (unencoded) text). + + @param text raw text + @param baseUri base uri + @see #createFromEncoded(String, String) + */ + public TextNode(String text, String baseUri) { + this.baseUri = baseUri; + this.text = text; + } + + public String nodeName() { + return "#text"; + } + + /** + * Get the text content of this text node. + * @return Unencoded, normalised text. + * @see TextNode#getWholeText() + */ + public String text() { + return normaliseWhitespace(getWholeText()); + } + + /** + * Set the text content of this text node. + * @param text unencoded text + * @return this, for chaining + */ + public TextNode text(String text) { + this.text = text; + if (attributes != null) + attributes.put(TEXT_KEY, text); + return this; + } + + /** + Get the (unencoded) text of this text node, including any newlines and spaces present in the original. + @return text + */ + public String getWholeText() { + return attributes == null ? text : attributes.get(TEXT_KEY); + } + + /** + Test if this text node is blank -- that is, empty or only whitespace (including newlines). + @return true if this document is empty or only whitespace, false if it contains any text content. + */ + public boolean isBlank() { + return StringUtil.isBlank(getWholeText()); + } + + /** + * Split this text node into two nodes at the specified string offset. After splitting, this node will contain the + * original text up to the offset, and will have a new text node sibling containing the text after the offset. + * @param offset string offset point to split node at. + * @return the newly created text node containing the text after the offset. + */ + public TextNode splitText(int offset) { + Validate.isTrue(offset >= 0, "Split offset must be not be negative"); + Validate.isTrue(offset < text.length(), "Split offset must not be greater than current text length"); + + String head = getWholeText().substring(0, offset); + String tail = getWholeText().substring(offset); + text(head); + TextNode tailNode = new TextNode(tail, this.baseUri()); + if (parent() != null) + parent().addChildren(siblingIndex()+1, tailNode); + + return tailNode; + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + String html = Entities.escape(getWholeText(), out); + if (out.prettyPrint() && parent() instanceof Element && !((Element) parent()).preserveWhitespace()) { + html = normaliseWhitespace(html); + } + + if (out.prettyPrint() && siblingIndex() == 0 && parentNode instanceof Element && ((Element) parentNode).tag().formatAsBlock() && !isBlank()) + indent(accum, depth, out); + accum.append(html); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } + + /** + * Create a new TextNode from HTML encoded (aka escaped) data. + * @param encodedText Text containing encoded HTML (e.g. &lt;) + * @return TextNode containing unencoded data (e.g. <) + */ + public static TextNode createFromEncoded(String encodedText, String baseUri) { + String text = Entities.unescape(encodedText); + return new TextNode(text, baseUri); + } + + static String normaliseWhitespace(String text) { + text = StringUtil.normaliseWhitespace(text); + return text; + } + + static String stripLeadingWhitespace(String text) { + return text.replaceFirst("^\\s+", ""); + } + + static boolean lastCharIsWhitespace(StringBuilder sb) { + return sb.length() != 0 && sb.charAt(sb.length() - 1) == ' '; + } + + // attribute fiddling. create on first access. + private void ensureAttributes() { + if (attributes == null) { + attributes = new Attributes(); + attributes.put(TEXT_KEY, text); + } + } + + @Override + public String attr(String attributeKey) { + ensureAttributes(); + return super.attr(attributeKey); + } + + @Override + public Attributes attributes() { + ensureAttributes(); + return super.attributes(); + } + + @Override + public Node attr(String attributeKey, String attributeValue) { + ensureAttributes(); + return super.attr(attributeKey, attributeValue); + } + + @Override + public boolean hasAttr(String attributeKey) { + ensureAttributes(); + return super.hasAttr(attributeKey); + } + + @Override + public Node removeAttr(String attributeKey) { + ensureAttributes(); + return super.removeAttr(attributeKey); + } + + @Override + public String absUrl(String attributeKey) { + ensureAttributes(); + return super.absUrl(attributeKey); + } +} diff --git a/server/src/org/jsoup/nodes/XmlDeclaration.java b/server/src/org/jsoup/nodes/XmlDeclaration.java new file mode 100644 index 0000000000..80d4a0152f --- /dev/null +++ b/server/src/org/jsoup/nodes/XmlDeclaration.java @@ -0,0 +1,48 @@ +package org.jsoup.nodes; + +/** + An XML Declaration. + + @author Jonathan Hedley, jonathan@hedley.net */ +public class XmlDeclaration extends Node { + private static final String DECL_KEY = "declaration"; + private final boolean isProcessingInstruction; // <! if true, <? if false, declaration (and last data char should be ?) + + /** + Create a new XML declaration + @param data data + @param baseUri base uri + @param isProcessingInstruction is processing instruction + */ + public XmlDeclaration(String data, String baseUri, boolean isProcessingInstruction) { + super(baseUri); + attributes.put(DECL_KEY, data); + this.isProcessingInstruction = isProcessingInstruction; + } + + public String nodeName() { + return "#declaration"; + } + + /** + Get the unencoded XML declaration. + @return XML declaration + */ + public String getWholeDeclaration() { + return attributes.get(DECL_KEY); + } + + void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { + accum + .append("<") + .append(isProcessingInstruction ? "!" : "?") + .append(getWholeDeclaration()) + .append(">"); + } + + void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {} + + public String toString() { + return outerHtml(); + } +} diff --git a/server/src/org/jsoup/nodes/entities-base.properties b/server/src/org/jsoup/nodes/entities-base.properties new file mode 100644 index 0000000000..3d1d11e6c4 --- /dev/null +++ b/server/src/org/jsoup/nodes/entities-base.properties @@ -0,0 +1,106 @@ +AElig=000C6 +AMP=00026 +Aacute=000C1 +Acirc=000C2 +Agrave=000C0 +Aring=000C5 +Atilde=000C3 +Auml=000C4 +COPY=000A9 +Ccedil=000C7 +ETH=000D0 +Eacute=000C9 +Ecirc=000CA +Egrave=000C8 +Euml=000CB +GT=0003E +Iacute=000CD +Icirc=000CE +Igrave=000CC +Iuml=000CF +LT=0003C +Ntilde=000D1 +Oacute=000D3 +Ocirc=000D4 +Ograve=000D2 +Oslash=000D8 +Otilde=000D5 +Ouml=000D6 +QUOT=00022 +REG=000AE +THORN=000DE +Uacute=000DA +Ucirc=000DB +Ugrave=000D9 +Uuml=000DC +Yacute=000DD +aacute=000E1 +acirc=000E2 +acute=000B4 +aelig=000E6 +agrave=000E0 +amp=00026 +aring=000E5 +atilde=000E3 +auml=000E4 +brvbar=000A6 +ccedil=000E7 +cedil=000B8 +cent=000A2 +copy=000A9 +curren=000A4 +deg=000B0 +divide=000F7 +eacute=000E9 +ecirc=000EA +egrave=000E8 +eth=000F0 +euml=000EB +frac12=000BD +frac14=000BC +frac34=000BE +gt=0003E +iacute=000ED +icirc=000EE +iexcl=000A1 +igrave=000EC +iquest=000BF +iuml=000EF +laquo=000AB +lt=0003C +macr=000AF +micro=000B5 +middot=000B7 +nbsp=000A0 +not=000AC +ntilde=000F1 +oacute=000F3 +ocirc=000F4 +ograve=000F2 +ordf=000AA +ordm=000BA +oslash=000F8 +otilde=000F5 +ouml=000F6 +para=000B6 +plusmn=000B1 +pound=000A3 +quot=00022 +raquo=000BB +reg=000AE +sect=000A7 +shy=000AD +sup1=000B9 +sup2=000B2 +sup3=000B3 +szlig=000DF +thorn=000FE +times=000D7 +uacute=000FA +ucirc=000FB +ugrave=000F9 +uml=000A8 +uuml=000FC +yacute=000FD +yen=000A5 +yuml=000FF diff --git a/server/src/org/jsoup/nodes/entities-full.properties b/server/src/org/jsoup/nodes/entities-full.properties new file mode 100644 index 0000000000..92f124f408 --- /dev/null +++ b/server/src/org/jsoup/nodes/entities-full.properties @@ -0,0 +1,2032 @@ +AElig=000C6 +AMP=00026 +Aacute=000C1 +Abreve=00102 +Acirc=000C2 +Acy=00410 +Afr=1D504 +Agrave=000C0 +Alpha=00391 +Amacr=00100 +And=02A53 +Aogon=00104 +Aopf=1D538 +ApplyFunction=02061 +Aring=000C5 +Ascr=1D49C +Assign=02254 +Atilde=000C3 +Auml=000C4 +Backslash=02216 +Barv=02AE7 +Barwed=02306 +Bcy=00411 +Because=02235 +Bernoullis=0212C +Beta=00392 +Bfr=1D505 +Bopf=1D539 +Breve=002D8 +Bscr=0212C +Bumpeq=0224E +CHcy=00427 +COPY=000A9 +Cacute=00106 +Cap=022D2 +CapitalDifferentialD=02145 +Cayleys=0212D +Ccaron=0010C +Ccedil=000C7 +Ccirc=00108 +Cconint=02230 +Cdot=0010A +Cedilla=000B8 +CenterDot=000B7 +Cfr=0212D +Chi=003A7 +CircleDot=02299 +CircleMinus=02296 +CirclePlus=02295 +CircleTimes=02297 +ClockwiseContourIntegral=02232 +CloseCurlyDoubleQuote=0201D +CloseCurlyQuote=02019 +Colon=02237 +Colone=02A74 +Congruent=02261 +Conint=0222F +ContourIntegral=0222E +Copf=02102 +Coproduct=02210 +CounterClockwiseContourIntegral=02233 +Cross=02A2F +Cscr=1D49E +Cup=022D3 +CupCap=0224D +DD=02145 +DDotrahd=02911 +DJcy=00402 +DScy=00405 +DZcy=0040F +Dagger=02021 +Darr=021A1 +Dashv=02AE4 +Dcaron=0010E +Dcy=00414 +Del=02207 +Delta=00394 +Dfr=1D507 +DiacriticalAcute=000B4 +DiacriticalDot=002D9 +DiacriticalDoubleAcute=002DD +DiacriticalGrave=00060 +DiacriticalTilde=002DC +Diamond=022C4 +DifferentialD=02146 +Dopf=1D53B +Dot=000A8 +DotDot=020DC +DotEqual=02250 +DoubleContourIntegral=0222F +DoubleDot=000A8 +DoubleDownArrow=021D3 +DoubleLeftArrow=021D0 +DoubleLeftRightArrow=021D4 +DoubleLeftTee=02AE4 +DoubleLongLeftArrow=027F8 +DoubleLongLeftRightArrow=027FA +DoubleLongRightArrow=027F9 +DoubleRightArrow=021D2 +DoubleRightTee=022A8 +DoubleUpArrow=021D1 +DoubleUpDownArrow=021D5 +DoubleVerticalBar=02225 +DownArrow=02193 +DownArrowBar=02913 +DownArrowUpArrow=021F5 +DownBreve=00311 +DownLeftRightVector=02950 +DownLeftTeeVector=0295E +DownLeftVector=021BD +DownLeftVectorBar=02956 +DownRightTeeVector=0295F +DownRightVector=021C1 +DownRightVectorBar=02957 +DownTee=022A4 +DownTeeArrow=021A7 +Downarrow=021D3 +Dscr=1D49F +Dstrok=00110 +ENG=0014A +ETH=000D0 +Eacute=000C9 +Ecaron=0011A +Ecirc=000CA +Ecy=0042D +Edot=00116 +Efr=1D508 +Egrave=000C8 +Element=02208 +Emacr=00112 +EmptySmallSquare=025FB +EmptyVerySmallSquare=025AB +Eogon=00118 +Eopf=1D53C +Epsilon=00395 +Equal=02A75 +EqualTilde=02242 +Equilibrium=021CC +Escr=02130 +Esim=02A73 +Eta=00397 +Euml=000CB +Exists=02203 +ExponentialE=02147 +Fcy=00424 +Ffr=1D509 +FilledSmallSquare=025FC +FilledVerySmallSquare=025AA +Fopf=1D53D +ForAll=02200 +Fouriertrf=02131 +Fscr=02131 +GJcy=00403 +GT=0003E +Gamma=00393 +Gammad=003DC +Gbreve=0011E +Gcedil=00122 +Gcirc=0011C +Gcy=00413 +Gdot=00120 +Gfr=1D50A +Gg=022D9 +Gopf=1D53E +GreaterEqual=02265 +GreaterEqualLess=022DB +GreaterFullEqual=02267 +GreaterGreater=02AA2 +GreaterLess=02277 +GreaterSlantEqual=02A7E +GreaterTilde=02273 +Gscr=1D4A2 +Gt=0226B +HARDcy=0042A +Hacek=002C7 +Hat=0005E +Hcirc=00124 +Hfr=0210C +HilbertSpace=0210B +Hopf=0210D +HorizontalLine=02500 +Hscr=0210B +Hstrok=00126 +HumpDownHump=0224E +HumpEqual=0224F +IEcy=00415 +IJlig=00132 +IOcy=00401 +Iacute=000CD +Icirc=000CE +Icy=00418 +Idot=00130 +Ifr=02111 +Igrave=000CC +Im=02111 +Imacr=0012A +ImaginaryI=02148 +Implies=021D2 +Int=0222C +Integral=0222B +Intersection=022C2 +InvisibleComma=02063 +InvisibleTimes=02062 +Iogon=0012E +Iopf=1D540 +Iota=00399 +Iscr=02110 +Itilde=00128 +Iukcy=00406 +Iuml=000CF +Jcirc=00134 +Jcy=00419 +Jfr=1D50D +Jopf=1D541 +Jscr=1D4A5 +Jsercy=00408 +Jukcy=00404 +KHcy=00425 +KJcy=0040C +Kappa=0039A +Kcedil=00136 +Kcy=0041A +Kfr=1D50E +Kopf=1D542 +Kscr=1D4A6 +LJcy=00409 +LT=0003C +Lacute=00139 +Lambda=0039B +Lang=027EA +Laplacetrf=02112 +Larr=0219E +Lcaron=0013D +Lcedil=0013B +Lcy=0041B +LeftAngleBracket=027E8 +LeftArrow=02190 +LeftArrowBar=021E4 +LeftArrowRightArrow=021C6 +LeftCeiling=02308 +LeftDoubleBracket=027E6 +LeftDownTeeVector=02961 +LeftDownVector=021C3 +LeftDownVectorBar=02959 +LeftFloor=0230A +LeftRightArrow=02194 +LeftRightVector=0294E +LeftTee=022A3 +LeftTeeArrow=021A4 +LeftTeeVector=0295A +LeftTriangle=022B2 +LeftTriangleBar=029CF +LeftTriangleEqual=022B4 +LeftUpDownVector=02951 +LeftUpTeeVector=02960 +LeftUpVector=021BF +LeftUpVectorBar=02958 +LeftVector=021BC +LeftVectorBar=02952 +Leftarrow=021D0 +Leftrightarrow=021D4 +LessEqualGreater=022DA +LessFullEqual=02266 +LessGreater=02276 +LessLess=02AA1 +LessSlantEqual=02A7D +LessTilde=02272 +Lfr=1D50F +Ll=022D8 +Lleftarrow=021DA +Lmidot=0013F +LongLeftArrow=027F5 +LongLeftRightArrow=027F7 +LongRightArrow=027F6 +Longleftarrow=027F8 +Longleftrightarrow=027FA +Longrightarrow=027F9 +Lopf=1D543 +LowerLeftArrow=02199 +LowerRightArrow=02198 +Lscr=02112 +Lsh=021B0 +Lstrok=00141 +Lt=0226A +Map=02905 +Mcy=0041C +MediumSpace=0205F +Mellintrf=02133 +Mfr=1D510 +MinusPlus=02213 +Mopf=1D544 +Mscr=02133 +Mu=0039C +NJcy=0040A +Nacute=00143 +Ncaron=00147 +Ncedil=00145 +Ncy=0041D +NegativeMediumSpace=0200B +NegativeThickSpace=0200B +NegativeThinSpace=0200B +NegativeVeryThinSpace=0200B +NestedGreaterGreater=0226B +NestedLessLess=0226A +NewLine=0000A +Nfr=1D511 +NoBreak=02060 +NonBreakingSpace=000A0 +Nopf=02115 +Not=02AEC +NotCongruent=02262 +NotCupCap=0226D +NotDoubleVerticalBar=02226 +NotElement=02209 +NotEqual=02260 +NotExists=02204 +NotGreater=0226F +NotGreaterEqual=02271 +NotGreaterLess=02279 +NotGreaterTilde=02275 +NotLeftTriangle=022EA +NotLeftTriangleEqual=022EC +NotLess=0226E +NotLessEqual=02270 +NotLessGreater=02278 +NotLessTilde=02274 +NotPrecedes=02280 +NotPrecedesSlantEqual=022E0 +NotReverseElement=0220C +NotRightTriangle=022EB +NotRightTriangleEqual=022ED +NotSquareSubsetEqual=022E2 +NotSquareSupersetEqual=022E3 +NotSubsetEqual=02288 +NotSucceeds=02281 +NotSucceedsSlantEqual=022E1 +NotSupersetEqual=02289 +NotTilde=02241 +NotTildeEqual=02244 +NotTildeFullEqual=02247 +NotTildeTilde=02249 +NotVerticalBar=02224 +Nscr=1D4A9 +Ntilde=000D1 +Nu=0039D +OElig=00152 +Oacute=000D3 +Ocirc=000D4 +Ocy=0041E +Odblac=00150 +Ofr=1D512 +Ograve=000D2 +Omacr=0014C +Omega=003A9 +Omicron=0039F +Oopf=1D546 +OpenCurlyDoubleQuote=0201C +OpenCurlyQuote=02018 +Or=02A54 +Oscr=1D4AA +Oslash=000D8 +Otilde=000D5 +Otimes=02A37 +Ouml=000D6 +OverBar=0203E +OverBrace=023DE +OverBracket=023B4 +OverParenthesis=023DC +PartialD=02202 +Pcy=0041F +Pfr=1D513 +Phi=003A6 +Pi=003A0 +PlusMinus=000B1 +Poincareplane=0210C +Popf=02119 +Pr=02ABB +Precedes=0227A +PrecedesEqual=02AAF +PrecedesSlantEqual=0227C +PrecedesTilde=0227E +Prime=02033 +Product=0220F +Proportion=02237 +Proportional=0221D +Pscr=1D4AB +Psi=003A8 +QUOT=00022 +Qfr=1D514 +Qopf=0211A +Qscr=1D4AC +RBarr=02910 +REG=000AE +Racute=00154 +Rang=027EB +Rarr=021A0 +Rarrtl=02916 +Rcaron=00158 +Rcedil=00156 +Rcy=00420 +Re=0211C +ReverseElement=0220B +ReverseEquilibrium=021CB +ReverseUpEquilibrium=0296F +Rfr=0211C +Rho=003A1 +RightAngleBracket=027E9 +RightArrow=02192 +RightArrowBar=021E5 +RightArrowLeftArrow=021C4 +RightCeiling=02309 +RightDoubleBracket=027E7 +RightDownTeeVector=0295D +RightDownVector=021C2 +RightDownVectorBar=02955 +RightFloor=0230B +RightTee=022A2 +RightTeeArrow=021A6 +RightTeeVector=0295B +RightTriangle=022B3 +RightTriangleBar=029D0 +RightTriangleEqual=022B5 +RightUpDownVector=0294F +RightUpTeeVector=0295C +RightUpVector=021BE +RightUpVectorBar=02954 +RightVector=021C0 +RightVectorBar=02953 +Rightarrow=021D2 +Ropf=0211D +RoundImplies=02970 +Rrightarrow=021DB +Rscr=0211B +Rsh=021B1 +RuleDelayed=029F4 +SHCHcy=00429 +SHcy=00428 +SOFTcy=0042C +Sacute=0015A +Sc=02ABC +Scaron=00160 +Scedil=0015E +Scirc=0015C +Scy=00421 +Sfr=1D516 +ShortDownArrow=02193 +ShortLeftArrow=02190 +ShortRightArrow=02192 +ShortUpArrow=02191 +Sigma=003A3 +SmallCircle=02218 +Sopf=1D54A +Sqrt=0221A +Square=025A1 +SquareIntersection=02293 +SquareSubset=0228F +SquareSubsetEqual=02291 +SquareSuperset=02290 +SquareSupersetEqual=02292 +SquareUnion=02294 +Sscr=1D4AE +Star=022C6 +Sub=022D0 +Subset=022D0 +SubsetEqual=02286 +Succeeds=0227B +SucceedsEqual=02AB0 +SucceedsSlantEqual=0227D +SucceedsTilde=0227F +SuchThat=0220B +Sum=02211 +Sup=022D1 +Superset=02283 +SupersetEqual=02287 +Supset=022D1 +THORN=000DE +TRADE=02122 +TSHcy=0040B +TScy=00426 +Tab=00009 +Tau=003A4 +Tcaron=00164 +Tcedil=00162 +Tcy=00422 +Tfr=1D517 +Therefore=02234 +Theta=00398 +ThinSpace=02009 +Tilde=0223C +TildeEqual=02243 +TildeFullEqual=02245 +TildeTilde=02248 +Topf=1D54B +TripleDot=020DB +Tscr=1D4AF +Tstrok=00166 +Uacute=000DA +Uarr=0219F +Uarrocir=02949 +Ubrcy=0040E +Ubreve=0016C +Ucirc=000DB +Ucy=00423 +Udblac=00170 +Ufr=1D518 +Ugrave=000D9 +Umacr=0016A +UnderBar=0005F +UnderBrace=023DF +UnderBracket=023B5 +UnderParenthesis=023DD +Union=022C3 +UnionPlus=0228E +Uogon=00172 +Uopf=1D54C +UpArrow=02191 +UpArrowBar=02912 +UpArrowDownArrow=021C5 +UpDownArrow=02195 +UpEquilibrium=0296E +UpTee=022A5 +UpTeeArrow=021A5 +Uparrow=021D1 +Updownarrow=021D5 +UpperLeftArrow=02196 +UpperRightArrow=02197 +Upsi=003D2 +Upsilon=003A5 +Uring=0016E +Uscr=1D4B0 +Utilde=00168 +Uuml=000DC +VDash=022AB +Vbar=02AEB +Vcy=00412 +Vdash=022A9 +Vdashl=02AE6 +Vee=022C1 +Verbar=02016 +Vert=02016 +VerticalBar=02223 +VerticalLine=0007C +VerticalSeparator=02758 +VerticalTilde=02240 +VeryThinSpace=0200A +Vfr=1D519 +Vopf=1D54D +Vscr=1D4B1 +Vvdash=022AA +Wcirc=00174 +Wedge=022C0 +Wfr=1D51A +Wopf=1D54E +Wscr=1D4B2 +Xfr=1D51B +Xi=0039E +Xopf=1D54F +Xscr=1D4B3 +YAcy=0042F +YIcy=00407 +YUcy=0042E +Yacute=000DD +Ycirc=00176 +Ycy=0042B +Yfr=1D51C +Yopf=1D550 +Yscr=1D4B4 +Yuml=00178 +ZHcy=00416 +Zacute=00179 +Zcaron=0017D +Zcy=00417 +Zdot=0017B +ZeroWidthSpace=0200B +Zeta=00396 +Zfr=02128 +Zopf=02124 +Zscr=1D4B5 +aacute=000E1 +abreve=00103 +ac=0223E +acd=0223F +acirc=000E2 +acute=000B4 +acy=00430 +aelig=000E6 +af=02061 +afr=1D51E +agrave=000E0 +alefsym=02135 +aleph=02135 +alpha=003B1 +amacr=00101 +amalg=02A3F +amp=00026 +and=02227 +andand=02A55 +andd=02A5C +andslope=02A58 +andv=02A5A +ang=02220 +ange=029A4 +angle=02220 +angmsd=02221 +angmsdaa=029A8 +angmsdab=029A9 +angmsdac=029AA +angmsdad=029AB +angmsdae=029AC +angmsdaf=029AD +angmsdag=029AE +angmsdah=029AF +angrt=0221F +angrtvb=022BE +angrtvbd=0299D +angsph=02222 +angst=000C5 +angzarr=0237C +aogon=00105 +aopf=1D552 +ap=02248 +apE=02A70 +apacir=02A6F +ape=0224A +apid=0224B +apos=00027 +approx=02248 +approxeq=0224A +aring=000E5 +ascr=1D4B6 +ast=0002A +asymp=02248 +asympeq=0224D +atilde=000E3 +auml=000E4 +awconint=02233 +awint=02A11 +bNot=02AED +backcong=0224C +backepsilon=003F6 +backprime=02035 +backsim=0223D +backsimeq=022CD +barvee=022BD +barwed=02305 +barwedge=02305 +bbrk=023B5 +bbrktbrk=023B6 +bcong=0224C +bcy=00431 +bdquo=0201E +becaus=02235 +because=02235 +bemptyv=029B0 +bepsi=003F6 +bernou=0212C +beta=003B2 +beth=02136 +between=0226C +bfr=1D51F +bigcap=022C2 +bigcirc=025EF +bigcup=022C3 +bigodot=02A00 +bigoplus=02A01 +bigotimes=02A02 +bigsqcup=02A06 +bigstar=02605 +bigtriangledown=025BD +bigtriangleup=025B3 +biguplus=02A04 +bigvee=022C1 +bigwedge=022C0 +bkarow=0290D +blacklozenge=029EB +blacksquare=025AA +blacktriangle=025B4 +blacktriangledown=025BE +blacktriangleleft=025C2 +blacktriangleright=025B8 +blank=02423 +blk12=02592 +blk14=02591 +blk34=02593 +block=02588 +bnot=02310 +bopf=1D553 +bot=022A5 +bottom=022A5 +bowtie=022C8 +boxDL=02557 +boxDR=02554 +boxDl=02556 +boxDr=02553 +boxH=02550 +boxHD=02566 +boxHU=02569 +boxHd=02564 +boxHu=02567 +boxUL=0255D +boxUR=0255A +boxUl=0255C +boxUr=02559 +boxV=02551 +boxVH=0256C +boxVL=02563 +boxVR=02560 +boxVh=0256B +boxVl=02562 +boxVr=0255F +boxbox=029C9 +boxdL=02555 +boxdR=02552 +boxdl=02510 +boxdr=0250C +boxh=02500 +boxhD=02565 +boxhU=02568 +boxhd=0252C +boxhu=02534 +boxminus=0229F +boxplus=0229E +boxtimes=022A0 +boxuL=0255B +boxuR=02558 +boxul=02518 +boxur=02514 +boxv=02502 +boxvH=0256A +boxvL=02561 +boxvR=0255E +boxvh=0253C +boxvl=02524 +boxvr=0251C +bprime=02035 +breve=002D8 +brvbar=000A6 +bscr=1D4B7 +bsemi=0204F +bsim=0223D +bsime=022CD +bsol=0005C +bsolb=029C5 +bsolhsub=027C8 +bull=02022 +bullet=02022 +bump=0224E +bumpE=02AAE +bumpe=0224F +bumpeq=0224F +cacute=00107 +cap=02229 +capand=02A44 +capbrcup=02A49 +capcap=02A4B +capcup=02A47 +capdot=02A40 +caret=02041 +caron=002C7 +ccaps=02A4D +ccaron=0010D +ccedil=000E7 +ccirc=00109 +ccups=02A4C +ccupssm=02A50 +cdot=0010B +cedil=000B8 +cemptyv=029B2 +cent=000A2 +centerdot=000B7 +cfr=1D520 +chcy=00447 +check=02713 +checkmark=02713 +chi=003C7 +cir=025CB +cirE=029C3 +circ=002C6 +circeq=02257 +circlearrowleft=021BA +circlearrowright=021BB +circledR=000AE +circledS=024C8 +circledast=0229B +circledcirc=0229A +circleddash=0229D +cire=02257 +cirfnint=02A10 +cirmid=02AEF +cirscir=029C2 +clubs=02663 +clubsuit=02663 +colon=0003A +colone=02254 +coloneq=02254 +comma=0002C +commat=00040 +comp=02201 +compfn=02218 +complement=02201 +complexes=02102 +cong=02245 +congdot=02A6D +conint=0222E +copf=1D554 +coprod=02210 +copy=000A9 +copysr=02117 +crarr=021B5 +cross=02717 +cscr=1D4B8 +csub=02ACF +csube=02AD1 +csup=02AD0 +csupe=02AD2 +ctdot=022EF +cudarrl=02938 +cudarrr=02935 +cuepr=022DE +cuesc=022DF +cularr=021B6 +cularrp=0293D +cup=0222A +cupbrcap=02A48 +cupcap=02A46 +cupcup=02A4A +cupdot=0228D +cupor=02A45 +curarr=021B7 +curarrm=0293C +curlyeqprec=022DE +curlyeqsucc=022DF +curlyvee=022CE +curlywedge=022CF +curren=000A4 +curvearrowleft=021B6 +curvearrowright=021B7 +cuvee=022CE +cuwed=022CF +cwconint=02232 +cwint=02231 +cylcty=0232D +dArr=021D3 +dHar=02965 +dagger=02020 +daleth=02138 +darr=02193 +dash=02010 +dashv=022A3 +dbkarow=0290F +dblac=002DD +dcaron=0010F +dcy=00434 +dd=02146 +ddagger=02021 +ddarr=021CA +ddotseq=02A77 +deg=000B0 +delta=003B4 +demptyv=029B1 +dfisht=0297F +dfr=1D521 +dharl=021C3 +dharr=021C2 +diam=022C4 +diamond=022C4 +diamondsuit=02666 +diams=02666 +die=000A8 +digamma=003DD +disin=022F2 +div=000F7 +divide=000F7 +divideontimes=022C7 +divonx=022C7 +djcy=00452 +dlcorn=0231E +dlcrop=0230D +dollar=00024 +dopf=1D555 +dot=002D9 +doteq=02250 +doteqdot=02251 +dotminus=02238 +dotplus=02214 +dotsquare=022A1 +doublebarwedge=02306 +downarrow=02193 +downdownarrows=021CA +downharpoonleft=021C3 +downharpoonright=021C2 +drbkarow=02910 +drcorn=0231F +drcrop=0230C +dscr=1D4B9 +dscy=00455 +dsol=029F6 +dstrok=00111 +dtdot=022F1 +dtri=025BF +dtrif=025BE +duarr=021F5 +duhar=0296F +dwangle=029A6 +dzcy=0045F +dzigrarr=027FF +eDDot=02A77 +eDot=02251 +eacute=000E9 +easter=02A6E +ecaron=0011B +ecir=02256 +ecirc=000EA +ecolon=02255 +ecy=0044D +edot=00117 +ee=02147 +efDot=02252 +efr=1D522 +eg=02A9A +egrave=000E8 +egs=02A96 +egsdot=02A98 +el=02A99 +elinters=023E7 +ell=02113 +els=02A95 +elsdot=02A97 +emacr=00113 +empty=02205 +emptyset=02205 +emptyv=02205 +emsp13=02004 +emsp14=02005 +emsp=02003 +eng=0014B +ensp=02002 +eogon=00119 +eopf=1D556 +epar=022D5 +eparsl=029E3 +eplus=02A71 +epsi=003B5 +epsilon=003B5 +epsiv=003F5 +eqcirc=02256 +eqcolon=02255 +eqsim=02242 +eqslantgtr=02A96 +eqslantless=02A95 +equals=0003D +equest=0225F +equiv=02261 +equivDD=02A78 +eqvparsl=029E5 +erDot=02253 +erarr=02971 +escr=0212F +esdot=02250 +esim=02242 +eta=003B7 +eth=000F0 +euml=000EB +euro=020AC +excl=00021 +exist=02203 +expectation=02130 +exponentiale=02147 +fallingdotseq=02252 +fcy=00444 +female=02640 +ffilig=0FB03 +fflig=0FB00 +ffllig=0FB04 +ffr=1D523 +filig=0FB01 +flat=0266D +fllig=0FB02 +fltns=025B1 +fnof=00192 +fopf=1D557 +forall=02200 +fork=022D4 +forkv=02AD9 +fpartint=02A0D +frac12=000BD +frac13=02153 +frac14=000BC +frac15=02155 +frac16=02159 +frac18=0215B +frac23=02154 +frac25=02156 +frac34=000BE +frac35=02157 +frac38=0215C +frac45=02158 +frac56=0215A +frac58=0215D +frac78=0215E +frasl=02044 +frown=02322 +fscr=1D4BB +gE=02267 +gEl=02A8C +gacute=001F5 +gamma=003B3 +gammad=003DD +gap=02A86 +gbreve=0011F +gcirc=0011D +gcy=00433 +gdot=00121 +ge=02265 +gel=022DB +geq=02265 +geqq=02267 +geqslant=02A7E +ges=02A7E +gescc=02AA9 +gesdot=02A80 +gesdoto=02A82 +gesdotol=02A84 +gesles=02A94 +gfr=1D524 +gg=0226B +ggg=022D9 +gimel=02137 +gjcy=00453 +gl=02277 +glE=02A92 +gla=02AA5 +glj=02AA4 +gnE=02269 +gnap=02A8A +gnapprox=02A8A +gne=02A88 +gneq=02A88 +gneqq=02269 +gnsim=022E7 +gopf=1D558 +grave=00060 +gscr=0210A +gsim=02273 +gsime=02A8E +gsiml=02A90 +gt=0003E +gtcc=02AA7 +gtcir=02A7A +gtdot=022D7 +gtlPar=02995 +gtquest=02A7C +gtrapprox=02A86 +gtrarr=02978 +gtrdot=022D7 +gtreqless=022DB +gtreqqless=02A8C +gtrless=02277 +gtrsim=02273 +hArr=021D4 +hairsp=0200A +half=000BD +hamilt=0210B +hardcy=0044A +harr=02194 +harrcir=02948 +harrw=021AD +hbar=0210F +hcirc=00125 +hearts=02665 +heartsuit=02665 +hellip=02026 +hercon=022B9 +hfr=1D525 +hksearow=02925 +hkswarow=02926 +hoarr=021FF +homtht=0223B +hookleftarrow=021A9 +hookrightarrow=021AA +hopf=1D559 +horbar=02015 +hscr=1D4BD +hslash=0210F +hstrok=00127 +hybull=02043 +hyphen=02010 +iacute=000ED +ic=02063 +icirc=000EE +icy=00438 +iecy=00435 +iexcl=000A1 +iff=021D4 +ifr=1D526 +igrave=000EC +ii=02148 +iiiint=02A0C +iiint=0222D +iinfin=029DC +iiota=02129 +ijlig=00133 +imacr=0012B +image=02111 +imagline=02110 +imagpart=02111 +imath=00131 +imof=022B7 +imped=001B5 +in=02208 +incare=02105 +infin=0221E +infintie=029DD +inodot=00131 +int=0222B +intcal=022BA +integers=02124 +intercal=022BA +intlarhk=02A17 +intprod=02A3C +iocy=00451 +iogon=0012F +iopf=1D55A +iota=003B9 +iprod=02A3C +iquest=000BF +iscr=1D4BE +isin=02208 +isinE=022F9 +isindot=022F5 +isins=022F4 +isinsv=022F3 +isinv=02208 +it=02062 +itilde=00129 +iukcy=00456 +iuml=000EF +jcirc=00135 +jcy=00439 +jfr=1D527 +jmath=00237 +jopf=1D55B +jscr=1D4BF +jsercy=00458 +jukcy=00454 +kappa=003BA +kappav=003F0 +kcedil=00137 +kcy=0043A +kfr=1D528 +kgreen=00138 +khcy=00445 +kjcy=0045C +kopf=1D55C +kscr=1D4C0 +lAarr=021DA +lArr=021D0 +lAtail=0291B +lBarr=0290E +lE=02266 +lEg=02A8B +lHar=02962 +lacute=0013A +laemptyv=029B4 +lagran=02112 +lambda=003BB +lang=027E8 +langd=02991 +langle=027E8 +lap=02A85 +laquo=000AB +larr=02190 +larrb=021E4 +larrbfs=0291F +larrfs=0291D +larrhk=021A9 +larrlp=021AB +larrpl=02939 +larrsim=02973 +larrtl=021A2 +lat=02AAB +latail=02919 +late=02AAD +lbarr=0290C +lbbrk=02772 +lbrace=0007B +lbrack=0005B +lbrke=0298B +lbrksld=0298F +lbrkslu=0298D +lcaron=0013E +lcedil=0013C +lceil=02308 +lcub=0007B +lcy=0043B +ldca=02936 +ldquo=0201C +ldquor=0201E +ldrdhar=02967 +ldrushar=0294B +ldsh=021B2 +le=02264 +leftarrow=02190 +leftarrowtail=021A2 +leftharpoondown=021BD +leftharpoonup=021BC +leftleftarrows=021C7 +leftrightarrow=02194 +leftrightarrows=021C6 +leftrightharpoons=021CB +leftrightsquigarrow=021AD +leftthreetimes=022CB +leg=022DA +leq=02264 +leqq=02266 +leqslant=02A7D +les=02A7D +lescc=02AA8 +lesdot=02A7F +lesdoto=02A81 +lesdotor=02A83 +lesges=02A93 +lessapprox=02A85 +lessdot=022D6 +lesseqgtr=022DA +lesseqqgtr=02A8B +lessgtr=02276 +lesssim=02272 +lfisht=0297C +lfloor=0230A +lfr=1D529 +lg=02276 +lgE=02A91 +lhard=021BD +lharu=021BC +lharul=0296A +lhblk=02584 +ljcy=00459 +ll=0226A +llarr=021C7 +llcorner=0231E +llhard=0296B +lltri=025FA +lmidot=00140 +lmoust=023B0 +lmoustache=023B0 +lnE=02268 +lnap=02A89 +lnapprox=02A89 +lne=02A87 +lneq=02A87 +lneqq=02268 +lnsim=022E6 +loang=027EC +loarr=021FD +lobrk=027E6 +longleftarrow=027F5 +longleftrightarrow=027F7 +longmapsto=027FC +longrightarrow=027F6 +looparrowleft=021AB +looparrowright=021AC +lopar=02985 +lopf=1D55D +loplus=02A2D +lotimes=02A34 +lowast=02217 +lowbar=0005F +loz=025CA +lozenge=025CA +lozf=029EB +lpar=00028 +lparlt=02993 +lrarr=021C6 +lrcorner=0231F +lrhar=021CB +lrhard=0296D +lrm=0200E +lrtri=022BF +lsaquo=02039 +lscr=1D4C1 +lsh=021B0 +lsim=02272 +lsime=02A8D +lsimg=02A8F +lsqb=0005B +lsquo=02018 +lsquor=0201A +lstrok=00142 +lt=0003C +ltcc=02AA6 +ltcir=02A79 +ltdot=022D6 +lthree=022CB +ltimes=022C9 +ltlarr=02976 +ltquest=02A7B +ltrPar=02996 +ltri=025C3 +ltrie=022B4 +ltrif=025C2 +lurdshar=0294A +luruhar=02966 +mDDot=0223A +macr=000AF +male=02642 +malt=02720 +maltese=02720 +map=021A6 +mapsto=021A6 +mapstodown=021A7 +mapstoleft=021A4 +mapstoup=021A5 +marker=025AE +mcomma=02A29 +mcy=0043C +mdash=02014 +measuredangle=02221 +mfr=1D52A +mho=02127 +micro=000B5 +mid=02223 +midast=0002A +midcir=02AF0 +middot=000B7 +minus=02212 +minusb=0229F +minusd=02238 +minusdu=02A2A +mlcp=02ADB +mldr=02026 +mnplus=02213 +models=022A7 +mopf=1D55E +mp=02213 +mscr=1D4C2 +mstpos=0223E +mu=003BC +multimap=022B8 +mumap=022B8 +nLeftarrow=021CD +nLeftrightarrow=021CE +nRightarrow=021CF +nVDash=022AF +nVdash=022AE +nabla=02207 +nacute=00144 +nap=02249 +napos=00149 +napprox=02249 +natur=0266E +natural=0266E +naturals=02115 +nbsp=000A0 +ncap=02A43 +ncaron=00148 +ncedil=00146 +ncong=02247 +ncup=02A42 +ncy=0043D +ndash=02013 +ne=02260 +neArr=021D7 +nearhk=02924 +nearr=02197 +nearrow=02197 +nequiv=02262 +nesear=02928 +nexist=02204 +nexists=02204 +nfr=1D52B +nge=02271 +ngeq=02271 +ngsim=02275 +ngt=0226F +ngtr=0226F +nhArr=021CE +nharr=021AE +nhpar=02AF2 +ni=0220B +nis=022FC +nisd=022FA +niv=0220B +njcy=0045A +nlArr=021CD +nlarr=0219A +nldr=02025 +nle=02270 +nleftarrow=0219A +nleftrightarrow=021AE +nleq=02270 +nless=0226E +nlsim=02274 +nlt=0226E +nltri=022EA +nltrie=022EC +nmid=02224 +nopf=1D55F +not=000AC +notin=02209 +notinva=02209 +notinvb=022F7 +notinvc=022F6 +notni=0220C +notniva=0220C +notnivb=022FE +notnivc=022FD +npar=02226 +nparallel=02226 +npolint=02A14 +npr=02280 +nprcue=022E0 +nprec=02280 +nrArr=021CF +nrarr=0219B +nrightarrow=0219B +nrtri=022EB +nrtrie=022ED +nsc=02281 +nsccue=022E1 +nscr=1D4C3 +nshortmid=02224 +nshortparallel=02226 +nsim=02241 +nsime=02244 +nsimeq=02244 +nsmid=02224 +nspar=02226 +nsqsube=022E2 +nsqsupe=022E3 +nsub=02284 +nsube=02288 +nsubseteq=02288 +nsucc=02281 +nsup=02285 +nsupe=02289 +nsupseteq=02289 +ntgl=02279 +ntilde=000F1 +ntlg=02278 +ntriangleleft=022EA +ntrianglelefteq=022EC +ntriangleright=022EB +ntrianglerighteq=022ED +nu=003BD +num=00023 +numero=02116 +numsp=02007 +nvDash=022AD +nvHarr=02904 +nvdash=022AC +nvinfin=029DE +nvlArr=02902 +nvrArr=02903 +nwArr=021D6 +nwarhk=02923 +nwarr=02196 +nwarrow=02196 +nwnear=02927 +oS=024C8 +oacute=000F3 +oast=0229B +ocir=0229A +ocirc=000F4 +ocy=0043E +odash=0229D +odblac=00151 +odiv=02A38 +odot=02299 +odsold=029BC +oelig=00153 +ofcir=029BF +ofr=1D52C +ogon=002DB +ograve=000F2 +ogt=029C1 +ohbar=029B5 +ohm=003A9 +oint=0222E +olarr=021BA +olcir=029BE +olcross=029BB +oline=0203E +olt=029C0 +omacr=0014D +omega=003C9 +omicron=003BF +omid=029B6 +ominus=02296 +oopf=1D560 +opar=029B7 +operp=029B9 +oplus=02295 +or=02228 +orarr=021BB +ord=02A5D +order=02134 +orderof=02134 +ordf=000AA +ordm=000BA +origof=022B6 +oror=02A56 +orslope=02A57 +orv=02A5B +oscr=02134 +oslash=000F8 +osol=02298 +otilde=000F5 +otimes=02297 +otimesas=02A36 +ouml=000F6 +ovbar=0233D +par=02225 +para=000B6 +parallel=02225 +parsim=02AF3 +parsl=02AFD +part=02202 +pcy=0043F +percnt=00025 +period=0002E +permil=02030 +perp=022A5 +pertenk=02031 +pfr=1D52D +phi=003C6 +phiv=003D5 +phmmat=02133 +phone=0260E +pi=003C0 +pitchfork=022D4 +piv=003D6 +planck=0210F +planckh=0210E +plankv=0210F +plus=0002B +plusacir=02A23 +plusb=0229E +pluscir=02A22 +plusdo=02214 +plusdu=02A25 +pluse=02A72 +plusmn=000B1 +plussim=02A26 +plustwo=02A27 +pm=000B1 +pointint=02A15 +popf=1D561 +pound=000A3 +pr=0227A +prE=02AB3 +prap=02AB7 +prcue=0227C +pre=02AAF +prec=0227A +precapprox=02AB7 +preccurlyeq=0227C +preceq=02AAF +precnapprox=02AB9 +precneqq=02AB5 +precnsim=022E8 +precsim=0227E +prime=02032 +primes=02119 +prnE=02AB5 +prnap=02AB9 +prnsim=022E8 +prod=0220F +profalar=0232E +profline=02312 +profsurf=02313 +prop=0221D +propto=0221D +prsim=0227E +prurel=022B0 +pscr=1D4C5 +psi=003C8 +puncsp=02008 +qfr=1D52E +qint=02A0C +qopf=1D562 +qprime=02057 +qscr=1D4C6 +quaternions=0210D +quatint=02A16 +quest=0003F +questeq=0225F +quot=00022 +rAarr=021DB +rArr=021D2 +rAtail=0291C +rBarr=0290F +rHar=02964 +racute=00155 +radic=0221A +raemptyv=029B3 +rang=027E9 +rangd=02992 +range=029A5 +rangle=027E9 +raquo=000BB +rarr=02192 +rarrap=02975 +rarrb=021E5 +rarrbfs=02920 +rarrc=02933 +rarrfs=0291E +rarrhk=021AA +rarrlp=021AC +rarrpl=02945 +rarrsim=02974 +rarrtl=021A3 +rarrw=0219D +ratail=0291A +ratio=02236 +rationals=0211A +rbarr=0290D +rbbrk=02773 +rbrace=0007D +rbrack=0005D +rbrke=0298C +rbrksld=0298E +rbrkslu=02990 +rcaron=00159 +rcedil=00157 +rceil=02309 +rcub=0007D +rcy=00440 +rdca=02937 +rdldhar=02969 +rdquo=0201D +rdquor=0201D +rdsh=021B3 +real=0211C +realine=0211B +realpart=0211C +reals=0211D +rect=025AD +reg=000AE +rfisht=0297D +rfloor=0230B +rfr=1D52F +rhard=021C1 +rharu=021C0 +rharul=0296C +rho=003C1 +rhov=003F1 +rightarrow=02192 +rightarrowtail=021A3 +rightharpoondown=021C1 +rightharpoonup=021C0 +rightleftarrows=021C4 +rightleftharpoons=021CC +rightrightarrows=021C9 +rightsquigarrow=0219D +rightthreetimes=022CC +ring=002DA +risingdotseq=02253 +rlarr=021C4 +rlhar=021CC +rlm=0200F +rmoust=023B1 +rmoustache=023B1 +rnmid=02AEE +roang=027ED +roarr=021FE +robrk=027E7 +ropar=02986 +ropf=1D563 +roplus=02A2E +rotimes=02A35 +rpar=00029 +rpargt=02994 +rppolint=02A12 +rrarr=021C9 +rsaquo=0203A +rscr=1D4C7 +rsh=021B1 +rsqb=0005D +rsquo=02019 +rsquor=02019 +rthree=022CC +rtimes=022CA +rtri=025B9 +rtrie=022B5 +rtrif=025B8 +rtriltri=029CE +ruluhar=02968 +rx=0211E +sacute=0015B +sbquo=0201A +sc=0227B +scE=02AB4 +scap=02AB8 +scaron=00161 +sccue=0227D +sce=02AB0 +scedil=0015F +scirc=0015D +scnE=02AB6 +scnap=02ABA +scnsim=022E9 +scpolint=02A13 +scsim=0227F +scy=00441 +sdot=022C5 +sdotb=022A1 +sdote=02A66 +seArr=021D8 +searhk=02925 +searr=02198 +searrow=02198 +sect=000A7 +semi=0003B +seswar=02929 +setminus=02216 +setmn=02216 +sext=02736 +sfr=1D530 +sfrown=02322 +sharp=0266F +shchcy=00449 +shcy=00448 +shortmid=02223 +shortparallel=02225 +shy=000AD +sigma=003C3 +sigmaf=003C2 +sigmav=003C2 +sim=0223C +simdot=02A6A +sime=02243 +simeq=02243 +simg=02A9E +simgE=02AA0 +siml=02A9D +simlE=02A9F +simne=02246 +simplus=02A24 +simrarr=02972 +slarr=02190 +smallsetminus=02216 +smashp=02A33 +smeparsl=029E4 +smid=02223 +smile=02323 +smt=02AAA +smte=02AAC +softcy=0044C +sol=0002F +solb=029C4 +solbar=0233F +sopf=1D564 +spades=02660 +spadesuit=02660 +spar=02225 +sqcap=02293 +sqcup=02294 +sqsub=0228F +sqsube=02291 +sqsubset=0228F +sqsubseteq=02291 +sqsup=02290 +sqsupe=02292 +sqsupset=02290 +sqsupseteq=02292 +squ=025A1 +square=025A1 +squarf=025AA +squf=025AA +srarr=02192 +sscr=1D4C8 +ssetmn=02216 +ssmile=02323 +sstarf=022C6 +star=02606 +starf=02605 +straightepsilon=003F5 +straightphi=003D5 +strns=000AF +sub=02282 +subE=02AC5 +subdot=02ABD +sube=02286 +subedot=02AC3 +submult=02AC1 +subnE=02ACB +subne=0228A +subplus=02ABF +subrarr=02979 +subset=02282 +subseteq=02286 +subseteqq=02AC5 +subsetneq=0228A +subsetneqq=02ACB +subsim=02AC7 +subsub=02AD5 +subsup=02AD3 +succ=0227B +succapprox=02AB8 +succcurlyeq=0227D +succeq=02AB0 +succnapprox=02ABA +succneqq=02AB6 +succnsim=022E9 +succsim=0227F +sum=02211 +sung=0266A +sup1=000B9 +sup2=000B2 +sup3=000B3 +sup=02283 +supE=02AC6 +supdot=02ABE +supdsub=02AD8 +supe=02287 +supedot=02AC4 +suphsol=027C9 +suphsub=02AD7 +suplarr=0297B +supmult=02AC2 +supnE=02ACC +supne=0228B +supplus=02AC0 +supset=02283 +supseteq=02287 +supseteqq=02AC6 +supsetneq=0228B +supsetneqq=02ACC +supsim=02AC8 +supsub=02AD4 +supsup=02AD6 +swArr=021D9 +swarhk=02926 +swarr=02199 +swarrow=02199 +swnwar=0292A +szlig=000DF +target=02316 +tau=003C4 +tbrk=023B4 +tcaron=00165 +tcedil=00163 +tcy=00442 +tdot=020DB +telrec=02315 +tfr=1D531 +there4=02234 +therefore=02234 +theta=003B8 +thetasym=003D1 +thetav=003D1 +thickapprox=02248 +thicksim=0223C +thinsp=02009 +thkap=02248 +thksim=0223C +thorn=000FE +tilde=002DC +times=000D7 +timesb=022A0 +timesbar=02A31 +timesd=02A30 +tint=0222D +toea=02928 +top=022A4 +topbot=02336 +topcir=02AF1 +topf=1D565 +topfork=02ADA +tosa=02929 +tprime=02034 +trade=02122 +triangle=025B5 +triangledown=025BF +triangleleft=025C3 +trianglelefteq=022B4 +triangleq=0225C +triangleright=025B9 +trianglerighteq=022B5 +tridot=025EC +trie=0225C +triminus=02A3A +triplus=02A39 +trisb=029CD +tritime=02A3B +trpezium=023E2 +tscr=1D4C9 +tscy=00446 +tshcy=0045B +tstrok=00167 +twixt=0226C +twoheadleftarrow=0219E +twoheadrightarrow=021A0 +uArr=021D1 +uHar=02963 +uacute=000FA +uarr=02191 +ubrcy=0045E +ubreve=0016D +ucirc=000FB +ucy=00443 +udarr=021C5 +udblac=00171 +udhar=0296E +ufisht=0297E +ufr=1D532 +ugrave=000F9 +uharl=021BF +uharr=021BE +uhblk=02580 +ulcorn=0231C +ulcorner=0231C +ulcrop=0230F +ultri=025F8 +umacr=0016B +uml=000A8 +uogon=00173 +uopf=1D566 +uparrow=02191 +updownarrow=02195 +upharpoonleft=021BF +upharpoonright=021BE +uplus=0228E +upsi=003C5 +upsih=003D2 +upsilon=003C5 +upuparrows=021C8 +urcorn=0231D +urcorner=0231D +urcrop=0230E +uring=0016F +urtri=025F9 +uscr=1D4CA +utdot=022F0 +utilde=00169 +utri=025B5 +utrif=025B4 +uuarr=021C8 +uuml=000FC +uwangle=029A7 +vArr=021D5 +vBar=02AE8 +vBarv=02AE9 +vDash=022A8 +vangrt=0299C +varepsilon=003F5 +varkappa=003F0 +varnothing=02205 +varphi=003D5 +varpi=003D6 +varpropto=0221D +varr=02195 +varrho=003F1 +varsigma=003C2 +vartheta=003D1 +vartriangleleft=022B2 +vartriangleright=022B3 +vcy=00432 +vdash=022A2 +vee=02228 +veebar=022BB +veeeq=0225A +vellip=022EE +verbar=0007C +vert=0007C +vfr=1D533 +vltri=022B2 +vopf=1D567 +vprop=0221D +vrtri=022B3 +vscr=1D4CB +vzigzag=0299A +wcirc=00175 +wedbar=02A5F +wedge=02227 +wedgeq=02259 +weierp=02118 +wfr=1D534 +wopf=1D568 +wp=02118 +wr=02240 +wreath=02240 +wscr=1D4CC +xcap=022C2 +xcirc=025EF +xcup=022C3 +xdtri=025BD +xfr=1D535 +xhArr=027FA +xharr=027F7 +xi=003BE +xlArr=027F8 +xlarr=027F5 +xmap=027FC +xnis=022FB +xodot=02A00 +xopf=1D569 +xoplus=02A01 +xotime=02A02 +xrArr=027F9 +xrarr=027F6 +xscr=1D4CD +xsqcup=02A06 +xuplus=02A04 +xutri=025B3 +xvee=022C1 +xwedge=022C0 +yacute=000FD +yacy=0044F +ycirc=00177 +ycy=0044B +yen=000A5 +yfr=1D536 +yicy=00457 +yopf=1D56A +yscr=1D4CE +yucy=0044E +yuml=000FF +zacute=0017A +zcaron=0017E +zcy=00437 +zdot=0017C +zeetrf=02128 +zeta=003B6 +zfr=1D537 +zhcy=00436 +zigrarr=021DD +zopf=1D56B +zscr=1D4CF +zwj=0200D +zwnj=0200C diff --git a/server/src/org/jsoup/nodes/package-info.java b/server/src/org/jsoup/nodes/package-info.java new file mode 100644 index 0000000000..24b12803ff --- /dev/null +++ b/server/src/org/jsoup/nodes/package-info.java @@ -0,0 +1,4 @@ +/** + HTML document structure nodes. + */ +package org.jsoup.nodes;
\ No newline at end of file |