-
Notifications
You must be signed in to change notification settings - Fork 106
/
CodeHighlighter.kt
290 lines (248 loc) · 8.19 KB
/
CodeHighlighter.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
package io.github.kbiakov.codeview.highlight
import android.graphics.Color
import io.github.kbiakov.codeview.highlight.parser.ParseResult
import io.github.kbiakov.codeview.highlight.prettify.PrettifyParser
import java.util.*
/**
* Code highlighter is parses content & inserts necessary font tags
* accordingly to specified programming language & color theme.
*
* @author Kirill Biakov
*/
object CodeHighlighter {
private val LT_BRACE = "<".toRegex()
private const val LT_REGULAR = "<"
private const val LT_TMP = "^"
private val parser = PrettifyParser()
/**
* Highlight code content.
*
* @param codeLanguage Programming language
* @param rawSource Code source by one string
* @param colorTheme Color theme (see below)
* @return Highlighted code, string with necessary inserted color tags
*/
fun highlight(codeLanguage: String, rawSource: String, colorTheme: ColorThemeData): String {
val source = rawSource.escapeLT()
val results = parser.parse(codeLanguage, source)
val colorsMap = buildColorsMap(colorTheme)
val highlighted = StringBuilder()
results.forEach {
val color = colorsMap.getColor(it)
val content = parseContent(source, it)
highlighted.append(content.withFontParams(color))
}
return highlighted.toString()
}
// - Helpers
/**
* Parse user input by extracting highlighted content.
*
* @param codeContent Code content
* @param result Syntax unit
* @return Parsed content to highlight
*/
private fun parseContent(codeContent: String, result: ParseResult): String {
val length = result.offset + result.length
val content = codeContent.substring(result.offset, length)
return content.expandLT()
}
/**
* Color accessor from built color map for selected color theme.
*
* @param result Syntax unit
* @return Color for syntax unit
*/
private fun HashMap<String, String>.getColor(result: ParseResult) =
this[result.styleKeys[0]] ?: this["pln"]
/**
* Build fast accessor (as map) for selected color theme.
*
* @param colorTheme Color theme
* @return Colors map built from color theme
*/
private fun buildColorsMap(colorTheme: ColorThemeData) =
object : HashMap<String, String>() {
init {
val syntaxColors = colorTheme.syntaxColors
put("typ", syntaxColors.type.hex())
put("kwd", syntaxColors.keyword.hex())
put("lit", syntaxColors.literal.hex())
put("com", syntaxColors.comment.hex())
put("str", syntaxColors.string.hex())
put("pun", syntaxColors.punctuation.hex())
put("pln", syntaxColors.plain.hex())
put("tag", syntaxColors.tag.hex())
put("dec", syntaxColors.declaration.hex())
put("src", syntaxColors.plain.hex())
put("atn", syntaxColors.attrName.hex())
put("atv", syntaxColors.attrValue.hex())
put("nocode", syntaxColors.plain.hex())
}
}
// - Escaping/extracting "lower then" symbol
private fun String.escapeLT() = replace(LT_BRACE, LT_TMP)
private fun String.expandLT() = replace(LT_TMP, LT_REGULAR)
}
/**
* Color theme presets.
*/
enum class ColorTheme(
val syntaxColors: SyntaxColors = SyntaxColors(),
val numColor: Int,
val bgContent: Int,
val bgNum: Int,
val noteColor: Int) {
SOLARIZED_LIGHT(
numColor = 0x93A1A1,
bgContent = 0xFDF6E3,
bgNum = 0xEEE8D5,
noteColor = 0x657B83),
MONOKAI(
syntaxColors = SyntaxColors(
type = 0xA7E22E,
keyword = 0xFA2772,
literal = 0x66D9EE,
comment = 0x76715E,
string = 0xE6DB74,
punctuation = 0xC1C1C1,
plain = 0xF8F8F0,
tag = 0xF92672,
declaration = 0xFA2772,
attrName = 0xA6E22E,
attrValue = 0xE6DB74),
numColor = 0x48483E,
bgContent = 0x272822,
bgNum = 0x272822,
noteColor = 0xCFD0C2),
DEFAULT(
numColor = 0x99A8B7,
bgContent = 0xE9EDF4,
bgNum = 0xF2F2F6,
noteColor = 0x4C5D6E);
fun theme() = ColorThemeData(
syntaxColors,
numColor,
bgContent,
bgNum,
noteColor)
}
/**
* Custom color theme.
*/
data class ColorThemeData(
val syntaxColors: SyntaxColors = SyntaxColors(),
val numColor: Int,
val bgContent: Int,
val bgNum: Int,
val noteColor: Int) {
/**
* Decompose preset color theme to data.
* Use this form for using from Kotlin.
*/
fun with(
mySyntaxColors: SyntaxColors = syntaxColors,
myNumColor: Int = numColor,
myBgContent: Int = bgContent,
myBgNum: Int = bgNum,
myNoteColor: Int = noteColor
) = this
/**
* Decompose preset color theme to data.
* Use this form for using from Java.
*/
fun withSyntaxColors(mySyntaxColors: SyntaxColors) =
with(mySyntaxColors = mySyntaxColors)
fun withNumColor(myNumColor: Int) =
with(myNumColor = myNumColor)
fun withBgContent(myBgContent: Int) =
with(myBgContent = myBgContent)
fun withBgNum(myBgNum: Int) =
with(myBgNum = myBgNum)
fun withNoteColor(myNoteColor: Int) =
with(myNoteColor = myNoteColor)
}
/**
* Colors for highlighting code units.
*/
data class SyntaxColors(
val type: Int = 0x859900,
val keyword: Int = 0x268BD2,
val literal: Int = 0x269186,
val comment: Int = 0x93A1A1,
val string: Int = 0x269186,
val punctuation: Int = 0x586E75,
val plain: Int = 0x586E75,
val tag: Int = 0x859900,
val declaration: Int = 0x268BD2,
val attrName: Int = 0x268BD2,
val attrValue: Int = 0x269186)
/**
* Font presets.
*/
enum class Font {
Consolas,
CourierNew,
DejaVuSansMono,
DroidSansMonoSlashed,
Inconsolata,
Monaco;
companion object {
val Default = DroidSansMonoSlashed
}
}
// - Helpers
/**
* @return Converted hex int to color by adding alpha-channel
*/
fun Int.color() = try {
Color.parseColor("#FF${Integer.toHexString(this)}")
} catch (e: IllegalArgumentException) {
this
}
/**
* @return Converted hex int to hex string
*/
fun Int.hex() = "#${Integer.toHexString(this)}"
/**
* @return Is value equals to found or not condition
*/
fun Int.isFound() = this >= 0
fun Int.notFound() = this == -1
/**
* Apply font params to string.
*
* @param color Color as formatter string
* @return Formatted string
*/
fun String.withFontParams(color: String?): String {
val parametrizedString = StringBuilder()
var idx = 0
var newIdx = indexOf("\n")
if (newIdx.notFound()) // covers expected tag coverage (within only one line)
parametrizedString.append(inFontTag(color))
else { // may contain multiple lines with line breaks
// put tag on the borders (end & start of line, ..., end of tag)
do { // until closing tag is reached
val part = substring(idx .. newIdx - 1).inFontTag(color).plus("\n")
parametrizedString.append(part)
idx = newIdx + 1
newIdx = indexOf("\n", idx)
} while (newIdx.isFound())
if (idx != indexOf("\n")) // if not replaced only once (for multiline tag coverage)
parametrizedString.append(substring(idx).inFontTag(color))
}
return parametrizedString.toString()
}
/**
* @return String with escaped line break at start
*/
fun String.escLineBreakAtStart() =
if (startsWith("\n") && length >= 1)
substring(1)
else this
/**
* @return String surrounded by font tag
*/
fun String.inFontTag(color: String?) =
"<font color=\"$color\">${escLineBreakAtStart()}</font>"