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

org.fernice.flare.style.value.specified.Image.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.value.specified

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.ToCss
import org.fernice.flare.cssparser.Token
import org.fernice.flare.cssparser.newUnexpectedTokenError
import org.fernice.flare.panic
import org.fernice.flare.style.ParserContext
import org.fernice.flare.style.value.Context
import org.fernice.flare.style.value.SpecifiedValue
import org.fernice.flare.style.value.toComputedValue
import org.fernice.std.Either
import org.fernice.std.Err
import org.fernice.std.Ok
import org.fernice.std.Result
import org.fernice.std.unwrapOrElse
import java.io.Writer
import org.fernice.flare.style.value.computed.Circle as ComputedCircle
import org.fernice.flare.style.value.computed.ColorStop as ComputedColorStop
import org.fernice.flare.style.value.computed.Ellipse as ComputedEllipse
import org.fernice.flare.style.value.computed.EndingShape as ComputedEndingShape
import org.fernice.flare.style.value.computed.Gradient as ComputedGradient
import org.fernice.flare.style.value.computed.GradientItem as ComputedGradientItem
import org.fernice.flare.style.value.computed.GradientKind as ComputedGradientKind
import org.fernice.flare.style.value.computed.Image as ComputedImage
import org.fernice.flare.style.value.computed.LineDirection as ComputedLineDirection

typealias ImageLayer = Either

sealed class Image : SpecifiedValue, ToCss {

    data class Url(val url: ImageUrl) : Image()
    data class Gradient(val gradient: SpecifiedGradient) : Image()

    override fun toComputedValue(context: Context): ComputedImage {
        return when (this) {
            is Image.Url -> ComputedImage.Url(url.toComputedValue(context))
            is Image.Gradient -> ComputedImage.Gradient(gradient.toComputedValue(context))
        }
    }

    override fun toCss(writer: Writer) {
        when (this) {
            is Image.Url -> url.toCss(writer)
            is Image.Gradient -> gradient.toCss(writer)
        }
    }

    companion object {
        fun parse(context: ParserContext, input: Parser): Result {
            val url = input.tryParse { nestedParser -> ImageUrl.parse(context, nestedParser) }

            if (url is Ok) {
                return Ok(Url(url.value))
            }

            return when (val gradient = SpecifiedGradient.parse(context, input)) {
                is Ok -> Ok(Gradient(gradient.value))
                is Err -> gradient
            }
        }
    }
}

typealias Repeating = Boolean

private typealias SpecifiedGradient = Gradient

data class Gradient(
    val items: List,
    val repeating: Repeating,
    val kind: GradientKind,
) : SpecifiedValue, ToCss {

    override fun toComputedValue(context: Context): ComputedGradient {
        return ComputedGradient(
            items.toComputedValue(context),
            repeating,
            kind.toComputedValue(context)
        )
    }

    override fun toCss(writer: Writer) {
        if (repeating) {
            writer.append("repeating-")
        }

        writer.append(kind.label())
        writer.append("-gradient")

        var skipComma = when (kind) {
            is GradientKind.Linear -> {
                if (kind.direction.pointsDownwards()) {
                    true
                } else {
                    kind.direction.toCss(writer)
                    false
                }
            }
            is GradientKind.Radial -> {
                val omitShape = kind.shape is EndingShape.Ellipse
                        && kind.shape.ellipse is Ellipse.Extend
                        && kind.shape.ellipse.shapeExtend is ShapeExtend.FarthestCorner

                kind.position.toCss(writer)
                //fixme(kralli) there should be an angle
                if (!omitShape) {
                    writer.append(", ")
                    kind.shape.toCss(writer)
                }

                false
            }
        }

        for (item in items) {
            if (!skipComma) {
                writer.append(", ")
            }
            skipComma = false

            item.toCss(writer)
        }

        writer.append(')')
    }

    companion object {
        private enum class Shape {
            Linear,
            Radial
        }

        fun parse(context: ParserContext, input: Parser): Result {
            val location = input.sourceLocation()

            val function = when (val functionResult = input.expectFunction()) {
                is Ok -> functionResult.value
                is Err -> return functionResult
            }

            val (shape, repeating) = when (function) {
                "linear-gradient" -> Pair(Shape.Linear, false)
                "repeating-linear-gradient" -> Pair(Shape.Linear, true)
                "radial-gradient" -> Pair(Shape.Radial, false)
                "repeating-radial-gradient" -> Pair(Shape.Radial, true)
                else -> return Err(location.newUnexpectedTokenError(Token.Function(function)))
            }

            val parseResult = input.parseNestedBlock { nestedParser ->
                val shapeResult = when (shape) {
                    Shape.Linear -> GradientKind.parseLinear(context, nestedParser)
                    Shape.Radial -> GradientKind.parseRadial(context, nestedParser)
                }

                val shape = when (shapeResult) {
                    is Ok -> shapeResult.value
                    is Err -> return@parseNestedBlock shapeResult
                }

                val items = when (val itemsResult = GradientItem.parseCommaSeparated(context, nestedParser)) {
                    is Ok -> itemsResult.value
                    is Err -> return@parseNestedBlock itemsResult
                }

                Ok(Pair(shape, items))
            }

            val (kind, items) = when (parseResult) {
                is Ok -> parseResult.value
                is Err -> return parseResult
            }

            if (items.size < 2) {
                return Err(input.newError(ParseErrorKind.Unknown))
            }

            return Ok(
                Gradient(
                    items,
                    repeating,
                    kind
                )
            )
        }
    }
}

