From ecd15cc87bbb976a7e8553c1d071f9f39045a8d1 Mon Sep 17 00:00:00 2001 From: Kaida Akatsuki <60681223+vBread@users.noreply.github.com> Date: Wed, 28 Jul 2021 16:36:19 -0400 Subject: [PATCH] feat(code): add syntax highlighting for Crystal (#28) --- .../discord/simpleast/sample/SampleTexts.kt | 33 +++ .../com/discord/simpleast/code/CodeRules.kt | 10 + .../com/discord/simpleast/code/Crystal.kt | 135 +++++++++++ .../simpleast/code/CrystalRulesTest.kt | 210 ++++++++++++++++++ 4 files changed, 388 insertions(+) create mode 100644 simpleast-core/src/main/java/com/discord/simpleast/code/Crystal.kt create mode 100644 simpleast-core/src/test/java/com/discord/simpleast/code/CrystalRulesTest.kt diff --git a/app/src/main/java/com/discord/simpleast/sample/SampleTexts.kt b/app/src/main/java/com/discord/simpleast/sample/SampleTexts.kt index d43ae46..74460a6 100644 --- a/app/src/main/java/com/discord/simpleast/sample/SampleTexts.kt +++ b/app/src/main/java/com/discord/simpleast/sample/SampleTexts.kt @@ -197,6 +197,38 @@ object SampleTexts { ``` """ + private const val CODE_BLOCK_CRYSTAL = """ + Crystal code block: + ```cr + regex = /\bs|d\b/i + match = regex.match("start here but end here") + + # New Class + + @[Species(type: "human")] + class Person(T) + property balance + + def initialize(@name : String) + @age = 0 + @balance = 100 + end + + def grow(years = 1) + @age += years + end + + def info + puts "My name is #{@name} and I am #{@age}" + end + + def buy(item : T) + @balance -= 5 + end + end + ``` + """ + private const val CODE_BLOCK_JAVASCRIPT = """ JavaScript code block: ```js @@ -232,6 +264,7 @@ object SampleTexts { $CODE_BLOCK_RUST $CODE_BLOCK_SQL $CODE_BLOCK_XML + $CODE_BLOCK_CRYSTAL $CODE_BLOCK_JAVASCRIPT That should do it.... diff --git a/simpleast-core/src/main/java/com/discord/simpleast/code/CodeRules.kt b/simpleast-core/src/main/java/com/discord/simpleast/code/CodeRules.kt index c3584a2..48757f5 100644 --- a/simpleast-core/src/main/java/com/discord/simpleast/code/CodeRules.kt +++ b/simpleast-core/src/main/java/com/discord/simpleast/code/CodeRules.kt @@ -189,6 +189,13 @@ object CodeRules { PATTERN_TEXT.toMatchGroupRule(), ) + val crystalRules = createGenericCodeRules( + codeStyleProviders, + additionalRules = Crystal.createCrystalCodeRules(codeStyleProviders), + definitions = arrayOf("def", "class"), + builtIns = Crystal.BUILT_INS, + keywords = Crystal.KEYWORDS) + val javascriptRules = createGenericCodeRules( codeStyleProviders, additionalRules = JavaScript.createCodeRules(codeStyleProviders), @@ -216,6 +223,9 @@ object CodeRules { "xml" to xmlRules, "http" to xmlRules, + "cr" to crystalRules, + "crystal" to crystalRules, + "js" to javascriptRules, "javascript" to javascriptRules, ) diff --git a/simpleast-core/src/main/java/com/discord/simpleast/code/Crystal.kt b/simpleast-core/src/main/java/com/discord/simpleast/code/Crystal.kt new file mode 100644 index 0000000..261d8d9 --- /dev/null +++ b/simpleast-core/src/main/java/com/discord/simpleast/code/Crystal.kt @@ -0,0 +1,135 @@ +package com.discord.simpleast.code + +import com.discord.simpleast.code.CodeRules.toMatchGroupRule +import com.discord.simpleast.core.node.Node +import com.discord.simpleast.core.node.StyleNode +import com.discord.simpleast.core.parser.ParseSpec +import com.discord.simpleast.core.parser.Parser +import com.discord.simpleast.core.parser.Rule +import java.util.regex.Matcher +import java.util.regex.Pattern + +object Crystal { + + val KEYWORDS: Array = arrayOf( + "true|false|nil", + "module|require|include|extend|lib", + "abstract|private|protected", + "annotation|class|finalize|new|initialize|allocate|self|super", + "union|typeof|forall|is_a?|nil?|as?|as|responds_to?|alias|type", + "property|getter|setter|struct|of", + "previous_def|method|fun|enum|macro", + "rescue|raise|begin|end|ensure", + "if|else|elsif|then|unless|until", + "for|in|of|do|when|select|with", + "while|break|next|yield|case", + "print|puts|return", + ) + + val BUILT_INS = arrayOf( + "Nil|Bool|true|false|Void|NoReturn", + "Number|BigDecimal|BigRational|BigFloat|BigInt", + "Int|Int8|Int16|Int32|Int64|UInt8|UInt16|UInt32|UInt64|Float|Float32|Float64", + "Char|String|Symbol|Regex", + "StaticArray|Array|Set|Hash|Range|Tuple|NamedTuple|Union|BitArray", + "Proc|Command|Enum|Class", + "Reference|Value|Struct|Object|Pointer", + "Exception|ArgumentError|KeyError|TypeCastError|IndexError|RuntimeError|NilAssertionError|InvalidBigDecimalException|NotImplementedError|OverflowError", + "pointerof|sizeof|instance_sizeof|offsetof|uninitialized" + ) + + class FunctionNode( + pre: String, signature: String, params: String?, + codeStyleProviders: CodeStyleProviders + ): Node.Parent( + StyleNode.TextStyledNode(pre, codeStyleProviders.keywordStyleProvider), + StyleNode.TextStyledNode(signature, codeStyleProviders.identifierStyleProvider), + params?.let { StyleNode.TextStyledNode(it, codeStyleProviders.paramsStyleProvider) }, + ) { + companion object { + + /** + * Matches against a crystal function declaration + * + * ``` + * def initialize(val : T) + * def increment(amount) + * private def log + * ``` + */ + private val PATTERN_CRYSTAL_FUNC = + Pattern.compile("""^(def)( +\w+)( *\( *(?:@\w+ +: +\w*)?\w+(?: +[:=] +.*)? *\))?(?!.+)""") + + fun createFunctionRule(codeStyleProviders: CodeStyleProviders) = + object : Rule, S>(PATTERN_CRYSTAL_FUNC) { + override fun parse(matcher: Matcher, parser: Parser, S>, state: S): ParseSpec { + val definition = matcher.group(1) + val signature = matcher.group(2) + val params = matcher.group(3) + return ParseSpec.createTerminal(FunctionNode(definition!!, signature!!, params, codeStyleProviders), state) + } + } + } + } + + private val PATTERN_CRYSTAL_COMMENTS = + Pattern.compile("""^(#.*)""") + + /** + * Matches against crystal annotations + * + * ``` + * @[Annotation(key: "value")] + * @[Annotation(1, 2, 3)] + * @[Annotation] + * ``` + */ + private val PATTERN_CRYSTAL_ANNOTATION = + Pattern.compile("""^@\[(\w+)(?:\(.+\))?]""") + + /** + * Matches against a crystal string or character + * + * ``` + * "hello" + * "hello + * world" + * 'a' + * ``` + */ + private val PATTERN_CRYSTAL_STRINGS = + Pattern.compile("""^"[\s\S]*?(?)?|>(?=[>=\s])[>=]?(?:(?<==)>)?)|\[][?=]?|(?:!(?=[=~\s])[=~]?|=?(?:~|==?)))(?:(? createCrystalCodeRules( + codeStyleProviders: CodeStyleProviders + ): List, S>> = + listOf( + PATTERN_CRYSTAL_COMMENTS.toMatchGroupRule(stylesProvider = codeStyleProviders.commentStyleProvider), + PATTERN_CRYSTAL_STRINGS.toMatchGroupRule(stylesProvider = codeStyleProviders.literalStyleProvider), + PATTERN_CRYSTAL_REGEX.toMatchGroupRule(stylesProvider = codeStyleProviders.literalStyleProvider), + PATTERN_CRYSTAL_ANNOTATION.toMatchGroupRule(stylesProvider = codeStyleProviders.genericsStyleProvider), + PATTERN_CRYSTAL_SYMBOL.toMatchGroupRule(stylesProvider = codeStyleProviders.literalStyleProvider), + FunctionNode.createFunctionRule(codeStyleProviders), + ) +} \ No newline at end of file diff --git a/simpleast-core/src/test/java/com/discord/simpleast/code/CrystalRulesTest.kt b/simpleast-core/src/test/java/com/discord/simpleast/code/CrystalRulesTest.kt new file mode 100644 index 0000000..d7e6a55 --- /dev/null +++ b/simpleast-core/src/test/java/com/discord/simpleast/code/CrystalRulesTest.kt @@ -0,0 +1,210 @@ +package com.discord.simpleast.code + +import com.discord.simpleast.assertNodeContents +import com.discord.simpleast.core.node.Node +import com.discord.simpleast.core.node.StyleNode +import com.discord.simpleast.core.parser.Parser +import com.discord.simpleast.core.simple.SimpleMarkdownRules +import com.discord.simpleast.core.utils.TreeMatcher +import org.junit.Before +import org.junit.Test + +class CrystalRulesTest { + + private class TestState + + private lateinit var parser: Parser, TestState> + private lateinit var treeMatcher: TreeMatcher + + @Before + fun setup() { + val codeStyleProviders = CodeStyleProviders() + parser = Parser() + parser + .addRule(CodeRules.createCodeRule( + codeStyleProviders.defaultStyleProvider, + CodeRules.createCodeLanguageMap(codeStyleProviders)) + ) + .addRules(SimpleMarkdownRules.createSimpleMarkdownRules()) + treeMatcher = TreeMatcher() + treeMatcher.registerDefaultMatchers() + } + + @Test + fun comments() { + val ast = parser.parse(""" + ```cr + # Single line comment + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + """ + # Single line comment + """.trimIndent()) + } + + @Test + fun strings() { + val ast = parser.parse(""" + ```cr + hello = "hello" + "world" + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + """"hello"""", + """"world"""" + ) + } + + @Test + fun stringsMultiline() { + val ast = parser.parse(""" + ```cr + text = " + hello + world + " + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + """ + ${'"'} + hello + world + ${'"'} + """.trimIndent(), + ) + } + + @Test + fun regex() { + val ast = parser.parse(""" + ```cr + /^ +/m + /(\d+)-(?:\w{2,5}|0{5})/i + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "/^ +/m", + """/(\d+)-(?:\w{2,5}|0{5})/i""" + ) + } + + @Test + fun annotations() { + val ast = parser.parse(""" + ```cr + @[Post] + @[KeyValue(key: "value")] + @[Positional("string", 123, false)] + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "@[Post]", + "@[KeyValue(key: \"value\")]", + "@[Positional(\"string\", 123, false)]" + ) + } + + @Test + fun symbols() { + val ast = parser.parse(""" + ```cr + :symbol + :[]= + :<< + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + ":symbol", + ":[]=", + ":<<" + ) + } + + @Test + fun functions() { + val ast = parser.parse(""" + ```cr + private def build(id : Int32) + protected def find(@query : String) + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "def build(id : Int32)", + "def find(@query : String)") + } + + @Test + fun keywords() { + val ast = parser.parse(""" + ```cr + module CrystalTest + property length + + macro printer() + + begin + raise + rescue + ensure + + if true + else false + end + + close_door unless door_closed? + + case state + when dirty + + while + break + next + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "module", "property", "macro", + "begin", "raise", "rescue", "ensure", + "if", "true", "else", "false", "end", + "unless", "case", "when", + "while", "break", "next", + ) + } + + @Test + fun numbers() { + val ast = parser.parse(""" + ```cr + x = 0 + x += 12 + add(123, 456) + ``` + """.trimIndent(), TestState()) + ast.assertNodeContents>( + "0", "12", "123", "456" + ) + } + + @Test + fun classDef() { + val ast = parser.parse(""" + ```cr + class Person + def initialize + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>("class Person") + ast.assertNodeContents>("def initialize") + } +} \ No newline at end of file