From 13d753b5cc3d36649e65a7d7e29a336851071404 Mon Sep 17 00:00:00 2001 From: Giorgio Garofalo Date: Sat, 14 Sep 2024 14:04:23 +0200 Subject: [PATCH] Add RGB(A) color decoding --- .../quarkdown/function/value/ValueFactory.kt | 12 +---- .../eu/iamgio/quarkdown/misc/color/Color.kt | 5 +- .../misc/color/decoder/ColorDecoder.kt | 6 +-- .../misc/color/decoder/RgbaColorDecoder.kt | 50 +++++++++++++++++++ .../quarkdown/parser/InlineTokenParser.kt | 6 ++- .../eu/iamgio/quarkdown/ValueFactoryTest.kt | 6 +++ .../iamgio/quarkdown/test/FullPipelineTest.kt | 13 +++++ 7 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/RgbaColorDecoder.kt diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt index 250b1571..9c1f450d 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/function/value/ValueFactory.kt @@ -19,8 +19,6 @@ import eu.iamgio.quarkdown.function.value.data.Lambda import eu.iamgio.quarkdown.function.value.data.Range import eu.iamgio.quarkdown.lexer.Lexer import eu.iamgio.quarkdown.misc.color.Color -import eu.iamgio.quarkdown.misc.color.decoder.ColorNameDecoder -import eu.iamgio.quarkdown.misc.color.decoder.HexColorDecoder import eu.iamgio.quarkdown.misc.color.decoder.decode import eu.iamgio.quarkdown.pipeline.error.UnattachedPipelineException import eu.iamgio.quarkdown.util.iterator @@ -158,15 +156,7 @@ object ValueFactory { */ @FromDynamicType(Color::class) fun color(raw: String): ObjectValue { - val decoders = - arrayOf( - // #FF0000 - HexColorDecoder, - // red - ColorNameDecoder, - ) - - return Color.decode(raw, *decoders)?.let(::ObjectValue) + return Color.decode(raw)?.let(::ObjectValue) ?: throw IllegalArgumentException("Invalid color: $raw") } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/Color.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/Color.kt index 55959d87..940bca20 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/Color.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/Color.kt @@ -21,5 +21,8 @@ data class Color( /** * @see eu.iamgio.quarkdown.misc.color.decoder.decode */ - companion object + companion object { + const val MAX_RGB = 255 + const val MAX_ALPHA = 1.0 + } } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/ColorDecoder.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/ColorDecoder.kt index 98a0a285..16a4d274 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/ColorDecoder.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/ColorDecoder.kt @@ -5,7 +5,7 @@ import eu.iamgio.quarkdown.misc.color.Color /** * A strategy to decode a [Color] from a raw string. */ -interface ColorDecoder { +sealed interface ColorDecoder { /** * Decodes a [Color] from a raw string. * @param raw raw string @@ -17,10 +17,10 @@ interface ColorDecoder { /** * Decodes a [Color] from a raw string using the first decoder that successfully decodes it. * @param raw raw string - * @param decoders ordered list of decoders + * @param decoders ordered list of decoders. Defaults to all decoders. * @return a successfully decoded [Color], or `null` if no decoder can decode it */ fun Color.Companion.decode( raw: String, - vararg decoders: ColorDecoder, + vararg decoders: ColorDecoder = arrayOf(HexColorDecoder, RgbColorDecoder, RgbaColorDecoder, ColorNameDecoder), ): Color? = decoders.firstNotNullOfOrNull { it.decode(raw) } diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/RgbaColorDecoder.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/RgbaColorDecoder.kt new file mode 100644 index 00000000..053eb469 --- /dev/null +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/decoder/RgbaColorDecoder.kt @@ -0,0 +1,50 @@ +package eu.iamgio.quarkdown.misc.color.decoder + +import eu.iamgio.quarkdown.misc.color.Color + +/** + * Given a regex match result, extracts RGB components from it. + * @return a list of red, green and blue components. Any can be `null` if the component is invalid. + */ +private fun extractRGBComponents(match: MatchResult): List = + match.destructured.toList().map { component -> + component.toIntOrNull()?.takeIf { it <= Color.MAX_RGB } + } + +/** + * Decodes a [Color] from an RGB string (e.g. `rgb(255, 100, 25)`). + */ +object RgbColorDecoder : ColorDecoder { + override fun decode(raw: String): Color? { + if (!raw.startsWith("rgb(")) return null + + val match = Regex("rgb\\((\\d{1,3}), ?(\\d{1,3}), ?(\\d{1,3})\\)").find(raw) ?: return null + val (r, g, b) = extractRGBComponents(match) + + return if (r != null && g != null && b != null) { + Color(r, g, b) + } else { + null + } + } +} + +/** + * Decodes a [Color] from an RGBA string (e.g. `rgba(255, 100, 25, 0.5)`). + */ +object RgbaColorDecoder : ColorDecoder { + override fun decode(raw: String): Color? { + if (!raw.startsWith("rgba(")) return null + + val match = Regex("rgba\\((\\d{1,3}), ?(\\d{1,3}), ?(\\d{1,3}), ?([0-9.]+)\\)").find(raw) ?: return null + val (r, g, b) = extractRGBComponents(match) + + val a = match.destructured.component4().toDoubleOrNull()?.takeIf { it <= Color.MAX_ALPHA } + + return if (r != null && g != null && b != null && a != null) { + Color(r, g, b, a) + } else { + null + } + } +} diff --git a/core/src/main/kotlin/eu/iamgio/quarkdown/parser/InlineTokenParser.kt b/core/src/main/kotlin/eu/iamgio/quarkdown/parser/InlineTokenParser.kt index 04482612..7a1791e2 100644 --- a/core/src/main/kotlin/eu/iamgio/quarkdown/parser/InlineTokenParser.kt +++ b/core/src/main/kotlin/eu/iamgio/quarkdown/parser/InlineTokenParser.kt @@ -42,6 +42,8 @@ import eu.iamgio.quarkdown.lexer.tokens.TextSymbolToken import eu.iamgio.quarkdown.lexer.tokens.UrlAutolinkToken import eu.iamgio.quarkdown.misc.color.Color import eu.iamgio.quarkdown.misc.color.decoder.HexColorDecoder +import eu.iamgio.quarkdown.misc.color.decoder.RgbColorDecoder +import eu.iamgio.quarkdown.misc.color.decoder.RgbaColorDecoder import eu.iamgio.quarkdown.misc.color.decoder.decode import eu.iamgio.quarkdown.util.iterator import eu.iamgio.quarkdown.util.nextOrNull @@ -225,8 +227,8 @@ class InlineTokenParser(private val context: MutableContext) : InlineTokenVisito // Additional content brought by the code span. // If null, no additional content is present. val content: CodeSpan.ContentInfo? = - // Color decoding. - Color.decode(text, HexColorDecoder)?.let(CodeSpan::ColorContent) + // Color decoding. Named colors are disabled due to performance reasons. + Color.decode(text, HexColorDecoder, RgbColorDecoder, RgbaColorDecoder)?.let(CodeSpan::ColorContent) return CodeSpan(text, content) } diff --git a/core/src/test/kotlin/eu/iamgio/quarkdown/ValueFactoryTest.kt b/core/src/test/kotlin/eu/iamgio/quarkdown/ValueFactoryTest.kt index 360201fc..5ece0c37 100644 --- a/core/src/test/kotlin/eu/iamgio/quarkdown/ValueFactoryTest.kt +++ b/core/src/test/kotlin/eu/iamgio/quarkdown/ValueFactoryTest.kt @@ -117,8 +117,14 @@ class ValueFactoryTest { assertEquals(Color(0, 0, 0), ValueFactory.color("#000000").unwrappedValue) assertEquals(Color(0, 0, 0), ValueFactory.color("BLACK").unwrappedValue) assertEquals(Color(145, 168, 50), ValueFactory.color("#91a832").unwrappedValue) + assertEquals(Color(145, 168, 50), ValueFactory.color("rgb(145, 168, 50)").unwrappedValue) + assertEquals(Color(120, 111, 93), ValueFactory.color("rgb(120,111,93)").unwrappedValue) + assertEquals(Color(120, 111, 93, 0.5), ValueFactory.color("rgba(120, 111, 93, 0.5)").unwrappedValue) assertFails { ValueFactory.color("abc") } assertFails { ValueFactory.color("#hello") } + assertFails { ValueFactory.color("rgb(300, 200, 200)") } + assertFails { ValueFactory.color("rgb(300, 200, 200, 0.8)") } + assertFails { ValueFactory.color("rgba(100, 200, 200, 1.5)") } } @Test diff --git a/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt b/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt index 8f1869b8..5d621a9e 100644 --- a/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt +++ b/test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt @@ -312,6 +312,19 @@ class FullPipelineTest { assertFalse(attributes.hasCode) } + execute("`rgba(200, 100, 50, 0.5)`") { + assertEquals( + "

" + + "" + + "rgba(200, 100, 50, 0.5)" + + "" + + "" + + "

", + it, + ) + assertFalse(attributes.hasCode) + } + execute("```\nprintln(\"Hello, world!\")\n```") { assertEquals("
println("Hello, world!")
", it) assertTrue(attributes.hasCode)