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

org.fernice.flare.style.properties.cascade.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.style.properties

import org.fernice.flare.dom.Device
import org.fernice.flare.dom.Element
import org.fernice.flare.font.FontMetricsProvider
import org.fernice.flare.selector.PseudoElement
import org.fernice.flare.style.ComputedValues
import org.fernice.flare.style.StyleBuilder
import org.fernice.flare.style.properties.custom.Name
import org.fernice.flare.style.properties.custom.SubstitutionCache
import org.fernice.flare.style.properties.custom.VariableValue
import org.fernice.flare.style.ruletree.CascadeLevel
import org.fernice.flare.style.ruletree.RuleNode
import org.fernice.flare.style.PerOrigin
import org.fernice.flare.style.value.Context

data class DeclarationAndCascadeLevel(val declaration: PropertyDeclaration, val cascadeLevel: CascadeLevel)

fun cascade(
    device: Device,
    element: Element,
    pseudoElement: PseudoElement?,
    ruleNode: RuleNode,
    previousStyle: ComputedValues?,
    parentStyle: ComputedValues?,
    parentStyleIgnoringFirstLine: ComputedValues?,
    fontMetricsProvider: FontMetricsProvider,
): ComputedValues {
    val declarations = ruleNode.selfAndAncestors().flatMap { node ->
        val level = node.priority.level

        val sequence = when (val source = node.source?.get()) {
            null -> emptySequence()
            else -> source.declarations.asSequence(level.importance, reversed = true)
        }

        sequence.map { DeclarationAndCascadeLevel(it, level) }
    }

    return applyDeclarations(
        device,
        element,
        pseudoElement,
        ruleNode,
        declarations,
        previousStyle,
        parentStyle,
        parentStyleIgnoringFirstLine,
        fontMetricsProvider
    )
}

fun applyDeclarations(
    device: Device,
    element: Element,
    pseudoElement: PseudoElement?,

    ruleNode: RuleNode,
    declarations: Sequence,

    previousStyle: ComputedValues?,
    parentStyle: ComputedValues?,
    parentStyleIgnoringFirstLine: ComputedValues?,

    fontMetricsProvider: FontMetricsProvider,
): ComputedValues {
    val inheritedStyle = parentStyle ?: device.defaultComputedValues()

    val customProperties = buildCustomProperties(previousStyle?.customProperties, inheritedStyle.customProperties) {
        process(declarations)
    }

    val properties = buildProperties(ruleNode, previousStyle, customProperties) {
        process(declarations)
    }

    val context = Context(
        element.isRoot(),
        StyleBuilder.from(
            device,

            customProperties,
            properties,

            ruleNode,

            parentStyle,
            parentStyleIgnoringFirstLine
        ),
        fontMetricsProvider,
    )

    for ((longhandId, declaration) in properties) {
        if (!longhandId.isEarlyProperty) {
            continue
        }

        longhandId.cascadeProperty(declaration, context)
    }

    for ((longhandId, declaration) in properties) {
        if (longhandId.isEarlyProperty) {
            continue
        }

        longhandId.cascadeProperty(declaration, context)
    }

    return context.builder.build()
}

private inline fun buildCustomProperties(
    previous: CustomPropertiesList?,
    inherited: CustomPropertiesList?,
    block: CustomPropertiesListBuilder.() -> Unit,
): CustomPropertiesList? {
    val builder = CustomPropertiesListBuilder(previous, inherited)
    builder.block()
    return builder.build()
}

class CustomPropertiesList(
    properties: Map,
) : Iterable> {

    private val properties: List> = properties.toList().sortedBy { (name, _) -> name }

    fun get(name: Name): VariableValue? {
        val index = properties.binarySearch { (candidate, _) -> candidate.compareTo(name) }
        return properties.getOrNull(index)?.second
    }

    override fun iterator(): Iterator> = properties.iterator()

    fun toMap(): Map = properties.toMap()
    fun toMutableMap(): MutableMap = properties.toMap(mutableMapOf())

    fun isCompatible(properties: Map): Boolean {
        if (properties.size != this.properties.size) return false
        for ((name, value) in this.properties) {
            if (properties[name] !== value) return false
        }
        return true
    }
}