sealed class GradientItem : SpecifiedValue, ToCss {

    data class ColorStop(val colorStop: SpecifiedColorStop) : GradientItem()
    data class InterpolationHint(val hint: LengthOrPercentage) : GradientItem()

    override fun toComputedValue(context: Context): ComputedGradientItem {
        return when (this) {
            is GradientItem.ColorStop -> ComputedGradientItem.ColorStop(colorStop.toComputedValue(context))
            is GradientItem.InterpolationHint -> ComputedGradientItem.InterpolationHint(hint.toComputedValue(context))
        }
    }

    override fun toCss(writer: Writer) {
        return when (this) {
            is GradientItem.ColorStop -> colorStop.toCss(writer)
            is GradientItem.InterpolationHint -> hint.toCss(writer)
        }
    }

    companion object {
        fun parseCommaSeparated(context: ParserContext, input: Parser): Result, ParseError> {
            var seenStop = false
            val items: MutableList = mutableListOf()

            loop@
            while (true) {
                when (val result = input.parseUntilBefore(Delimiters.Comma) { nestedParser ->
                    if (seenStop) {
                        val hint = nestedParser.tryParse { parser -> LengthOrPercentage.parse(context, parser) }

                        if (hint is Ok) {
                            seenStop = false
                            items.add(InterpolationHint(hint.value))
                            return@parseUntilBefore Ok()
                        }
                    }

                    val stop = when (val result = org.fernice.flare.style.value.specified.ColorStop.parse(context, nestedParser)) {
                        is Ok -> result.value
                        is Err -> return@parseUntilBefore result
                    }

                    val secondPosition = nestedParser.tryParse { parser -> LengthOrPercentage.parse(context, parser) }

                    if (secondPosition is Ok) {
                        items.add(GradientItem.ColorStop(stop))
                        items.add(
                            GradientItem.ColorStop(
                                org.fernice.flare.style.value.specified.ColorStop(
                                    stop.color,
                                    secondPosition.value
                                )
                            )
                        )
                    } else {
                        items.add(GradientItem.ColorStop(stop))
                    }

                    seenStop = true
                    Ok()
                }) {
                    is Err -> return result
                    else -> {}
                }

                when (val token = input.next()) {
                    is Ok -> {
                        when (token.value) {
                            !is Token.Comma -> panic("unreachable")
                            else -> continue@loop
                        }
                    }
                    is Err -> break@loop
                }
            }

            if (!seenStop || items.size < 2) {
                return Err(input.newError(ParseErrorKind.Unknown))
            }

            return Ok(items)
        }
    }
}

private typealias SpecifiedColorStop = ColorStop

data class ColorStop(
    val color: RGBAColor,
    val position: LengthOrPercentage?,
) : SpecifiedValue, ToCss {

    override fun toComputedValue(context: Context): ComputedColorStop {
        return ComputedColorStop(
            color.toComputedValue(context),
            position?.toComputedValue(context)
        )
    }

    override fun toCss(writer: Writer) {
        color.toCss(writer)
        position?.toCss(writer)
    }

    companion object {
        fun parse(context: ParserContext, input: Parser): Result {
            val color = when (val color = RGBAColor.parse(context, input)) {
                is Ok -> color.value
                is Err -> return color
            }

            val position = input.tryParse { nestedParser -> LengthOrPercentage.parse(context, nestedParser) }.ok()

            return Ok(
                ColorStop(
                    color,
                    position
                )
            )
        }
    }
}

sealed class GradientKind : SpecifiedValue {

    data class Linear(val direction: LineDirection) : GradientKind()
    data class Radial(val shape: EndingShape, val position: Position) : GradientKind()

    override fun toComputedValue(context: Context): ComputedGradientKind {
        return when (this) {
            is GradientKind.Linear -> ComputedGradientKind.Linear(direction.toComputedValue(context))
            is GradientKind.Radial -> ComputedGradientKind.Radial(shape.toComputedValue(context), position.toComputedValue(context))
        }
    }

