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

org.fernice.flare.style.value.specified.Font.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.std.Err
import org.fernice.std.Ok
import org.fernice.std.Result
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.cssparser.toCssJoining
import org.fernice.flare.style.AllowQuirks
import org.fernice.flare.style.Parse
import org.fernice.flare.style.ParseQuirky
import org.fernice.flare.style.ParserContext
import org.fernice.flare.style.value.Context
import org.fernice.flare.style.value.FontBaseSize
import org.fernice.flare.style.value.SpecifiedValue
import org.fernice.flare.style.value.computed.Au
import org.fernice.flare.style.value.computed.FontFamilyList
import org.fernice.flare.style.value.computed.NonNegativeLength
import org.fernice.flare.style.value.computed.PixelLength
import org.fernice.flare.style.value.computed.SingleFontFamily
import org.fernice.flare.style.value.computed.toNonNegative
import org.fernice.std.map
import java.io.Writer
import org.fernice.flare.style.value.computed.FontFamily as ComputedFontFamily
import org.fernice.flare.style.value.computed.FontSize as ComputedFontSize
import org.fernice.flare.style.value.computed.FontWeight as ComputedFontWeight

sealed class FontWeight : SpecifiedValue {

    data class Absolute(val value: AbsoluteFontWeight) : FontWeight()

    object Bolder : FontWeight()

    object Lighter : FontWeight()

    final override fun toComputedValue(context: Context): ComputedFontWeight {
        return when (this) {
            is Absolute -> value.toComputedValue(context)
            is Bolder -> context.builder.getParentFont().fontWeight.lighter()
            is Lighter -> context.builder.getParentFont().fontWeight.bolder()
        }
    }

    companion object {

        fun parse(context: ParserContext, input: Parser): Result {
            when (val result = input.tryParse { parser -> AbsoluteFontWeight.parse(context, parser) }) {
                is Ok -> return Ok(Absolute(result.value))
                else -> {}
            }

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

            return Ok(
                when (ident.lowercase()) {
                    "bolder" -> Bolder
                    "lighter" -> Lighter
                    else -> return Err(location.newUnexpectedTokenError(Token.Identifier(ident)))
                }
            )
        }
    }
}

private const val MIN_FONT_WEIGHT = 1f
private const val MAX_FONT_WEIGHT = 1000f

sealed class AbsoluteFontWeight : SpecifiedValue {

    data class Weight(val value: Number) : AbsoluteFontWeight()

    object Normal : AbsoluteFontWeight()

    object Bold : AbsoluteFontWeight()

    override fun toComputedValue(context: Context): ComputedFontWeight {
        return when (this) {
            is Weight -> ComputedFontWeight(value.value.coerceAtLeast(MIN_FONT_WEIGHT).coerceAtMost(MAX_FONT_WEIGHT))
            is Normal -> ComputedFontWeight.Normal
            is Bold -> ComputedFontWeight.Bold
        }
    }

    companion object {

        fun parse(context: ParserContext, input: Parser): Result {
            when (val result = input.tryParse { parser -> Number.parse(context, parser) }) {
                is Ok -> {
                    val number = result.value

                    if (!number.wasCalc() && (number.value < MIN_FONT_WEIGHT || number.value > MAX_FONT_WEIGHT)) {
                        return Err(input.newError(ParseErrorKind.Unspecified))
                    }
                    return Ok(Weight(number))
                }
                else -> {}
            }

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

            return Ok(
                when (ident.lowercase()) {
                    "normal" -> Normal
                    "bold" -> Bold
                    else -> return Err(location.newUnexpectedTokenError(Token.Identifier(ident)))
                }
            )
        }
    }
}

sealed class FontFamily : SpecifiedValue, ToCss {

    data class Values(val values: FontFamilyList) : FontFamily()

    override fun toComputedValue(context: Context): ComputedFontFamily {
        return when (this) {
            is Values -> ComputedFontFamily(values)
        }
    }

    override fun toCss(writer: Writer) {
        when (this) {
            is Values -> values.toCssJoining(writer, ", ")
        }
    }

    companion object {

        fun parse(input: Parser): Result {
            return input.parseCommaSeparated(SingleFontFamily.Contract::parse)
                .map { Values(FontFamilyList(it)) }
        }
    }
}

sealed class FontSize : SpecifiedValue, ToCss {

    data class Length(val lop: LengthOrPercentage) : FontSize()

    data class Keyword(val keyword: KeywordInfo) : FontSize()

    object Smaller : FontSize()

    object Larger : FontSize()

