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

org.fernice.flare.selector.Matching.kt Maven / Gradle / Ivy

/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */
package org.fernice.flare.selector

import org.fernice.flare.dom.Element
import org.fernice.std.checkedDiv
import org.fernice.std.checkedSub

/**
 * Checks if the [selector] matches the [element]. Performs a fast reject if both the [AncestorHashes]
 * of the selector and the [BloomFilter] of the element's parent is given. See [mayMatch] for the premise
 * of fast rejecting.
 */
internal fun matchesSelector(
    selector: Selector,
    hashes: AncestorHashes?,
    element: Element,
    context: MatchingContext,
): Boolean {

    if (hashes != null) {
        context.bloomFilter?.let { bloomFilter ->
            if (!mayMatch(hashes, bloomFilter)) {
                return false
            }
        }
    }

    return matchesComplexSelector(selector.iterator(), element, context)
}

/**
 * Performs a fast reject if any of the [AncestorHashes] is not contained in the bloom filter. This work on
 * the premise that the AncestorHashes are an excerpt of all relevant components in a selector that match a parent
 * and are hashable. In combination with a [BloomFilter], filled with all corresponding hashes of all parents,
 * a selector can be fast rejected if an ancestor hash is not contained the BloomFilter as that would be the
 * requirement for them to match. This optimization does only work for selector that do have a parental combinator.
 */
private fun mayMatch(hashes: AncestorHashes, bloomFilter: BloomFilter): Boolean {
    for (i in 0 until 3) {
        val hash = hashes.packedHashes[i]
        if (hash == 0) {
            return true
        }

        if (!bloomFilter.mightContainHash(hash and HASH_BLOOM_MASK)) {
            return false
        }
    }

    val fourth = hashes.fourthHash()
    return fourth == 0 || bloomFilter.mightContainHash(fourth)
}

private enum class MatchResult {

    MATCHED,

    NOT_MATCHED_GLOBALLY,

    NOT_MATCHED_RESTART_FROM_CLOSEST_DESCENDANT,

    NOT_MATCHED_RESTART_FROM_CLOSET_LATER_SIBLING
}

private fun matchesComplexSelectors(
    selectors: List,
    element: Element,
    context: MatchingContext,
): Boolean {
    for (selector in selectors) {
        if (matchesComplexSelector(selector.iterator(), element, context)) {
            return true
        }
    }
    return false
}

private fun matchesComplexSelector(
    iterator: SelectorIterator,
    element: Element,
    context: MatchingContext,
): Boolean {
    return when (matchesComplexSelectorInternal(iterator, element, context)) {
        MatchResult.MATCHED -> true
        else -> false
    }
}

private fun matchesComplexSelectorInternal(
    iterator: SelectorIterator,
    element: Element,
    context: MatchingContext,
): MatchResult {
    val matchesCompoundSelector = matchesCompoundSelector(iterator, element, context)

    if (!matchesCompoundSelector) return MatchResult.NOT_MATCHED_RESTART_FROM_CLOSET_LATER_SIBLING

    if (!iterator.hasNextSequence()) return MatchResult.MATCHED
    val combinator = iterator.nextSequence()

    val candidateNotFound = when (combinator) {
        Combinator.NextSibling,
        Combinator.LaterSibling,
        -> MatchResult.NOT_MATCHED_RESTART_FROM_CLOSEST_DESCENDANT

        Combinator.Descendant,
        Combinator.Child,
        Combinator.Part,
        Combinator.SlotAssignment,
        Combinator.PseudoElement,
        -> MatchResult.NOT_MATCHED_GLOBALLY
    }

    var visitedHandling = when {
        combinator.isSibling() -> VisitedHandlingMode.AllLinksUnvisited
        else -> context.visitedHandling
    }

    var nextElement = element
    while (true) {
        if (nextElement.isLink()) {
            visitedHandling = VisitedHandlingMode.AllLinksUnvisited
        }

        nextElement = nextElementForCombinator(nextElement, combinator)
            ?: return candidateNotFound

        val result = context.withVisitedHandling(visitedHandling) {
            matchesComplexSelectorInternal(iterator.clone(), nextElement, context)
        }

        when {
            result == MatchResult.MATCHED ||
                    result == MatchResult.NOT_MATCHED_GLOBALLY ||
                    combinator == Combinator.NextSibling -> {
                return result
            }

            combinator == Combinator.PseudoElement ||
                    combinator == Combinator.Child -> {
                return MatchResult.NOT_MATCHED_RESTART_FROM_CLOSEST_DESCENDANT
            }

            result == MatchResult.NOT_MATCHED_RESTART_FROM_CLOSEST_DESCENDANT &&
                    combinator == Combinator.LaterSibling -> {
                return result
            }
        }

    }
}