    fun label(): String {
        return when (this) {
            is GradientKind.Linear -> "linear"
            is GradientKind.Radial -> "radial"
        }
    }

    companion object {
        fun parseLinear(context: ParserContext, input: Parser): Result {
            val result = input.tryParse { nestedParser -> LineDirection.parse(context, nestedParser) }

            val direction = if (result is Ok) {
                val comma = input.expectComma()

                if (comma is Err) {
                    return comma
                }

                result.value
            } else {
                LineDirection.Vertical(Y.Bottom)
            }

            return Ok(Linear(direction))
        }

        fun parseRadial(context: ParserContext, input: Parser): Result {
            val shapeResult = input.tryParse { nestedParser -> EndingShape.parse(context, nestedParser) }

            val positionResult = input.tryParse { nestedParser ->
                val atIdent = nestedParser.expectIdentifierMatching("at")

                if (atIdent is Err) {
                    return atIdent
                }

                Position.parse(context, nestedParser)
            }.ok()

            if (shapeResult.isOk() || positionResult != null) {
                val comma = input.expectComma()

                if (comma is Err) {
                    return comma
                }
            }

            val shape = shapeResult.unwrapOrElse { EndingShape.Ellipse(Ellipse.Extend(ShapeExtend.FarthestCorner)) }

            val position = positionResult ?: Position.center()

            return Ok(Radial(shape, position))
        }
    }
}

private typealias ImageAngle = Angle

sealed class LineDirection : SpecifiedValue, ToCss {

    data class Angle(val angle: ImageAngle) : LineDirection()
    data class Horizontal(val x: X) : LineDirection()
    data class Vertical(val y: Y) : LineDirection()
    data class Corner(val x: X, val y: Y) : LineDirection()

    override fun toComputedValue(context: Context): ComputedLineDirection {
        return when (this) {
            is LineDirection.Angle -> ComputedLineDirection.Angle(angle.toComputedValue(context))
            is LineDirection.Horizontal -> ComputedLineDirection.Horizontal(x)
            is LineDirection.Vertical -> ComputedLineDirection.Vertical(y)
            is LineDirection.Corner -> ComputedLineDirection.Corner(x, y)
        }
    }

    fun pointsDownwards(): Boolean {
        return false
    }

    override fun toCss(writer: Writer) {
        TODO("implement toCss(Writer) for LineDirection")
    }

    companion object {
        fun parse(context: ParserContext, input: Parser): Result {
            val angle = input.tryParse { nestedParser -> org.fernice.flare.style.value.specified.Angle.parseAllowingUnitless(context, nestedParser) }

            if (angle is Ok) {
                return Ok(Angle(angle.value))
            }

            return input.tryParse { nestedParser ->
                val toIdent = nestedParser.expectIdentifierMatching("to")

                if (toIdent is Err) {
                    return toIdent
                }

                val x = nestedParser.tryParse(X.Companion::parse)

                if (x is Ok) {
                    val y = nestedParser.tryParse(Y.Companion::parse)

                    if (y is Ok) {
                        return Ok(Corner(x.value, y.value))
                    }

                    return Ok(Horizontal(x.value))
                }

                val y = Y.parse(nestedParser)

                return when (y) {
                    is Ok -> {
                        val lateX = nestedParser.tryParse(X.Companion::parse)

                        if (lateX is Ok) {
                            return Ok(Corner(lateX.value, y.value))
                        }

                        Ok(Vertical(y.value))
                    }
                    is Err -> y
                }
            }
        }
    }
}

sealed class EndingShape : SpecifiedValue, ToCss {

    data class Circle(val circle: SpecifiedCircle) : EndingShape()
    data class Ellipse(val ellipse: SpecifiedEllipse) : EndingShape()

    override fun toComputedValue(context: Context): ComputedEndingShape {
        return when (this) {
            is EndingShape.Circle -> ComputedEndingShape.Circle(circle.toComputedValue(context))
            is EndingShape.Ellipse -> ComputedEndingShape.Ellipse(ellipse.toComputedValue(context))
        }
    }

    override fun toCss(writer: Writer) {
        writer.append()
    }