    fun toComputedValueAgainst(context: Context, baseSize: FontBaseSize): ComputedFontSize {
        fun composeKeyword(context: Context, factor: Float): KeywordInfo? {
            return context
                .style()
                .getParentFont()
                .fontSize
                .keywordInfo
                ?.compose(factor, Au(0).toNonNegative())
        }

        return when (this) {
            is Length -> {
                var info: KeywordInfo? = null

                val size = when (lop) {
                    is LengthOrPercentage.Length -> {
                        when (val length = lop.length) {
                            is NoCalcLength.FontRelative -> {
                                if (length.length is FontRelativeLength.Em) {
                                    info = composeKeyword(context, length.length.value)
                                }

                                length.length.toComputedValue(context, baseSize).toNonNegative()
                            }
                            else -> length.toComputedValue(context).toNonNegative()
                        }
                    }
                    is LengthOrPercentage.Percentage -> {
                        info = composeKeyword(context, lop.percentage.value)
                        baseSize.resolve(context)
                            .scaleBy(lop.percentage.value)
                            .toNonNegative()
                    }
                    is LengthOrPercentage.Calc -> {
                        val calc = lop.calc
                        val parent = context.style().getParentFont().fontSize

                        if (calc.em != null || calc.percentage != null && parent.keywordInfo != null) {
                            val ratio = (calc.em ?: 0f) + (calc.percentage?.value ?: 0f)

                            val abs = calc.toComputedValue(context, FontBaseSize.InheritStyleButStripEmUnits)
                                .lengthComponent()
                                .toNonNegative()

                            info = parent.keywordInfo?.compose(ratio, abs)
                        }

                        val computed = calc.toComputedValue(context, baseSize)
                        val used = computed.toUsedValue(baseSize.resolve(context)) ?: error("conversion should have resulted in size")
                        used.toNonNegative()
                    }
                }

                ComputedFontSize(
                    size,
                    info
                )
            }
            is Keyword -> {
                ComputedFontSize(
                    keyword.toComputedValue(context),
                    keyword
                )
            }
            is Smaller -> {
                ComputedFontSize(
                    FontRelativeLength.Em(1f / LARGE_FONT_SIZE_RATION)
                        .toComputedValue(context, baseSize)
                        .toNonNegative(),
                    composeKeyword(context, 1f / LARGE_FONT_SIZE_RATION)
                )
            }
            is Larger -> ComputedFontSize(
                FontRelativeLength.Em(LARGE_FONT_SIZE_RATION)
                    .toComputedValue(context, baseSize)
                    .toNonNegative(),
                composeKeyword(context, LARGE_FONT_SIZE_RATION)
            )
        }
    }

    final override fun toComputedValue(context: Context): ComputedFontSize {
        return toComputedValueAgainst(context, FontBaseSize.CurrentStyle)
    }

    final override fun toCss(writer: Writer) {
        when (this) {
            is Length -> lop.toCss(writer)
            is Keyword -> keyword.toCss(writer)
            is Larger -> writer.append("larger")
            is Smaller -> writer.append("smaller")
        }
    }

    companion object : Parse, ParseQuirky {

        override fun parse(context: ParserContext, input: Parser): Result {
            return parseQuirky(context, input, AllowQuirks.No)
        }

        override fun parseQuirky(context: ParserContext, input: Parser, allowQuirks: AllowQuirks): Result {
            val lopResult = input.tryParse { parser -> LengthOrPercentage.parseNonNegativeQuirky(context, parser, allowQuirks) }

            if (lopResult is Ok) {
                return Ok(Length(lopResult.value))
            }

            val keyword = input.tryParse(KeywordSize.Companion::parse)

            if (keyword is Ok) {
                return Ok(Keyword(KeywordInfo.from(keyword.value)))
            }

            val location = input.sourceLocation()

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

            return when (identifier) {
                "smaller" -> Ok(Smaller)
                "larger" -> Ok(Larger)

                else -> Err(location.newUnexpectedTokenError(Token.Identifier(identifier)))
            }
        }

        private const val LARGE_FONT_SIZE_RATION = 1.2f
    }
}

data class KeywordInfo(
    val keyword: KeywordSize,
    val factor: Float,
    val offset: NonNegativeLength,
) : SpecifiedValue, ToCss {

    companion object {

        fun from(keyword: KeywordSize): KeywordInfo {
            return KeywordInfo(
                keyword,
                1f,
                NonNegativeLength(PixelLength(0f))
            )
        }
    }

    fun compose(factor: Float, offset: NonNegativeLength): KeywordInfo {
        return KeywordInfo(
            keyword,
            this.factor * factor,
            this.offset + offset
        )
    }

    override fun toComputedValue(context: Context): NonNegativeLength {
        val base = keyword.toComputedValue(context)
        return base.scaleBy(factor) + offset
    }

    override fun toCss(writer: Writer) = keyword.toCss(writer)
}

sealed class KeywordSize : SpecifiedValue, ToCss {

    object XXSmall : KeywordSize()

    object XSmall : KeywordSize()

    object Small : KeywordSize()

    object Medium : KeywordSize()

    object Large : KeywordSize()

    object XLarge : KeywordSize()

    object XXLarge : KeywordSize()

    final override fun toComputedValue(context: Context): NonNegativeLength {
        val systemFontSize = context.style().device.systemFontSize
        return when (this) {
            is XXSmall -> (systemFontSize * 3 / 5).toNonNegative()
            is XSmall -> (systemFontSize * 3 / 4).toNonNegative()
            is Small -> (systemFontSize * 8 / 9).toNonNegative()
            is Medium -> (systemFontSize).toNonNegative()
            is Large -> (systemFontSize * 6 / 5).toNonNegative()
            is XLarge -> (systemFontSize * 3 / 2).toNonNegative()
            is XXLarge -> (systemFontSize * 2).toNonNegative()
        }
    }

    final override fun toCss(writer: Writer) {
        writer.append(
            when (this) {
                is XXSmall -> "xx-small"
                is XSmall -> "x-small"
                is Small -> "small"
                is Medium -> "medium"
                is Large -> "large"
                is XLarge -> "x-large"
                is XXLarge -> "xx-large"
            }
        )
    }

    companion object {

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

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

            return when (identifier.lowercase()) {
                "xx-small" -> Ok(XXSmall)
                "x-small" -> Ok(XSmall)
                "small" -> Ok(Small)
                "medium" -> Ok(Medium)
                "large" -> Ok(Large)
                "x-large" -> Ok(XLarge)
                "xx-large" -> Ok(XXLarge)

                else -> Err(location.newUnexpectedTokenError(Token.Identifier(identifier)))
            }
        }

        private const val FONT_MEDIUM_PX = 16
    }
}






© 2015 - 2025 Weber Informatics LLC | Privacy Policy