class CustomPropertiesListBuilder(
    private val previous: CustomPropertiesList?,
    private val inherited: CustomPropertiesList?,
) {
    private var customProperties: MutableMap? = null

    fun process(
        declarations: Sequence,
    ) {
        val seen = mutableSetOf()
        val reverted = PerOrigin { mutableSetOf() }

        for ((declaration, level) in declarations) {
            if (declaration !is PropertyDeclaration.Custom) continue

            val (name, value) = declaration.declaration

            val origin = level.origin
            if (reverted.peek(origin)?.contains(name) == true) {
                continue
            }

            if (!seen.add(name)) {
                continue
            }

            if (!valueMayAffectStyle(name, value)) {
                continue
            }

            var customProperties = this.customProperties
            if (customProperties == null) {
                customProperties = inherited?.toMutableMap() ?: mutableMapOf()
                this.customProperties = customProperties
            }

            when (value) {
                is CustomDeclarationValue.Value -> {
                    customProperties[name] = value.value
                }

                is CustomDeclarationValue.CssWideKeyword -> {
                    when (value.keyword) {
                        CssWideKeyword.Revert -> {
                            seen.remove(name)
                            for (relevantOrigin in origin.selfAndPrevious()) {
                                reverted.get(relevantOrigin).add(name)
                            }
                        }

                        CssWideKeyword.Initial -> {
                            customProperties.remove(name)
                        }

                        CssWideKeyword.Unset,
                        CssWideKeyword.Inherit,
                        -> error("should have already been handled in valueMayAffectStyle()")
                    }
                }
            }
        }
    }

    private fun valueMayAffectStyle(name: Name, value: CustomDeclarationValue): Boolean {
        if (value is CustomDeclarationValue.CssWideKeyword) {
            when (value.keyword) {
                CssWideKeyword.Unset,
                CssWideKeyword.Inherit,
                -> return false

                else -> {}
            }
        }

        val existingValue = customProperties?.get(name) ?: inherited?.get(name)

        if (existingValue == null && value is CustomDeclarationValue.CssWideKeyword && value.keyword == CssWideKeyword.Initial) {
            return false
        }

        if (existingValue != null && value is CustomDeclarationValue.Value && existingValue === value.value) {
            return false
        }

        return true
    }

    fun build(): CustomPropertiesList? {
        val customProperties = customProperties ?: return inherited

        val previous = previous
        if (previous?.isCompatible(customProperties) == true) return previous

        return CustomPropertiesList(customProperties)
    }
}

private inline fun buildProperties(
    ruleNode: RuleNode,
    previousStyle: ComputedValues?,
    customProperties: CustomPropertiesList?,
    block: PropertiesListBuilder.() -> Unit,
): PropertiesList {
    val previousProperties = previousStyle?.properties
    return if (previousProperties == null || customProperties !== previousStyle.customProperties || ruleNode !== previousStyle.ruleNode) {
        val builder = PropertiesListBuilder(customProperties)
        builder.block()
        builder.build()
    } else {
        previousProperties
    }
}

class PropertiesList(
    private val properties: List>,
) : Iterable> {

    override fun iterator(): Iterator> = properties.iterator()

    fun toMap(): Map = properties.toMap()
}

class PropertiesListBuilder(
    private val customProperties: CustomPropertiesList?,
) {
    private val substitutionCache = SubstitutionCache()

    private val seen = LonghandIdSet()
    private val reverted = PerOrigin { LonghandIdSet() }

    private val properties = mutableListOf>()

    fun process(
        declarations: Sequence,
    ) {
        for ((declaration, level) in declarations) {
            val longhandId = when (val id = declaration.id) {
                is PropertyDeclarationId.Longhand -> id.id
                is PropertyDeclarationId.Custom -> continue
            }

            if (seen.contains(longhandId)) {
                continue
            }

            val origin = level.origin

            if (reverted.peek(origin)?.contains(longhandId) == true) {
                continue
            }

            val substitutedDeclaration = substituteVariables(declaration)

            val cssWideKeyword = substitutedDeclaration.getCssWideKeyword()
            if (cssWideKeyword == CssWideKeyword.Revert) {
                for (relevantOrigin in origin.selfAndPrevious()) {
                    reverted.get(relevantOrigin).add(longhandId)
                }
                continue
            }

            seen.add(longhandId)

            val inherited = longhandId.isInherited
            val unset = when (cssWideKeyword) {
                CssWideKeyword.Unset -> true
                CssWideKeyword.Initial -> !inherited
                CssWideKeyword.Inherit -> inherited
                else -> false
            }

            if (unset) {
                continue
            }

            properties.add(longhandId to substitutedDeclaration)
        }
    }

    private fun substituteVariables(
        declaration: PropertyDeclaration,
    ): PropertyDeclaration {
        if (declaration !is PropertyDeclaration.WithVariables) return declaration

        return declaration.declaration.substituteVariables(customProperties, substitutionCache)
    }

    private fun PropertyDeclaration.getCssWideKeyword(): CssWideKeyword? {
        if (this !is PropertyDeclaration.CssWideKeyword) return null
        return declaration.keyword
    }

    fun build(): PropertiesList {
        return PropertiesList(properties)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy