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

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

import org.fernice.flare.cssparser.BlockType
import org.fernice.flare.cssparser.Delimiters
import org.fernice.flare.cssparser.ParseError
import org.fernice.flare.cssparser.ParseErrorKind
import org.fernice.flare.cssparser.Parser
import org.fernice.flare.cssparser.ParserInput
import org.fernice.flare.cssparser.Token
import org.fernice.flare.style.Origin
import org.fernice.flare.style.ParseMode
import org.fernice.flare.style.ParserContext
import org.fernice.flare.style.QuirksMode
import org.fernice.flare.style.properties.CssWideKeyword
import org.fernice.flare.style.properties.CssWideKeywordDeclaration
import org.fernice.flare.style.properties.CustomPropertiesList
import org.fernice.flare.style.properties.LonghandId
import org.fernice.flare.style.properties.PropertyDeclaration
import org.fernice.flare.style.properties.PropertyDeclarationId
import org.fernice.flare.style.properties.ShorthandId
import org.fernice.flare.url.Url
import org.fernice.std.Err
import org.fernice.std.Ok
import org.fernice.std.Result
import org.fernice.std.map
import org.fernice.std.unwrap
import org.fernice.std.unwrapErr
import org.fernice.std.unwrapOrElse
import org.fernice.std.unwrapOrNull
import java.util.LinkedList
import kotlin.text.StringBuilder

class VariableValue(
    val block: TemplateValue.Block,
) {

    override fun toString(): String = "VariableValue($block)"
}

class UnparsedValue(
    val block: TemplateValue.Block,
    val url: Url,
    val fromShorthand: ShorthandId?,
) {

    fun substituteVariables(
        longhandId: LonghandId,
        customProperties: CustomPropertiesList?,
        substitutionCache: SubstitutionCache,
    ): PropertyDeclaration {
        val shorthandId = fromShorthand
        if (shorthandId != null) {
            val key = (shorthandId to longhandId)
            val declaration = substitutionCache.find(key)
            if (declaration != null) return declaration
        }

        val invalidAtComputedValueTime = {
            val keyword = if (longhandId.isInherited) CssWideKeyword.Inherit else CssWideKeyword.Initial
            PropertyDeclaration.CssWideKeyword(CssWideKeywordDeclaration(longhandId, keyword))
        }

        val css = performSubstitution(customProperties) ?: return invalidAtComputedValueTime()

        val context = ParserContext.from(
            origin = Origin.Author, // technically not correct
            urlData = url,
            ruleType = null, // technically not correct
            parseMode = ParseMode.Default, // technically not correct
            quirksMode = QuirksMode.NoQuirks, // technically not correct
        )

        val input = Parser.from(ParserInput(css))
        input.skipWhitespace()

        input.tryParse { org.fernice.flare.style.properties.CssWideKeyword.parse(it) }
            .map { PropertyDeclaration.CssWideKeyword(CssWideKeywordDeclaration(longhandId, it)) }
            .unwrapErr { return it.value }

        if (shorthandId == null) {
            return input.parseEntirely { longhandId.parseValue(context, input) }
                .unwrapOrElse { invalidAtComputedValueTime() }
        }

        val declarations = mutableListOf()

        if (shorthandId.parseInto(declarations, context, input).isErr()) {
            return invalidAtComputedValueTime()
        }

        for (declaration in declarations) {
            val id = declaration.id as PropertyDeclarationId.Longhand
            val key = (shorthandId to id.id)
            substitutionCache.put(key, declaration)
        }

        val key = (shorthandId to longhandId)
        return substitutionCache.find(key) ?: error("shorthand $shorthandId did not yield longhand $longhandId")
    }

    private fun performSubstitution(customProperties: CustomPropertiesList?): String? {
        if (customProperties == null) return null

        return substitute(block, customProperties.toMap(), LinkedList())
    }

    private fun substitute(block: TemplateValue.Block, variables: Map, stack: LinkedList): String? {
        return buildString {
            for (value in block.values) {
                when (value) {
                    is TemplateValue.Text -> {
                        if (requiresDelimiter()) append(" ")
                        append(value.text)
                    }

                    is TemplateValue.Block -> error("template value should have been simplified")
                    is TemplateValue.Variable -> {
                        if (requiresDelimiter()) append(" ")
                        append(resolve(value, variables, stack) ?: return null)
                    }
                }
            }
        }
    }

    private fun resolve(variable: TemplateValue.Variable, variables: Map, stack: LinkedList): String? {
        val name = variable.name
        val fallback = variable.fallback

        if (!stack.contains(name)) {
            val value = variables[name]?.block
            if (value != null) {
                stack.push(name)
                val substitution = substitute(value, variables, stack)
                stack.pop()
                if (substitution != null) return substitution
            }
        }

        if (fallback != null) {
            return substitute(fallback, variables, stack)
        }

        return null
    }

    private fun StringBuilder.requiresDelimiter(): Boolean {
        return isNotEmpty() && get(lastIndex) != ' '
    }

    override fun toString(): String = "UnparsedValue($block, fromShorthand: $fromShorthand)"
}

