summaryrefslogtreecommitdiff
path: root/docs/src
diff options
context:
space:
mode:
authorHyunWoo Lee <l2hyunwoo@gmail.com>2025-12-22 17:15:00 +0300
committerHyunWoo Lee <l2hyunwoo@gmail.com>2025-12-22 17:15:00 +0300
commit8513ecf0a348dc23bc15c977dbfc5fbb4d4c312d (patch)
treee63548fd0767be16b5e3c3bcb0e2fce098e2865b /docs/src
parentf28f9ad3089b25fa29f9f1fefd9ed1572c9b3f8c (diff)
downloadcolorpicker-compose-8513ecf0a348dc23bc15c977dbfc5fbb4d4c312d.tar.xz
feat: add code highlighter
Diffstat (limited to 'docs/src')
-rw-r--r--docs/src/wasmJsMain/kotlin/docs/component/CodeBlock.kt324
-rw-r--r--docs/src/wasmJsMain/kotlin/docs/screen/InstallationScreen.kt3
-rw-r--r--docs/src/wasmJsMain/kotlin/docs/theme/DocsTheme.kt33
3 files changed, 357 insertions, 3 deletions
diff --git a/docs/src/wasmJsMain/kotlin/docs/component/CodeBlock.kt b/docs/src/wasmJsMain/kotlin/docs/component/CodeBlock.kt
index 982ce0c..26c4f21 100644
--- a/docs/src/wasmJsMain/kotlin/docs/component/CodeBlock.kt
+++ b/docs/src/wasmJsMain/kotlin/docs/component/CodeBlock.kt
@@ -24,17 +24,41 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
+import docs.theme.CodeHighlightColors
import docs.theme.DocsTheme
+enum class CodeLanguage {
+ KOTLIN,
+ GROOVY,
+ TOML,
+ PLAIN,
+}
+
@Composable
fun CodeBlock(
code: String,
modifier: Modifier = Modifier,
+ language: CodeLanguage = CodeLanguage.KOTLIN,
) {
+ val colors = DocsTheme.colors.codeHighlight
+ val highlightedCode = remember(code, language, colors) {
+ when (language) {
+ CodeLanguage.KOTLIN -> highlightKotlin(code, colors)
+ CodeLanguage.GROOVY -> highlightGroovy(code, colors)
+ CodeLanguage.TOML -> highlightToml(code, colors)
+ CodeLanguage.PLAIN -> AnnotatedString(code)
+ }
+ }
+
Box(
modifier = modifier
.fillMaxWidth()
@@ -44,10 +68,306 @@ fun CodeBlock(
.padding(16.dp),
) {
Text(
- text = code,
+ text = highlightedCode,
style = DocsTheme.typography.code,
- color = DocsTheme.colors.onSurface,
fontFamily = FontFamily.Monospace,
)
}
}
+
+private fun highlightKotlin(code: String, colors: CodeHighlightColors): AnnotatedString {
+ return buildAnnotatedString {
+ val keywords = setOf(
+ "fun", "val", "var", "class", "object", "interface", "enum", "sealed",
+ "if", "else", "when", "for", "while", "do", "return", "break", "continue",
+ "import", "package", "private", "public", "internal", "protected", "open",
+ "override", "abstract", "final", "companion", "data", "inline", "suspend",
+ "by", "in", "out", "is", "as", "try", "catch", "finally", "throw", "true",
+ "false", "null", "this", "super", "it", "get", "set", "const", "lateinit",
+ "expect", "actual",
+ )
+
+ var i = 0
+ while (i < code.length) {
+ when {
+ // Multi-line comment
+ code.startsWith("/*", i) -> {
+ val end = code.indexOf("*/", i + 2).let { if (it == -1) code.length else it + 2 }
+ withStyle(SpanStyle(color = colors.comment)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Single-line comment
+ code.startsWith("//", i) -> {
+ val end = code.indexOf('\n', i).let { if (it == -1) code.length else it }
+ withStyle(SpanStyle(color = colors.comment)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Triple-quoted string
+ code.startsWith("\"\"\"", i) -> {
+ val end = code.indexOf("\"\"\"", i + 3).let { if (it == -1) code.length else it + 3 }
+ withStyle(SpanStyle(color = colors.string)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // String
+ code[i] == '"' -> {
+ val end = findStringEnd(code, i + 1, '"')
+ withStyle(SpanStyle(color = colors.string)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Char
+ code[i] == '\'' -> {
+ val end = findStringEnd(code, i + 1, '\'')
+ withStyle(SpanStyle(color = colors.string)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Annotation
+ code[i] == '@' -> {
+ val end = findIdentifierEnd(code, i + 1)
+ withStyle(SpanStyle(color = colors.annotation)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Number
+ code[i].isDigit() || (code[i] == '.' && i + 1 < code.length && code[i + 1].isDigit()) -> {
+ val end = findNumberEnd(code, i)
+ withStyle(SpanStyle(color = colors.number)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Identifier or keyword
+ code[i].isLetter() || code[i] == '_' -> {
+ val end = findIdentifierEnd(code, i)
+ val word = code.substring(i, end)
+ when {
+ word in keywords -> withStyle(SpanStyle(color = colors.keyword)) { append(word) }
+ word.first().isUpperCase() -> withStyle(SpanStyle(color = colors.type)) { append(word) }
+ else -> withStyle(SpanStyle(color = colors.plain)) { append(word) }
+ }
+ i = end
+ }
+ // Other characters
+ else -> {
+ withStyle(SpanStyle(color = colors.plain)) {
+ append(code[i])
+ }
+ i++
+ }
+ }
+ }
+ }
+}
+
+private fun highlightGroovy(code: String, colors: CodeHighlightColors): AnnotatedString {
+ return buildAnnotatedString {
+ val keywords = setOf(
+ "def", "class", "interface", "enum", "if", "else", "for", "while", "do",
+ "switch", "case", "default", "break", "continue", "return", "try", "catch",
+ "finally", "throw", "import", "package", "new", "true", "false", "null",
+ "this", "super", "static", "final", "public", "private", "protected",
+ "implementation", "api", "dependencies", "plugins", "id", "version",
+ )
+
+ var i = 0
+ while (i < code.length) {
+ when {
+ // Multi-line comment
+ code.startsWith("/*", i) -> {
+ val end = code.indexOf("*/", i + 2).let { if (it == -1) code.length else it + 2 }
+ withStyle(SpanStyle(color = colors.comment)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Single-line comment
+ code.startsWith("//", i) -> {
+ val end = code.indexOf('\n', i).let { if (it == -1) code.length else it }
+ withStyle(SpanStyle(color = colors.comment)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // String (double quote)
+ code[i] == '"' -> {
+ val end = findStringEnd(code, i + 1, '"')
+ withStyle(SpanStyle(color = colors.string)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // String (single quote)
+ code[i] == '\'' -> {
+ val end = findStringEnd(code, i + 1, '\'')
+ withStyle(SpanStyle(color = colors.string)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Number
+ code[i].isDigit() -> {
+ val end = findNumberEnd(code, i)
+ withStyle(SpanStyle(color = colors.number)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Identifier or keyword
+ code[i].isLetter() || code[i] == '_' -> {
+ val end = findIdentifierEnd(code, i)
+ val word = code.substring(i, end)
+ when {
+ word in keywords -> withStyle(SpanStyle(color = colors.keyword)) { append(word) }
+ word.first().isUpperCase() -> withStyle(SpanStyle(color = colors.type)) { append(word) }
+ else -> withStyle(SpanStyle(color = colors.plain)) { append(word) }
+ }
+ i = end
+ }
+ // Other characters
+ else -> {
+ withStyle(SpanStyle(color = colors.plain)) {
+ append(code[i])
+ }
+ i++
+ }
+ }
+ }
+ }
+}
+
+private fun highlightToml(code: String, colors: CodeHighlightColors): AnnotatedString {
+ return buildAnnotatedString {
+ var i = 0
+ while (i < code.length) {
+ when {
+ // Comment
+ code[i] == '#' -> {
+ val end = code.indexOf('\n', i).let { if (it == -1) code.length else it }
+ withStyle(SpanStyle(color = colors.comment)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Section header [section]
+ code[i] == '[' -> {
+ val end = code.indexOf(']', i).let { if (it == -1) code.length else it + 1 }
+ withStyle(SpanStyle(color = colors.keyword)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // String
+ code[i] == '"' -> {
+ val end = findStringEnd(code, i + 1, '"')
+ withStyle(SpanStyle(color = colors.string)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Number
+ code[i].isDigit() -> {
+ val end = findNumberEnd(code, i)
+ withStyle(SpanStyle(color = colors.number)) {
+ append(code.substring(i, end))
+ }
+ i = end
+ }
+ // Key (identifier before =)
+ code[i].isLetter() || code[i] == '_' || code[i] == '-' -> {
+ val end = findTomlKeyEnd(code, i)
+ val word = code.substring(i, end)
+ val isKey = code.substring(end).trimStart().startsWith("=")
+ if (isKey) {
+ withStyle(SpanStyle(color = colors.property)) { append(word) }
+ } else {
+ withStyle(SpanStyle(color = colors.plain)) { append(word) }
+ }
+ i = end
+ }
+ // Other characters
+ else -> {
+ withStyle(SpanStyle(color = colors.plain)) {
+ append(code[i])
+ }
+ i++
+ }
+ }
+ }
+ }
+}
+
+private fun findStringEnd(code: String, start: Int, quote: Char): Int {
+ var i = start
+ while (i < code.length) {
+ when {
+ code[i] == '\\' && i + 1 < code.length -> i += 2
+ code[i] == quote -> return i + 1
+ code[i] == '\n' -> return i
+ else -> i++
+ }
+ }
+ return code.length
+}
+
+private fun findIdentifierEnd(code: String, start: Int): Int {
+ var i = start
+ while (i < code.length && (code[i].isLetterOrDigit() || code[i] == '_')) {
+ i++
+ }
+ return i
+}
+
+private fun findTomlKeyEnd(code: String, start: Int): Int {
+ var i = start
+ while (i < code.length && (code[i].isLetterOrDigit() || code[i] == '_' || code[i] == '-')) {
+ i++
+ }
+ return i
+}
+
+private fun Char.isHexDigit(): Boolean =
+ this.isDigit() || this in 'a'..'f' || this in 'A'..'F'
+
+private fun findNumberEnd(code: String, start: Int): Int {
+ var i = start
+ var hasDecimal = false
+ var hasExponent = false
+
+ while (i < code.length) {
+ when {
+ code[i].isDigit() -> i++
+ code[i] == '.' && !hasDecimal && !hasExponent -> {
+ hasDecimal = true
+ i++
+ }
+ (code[i] == 'e' || code[i] == 'E') && !hasExponent -> {
+ hasExponent = true
+ i++
+ if (i < code.length && (code[i] == '+' || code[i] == '-')) i++
+ }
+ code[i] == 'f' || code[i] == 'F' || code[i] == 'L' || code[i] == 'd' || code[i] == 'D' -> {
+ i++
+ break
+ }
+ code[i] == 'x' || code[i] == 'X' -> {
+ i++
+ while (i < code.length && code[i].isHexDigit()) {
+ i++
+ }
+ break
+ }
+ else -> break
+ }
+ }
+ return i
+}
diff --git a/docs/src/wasmJsMain/kotlin/docs/screen/InstallationScreen.kt b/docs/src/wasmJsMain/kotlin/docs/screen/InstallationScreen.kt
index bd4c0f0..4c058cc 100644
--- a/docs/src/wasmJsMain/kotlin/docs/screen/InstallationScreen.kt
+++ b/docs/src/wasmJsMain/kotlin/docs/screen/InstallationScreen.kt
@@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import docs.component.CodeBlock
+import docs.component.CodeLanguage
import docs.config.BuildConfig
import docs.theme.DocsTheme
@@ -80,7 +81,7 @@ fun InstallationScreen() {
append("version.ref = \"colorpicker\" }")
}
- CodeBlock(code = versionCatalogCode)
+ CodeBlock(code = versionCatalogCode, language = CodeLanguage.TOML)
Spacer(modifier = Modifier.height(32.dp))
diff --git a/docs/src/wasmJsMain/kotlin/docs/theme/DocsTheme.kt b/docs/src/wasmJsMain/kotlin/docs/theme/DocsTheme.kt
index 5ce6ea0..3fff946 100644
--- a/docs/src/wasmJsMain/kotlin/docs/theme/DocsTheme.kt
+++ b/docs/src/wasmJsMain/kotlin/docs/theme/DocsTheme.kt
@@ -25,6 +25,18 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
@Immutable
+data class CodeHighlightColors(
+ val keyword: Color,
+ val string: Color,
+ val number: Color,
+ val comment: Color,
+ val annotation: Color,
+ val type: Color,
+ val property: Color,
+ val plain: Color,
+)
+
+@Immutable
data class DocsColors(
val primary: Color,
val onPrimary: Color,
@@ -38,6 +50,7 @@ data class DocsColors(
val sidebarActive: Color,
val divider: Color,
val codeBackground: Color,
+ val codeHighlight: CodeHighlightColors,
val success: Color,
val warning: Color,
val error: Color,
@@ -67,6 +80,16 @@ private fun darkColors(primary: Color) = DocsColors(
sidebarActive = primary.copy(alpha = 0.15f),
divider = Color(0xFF333333),
codeBackground = Color(0xFF1A1A2E),
+ codeHighlight = CodeHighlightColors(
+ keyword = Color(0xFFCC7832),
+ string = Color(0xFF6A8759),
+ number = Color(0xFF6897BB),
+ comment = Color(0xFF808080),
+ annotation = Color(0xFFBBB529),
+ type = Color(0xFFA9B7C6),
+ property = Color(0xFF9876AA),
+ plain = Color(0xFFA9B7C6),
+ ),
success = Color(0xFF4CAF50),
warning = Color(0xFFFFC107),
error = Color(0xFFEF5350),
@@ -85,6 +108,16 @@ private fun lightColors(primary: Color) = DocsColors(
sidebarActive = primary.copy(alpha = 0.15f),
divider = Color(0xFFE0E0E0),
codeBackground = Color(0xFFF5F5F5),
+ codeHighlight = CodeHighlightColors(
+ keyword = Color(0xFF0000FF),
+ string = Color(0xFF067D17),
+ number = Color(0xFF1750EB),
+ comment = Color(0xFF8C8C8C),
+ annotation = Color(0xFFBBB529),
+ type = Color(0xFF000000),
+ property = Color(0xFF871094),
+ plain = Color(0xFF000000),
+ ),
success = Color(0xFF4CAF50),
warning = Color(0xFFFF9800),
error = Color(0xFFF44336),