diff options
| author | HyunWoo Lee <l2hyunwoo@gmail.com> | 2025-12-22 17:15:00 +0300 |
|---|---|---|
| committer | HyunWoo Lee <l2hyunwoo@gmail.com> | 2025-12-22 17:15:00 +0300 |
| commit | 8513ecf0a348dc23bc15c977dbfc5fbb4d4c312d (patch) | |
| tree | e63548fd0767be16b5e3c3bcb0e2fce098e2865b /docs/src | |
| parent | f28f9ad3089b25fa29f9f1fefd9ed1572c9b3f8c (diff) | |
| download | colorpicker-compose-8513ecf0a348dc23bc15c977dbfc5fbb4d4c312d.tar.xz | |
feat: add code highlighter
Diffstat (limited to 'docs/src')
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), |