sealed class TemplateValue {
    data class Text(val text: String) : TemplateValue()
    data class Block(val values: List) : TemplateValue()
    data class Variable(val name: Name, val fallback: Block?) : TemplateValue()

    fun toCss(): String {
        return when (this) {
            is Text -> text
            is Block -> buildString {
                for (value in values) {
                    append(value.toCss())
                }
            }

            is Variable -> buildString {
                append("var(")
                append(name)
                if (fallback != null) {
                    append(",")
                    append(fallback.toCss())
                }
                append(")")
            }
        }
    }

    companion object {

        fun parse(input: Parser): Result {
            return input.parseUntilBefore(Delimiters.Bang or Delimiters.SemiColon) { scopedInput ->
                val state = input.state()
                input.nextIncludingWhitespace().unwrap { return@parseUntilBefore it }
                input.reset(state)

                parseBlock(scopedInput)
                    .map { it.simplify() }
            }
        }

        private fun parseBlock(input: Parser): Result {
            val values = mutableListOf()

            var segmentStart = input.sourcePosition()
            var tokenStart = input.sourcePosition()
            var token = input.nextIncludingWhitespaceAndComment()
                .unwrapOrNull()

            val missingClosingCharacters = StringBuilder()

            while (token != null) {
                when (token) {
                    is Token.Comment -> {
                        val slice = input.sliceFrom(tokenStart)
                        if (!slice.endsWith("*/")) {
                            missingClosingCharacters.append(if (slice.endsWith("*")) "/" else "*/")
                        }
                    }

                    is Token.BadUrl -> {
                        return Err(input.newError(ValueParseErrorKind.BadUrlInDeclarationValueBlock(token.url)))
                    }

                    is Token.BadString -> {
                        return Err(input.newError(ValueParseErrorKind.BadStringInDeclarationValueBlock(token.value)))
                    }

                    is Token.RParen -> {
                        return Err(input.newError(ValueParseErrorKind.UnbalancedCloseParenthesisInDeclarationValueBlock))
                    }

                    is Token.RBracket -> {
                        return Err(input.newError(ValueParseErrorKind.UnbalancedCloseBracketInDeclarationValueBlock))
                    }

                    is Token.RBrace -> {
                        return Err(input.newError(ValueParseErrorKind.UnbalancedCloseBraceInDeclarationValueBlock))
                    }

                    is Token.Function,
                    is Token.LParen,
                    is Token.LBracket,
                    is Token.LBrace,
                    -> {
                        if (token is Token.Function && token.name.equals("var", ignoreCase = true)) {
                            values.add(Text(input.slice(segmentStart, tokenStart)))

                            values.add(input.parseNestedBlock { scopedInput ->
                                parseVarFunction(scopedInput)
                            }.unwrap { return it })

                            segmentStart = input.sourcePosition()
                        } else {
                            val blockType = BlockType.opening(token) ?: error("expected block type")

                            values.add(Text(input.sliceFrom(segmentStart)))
                            values.add(input.parseNestedBlock { scopedInput ->
                                parseBlock(scopedInput)
                            }.unwrap { return it })
                            values.add(Text(blockType.closing))

                            segmentStart = input.sourcePosition()
                        }
                    }

                    is Token.Identifier,
                    is Token.AtKeyword,
                    is Token.Hash,
                    is Token.IdHash,
                    is Token.UnquotedUrl,
                    is Token.Dimension,
                    -> {
                        val value = when (token) {
                            is Token.Identifier -> token.name
                            is Token.AtKeyword -> token.name
                            is Token.Hash -> token.value
                            is Token.IdHash -> token.value
                            is Token.UnquotedUrl -> token.url
                            is Token.Dimension -> token.unit
                            else -> error("no value access defined for token $token")
                        }

                        if (value.endsWith('\uFFFD') && input.sliceFrom(tokenStart).endsWith("\\")) {
                            missingClosingCharacters.append('\uFFFD')
                        }

                        if (token is Token.UnquotedUrl) {
                            val slice = input.sliceFrom(tokenStart)
                            if (!slice.endsWith(")")) {
                                missingClosingCharacters.append(")")
                            }
                        }
                    }

                    else -> {}
                }

                tokenStart = input.sourcePosition()
                token = input.nextIncludingWhitespaceAndComment()
                    .unwrapOrNull()
            }

            values.add(Text(input.slice(segmentStart, tokenStart)))
            values.add(Text(missingClosingCharacters.toString()))

            return Ok(Block(values))
        }

        private fun parseVarFunction(input: Parser): Result {
            val identifier = input.expectIdentifier().unwrap { return it }
            val name = Name.parse(identifier).unwrap { return Err(input.newError(ValueParseErrorKind.ExpectedCustomPropertyName)) }
            val fallback = if (input.tryParse { it.expectComma() }.isOk()) {
                parseFallback(input).unwrap { return it }
            } else {
                null
            }
            return Ok(Variable(name, fallback))
        }

        private fun parseFallback(input: Parser): Result {
            return parseBlock(input)
        }
    }
}


