All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.fleeksoft.ksoup.nodes.Element.kt Maven / Gradle / Ivy

package com.fleeksoft.ksoup.nodes

import com.fleeksoft.ksoup.helper.ChangeNotifyingArrayList
import com.fleeksoft.ksoup.helper.Validate
import com.fleeksoft.ksoup.internal.Normalizer.normalize
import com.fleeksoft.ksoup.internal.StringUtil
import com.fleeksoft.ksoup.jsSupportedRegex
import com.fleeksoft.ksoup.nodes.TextNode.Companion.lastCharIsWhitespace
import com.fleeksoft.ksoup.parser.ParseSettings
import com.fleeksoft.ksoup.parser.Parser
import com.fleeksoft.ksoup.parser.Tag
import com.fleeksoft.ksoup.parser.TokenQueue.Companion.escapeCssIdentifier
import com.fleeksoft.ksoup.ported.AtomicBoolean
import com.fleeksoft.ksoup.ported.Collections
import com.fleeksoft.ksoup.ported.Consumer
import com.fleeksoft.ksoup.ported.PatternSyntaxException
import com.fleeksoft.ksoup.select.*
import okio.IOException
import kotlin.js.JsName
import kotlin.jvm.JvmOverloads

/**
 * An 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.
 */

public open class Element : Node {
    private var tag: Tag
    private var _baseUri: String? = null // just for clone

    // points to child elements shadowed from node children
    private var shadowChildrenRef: List? = null
    internal var childNodes: MutableList = EmptyNodes

    // field is nullable but all methods for attributes are non-null
    internal var attributes: Attributes? = null

    /**
     * Create a new, standalone element, in the specified namespace.
     * @param tag tag name
     * @param namespace namespace for this element
     */
    public constructor(tag: String, namespace: String) : this(
        Tag.valueOf(
            tag,
            namespace,
            ParseSettings.preserveCase,
        ),
        null,
    )

    /**
     * Create a new, standalone element, in the HTML namespace.
     * @param tag tag name
     * @see .Element
     */
    public constructor(tag: String) : this(
        Tag.valueOf(
            tag,
            Parser.NamespaceHtml,
            ParseSettings.preserveCase,
        ),
        "",
        null,
    )

    /**
     * Create a new, standalone Element. (Standalone in that it has no parent.)
     *
     * @param tag tag of this element
     * @param baseUri the base URI (optional, may be null to inherit from parent, or "" to clear parent's)
     * @param attributes initial attributes (optional, may be null)
     * @see #appendChild(Node)
     * @see #appendElement(String)
     */
    public constructor(tag: Tag, baseUri: String?, attributes: Attributes?) {
        childNodes = EmptyNodes.toMutableList()
        this.attributes = attributes
        this.tag = tag
        _baseUri = baseUri
        if (baseUri != null) this.setBaseUri(baseUri)
    }

    /**
     * Create a new Element from a Tag and a base URI.
     *
     * @param tag element tag
     * @param baseUri the base URI of this element. Optional, and will inherit from its parent, if any.
     * @see Tag.valueOf
     */
    public constructor(tag: Tag, baseUri: String?) : this(tag, baseUri, null)

    /**
     * Internal test to check if a nodelist object has been created.
     */
    public fun hasChildNodes(): Boolean {
        return childNodes != EmptyNodes
    }

    public override fun ensureChildNodes(): MutableList {
        if (childNodes == EmptyNodes) {
            childNodes = NodeList(owner = this, initialCapacity = 4) as MutableList
        }
        return childNodes
    }

    public override fun hasAttributes(): Boolean {
        return attributes != null
    }

    override fun attributes(): Attributes {
        if (attributes == null) {
            // not using hasAttributes, as doesn't clear warning
            attributes = Attributes()
        }
        return attributes!!
    }

    override fun baseUri(): String {
        return searchUpForAttribute(this, BaseUriKey)
    }

    public override fun doSetBaseUri(baseUri: String?) {
        attributes().put(BaseUriKey, baseUri)
    }

    override fun childNodeSize(): Int {
        return childNodes.size
    }

    override fun nodeName(): String {
        return tag.name
    }

    /**
     * Get the name of the tag for this element. E.g. `div`. If you are using [ case preserving parsing][ParseSettings.preserveCase], this will return the source's original case.
     *
     * @return the tag name
     */
    public fun tagName(): String {
        return tag.name
    }