private fun nextElementForCombinator(element: Element, combinator: Combinator): Element? {
    return when (combinator) {
        Combinator.NextSibling, Combinator.LaterSibling -> element.previousSibling
        Combinator.Child, Combinator.Descendant -> element.parent
        Combinator.Part -> null
        Combinator.SlotAssignment -> null
        Combinator.PseudoElement -> element.owner
    }
}

private fun matchesRelativeSelectors(
    selectors: List,
    element: Element,
    context: MatchingContext,
): Boolean {
    for ((selector, matchHint) in selectors) {
        var (traverseSubtree, traverseSiblings, nextElement) = when (matchHint) {
            RelativeSelectorMatchHint.InChild -> Triple(false, true, element.firstChild)
            RelativeSelectorMatchHint.InSubtree -> Triple(true, true, element.firstChild)
            RelativeSelectorMatchHint.InSibling -> Triple(false, true, element.nextSibling)
            RelativeSelectorMatchHint.InSiblingSubtree -> Triple(true, true, element.nextSibling)
            RelativeSelectorMatchHint.InNextSibling -> Triple(false, false, element.nextSibling)
            RelativeSelectorMatchHint.InNextSiblingSubtree -> Triple(true, false, element.nextSibling)
        }
        while (nextElement != null) {
            if (matchesComplexSelector(selector.iterator(), nextElement, context)) return true
            if (traverseSubtree && matchesRelativeSelectorSubtree(selector, element, context)) return true
            if (!traverseSiblings) break
            nextElement = nextElement.nextSibling
        }
    }

    return false
}

private fun matchesRelativeSelectorSubtree(
    selector: Selector,
    element: Element,
    context: MatchingContext,
): Boolean {
    var nextElement = element.firstChild
    while (nextElement != null) {
        if (matchesComplexSelector(selector.iterator(), element, context)) return true
        if (matchesRelativeSelectorSubtree(selector, element, context)) return true
        nextElement = element.nextSibling
    }
    return false
}

private fun matchesCompoundSelector(iterator: SelectorIterator, element: Element, context: MatchingContext): Boolean {
    var nextSelector = iterator.nextOrNull() ?: return true

    if (nextSelector is Component.LocalName) {
        if (!matchesLocalName(element, nextSelector.localName, nextSelector.localNameLower)) return false

        nextSelector = iterator.nextOrNull() ?: return true
    }

    if (nextSelector is Component.ID) {
        if (!element.hasID(nextSelector.id)) return false

        nextSelector = iterator.nextOrNull() ?: return true
    }

    while (nextSelector is Component.Class) {
        if (!element.hasClass(nextSelector.styleClass)) return false

        nextSelector = iterator.nextOrNull() ?: return true
    }

    while (true) {
        if (!matchesSimpleSelector(nextSelector, element, context)) return false

        nextSelector = iterator.nextOrNull() ?: return true
    }
}