@Suppress("UNCHECKED_CAST")
fun  T.simplify(): T {
    return when (this) {
        is TemplateValue.Text -> this
        is TemplateValue.Block -> {
            val inlined = mutableListOf()
            for (value in values) {
                val simplifiedValue = value.simplify()

                if (simplifiedValue is TemplateValue.Block) {
                    inlined.addAll(simplifiedValue.values)
                } else {
                    inlined.add(simplifiedValue)
                }
            }

            val coerced = mutableListOf()
            var previousText: TemplateValue.Text? = null
            for (value in inlined) {
                if (value is TemplateValue.Text) {
                    previousText = if (previousText != null) {
                        TemplateValue.Text(previousText.text + value.text)
                    } else {
                        value
                    }
                    continue
                }
                if (previousText != null && previousText.text.isNotEmpty()) {
                    coerced.add(previousText)
                    previousText = null
                }
                coerced.add(value)
            }
            if (previousText != null && previousText.text.isNotEmpty()) {
                coerced.add(previousText)
            }

            TemplateValue.Block(coerced) as T
        }

        is TemplateValue.Variable -> TemplateValue.Variable(name, fallback?.simplify()) as T
        else -> error("unreachable")
    }
}

sealed class ValueParseErrorKind : ParseErrorKind() {

    data class BadUrlInDeclarationValueBlock(val url: String) : ValueParseErrorKind()
    data class BadStringInDeclarationValueBlock(val url: String) : ValueParseErrorKind()
    object UnbalancedCloseParenthesisInDeclarationValueBlock : ValueParseErrorKind()
    object UnbalancedCloseBracketInDeclarationValueBlock : ValueParseErrorKind()
    object UnbalancedCloseBraceInDeclarationValueBlock : ValueParseErrorKind()
    object ExpectedCustomPropertyName : ValueParseErrorKind()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy