1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

Lib | Code syntax highlighter. First iteration (#1033)

This commit is contained in:
Evgenii Kozlov 2020-10-28 21:54:32 +03:00 committed by GitHub
parent 2a8ffedd8c
commit 20767b3d1c
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 575 additions and 3 deletions

View file

@ -85,6 +85,7 @@ dependencies {
implementation project(':library-kanban-widget')
implementation project(':library-page-icon-picker-widget')
implementation project(':library-emojifier')
implementation project(':library-syntax-highlighter')
implementation project(':analytics')
def applicationDependencies = rootProject.ext.mainApplication

View file

@ -0,0 +1,10 @@
{
"keyword": "#d73a49",
"comment": "#6a737d",
"function": "#6f42c1",
"number": "#0366d6",
"boolean": "#0366d6",
"operator": "#0366d6",
"string": "#05264c",
"property": "#0366d6"
}

View file

@ -0,0 +1,32 @@
{
"keywords": [
],
"operators": [],
"other": [
{
"pattern": "[-a-z0-9]+(?=\\()",
"color": "#6f42c1",
"key": "function"
},
{
"pattern": "[-_a-z\\xA0-\\uFFFF][-\\w\\xA0-\\uFFFF]*(?=\\s*:)",
"color": "#0366d6",
"key": "property"
},
{
"pattern": "!important\\b",
"color": "#d73a49",
"key": "important"
},
{
"pattern": "\\/\\*[\\s\\S]*?\\*\\/",
"color": "#6a737d",
"key": "comment"
},
{
"pattern": "(\"|')(?:\\\\(?:\\r\\n|[\\s\\S])|(?!\\1)[^\\\\\\r\\n])*\\1",
"color": "#05264c",
"key": "string"
}
]
}

View file

@ -0,0 +1,38 @@
{
"keywords": [
{
"pattern": "\\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\\b",
"color": "#d73a49",
"key": "keyword"
}
],
"operators": [
{
"pattern": "[*\\/%^!=]=?|\\+[=+]?|-[=-]?|\\|[=|]?|&(?:=|&|\\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\\.\\.\\.",
"color": "#0366d6",
"key": "operator"
}
],
"other": [
{
"pattern": "([a-zA-Z_{1}][a-zA-Z0-9_]+)(?=\\()",
"color": "#6f42c1",
"key": "function"
},
{
"pattern": "\\b(?:_|iota|nil|true|false)\\b",
"color": "#0366d6",
"key": "boolean"
},
{
"pattern": "(?:\\b0x[a-f\\d]+|(?:\\b\\d+\\.?\\d*|\\B\\.\\d+)(?:e[-+]?\\d+)?)i?",
"color": "#0366d6",
"key": "number"
},
{
"pattern": "([\"'`])(?:\\\\[\\s\\S]|(?!\\1)[^\\\\])*\\1",
"color": "#05264c",
"key": "string"
}
]
}

View file

@ -0,0 +1,37 @@
{
"keywords": [
{
"pattern": "\\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\\b",
"color": "#bd2c00",
"key": "keyword"
}
],
"operators": [],
"other": [
{
"pattern": "\\(|\\)|\\[|\\]|:|;|\\.|\\||;|\\&|\\{|\\}",
"color": "#b71c1c",
"key": ""
},
{
"pattern": "@\\w+",
"color": "#b87333",
"key": "annotation"
},
{
"pattern": "\\b\\d+[\\.]?\\d*([eE]\\-?\\d+)?[lLdDfF]?\\b|\\b0x[a-fA-F\\d]+\\b",
"color": "#f4511e",
"key:": ""
},
{
"pattern": "(\\\"(.*)\\\"|\\\"(.*)\\\")",
"color": "#ff0000",
"key": ""
},
{
"pattern": "\\b[A-Z](?:\\w*[a-z]\\w*)?\\b",
"color": "#6e5494",
"key": "class"
}
]
}

View file

@ -0,0 +1,48 @@
{
"keywords": [
{
"pattern": "(^|[^.]|\\.\\.\\.\\s*)\\b(?:as|async(?=\\s*(?:function\\b|\\(|[$\\w\\xA0-\\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\\s*[\\[$\\w\\xA0-\\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\\b",
"color": "#bd2c00",
"key": "keyword"
}
],
"operators": [
{
"pattern": "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]",
"color": "#bd2c00",
"key": "keyword"
}
],
"other": [
{
"pattern": "#?[_$a-zA-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()",
"color": "#6e5494",
"key": "function"
},
{
"pattern": "\\(|\\)|\\[|\\]|:|;|\\.|\\||;|\\&|\\{|\\}",
"color": "#b71c1c",
"key": ""
},
{
"pattern": "@\\w+",
"color": "#b87333",
"key": "annotation"
},
{
"pattern": "\\b(?:(?:0[xX](?:[\\dA-Fa-f](?:_[\\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\\d(?:_\\d)?)+n|NaN|Infinity)\\b|(?:\\b(?:\\d(?:_\\d)?)+\\.?(?:\\d(?:_\\d)?)*|\\B\\.(?:\\d(?:_\\d)?)+)(?:[Ee][+-]?(?:\\d(?:_\\d)?)+)?",
"color": "#f4511e",
"key": "number"
},
{
"pattern": "(\\\"(.*)\\\"|\\\"(.*)\\\")",
"color": "#4078c0",
"key": ""
},
{
"pattern": "(^|[^$\\w\\xA0-\\uFFFF])[_$A-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\.(?:prototype|constructor))",
"color": "#6e5494",
"key": "class"
}
]
}

View file

@ -0,0 +1,41 @@
{
"keywords": [],
"operators": [],
"other": [
{
"pattern": "\"(?:\\\\.|[^\\\\\"\\r\\n])*\"(?=\\s*:)",
"color": "#b71c1c",
"key": "property"
},
{
"pattern": "\"(?!\\s*:)(?:\\\\.|[^\\\\\"\\r\\n])*\"(?!\\s*:)",
"color": "#4078c0",
"key": "string"
},
{
"pattern": ":",
"color": "#6e5494",
"key": "operator"
},
{
"pattern": "-?\\b\\d+(?:\\.\\d+)?(?:e[+-]?\\d+)?\\b",
"color": "#6e5494",
"key": "number"
},
{
"pattern": "\\b(?:true|false)\\b",
"color": "#6e5494",
"key": "boolean"
},
{
"pattern": "\\bnull\\b",
"color": "#6e5494",
"key": "null"
},
{
"pattern": "[{}[\\\\],]",
"color": "#b8b8b8",
"key": "punctuation"
}
]
}

View file

@ -0,0 +1,37 @@
{
"keywords": [
{
"pattern": "\\w+(?=\\s*\\()",
"color": "#6f42c1",
"key": "function"
},
{
"pattern": "(^|[^.])\\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\\b",
"color": "#d73a49",
"key": "keyword"
}
],
"operators": [],
"other": [
{
"pattern": "@\\w+",
"color": "#b87333",
"key": "annotation"
},
{
"pattern": "\\b\\d+[\\.]?\\d*([eE]\\-?\\d+)?[lLdDfF]?\\b|\\b0x[a-fA-F\\d]+\\b",
"color": "#0366d6",
"key": "number"
},
{
"pattern": "(\\\"(.*)\\\"|\\\"(.*)\\\")",
"color": "#ff0000",
"key": ""
},
{
"pattern": "\\b[A-Z](?:\\w*[a-z]\\w*)?\\b",
"color": "#6e5494",
"key": "class"
}
]
}

View file

@ -0,0 +1,38 @@
{
"keywords": [
{
"pattern": "\\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\\b",
"color": "#d73a49",
"key": "keyword"
}
],
"operators": [
{
"pattern": "[-+%=]=?|!=|\\*\\*?=?|\\/\\/?=?|<[<=>]?|>[=>]?|[&|^~]",
"color": "#0366d6",
"key": "keyword"
}
],
"other": [
{
"pattern": "([a-zA-Z_{1}][a-zA-Z0-9_]+)(?=\\()",
"color": "#6f42c1",
"key": "function"
},
{
"pattern": "(^|[^\\\\])#.*",
"color": "#6a737d",
"key": "comment"
},
{
"pattern": "(?:\\b(?=\\d)|\\B(?=\\.))(?:0[bo])?(?:(?:\\d|0x[\\da-f])[\\da-f]*\\.?\\d*|\\.\\d+)(?:e[+-]?\\d+)?j?\\b",
"color": "#0366d6",
"key": "number"
},
{
"pattern": "(?:[rub]|rb|br)?(\"|')(?:\\\\.|(?!\\1)[^\\\\\\r\\n])*\\1",
"color": "#05264c",
"key": "string"
}
]
}

View file

@ -0,0 +1,23 @@
{
"keywords": [
{
"pattern": "",
"color": "",
"key": ""
}
],
"operators": [
{
"pattern": "",
"color": "",
"key": ""
}
],
"other": [
{
"pattern": "",
"color": "",
"key": ""
}
]
}

View file

@ -0,0 +1,53 @@
{
"keywords": [
{
"pattern": "\\b(?:abstract|as|asserts|async|await|break|case|catch|class|const|constructor|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|is|keyof|let|module|namespace|new|null|of|package|private|protected|public|readonly|return|require|set|static|super|switch|this|throw|try|type|typeof|undefined|var|void|while|with|yield)\\b",
"color": "#bd2c00",
"key": "keyword"
},
{
"pattern": "\\b(?:string|Function|any|number|boolean|Array|symbol|console|Promise|unknown|never)\\b",
"color": "#bd2c00",
"key": "keyword"
}
],
"operators": [
{
"pattern": "--|\\+\\+|\\*\\*=?|=>|&&=?|\\|\\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\\.{3}|\\?\\?=?|\\?\\.?|[~:]",
"color": "#bd2c00",
"key": "keyword"
}
],
"other": [
{
"pattern": "#?[_$a-zA-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?=\\s*(?:\\.\\s*(?:apply|bind|call)\\s*)?\\()",
"color": "#6e5494",
"key": "function"
},
{
"pattern": "\\(|\\)|\\[|\\]|:|;|\\.|\\||;|\\&|\\{|\\}",
"color": "#b71c1c",
"key": ""
},
{
"pattern": "@\\w+",
"color": "#b87333",
"key": "annotation"
},
{
"pattern": "\\b(?:(?:0[xX](?:[\\dA-Fa-f](?:_[\\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\\d(?:_\\d)?)+n|NaN|Infinity)\\b|(?:\\b(?:\\d(?:_\\d)?)+\\.?(?:\\d(?:_\\d)?)*|\\B\\.(?:\\d(?:_\\d)?)+)(?:[Ee][+-]?(?:\\d(?:_\\d)?)+)?",
"color": "#f4511e",
"key": "number"
},
{
"pattern": "(\\\"(.*)\\\"|\\\"(.*)\\\")",
"color": "#4078c0",
"key": ""
},
{
"pattern": "(\\b(?:class|extends|implements|instanceof|interface|new|type)\\s+)(?!keyof\\b)[_$a-zA-Z\\xA0-\\uFFFF][$\\w\\xA0-\\uFFFF]*(?:\\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?",
"color": "#6e5494",
"key": "class"
}
]
}

View file

@ -32,6 +32,7 @@ buildscript {
classpath "com.google.protobuf:protobuf-gradle-plugin:0.8.12"
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
}
}

View file

@ -45,6 +45,7 @@ dependencies {
implementation project(':core-utils')
implementation project(':library-emojifier')
implementation project(':library-syntax-highlighter')
implementation applicationDependencies.appcompat
implementation applicationDependencies.kotlin

View file

@ -1,14 +1,21 @@
package com.anytypeio.anytype.core_ui.widgets.text
import android.content.Context
import android.text.Editable
import android.text.InputType.*
import android.text.TextWatcher
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatEditText
import com.anytypeio.anytype.core_ui.tools.DefaultTextWatcher
import com.anytypeio.anytype.library_syntax_highlighter.*
import timber.log.Timber
class CodeTextInputWidget : AppCompatEditText {
class CodeTextInputWidget : AppCompatEditText, SyntaxHighlighter {
override val rules: MutableList<Syntax> = mutableListOf()
override val source: Editable get() = editableText
private val syntaxTextWatcher = SyntaxTextWatcher { highlight() }
private val watchers: MutableList<TextWatcher> = mutableListOf()
@ -17,6 +24,7 @@ class CodeTextInputWidget : AppCompatEditText {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
setup()
setupSyntaxHighlighter()
}
constructor(
@ -25,12 +33,19 @@ class CodeTextInputWidget : AppCompatEditText {
defStyle: Int
) : super(context, attrs, defStyle) {
setup()
setupSyntaxHighlighter()
}
private fun setup() {
enableEditMode()
}
private fun setupSyntaxHighlighter() {
addRules(context.obtainSyntaxRules(Syntaxes.KOTLIN))
highlight()
addTextChangedListener(syntaxTextWatcher)
}
fun enableEditMode() {
setRawInputType(TYPE_CLASS_TEXT or TYPE_TEXT_FLAG_MULTI_LINE or TYPE_TEXT_FLAG_NO_SUGGESTIONS)
setHorizontallyScrolling(false)
@ -46,7 +61,7 @@ class CodeTextInputWidget : AppCompatEditText {
}
override fun addTextChangedListener(watcher: TextWatcher) {
watchers.add(watcher)
if (watcher !is SyntaxTextWatcher) watchers.add(watcher)
super.addTextChangedListener(watcher)
}

View file

@ -8,6 +8,7 @@ allprojects {
ext {
// Kotlin
kotlin_coroutines_version = '1.3.9'
kotlinx_serialization_json_version = '1.0.0'
// AndroidX
androidx_core_version = '1.5.0-alpha02'
@ -130,7 +131,7 @@ ext {
permissionDispCompiler: "org.permissionsdispatcher:permissionsdispatcher-processor:$permission_disp_version",
pickT: "com.github.HBiSoft:PickiT:$pickt_version",
urlcleaner: "com.shekhargulati.urlcleaner:urlcleaner:$urlcleaner_version",
kotlinxSerializationJson: "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinx_serialization_json_version",
crashlytics: "com.google.firebase:firebase-crashlytics:$crashlytics_version",
firebaseCore: "com.google.firebase:firebase-core:$firebase_core_version"
]

1
library-syntax-highlighter/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,58 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlinx-serialization'
android {
def config = rootProject.extensions.getByName("ext")
compileSdkVersion compile_sdk
defaultConfig {
minSdkVersion config["min_sdk"]
targetSdkVersion config["target_sdk"]
versionCode config["version_code"]
versionName config["version_name"]
testInstrumentationRunner config["test_runner"]
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
testOptions {
unitTests {
includeAndroidResources = true
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
def applicationDependencies = rootProject.ext.mainApplication
def unitTestDependencies = rootProject.ext.unitTesting
implementation applicationDependencies.appcompat
implementation applicationDependencies.kotlin
implementation applicationDependencies.coroutines
implementation applicationDependencies.androidxCore
implementation applicationDependencies.timber
implementation applicationDependencies.kotlinxSerializationJson
testImplementation unitTestDependencies.junit
testImplementation unitTestDependencies.kotlinTest
testImplementation unitTestDependencies.robolectric
testImplementation unitTestDependencies.androidXTestCore
}

View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1 @@
<manifest package="com.anytypeio.anytype.library_syntax_highlighter" />

View file

@ -0,0 +1,9 @@
package com.anytypeio.anytype.library_syntax_highlighter
import java.util.regex.Matcher
import java.util.regex.Pattern
class Syntax(val color: Int, val regex: String) {
private val pattern = Pattern.compile(regex)
fun matcher(txt: String): Matcher = pattern.matcher(txt)
}

View file

@ -0,0 +1,5 @@
package com.anytypeio.anytype.library_syntax_highlighter
import android.text.style.ForegroundColorSpan
class SyntaxColorSpan(color: Int) : ForegroundColorSpan(color)

View file

@ -0,0 +1,13 @@
package com.anytypeio.anytype.library_syntax_highlighter
import kotlinx.serialization.Serializable
@Serializable
data class SyntaxEntity(val pattern: String, val color: String, val key: String)
@Serializable
data class SyntaxDescriptor(
val keywords: List<SyntaxEntity>,
val operators: List<SyntaxEntity>,
val other: List<SyntaxEntity>
)

View file

@ -0,0 +1,41 @@
package com.anytypeio.anytype.library_syntax_highlighter
import android.text.Editable
import android.text.Spannable
/**
* @property [source] text containing source code, whose syntax we need to highlight
* @property [rules] set of syntax rules for a programming language
*/
interface SyntaxHighlighter {
val source: Editable
val rules: MutableList<Syntax>
fun addRules(new: List<Syntax>) {
rules.apply {
clear()
addAll(new)
}
}
fun highlight() {
clear()
rules.forEach { syntax ->
val matcher = syntax.matcher(source.toString())
while (matcher.find()) {
source.setSpan(
SyntaxColorSpan(syntax.color),
matcher.start(),
matcher.end(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
fun clear() {
val current = source.getSpans(0, source.length, SyntaxColorSpan::class.java)
current.forEach { span -> source.removeSpan(span) }
}
}

View file

@ -0,0 +1,12 @@
package com.anytypeio.anytype.library_syntax_highlighter
import android.text.Editable
import android.text.TextWatcher
class SyntaxTextWatcher(private val onAfterTextChanged: () -> Unit) : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
onAfterTextChanged()
}
}

View file

@ -0,0 +1,6 @@
package com.anytypeio.anytype.library_syntax_highlighter
object Syntaxes {
const val KOTLIN = "kotlin"
const val PYTHON = "python"
}

View file

@ -0,0 +1,28 @@
package com.anytypeio.anytype.library_syntax_highlighter
import android.content.Context
import android.graphics.Color
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import java.io.IOException
fun Context.obtainSyntaxRules(language: String): List<Syntax> {
val path = "syntax/${language}.json"
val json = obtainJsonDataFromAsset(path)
checkNotNull(json) { "Could not deserialize syntax rules from path: $path" }
val descriptor = Json.decodeFromString<SyntaxDescriptor>(json)
val rules = descriptor.let { it.keywords + it.operators + it.other }
return rules.map { s ->
Syntax(
regex = s.pattern,
color = Color.parseColor(s.color)
)
}
}
fun Context.obtainJsonDataFromAsset(path: String): String? = try {
assets.open(path).bufferedReader().use { stream -> stream.readText() }
} catch (e: IOException) {
e.printStackTrace()
null
}

View file

@ -12,6 +12,7 @@ include ':app',
':library-kanban-widget',
':library-page-icon-picker-widget',
':library-emojifier',
':library-syntax-highlighter',
':sample',
':clipboard',
':analytics'