private fun matchesSimpleSelector(component: Component, element: Element, context: MatchingContext): Boolean {
    return when (component) {
        is Component.ID -> element.hasID(component.id)
        is Component.Class -> element.hasClass(component.styleClass)
        is Component.LocalName -> matchesLocalName(element, component.localName, component.localNameLower)

        is Component.DefaultNamespace -> element.namespace == component.namespace
        is Component.Namespace -> element.namespace == component.namespace
        is Component.ExplicitNoNamespace -> element.namespace == null
        is Component.ExplicitUniversalType,
        is Component.ExplicitAnyNamespace,
        -> true

        is Component.Part -> false
        is Component.Slotted -> false

        is Component.NonTSPseudoClass -> element.matchNonTSPseudoClass(component.pseudoClass)
        is Component.NonTSFPseudoClass -> element.matchNonTSFPseudoClass(component.pseudoClass)
        is Component.PseudoElement -> element.matchPseudoElement(component.pseudoElement)

        is Component.Root -> element.isRoot()
        is Component.Empty -> element.isEmpty()
        is Component.Host -> false

        is Component.Nth -> {
            if (component.selectors.isEmpty()) {
                matchesGenericNthChild(element, context, component.data, component.selectors)
            } else {
                context.nest {
                    matchesGenericNthChild(element, context, component.data, component.selectors)
                }
            }
        }

        is Component.Is -> context.nest { matchesComplexSelectors(component.selectors, element, context) }
        is Component.Where -> context.nest { matchesComplexSelectors(component.selectors, element, context) }

        is Component.Negation -> context.nestForNegation { !matchesComplexSelectors(component.selectors, element, context) }

        is Component.Has -> context.nestForRelativeSelector(element) { matchesRelativeSelectors(component.selectors, element, context) }

        is Component.Scope -> element.isRoot()

        is Component.AttributeOther,
        is Component.AttributeInNoNamespaceExists,
        is Component.AttributeInNoNamespace,
        -> false

        is Component.RelativeSelectorAnchor -> {
            val anchor = context.relativeSelectorAnchor ?: error("relative selector outside of relative context")

            element === anchor
        }

        is Component.Combinator -> error("combinator in simple selector")
        is Component.ParentSelector -> error("not replaced & in selector")
    }
}

private fun matchesLocalName(element: Element, localName: String, localNameLower: String): Boolean {
    return element.localName == localNameLower
}

private fun matchesGenericNthChild(
    element: Element,
    context: MatchingContext,
    nthData: NthData,
    selectors: List,
): Boolean {
    val (type, a, b) = nthData
    val ofType = type.isOfType
    if (type.isOnly) {
        return matchesGenericNthChild(element, context, NthData.first(ofType), selectors)
                && matchesGenericNthChild(element, context, NthData.last(ofType), selectors)
    }

    val fromEnd = type.isFromEnd

    val edgeChildSelector = a == 0 && b == 0 && !ofType && selectors.isEmpty()

    if (selectors.isNotEmpty() && matchesComplexSelectors(selectors, element, context)) {
        return false
    }

    if (edgeChildSelector) {
        val sibling = when {
            fromEnd -> element.nextSibling
            else -> element.previousSibling
        }
        return sibling == null
    }

    val index = nthChildIndex(element, context, selectors, ofType, fromEnd)

    return when (val an = index.checkedSub(b)) {
        null -> false
        else -> when (val n = an.checkedDiv(a)) {
            null -> an == 0
            else -> n >= 0 && a * n == an
        }
    }
}

private fun nthChildIndex(
    element: Element,
    context: MatchingContext,
    selectors: List,
    ofType: Boolean,
    fromEnd: Boolean,
): Int {
    fun next(element: Element): Element? {
        return when {
            fromEnd -> element.nextSibling
            else -> element.previousSibling
        }
    }

    var index = 1
    var current = element

    while (true) {
        current = next(current) ?: break

        val matches = when {
            ofType -> isSameType(element, current)
            selectors.isNotEmpty() -> matchesComplexSelectors(selectors, current, context)
            else -> true
        }

        if (!matches) continue

        index++
    }

    return index
}

private fun isSameType(element: Element, other: Element): Boolean {
    return element.localName == other.localName && element.namespace == other.namespace
}

@Suppress("NOTHING_TO_INLINE")
private inline fun  Iterator.nextOrNull(): E? = if (hasNext()) next() else null




© 2015 - 2025 Weber Informatics LLC | Privacy Policy