    /**
     * Get the normalized name of this Element's tag. This will always be the lower-cased version of the tag, regardless
     * of the tag case preserving setting of the parser. For e.g., `
` and `
` both have a * normal name of `div`. * @return normal name */ override fun normalName(): String { return tag.normalName() } /** * Test if this Element has the specified normalized name, and is in the specified namespace. * @param normalName a normalized element name (e.g. `div`). * @param namespace the namespace * @return true if the element's normal name matches exactly, and is in the specified namespace * @since 1.17.2 */ @JsName("elementIs") public fun elementIs( normalName: String?, namespace: String, ): Boolean { return tag.normalName() == normalName && tag.namespace() == namespace } /** * Change (rename) the tag of this element. For example, convert a `` to a `
` with * `el.tagName("div");`. * * @param tagName new tag name for this element * @param namespace the new namespace for this element * @return this element, for chaining * @see Elements.tagName */ /** * Change (rename) the tag of this element. For example, convert a `` to a `
` with * `el.tagName("div");`. * * @param tagName new tag name for this element * @return this element, for chaining * @see Elements.tagName */ @JvmOverloads public fun tagName( tagName: String, namespace: String = tag.namespace(), ): Element { Validate.notEmptyParam(tagName, "tagName") Validate.notEmptyParam(namespace, "namespace") tag = Tag.valueOf( tagName, namespace, NodeUtils.parser(this).settings(), ) // maintains the case option of the original parse return this } /** * Get the Tag for this element. * * @return the tag object */ public fun tag(): Tag { return tag } public fun isBlock(): Boolean = tag.isBlock /** * Get the `id` attribute of this element. * * @return The id attribute, if present, or an empty string if not. */ public fun id(): String { return if (attributes != null) attributes!!.getIgnoreCase("id") else "" } /** * Set the `id` attribute of this element. * @param id the ID value to use * @return this Element, for chaining */ public fun id(id: String): Element { attr("id", id) return this } /** * 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 */ override fun attr( attributeKey: String, attributeValue: String?, ): Element { super.attr(attributeKey, attributeValue) return this } /** * Set a boolean attribute value on this element. Setting to `true` sets the attribute value to "" and * marks the attribute as boolean so no value is written out. Setting to `false` removes the attribute * with the same key if it exists. * * @param attributeKey the attribute key * @param attributeValue the attribute value * * @return this element */ public fun attr( attributeKey: String, attributeValue: Boolean, ): Element { attributes().put(attributeKey, attributeValue) return this } /** * Get an Attribute by key. Changes made via [Attribute.setKey], [Attribute.setValue] etc * will cascade back to this Element. * @param key the (case-sensitive) attribute key * @return the Attribute for this key, or null if not present. */ public fun attribute(key: String?): Attribute? { return if (hasAttributes()) attributes().attribute(key) else null } /** * Get this element's HTML5 custom data attributes. Each attribute in the element that has a key * starting with "data-" is included the dataset. * * * E.g., the element `
...` has the dataset * `package=com.fleeksoft.ksoup, language=java`. * * * 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. * * * You can find elements that have data attributes using the `[^data-]` attribute key prefix selector. * @return a map of `key=value` custom data attributes. */ public fun dataset(): Attributes.Dataset { return attributes().dataset() } override fun parent(): Element? { return parentNode as? Element } /** * Get this element's parent and ancestors, up to the document root. * @return this element's stack of parents, starting with the closest first. */ public fun parents(): Elements { val parents = Elements() var parent = parent() while (parent != null && !parent.nameIs("#root")) { parents.add(parent) parent = parent.parent() } return parents } /** * Get a child element of this element, by its 0-based index number. * * * 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, otherwise throws an `IndexOutOfBoundsException` * @see .childNode */ public fun child(index: Int): Element { return childElementsList()[index] } /** * Get the number of child nodes of this element that are elements. * * * This method works on the same filtered list like [.child]. Use [.childNodes] and [ ][.childNodeSize] to get the unfiltered Nodes (e.g. includes TextNodes etc.) * * * @return the number of child nodes that are elements * @see .children * @see .child */ public fun childrenSize(): Int { return childElementsList().size } /** * Get this element's child elements. * * * This is effectively a filter on [.childNodes] to get Element nodes. * * @return child elements. If this element has no children, returns an empty list. * @see .childNodes */ public fun children(): Elements { return Elements(childElementsList()) } /** * Maintains a shadow copy of this element's child elements. If the nodelist is changed, this cache is invalidated. * TODO - think about pulling this out as a helper as there are other shadow lists (like in Attributes) kept around. * @return a list of child elements */ public fun childElementsList(): List { if (childNodeSize() == 0) return EmptyChildren // short circuit creating empty var children: MutableList? = null if (shadowChildrenRef != null) { children = shadowChildrenRef!!.toMutableList() } if (shadowChildrenRef == null || children == null) { val size = childNodes.size children = ArrayList(size) for (i in 0 until size) { val node: Node = childNodes[i] if (node is Element) children.add(node) } shadowChildrenRef = children } return children } /** * Clears the cached shadow child elements. */ override fun nodelistChanged() { super.nodelistChanged() shadowChildrenRef = null } /** * Returns a Stream of this Element and all of its descendant Elements. The stream has document order. * @return a stream of this element and its descendants. * @see .nodeStream */ public fun stream(): Sequence { return NodeUtils.stream(this, Element::class) } /** * Get this element's child text nodes. The list is unmodifiable but the text nodes may be manipulated. * * * This is effectively a filter on [.childNodes] to get Text nodes. * @return child text nodes. If this element has no text nodes, returns an * empty list. * * For example, with the input HTML: `

One Two Three
Four

` with the `p` element selected: * * * `p.text()` = `"One Two Three Four"` * * `p.ownText()` = `"One Three Four"` * * `p.children()` = `Elements[,
]` * * `p.childNodes()` = `List["One ", , " Three ",
, " Four"]` * * `p.textNodes()` = `List["One ", " Three ", " Four"]` * */ public fun textNodes(): List { val textNodes: MutableList = ArrayList() for (node in childNodes) { if (node is TextNode) textNodes.add(node) } return Collections.unmodifiableList(textNodes) } /** * Get this element's child data nodes. The list is unmodifiable but the data nodes may be manipulated. * * * This is effectively a filter on [.childNodes] to get Data nodes. * * @return child data nodes. If this element has no data nodes, returns an * empty list. * @see .data */ public fun dataNodes(): List { val dataNodes: MutableList = ArrayList() for (node in childNodes) { if (node is DataNode) dataNodes.add(node) } return Collections.unmodifiableList(dataNodes) } /** * Find elements that match the [Selector] CSS query, with this element as the starting context. Matched elements * may include this element, or any of its children. * * This method is generally more powerful to use than the DOM-type `getElementBy*` methods, because * multiple filters can be combined, e.g.: * * * `el.select("a[href]")` - finds links (`a` tags with `href` attributes) * * `el.select("a[href*=example.com]")` - finds links pointing to example.com (loosely) * * * See the query syntax documentation in [com.fleeksoft.ksoup.select.Selector]. * * Also known as `querySelectorAll()` in the Web DOM. * * @param cssQuery a [Selector] CSS-like query * @return an [Elements] list containing elements that match the query (empty if none match) * @see Selector selector query syntax * * @see QueryParser.parse * @throws Selector.SelectorParseException (unchecked) on an invalid CSS query. */ public fun select(cssQuery: String): Elements { return Selector.select(cssQuery, this) } /** * Find elements that match the supplied Evaluator. This has the same functionality as [.select], but * may be useful if you are running the same query many times (on many documents) and want to save the overhead of * repeatedly parsing the CSS query. * @param evaluator an element evaluator * @return an [Elements] list containing elements that match the query (empty if none match) */ internal fun select(evaluator: Evaluator): Elements { return Selector.select(evaluator, this) } /** * Find the first Element that matches the [Selector] CSS query, with this element as the starting context. * * This is effectively the same as calling `element.select(query).first()`, but is more efficient as query * execution stops on the first hit. * * Also known as `querySelector()` in the Web DOM. * @param cssQuery cssQuery a [Selector] CSS-like query * @return the first matching element, or **`null`** if there is no match. * @see .expectFirst */ public fun selectFirst(cssQuery: String): Element? { return Selector.selectFirst(cssQuery, this) } /** * Finds the first Element that matches the supplied Evaluator, with this element as the starting context, or * `null` if none match. * * @param evaluator an element evaluator * @return the first matching element (walking down the tree, starting from this element), or `null` if none * match. */ internal fun selectFirst(evaluator: Evaluator): Element? { return Collector.findFirst(evaluator, this) } /** * Just like [.selectFirst], but if there is no match, throws an [IllegalArgumentException]. This * is useful if you want to simply abort processing on a failed match. * @param cssQuery a [Selector] CSS-like query * @return the first matching element * @throws IllegalArgumentException if no match is found */ public fun expectFirst(cssQuery: String): Element { return Validate.ensureNotNull( Selector.selectFirst(cssQuery, this), if (parent() != null) "No elements matched the query '$cssQuery' on element '${this.tagName()}'." else "No elements matched the query '$cssQuery' in the document.", ) as Element } /** * Checks if this element matches the given [Selector] CSS query. Also knows as `matches()` in the Web * DOM. * * @param cssQuery a [Selector] CSS query * @return if this element matches the query */ public fun `is`(cssQuery: String): Boolean { return `is`(QueryParser.parse(cssQuery)) } /** * Check if this element matches the given evaluator. * @param evaluator an element evaluator * @return if this element matches */ internal fun `is`(evaluator: Evaluator?): Boolean { return evaluator!!.matches(root(), this) } /** * Find the closest element up the tree of parents that matches the specified CSS query. Will return itself, an * ancestor, or `null` if there is no such matching element. * @param cssQuery a [Selector] CSS query * @return the closest ancestor element (possibly itself) that matches the provided evaluator. `null` if not * found. */ public fun closest(cssQuery: String): Element? { return closest(QueryParser.parse(cssQuery)) } /** * Find the closest element up the tree of parents that matches the specified evaluator. Will return itself, an * ancestor, or `null` if there is no such matching element. * @param evaluator a query evaluator * @return the closest ancestor element (possibly itself) that matches the provided evaluator. `null` if not * found. */ // @Nullable internal fun closest(evaluator: Evaluator): Element? { var el: Element? = this val root = root() do { if (evaluator.matches(root, el!!)) return el el = el.parent() } while (el != null) return null } /** * Insert a node to the end of this Element's children. The incoming node will be re-parented. * * @param child node to add. * @return this Element, for chaining * @see .prependChild * @see .insertChildren */ public fun appendChild(child: Node): Element { // was - Node#addChildren(child). short-circuits an array create and a loop. reparentChild(child) ensureChildNodes() childNodes.add(child) child.siblingIndex = childNodes.size - 1 return this } /** * Insert the given nodes to the end of this Element's children. * * @param children nodes to add * @return this Element, for chaining * @see .insertChildren */ public fun appendChildren(children: Collection): Element { insertChildren(-1, children) return this } /** * Add this element to the supplied parent element, as its next child. * * @param parent element to which this element will be appended * @return this element, so that you can continue modifying the element */ public fun appendTo(parent: Element): Element { parent.appendChild(this) return this } /** * Add a node to the start of this element's children. * * @param child node to add. * @return this element, so that you can add more child nodes or elements. */ public fun prependChild(child: Node): Element { addChildren(0, child) return this } /** * Insert the given nodes to the start of this Element's children. * * @param children nodes to add * @return this Element, for chaining * @see .insertChildren */ public fun prependChildren(children: Collection): Element { insertChildren(0, children) return this } /** * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first. * * @param index 0-based index to insert children at. Specify `0` to insert at the start, `-1` at the * end * @param children child nodes to insert * @return this element, for chaining. */ public fun insertChildren( index: Int, children: Collection, ): Element { var calculatedIndex = index val currentSize = childNodeSize() if (calculatedIndex < 0) calculatedIndex += currentSize + 1 // roll around Validate.isTrue(calculatedIndex in 0..currentSize, "Insert position out of bounds.") val nodeArray: Array = children.toTypedArray() addChildren(calculatedIndex, *nodeArray) return this } /** * Inserts the given child nodes into this element at the specified index. Current nodes will be shifted to the * right. The inserted nodes will be moved from their current parent. To prevent moving, copy the nodes first. * * @param index 0-based index to insert children at. Specify `0` to insert at the start, `-1` at the * end * @param children child nodes to insert * @return this element, for chaining. */ public fun insertChildren( index: Int, vararg children: Node, ): Element { var calculatedIndex = index val currentSize = childNodeSize() if (calculatedIndex < 0) calculatedIndex += currentSize + 1 // roll around Validate.isTrue(calculatedIndex in 0..currentSize, "Insert position out of bounds.") addChildren(calculatedIndex, *children) return this } /** * Create a new element by tag name and namespace, add it as this Element's last child. * * @param tagName the name of the tag (e.g. `div`). * @param namespace the namespace of the tag (e.g. [Parser.NamespaceHtml]) * @return the new element, in the specified namespace */ public fun appendElement( tagName: String, namespace: String = tag.namespace(), ): Element { val child = Element( Tag.valueOf(tagName, namespace, NodeUtils.parser(this).settings()), baseUri(), ) appendChild(child) return child } /** * Create a new element by tag name and namespace, and add it as this Element's first child. * * @param tagName the name of the tag (e.g. {@code div}). * @param namespace the namespace of the tag (e.g. {@link Parser#NamespaceHtml}) * @return the new element, in the specified namespace */ @JvmOverloads public fun prependElement( tagName: String, namespace: String = tag.namespace(), ): Element { val child = Element( Tag.valueOf(tagName, namespace, NodeUtils.parser(this).settings()), baseUri(), ) prependChild(child) return child } /** * Create and append a new TextNode to this element. * * @param text the (un-encoded) text to add * @return this element */ public fun appendText(text: String): Element { val node = TextNode(text) appendChild(node) return this } /** * Create and prepend a new TextNode to this element. * * @param text the decoded text to add * @return this element */ public fun prependText(text: String): Element { val node = TextNode(text) 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 */ public fun append(html: String): Element { val nodes: List = NodeUtils.parser(this).parseFragmentInput(html, this, baseUri()) addChildren(*nodes.toTypedArray()) 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 */ public fun prepend(html: String): Element { val nodes: List = NodeUtils.parser(this).parseFragmentInput(html, this, baseUri()) addChildren(0, *nodes.toTypedArray()) return this } /** * Insert the specified HTML into the DOM before this element (as a preceding sibling). * * @param html HTML to add before this element * @return this element, for chaining * @see .after */ override fun before(html: String): Element { return super.before(html) as Element } /** * Insert the specified node into the DOM before this node (as a preceding sibling). * @param node to add before this element * @return this Element, for chaining * @see .after */ override fun before(node: Node): Element { return super.before(node) as Element } /** * Insert the specified HTML into the DOM after this element (as a following sibling). * * @param html HTML to add after this element * @return this element, for chaining * @see .before */ override fun after(html: String): Element { return super.after(html) as Element } /** * Insert the specified node into the DOM after this node (as a following sibling). * @param node to add after this element * @return this element, for chaining * @see .before */ override fun after(node: Node): Element { return super.after(node) as Element } /** * Remove all the element's child nodes. Any attributes are left as-is. Each child node has its parent set to * `null`. * @return this element */ override fun empty(): Element { // Detach each of the children -> parent links: for (child in childNodes) { child.parentNode = null } childNodes.clear() return this } /** * Wrap the supplied HTML around this element. * * @param html HTML to wrap around this element, e.g. `
`. Can be arbitrarily deep. * @return this element, for chaining. */ override fun wrap(html: String): Element { return super.wrap(html) as Element } /** * Get a CSS selector that will uniquely select this element. * * * If the element has an ID, returns #id; * otherwise returns the parent (if any) CSS selector, followed by '>', * followed by a unique selector for the element (tag.class.class:nth-child(n)). * * * @return the CSS Path that can be used to retrieve the element in a selector. */ public fun cssSelector(): String { if (id().isNotEmpty()) { // prefer to return the ID - but check that it's actually unique first! val idSel = "#" + escapeCssIdentifier(id()) val doc: Document? = ownerDocument() if (doc != null) { val els: Elements = doc.select(idSel) if (els.size == 1 && els[0] === this) { // otherwise, continue to the nth-child impl return idSel } } else { return idSel // no ownerdoc, return the ID selector } } val selector: StringBuilder = StringUtil.borrowBuilder() var el: Element? = this while (el != null && el !is Document) { selector.insert(0, el.cssSelectorComponent()) el = el.parent() } return StringUtil.releaseBuilder(selector) } private fun cssSelectorComponent(): String { // Escape tagname, and translate HTML namespace ns:tag to CSS namespace syntax ns|tag val tagName: String = escapeCssIdentifier(tagName()).replace("\\:", "|") val selector: StringBuilder = StringUtil.borrowBuilder().append(tagName) // String classes = StringUtil.join(classNames().stream().map(TokenQueue::escapeCssIdentifier).iterator(), "."); val escapedClasses: StringUtil.StringJoiner = StringUtil.StringJoiner(".") for (name in classNames()) escapedClasses.add(escapeCssIdentifier(name)) val classes: String = escapedClasses.complete() if (classes.isNotEmpty()) selector.append('.').append(classes) val parent: Element? = parent() if (parent == null || parent is Document) { // don't add Document to selector, as will always have a html node return StringUtil.releaseBuilder(selector) } selector.insert(0, " > ") if (parent.select(selector.toString()).size > 1) { selector.append(":nth-child(${elementSiblingIndex() + 1})") } return StringUtil.releaseBuilder(selector) } /** * 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 fun siblingElements(): Elements { if (parentNode == null) return Elements() val elements = (parentNode as Element).childElementsList() val siblings = Elements() for (el in elements) if (el != this) siblings.add(el) return siblings } /** * Gets the next sibling element of this element. E.g., if a `div` contains two `p`s, * the `nextElementSibling` of the first `p` is the second `p`. * * * This is similar to [.nextSibling], but specifically finds only Elements * * @return the next element, or null if there is no next element * @see .previousElementSibling */ // @Nullable public fun nextElementSibling(): Element? { var next: Node = this while (next.nextSibling()?.also { next = it } != null) { if (next is Element) return next as Element } return null } /** * Get each of the sibling elements that come after this element. * * @return each of the element siblings after this element, or an empty list if there are no next sibling elements */ public fun nextElementSiblings(): Elements { return nextElementSiblings(true) } /** * Gets the previous element sibling of this element. * @return the previous element, or null if there is no previous element * @see .nextElementSibling */ // @Nullable public fun previousElementSibling(): Element? { var prev: Node = this while (prev.previousSibling()?.also { prev = it } != null) { if (prev is Element) return prev as Element } return null } /** * Get each of the element siblings before this element. * * @return the previous element siblings, or an empty list if there are none. */ public fun previousElementSiblings(): Elements { return nextElementSiblings(false) } private fun nextElementSiblings(next: Boolean): Elements { val els = Elements() if (parentNode == null) return els els.add(this) return if (next) els.nextAll() else els.prevAll() } /** * Gets the first Element sibling of this element. That may be this element. * @return the first sibling that is an element (aka the parent's first element child) */ public fun firstElementSibling(): Element? { val parent: Element? = parent() return if (parent != null) { parent.firstElementChild() } else { this // orphan is its own first sibling } } /** * 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 fun elementSiblingIndex(): Int { val parent: Element? = parent() return if (parent == null) { 0 } else { indexInList( this, parent.childElementsList(), ) } } /** * Gets the last element sibling of this element. That may be this element. * @return the last sibling that is an element (aka the parent's last element child) */ public fun lastElementSibling(): Element? { val parent: Element? = parent() return if (parent != null) { parent.lastElementChild() } else { this } } /** * Gets the first child of this Element that is an Element, or `null` if there is none. * @return the first Element child node, or null. * @see .firstChild * @see .lastElementChild */ public fun firstElementChild(): Element? { var child: Node? = firstChild() while (child != null) { if (child is Element) return child child = child.nextSibling() } return null } /** * Gets the last child of this Element that is an Element, or @{code null} if there is none. * @return the last Element child node, or null. * @see .lastChild * @see .firstElementChild */ public fun lastElementChild(): Element? { var child: Node? = lastChild() while (child != null) { if (child is Element) return child child = child.previousSibling() } 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 fun getElementsByTag(tagName: String?): Elements { Validate.notEmpty(tagName) val normalizedTagName = normalize(tagName) return Collector.collect(Evaluator.Tag(normalizedTagName), this) } /** * Find an element by ID, including or under this element. * * * 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 [Document.getElementById] * @param id The ID to search for. * @return The first matching element by ID, starting with this element, or null if none found. */ // @Nullable public fun getElementById(id: String): Element? { Validate.notEmpty(id) val elements: Elements = Collector.collect(Evaluator.Id(id), this) return if (elements.size > 0) elements[0] else null } /** * Find elements that have this class, including or under this element. Case-insensitive. * * * Elements can have multiple classes (e.g. `
`). This method * checks each class, so you can find the above with `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 * @see .classNames */ public fun getElementsByClass(className: String): Elements { Validate.notEmpty(className) return Collector.collect(Evaluator.Class(className), this) } /** * Find elements that have a named attribute set. Case-insensitive. * * @param key name of the attribute, e.g. `href` * @return elements that have this attribute, empty if none */ public fun getElementsByAttribute(key: String): Elements { Validate.notEmpty(key) val trimmedKey = key.trim { it <= ' ' } return Collector.collect(Evaluator.Attribute(trimmedKey), this) } /** * Find elements that have an attribute name starting with the supplied prefix. Use `data-` to find elements * that have HTML5 datasets. * @param keyPrefix name prefix of the attribute e.g. `data-` * @return elements that have attribute names that start with the prefix, empty if none. */ public fun getElementsByAttributeStarting(keyPrefix: String): Elements { Validate.notEmpty(keyPrefix) val trimmedKeyPrefix = keyPrefix.trim { it <= ' ' } return Collector.collect(Evaluator.AttributeStarting(trimmedKeyPrefix), 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 fun getElementsByAttributeValue( key: String, value: String, ): Elements { return Collector.collect(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 fun getElementsByAttributeValueNot( key: String, value: String, ): Elements { return Collector.collect(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 fun getElementsByAttributeValueStarting( key: String, valuePrefix: String, ): Elements { return Collector.collect(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 fun getElementsByAttributeValueEnding( key: String, valueSuffix: String, ): Elements { return Collector.collect(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 fun getElementsByAttributeValueContaining( key: String, match: String, ): Elements { return Collector.collect(Evaluator.AttributeWithValueContaining(key, match), this) } /** * Find elements that have an attribute whose value matches 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 fun getElementsByAttributeValueMatching( key: String, regex: Regex, ): Elements { return Collector.collect(Evaluator.AttributeWithValueMatching(key, regex), 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 [embedded flags](http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded) (such as (?i) and (?m) to control regex options. * @return elements that have attributes matching this regular expression */ public fun getElementsByAttributeValueMatching( key: String, regex: String, ): Elements { val pattern: Regex = try { jsSupportedRegex(regex) } catch (e: PatternSyntaxException) { throw 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 fun getElementsByIndexLessThan(index: Int): Elements { return Collector.collect(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 fun getElementsByIndexGreaterThan(index: Int): Elements { return Collector.collect(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 fun getElementsByIndexEquals(index: Int): Elements { return Collector.collect(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 fun getElementsContainingText(searchText: String): Elements { return Collector.collect(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 fun getElementsContainingOwnText(searchText: String): Elements { return Collector.collect(Evaluator.ContainsOwnText(searchText), this) } /** * Find elements whose text matches the supplied regular expression. * @param regex regular expression to match text against * @return elements matching the supplied regular expression. * @see Element.text */ public fun getElementsMatchingText(regex: Regex): Elements { return Collector.collect(Evaluator.Matches(regex), this) } /** * Find elements whose text matches the supplied regular expression. * @param regex regular expression to match text against. You can use [embedded flags](http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded) (such as (?i) and (?m) to control regex options. * @return elements matching the supplied regular expression. * @see Element.text */ public fun getElementsMatchingText(regex: String): Elements { val pattern: Regex = try { jsSupportedRegex(regex) } catch (e: PatternSyntaxException) { throw IllegalArgumentException("Pattern syntax error: $regex", e) } return getElementsMatchingText(pattern) } /** * Find elements whose own text matches the supplied regular expression. * @param regex regular expression to match text against * @return elements matching the supplied regular expression. * @see Element.ownText */ public fun getElementsMatchingOwnText(regex: Regex): Elements { return Collector.collect(Evaluator.MatchesOwn(regex), this) } /** * Find elements whose own text matches the supplied regular expression. * @param regex regular expression to match text against. You can use [embedded flags](http://java.sun.com/docs/books/tutorial/essential/regex/pattern.html#embedded) (such as (?i) and (?m) to control regex options. * @return elements matching the supplied regular expression. * @see Element.ownText */ public fun getElementsMatchingOwnText(regex: String): Elements { val pattern: Regex = try { jsSupportedRegex(regex) } catch (e: PatternSyntaxException) { throw IllegalArgumentException("Pattern syntax error: $regex", e) } return getElementsMatchingOwnText(pattern) } public fun getAllElements(): Elements = Collector.collect(Evaluator.AllElements(), this) /** * Gets the **normalized, combined text** of this element and all its children. Whitespace is normalized and * trimmed. * * For example, given HTML `

Hello there now!

`, `p.text()` returns `"Hello there * now!"` * * If you do not want normalized text, use [.wholeText]. If you want just the text of this node (and not * children), use [.ownText] * * Note that this method returns the textual content that would be presented to a reader. The contents of data * nodes (such as `