Skip to content

Commit

Permalink
Add RGB(A) color decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
iamgio committed Sep 14, 2024
1 parent b78d238 commit 13d753b
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -158,15 +156,7 @@ object ValueFactory {
*/
@FromDynamicType(Color::class)
fun color(raw: String): ObjectValue<Color> {
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")
}

Expand Down
5 changes: 4 additions & 1 deletion core/src/main/kotlin/eu/iamgio/quarkdown/misc/color/Color.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) }
Original file line number Diff line number Diff line change
@@ -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<Int?> =
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
6 changes: 6 additions & 0 deletions core/src/test/kotlin/eu/iamgio/quarkdown/ValueFactoryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions test/src/test/kotlin/eu/iamgio/quarkdown/test/FullPipelineTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,19 @@ class FullPipelineTest {
assertFalse(attributes.hasCode)
}

execute("`rgba(200, 100, 50, 0.5)`") {
assertEquals(
"<p>" +
"<span class=\"codespan-content\">" +
"<code>rgba(200, 100, 50, 0.5)</code>" +
"<span style=\"background-color: rgba(200, 100, 50, 0.5);\" class=\"color-preview\"></span>" +
"</span>" +
"</p>",
it,
)
assertFalse(attributes.hasCode)
}

execute("```\nprintln(\"Hello, world!\")\n```") {
assertEquals("<pre><code>println(&quot;Hello, world!&quot;)</code></pre>", it)
assertTrue(attributes.hasCode)
Expand Down

0 comments on commit 13d753b

Please sign in to comment.