    companion object {

        fun parse(context: ParserContext, input: Parser): Result {
            val extend = input.tryParse(ShapeExtend.Companion::parse)

            if (extend is Ok) {
                val circleIdent = input.tryParse { nestedParser -> nestedParser.expectIdentifierMatching("circle") }

                if (circleIdent is Ok) {
                    return Ok(Circle(org.fernice.flare.style.value.specified.Circle.Extend(extend.value)))
                }

                input.tryParse { nestedParser -> nestedParser.expectIdentifierMatching("ellipse") }

                return Ok(Ellipse(org.fernice.flare.style.value.specified.Ellipse.Extend(extend.value)))
            }

            val circleIdent = input.tryParse { nestedParser -> nestedParser.expectIdentifierMatching("circle") }

            if (circleIdent is Ok) {
                val lateExtend = input.tryParse(ShapeExtend.Companion::parse)

                if (lateExtend is Ok) {
                    return Ok(Circle(org.fernice.flare.style.value.specified.Circle.Extend(lateExtend.value)))
                }

                val length = input.tryParse { nestedParser -> Length.parse(context, nestedParser) }

                if (length is Ok) {
                    return Ok(Circle(org.fernice.flare.style.value.specified.Circle.Radius(length.value)))
                }

                return Ok(Circle(org.fernice.flare.style.value.specified.Circle.Extend(ShapeExtend.FarthestCorner)))
            }

            val ellipseIdent = input.tryParse { nestedParser -> nestedParser.expectIdentifierMatching("ellipse") }

            if (ellipseIdent is Ok) {
                val lateExtend = input.tryParse(ShapeExtend.Companion::parse)

                if (lateExtend is Ok) {
                    return Ok(Ellipse(org.fernice.flare.style.value.specified.Ellipse.Extend(lateExtend.value)))
                }

                val pair = input.tryParse> { nestedParser ->
                    val x = when (val x = LengthOrPercentage.parse(context, nestedParser)) {
                        is Ok -> x.value
                        is Err -> return@tryParse x
                    }

                    val y = when (val y = LengthOrPercentage.parse(context, nestedParser)) {
                        is Ok -> y.value
                        is Err -> return@tryParse y
                    }

                    Ok(Pair(x, y))
                }

                if (pair is Ok) {
                    val (x, y) = pair.value

                    return Ok(Ellipse(org.fernice.flare.style.value.specified.Ellipse.Radii(x, y)))
                }

                return Ok(Ellipse(org.fernice.flare.style.value.specified.Ellipse.Extend(ShapeExtend.FarthestCorner)))
            }

            return input.tryParse { nestedParser ->
                val x = when (val x = Percentage.parse(context, nestedParser)) {
                    is Ok -> x.value
                    is Err -> return@tryParse x
                }

                val yResult = nestedParser.tryParse { parser -> LengthOrPercentage.parse(context, parser) }

                val y = if (yResult is Ok) {
                    input.tryParse { parser -> parser.expectIdentifierMatching("ellipse") }

                    yResult.value
                } else {
                    input.tryParse { parser -> parser.expectIdentifierMatching("ellipse") }

                    val lateYResult = nestedParser.tryParse { parser -> LengthOrPercentage.parse(context, parser) }

                    when (lateYResult) {
                        is Ok -> lateYResult.value
                        is Err -> return@tryParse lateYResult
                    }
                }

                Ok(Ellipse(org.fernice.flare.style.value.specified.Ellipse.Radii(x.intoLengthOrPercentage(), y)))
            }
        }
    }
}

private typealias SpecifiedCircle = Circle

sealed class Circle : SpecifiedValue {

    data class Radius(val length: Length) : Circle()
    data class Extend(val shapeExtend: ShapeExtend) : Circle()

    override fun toComputedValue(context: Context): ComputedCircle {
        return when (this) {
            is Circle.Radius -> ComputedCircle.Radius(length.toComputedValue(context))
            is Circle.Extend -> ComputedCircle.Extend(shapeExtend)
        }
    }
}

private typealias  SpecifiedEllipse = Ellipse

sealed class Ellipse : SpecifiedValue {

    data class Radii(val horizontal: LengthOrPercentage, val vertical: LengthOrPercentage) : Ellipse()
    data class Extend(val shapeExtend: ShapeExtend) : Ellipse()

    override fun toComputedValue(context: Context): ComputedEllipse {
        return when (this) {
            is Ellipse.Radii -> ComputedEllipse.Radii(horizontal.toComputedValue(context), vertical.toComputedValue(context))
            is Ellipse.Extend -> ComputedEllipse.Extend(shapeExtend)
        }
    }
}

sealed class ShapeExtend {

    object ClosestSide : ShapeExtend()
    object FarthestSide : ShapeExtend()
    object ClosestCorner : ShapeExtend()
    object FarthestCorner : ShapeExtend()

    companion object {

        fun parse(input: Parser): Result {
            val location = input.sourceLocation()

            val ident = when (val ident = input.expectIdentifier()) {
                is Ok -> ident.value
                is Err -> return ident
            }

            return when (ident.lowercase()) {
                "closest-side" -> Ok(ClosestSide)
                "farthest-side" -> Ok(FarthestSide)
                "closest-corner" -> Ok(ClosestCorner)
                "farthest-corner" -> Ok(FarthestCorner)
                else -> Err(location.newUnexpectedTokenError(Token.Identifier(ident)))
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy