| 1 |
package io.oxygenhub.android.ui.code |
| 2 |
|
| 3 |
import androidx.compose.ui.graphics.Color |
| 4 |
import androidx.compose.ui.text.AnnotatedString |
| 5 |
import androidx.compose.ui.text.SpanStyle |
| 6 |
import androidx.compose.ui.text.font.FontWeight |
| 7 |
import dev.snipme.highlights.Highlights |
| 8 |
import dev.snipme.highlights.model.BoldHighlight |
| 9 |
import dev.snipme.highlights.model.ColorHighlight |
| 10 |
import dev.snipme.highlights.model.SyntaxLanguage |
| 11 |
import dev.snipme.highlights.model.SyntaxThemes |
| 12 |
|
| 13 |
/** The language to highlight a file as, from its name; plain text when unknown. */ |
| 14 |
fun languageOf(path: String): SyntaxLanguage { |
| 15 |
val name = path.substringAfterLast('/').lowercase() |
| 16 |
return when (name.substringAfterLast('.', "")) { |
| 17 |
"rs" -> SyntaxLanguage.RUST |
| 18 |
"kt", "kts" -> SyntaxLanguage.KOTLIN |
| 19 |
"java" -> SyntaxLanguage.JAVA |
| 20 |
"js", "mjs", "cjs", "jsx" -> SyntaxLanguage.JAVASCRIPT |
| 21 |
"ts", "tsx" -> SyntaxLanguage.TYPESCRIPT |
| 22 |
"py" -> SyntaxLanguage.PYTHON |
| 23 |
"sh", "bash", "zsh" -> SyntaxLanguage.SHELL |
| 24 |
"c", "h" -> SyntaxLanguage.C |
| 25 |
"cc", "cpp", "cxx", "hpp", "hh" -> SyntaxLanguage.CPP |
| 26 |
"go" -> SyntaxLanguage.GO |
| 27 |
"rb" -> SyntaxLanguage.RUBY |
| 28 |
"swift" -> SyntaxLanguage.SWIFT |
| 29 |
"dart" -> SyntaxLanguage.DART |
| 30 |
"cs" -> SyntaxLanguage.CSHARP |
| 31 |
"php" -> SyntaxLanguage.PHP |
| 32 |
"pl", "pm" -> SyntaxLanguage.PERL |
| 33 |
else -> if (name == "makefile" || name == "dockerfile") SyntaxLanguage.SHELL else SyntaxLanguage.DEFAULT |
| 34 |
} |
| 35 |
} |
| 36 |
|
| 37 |
/** |
| 38 |
* [code] highlighted for [path], one AnnotatedString per line, so a long |
| 39 |
* file is laid out a line at a time. Plain lines when the language is not |
| 40 |
* known or the file is too long to highlight quickly. |
| 41 |
*/ |
| 42 |
fun highlightLines(code: String, path: String, dark: Boolean): List<AnnotatedString> { |
| 43 |
val lines = code.split('\n') |
| 44 |
val language = languageOf(path) |
| 45 |
if (language == SyntaxLanguage.DEFAULT || code.length > MAX_HIGHLIGHT) return lines.map { AnnotatedString(it) } |
| 46 |
val whole = AnnotatedString.Builder(code) |
| 47 |
val spans = runCatching { |
| 48 |
Highlights.Builder().code(code).language(language).theme(SyntaxThemes.default(dark)).build().getHighlights() |
| 49 |
}.getOrDefault(emptyList()) |
| 50 |
for (h in spans) { |
| 51 |
val (start, end) = h.location.start to h.location.end |
| 52 |
if (start < 0 || end > code.length || start >= end) continue |
| 53 |
when (h) { |
| 54 |
is ColorHighlight -> whole.addStyle(SpanStyle(color = Color(0xFF000000.toInt() or h.rgb)), start, end) |
| 55 |
is BoldHighlight -> whole.addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) |
| 56 |
} |
| 57 |
} |
| 58 |
val text = whole.toAnnotatedString() |
| 59 |
var at = 0 |
| 60 |
return lines.map { line -> |
| 61 |
val piece = text.subSequence(at, at + line.length) |
| 62 |
at += line.length + 1 |
| 63 |
piece |
| 64 |
} |
| 65 |
} |
| 66 |
|
| 67 |
/** Beyond this many characters a file is shown plain: highlighting it would stall the screen. */ |
| 68 |
const val MAX_HIGHLIGHT = 200_000 |