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 74460a6..80a9988 100644 --- a/app/src/main/java/com/discord/simpleast/sample/SampleTexts.kt +++ b/app/src/main/java/com/discord/simpleast/sample/SampleTexts.kt @@ -233,22 +233,69 @@ object SampleTexts { JavaScript code block: ```js const { performance } = require('perf_hooks'); + function getMem() { return Object.entries(process.memoryUsage()) - .map(([K, V]) => `${'$'}{K}: ${'$'}{(V / (1024 ** 2)).toFixed(1)}MB`) + .map(([K, V]) => + `${'$'}{K}: ${'$'}{(V / (1024 ** 2)).toFixed(1)}MB`) .join('\n'); } + const memories = []; let timer = performance.now(); - for (let i = 0; i < 50; i++) { + + for (let i = 0; i < 50; i++) if (memories.length === 5) break; - else if (i % 5 === 0) memories.push(getMem()); - } + else if (i % 5 === 0) + memories.push(getMem()); + timer = performance.now() - timer; console.log(`Took ${'$'}{timer} ms`); ``` """ + + private const val CODE_BLOCK_TYPESCRIPT = """ + TypeScript code block: + ```ts + import { inspect } from 'util'; + import type { InspectOptions } from 'util'; + + interface LogOptions extends InspectOptions { + showDate?: boolean; + } + + class Logger { + private options: LogOptions; + + public constructor(loggerOptions: LogOptions = {}) { + this.options = loggerOptions; + } + + private log(value: any, options: LogOptions): void { + const showDate: boolean = + options.showDate ?? false; + + delete options.showDate; + + console.log((showDate ? + `[${'$'}{new Date().toLocaleTimeString()}] ` + : '') + inspect(value, options)); + } + + public info(value: any, options: LogOptions = this.options): void { + this.log(value, options); + } + } + + const logger: Logger = new Logger({ + showDate: true, + showHidden: true + }); + + logger.info(1); + ``` + """ const val CODE_BLOCKS = """ # Code block samples @@ -266,6 +313,7 @@ object SampleTexts { $CODE_BLOCK_XML $CODE_BLOCK_CRYSTAL $CODE_BLOCK_JAVASCRIPT + $CODE_BLOCK_TYPESCRIPT 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 ef674cc..4748b2c 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 @@ -116,9 +116,11 @@ object CodeRules { "string|bool|double|float|bytes", "int32|uint32|sint32|int64|unit64|sint64", "map"), - "required|repeated|optional|option|oneof|default|reserved", - "package|import", - "rpc|returns") + keywords = arrayOf( + "required|repeated|optional|option|oneof|default|reserved", + "package|import", + "rpc|returns") + ) val pythonRules = createGenericCodeRules( codeStyleProviders, @@ -133,12 +135,12 @@ object CodeRules { .toMatchGroupRule(stylesProvider = codeStyleProviders.genericsStyleProvider)), definitions = arrayOf("class", "def", "lambda"), builtIns = arrayOf("True|False|None"), - "from|import|global|nonlocal", - "async|await|class|self|cls|def|lambda", - "for|while|if|else|elif|break|continue|return", - "try|except|finally|raise|pass|yeild", - "in|as|is|del", - "and|or|not|assert", + keywords = arrayOf("from|import|global|nonlocal", + "async|await|class|self|cls|def|lambda", + "for|while|if|else|elif|break|continue|return", + "try|except|finally|raise|pass|yeild", + "in|as|is|del", + "and|or|not|assert") ) val rustRules = createGenericCodeRules( @@ -157,11 +159,11 @@ object CodeRules { "Arc|Rc|Box|Pin|Future", "true|false|bool|usize|i64|u64|u32|i32|str|String" ), - "let|mut|static|const|unsafe", - "crate|mod|extern|pub|pub(super)|use", - "struct|enum|trait|type|where|impl|dyn|async|await|move|self|fn", - "for|while|loop|if|else|match|break|continue|return|try", - "in|as|ref", + keywords = arrayOf("let|mut|static|const|unsafe", + "crate|mod|extern|pub|pub(super)|use", + "struct|enum|trait|type|where|impl|dyn|async|await|move|self|fn", + "for|while|loop|if|else|match|break|continue|return|try", + "in|as|ref") ) val xmlRules = listOf, S>>( @@ -203,6 +205,16 @@ object CodeRules { builtIns = JavaScript.BUILT_INS, keywords = JavaScript.KEYWORDS) + val typescriptRules = createGenericCodeRules( + codeStyleProviders, + additionalRules = TypeScript.createCodeRules(codeStyleProviders), + definitions = arrayOf("class", "interface", "enum", + "namespace", "module", "type"), + builtIns = TypeScript.BUILT_INS, + keywords = TypeScript.KEYWORDS, + types = TypeScript.TYPES + ) + return mapOf( "kt" to kotlinRules, "kotlin" to kotlinRules, @@ -228,6 +240,9 @@ object CodeRules { "js" to javascriptRules, "javascript" to javascriptRules, + + "ts" to typescriptRules, + "typescript" to typescriptRules ) } @@ -237,13 +252,17 @@ object CodeRules { private fun createGenericCodeRules( codeStyleProviders: CodeStyleProviders, additionalRules: List, S>>, - definitions: Array, builtIns: Array, vararg keywords: String + definitions: Array, + builtIns: Array, + keywords: Array, + types: Array = arrayOf(" ") ): List, S>> = additionalRules + listOf( createDefinitionRule(codeStyleProviders, *definitions), createWordPattern(*builtIns).toMatchGroupRule(stylesProvider = codeStyleProviders.genericsStyleProvider), createWordPattern(*keywords).toMatchGroupRule(stylesProvider = codeStyleProviders.keywordStyleProvider), + createWordPattern(*types).toMatchGroupRule(stylesProvider = codeStyleProviders.typesStyleProvider), PATTERN_NUMBERS.toMatchGroupRule(stylesProvider = codeStyleProviders.literalStyleProvider), PATTERN_LEADING_WS_CONSUMER.toMatchGroupRule(), PATTERN_TEXT.toMatchGroupRule(), diff --git a/simpleast-core/src/main/java/com/discord/simpleast/code/TypeScript.kt b/simpleast-core/src/main/java/com/discord/simpleast/code/TypeScript.kt new file mode 100644 index 0000000..2952d93 --- /dev/null +++ b/simpleast-core/src/main/java/com/discord/simpleast/code/TypeScript.kt @@ -0,0 +1,249 @@ +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 TypeScript { + + val KEYWORDS: Array = arrayOf( + "import|from|export|default|package", + "class|enum", + "function|super|extends|implements|arguments", + "var|let|const|static|get|set|new", + "return|break|continue|yield|void", + "if|else|for|while|do|switch|async|await|case|try|catch|finally|delete|throw|NaN|Infinity", + "of|in|instanceof|typeof", + "debugger|with", + "true|false|null|undefined", + "type|as|interface|public|private|protected|module|declare|namespace", + "abstract|keyof|readonly|is|asserts|infer|override|intrinsic" + ) + + val BUILT_INS: Array = arrayOf( + "String|Boolean|RegExp|Number|Date|Math|JSON|Symbol|BigInt|Atomics|DataView", + "Function|Promise|Generator|GeneratorFunction|AsyncFunction|AsyncGenerator|AsyncGeneratorFunction", + "Array|Object|Map|Set|WeakMap|WeakSet|Int8Array|Int16Array|Int32Array|Uint8Array|Uint16Array", + "Uint32Array|Uint8ClampedArray|Float32Array|Float64Array|BigInt64Array|BigUint64Array|Buffer", + "ArrayBuffer|SharedArrayBuffer", + "Reflect|Proxy|Intl|WebAssembly", + "console|process|require|isNaN|parseInt|parseFloat|encodeURI|decodeURI|encodeURIComponent", + "decodeURIComponent|this|global|globalThis|eval|isFinite|module", + "setTimeout|setInterval|clearTimeout|clearInterval|setImmediate|clearImmediate", + "queueMicrotask|document|window", + "Error|SyntaxError|TypeError|RangeError|ReferenceError|EvalError|InternalError|URIError", + "AggregateError|escape|unescape|URL|URLSearchParams|TextEncoder|TextDecoder", + "AbortController|AbortSignal|EventTarget|Event|MessageChannel", + "MessagePort|MessageEvent|FinalizationRegistry|WeakRef", + "regeneratorRuntime|performance", + "Iterable|Iterator|IterableIterator", + "Partial|Required|Readonly|Record|Pick|Omit|Exclude|Extract", + "NonNullable|Parameters|ConstructorParameters|ReturnType", + "InstanceType|ThisParameterType|OmitThisParameter", + "ThisType|Uppercase|Lowercase|Capitalize|Uncapitalize" + ) + + val TYPES: Array = arrayOf( + "string|number|boolean|object|symbol|any|unknown|bigint|never" + ) + + class FunctionNode( + pre: String, signature: String?, generics: String?, + codeStyleProviders: CodeStyleProviders + ) : Node.Parent( + StyleNode.TextStyledNode(pre, codeStyleProviders.keywordStyleProvider), + signature?.let { StyleNode.TextStyledNode(signature, codeStyleProviders.identifierStyleProvider) }, + generics?.let { StyleNode.TextStyledNode(generics, codeStyleProviders.genericsStyleProvider) }, + ) { + companion object { + /** + * Matches against a TypeScript function declaration. + * + * ``` + * function foo(bar: string) + * function baz() + * function control(target: T): T + * async test() + * static nice() + * function* generator() + * get token() + * set internals() + * ``` + */ + private val PATTERN_TYPESCRIPT_FUNC = + """^((?:function\*?|static|get|set|async)\s)(\s*[a-zA-Z_$][a-zA-Z0-9_$]*)?(\s*<.*>)?""".toRegex(RegexOption.DOT_MATCHES_ALL).toPattern() + + fun createFunctionRule(codeStyleProviders: CodeStyleProviders) = + object : Rule, S>(PATTERN_TYPESCRIPT_FUNC) { + override fun parse(matcher: Matcher, parser: Parser, S>, state: S): ParseSpec { + val pre = matcher.group(1) + val signature = matcher.group(2) + val generics = matcher.group(3) + return ParseSpec.createTerminal(FunctionNode(pre!!, signature, generics, codeStyleProviders), state) + } + } + } + } + + class FieldNode( + definition: String, name: String, + codeStyleProviders: CodeStyleProviders + ) : Node.Parent( + StyleNode.TextStyledNode(definition, codeStyleProviders.keywordStyleProvider), + StyleNode.TextStyledNode(name, codeStyleProviders.identifierStyleProvider), + ) { + companion object { + /** + * Matches against a TypeScript field definition. + * + * ``` + * var x = 1; + * let y = 5; + * const z = 10; + * const h: string = 'Hello world'; + * ``` + */ + private val PATTERN_TYPESCRIPT_FIELD = + Pattern.compile("""^(var|let|const)(\s+[a-zA-Z_$][a-zA-Z0-9_$]*)""") + + fun createFieldRule( + codeStyleProviders: CodeStyleProviders + ) = + object : Rule, S>(PATTERN_TYPESCRIPT_FIELD) { + override fun parse(matcher: Matcher, parser: Parser, S>, state: S): + ParseSpec { + val definition = matcher.group(1) + val name = matcher.group(2) + return ParseSpec.createTerminal( + FieldNode(definition!!, name!!, codeStyleProviders), state) + } + } + } + } + + class ObjectPropertyNode( + prefix: String, accessModifier: String?, property: String, suffix: String, + codeStyleProviders: CodeStyleProviders + ) : Node.Parent( + StyleNode.TextStyledNode(prefix, codeStyleProviders.defaultStyleProvider), + accessModifier?.let { StyleNode.TextStyledNode(accessModifier, codeStyleProviders.keywordStyleProvider) }, + StyleNode.TextStyledNode(property, codeStyleProviders.identifierStyleProvider), + StyleNode.TextStyledNode(suffix, codeStyleProviders.defaultStyleProvider), + ) { + companion object { + /** + * Matches against a TypeScript object property. + * + * ``` + * { foo: 'bar' } + * ``` + */ + private val PATTERN_TYPESCRIPT_OBJECT_PROPERTY = + Pattern.compile("""^([{\[(,;](?:\s*-)?)(\s*(?:public|private|protected|readonly))?(\s*[a-zA-Z0-9_$]+)((?:\s*\?)?\s*:)""") + + fun createObjectPropertyRule( + codeStyleProviders: CodeStyleProviders + ) = + object : Rule, S>(PATTERN_TYPESCRIPT_OBJECT_PROPERTY) { + override fun parse(matcher: Matcher, parser: Parser, S>, state: S): + ParseSpec { + val prefix = matcher.group(1) + val accessModifier = matcher.group(2) + val property = matcher.group(3) + val suffix = matcher.group(4) + return ParseSpec.createTerminal( + ObjectPropertyNode(prefix!!, accessModifier, property!!, suffix!!, codeStyleProviders), state) + } + } + } + } + + class DecoratorNode( + prefix: String, decorator: String, generics: String?, + codeStyleProviders: CodeStyleProviders + ) : Node.Parent( + StyleNode.TextStyledNode(prefix, codeStyleProviders.keywordStyleProvider), + StyleNode.TextStyledNode(decorator, codeStyleProviders.genericsStyleProvider), + generics?.let { StyleNode.TextStyledNode(generics, codeStyleProviders.genericsStyleProvider) } + ) { + companion object { + /** + * Matches against a TypeScript decorator. + * + * ``` + * @sealed + * @timed + * @wrap + * @expose() + * @log() + * ``` + */ + private val PATTERN_TYPESCRIPT_DECORATOR = + Pattern.compile("""^(@)(\s*[a-zA-Z_$][a-zA-Z0-9_$]*)(<.*>)?""", Pattern.DOTALL) + + fun createDecoratorRule( + codeStyleProviders: CodeStyleProviders + ) = + object : Rule, S>(PATTERN_TYPESCRIPT_DECORATOR) { + override fun parse(matcher: Matcher, parser: Parser, S>, state: S): + ParseSpec { + val prefix = matcher.group(1) + val decorator = matcher.group(2) + val generics = matcher.group(3) + return ParseSpec.createTerminal(DecoratorNode(prefix!!, decorator!!, generics, codeStyleProviders), state) + } + } + } + } + + /** + * Matches against a TypeScript regex. + * + * ``` + * /(.*)/ + * ``` + */ + private val PATTERN_TYPESCRIPT_REGEX = + Pattern.compile("""^/.+(? createCodeRules( + codeStyleProviders: CodeStyleProviders + ): List, S>> = + listOf( + PATTERN_TYPESCRIPT_COMMENTS.toMatchGroupRule(stylesProvider = codeStyleProviders.commentStyleProvider), + PATTERN_TYPESCRIPT_STRINGS.toMatchGroupRule(stylesProvider = codeStyleProviders.literalStyleProvider), + ObjectPropertyNode.createObjectPropertyRule(codeStyleProviders), + PATTERN_TYPESCRIPT_REGEX.toMatchGroupRule(stylesProvider = codeStyleProviders.literalStyleProvider), + FieldNode.createFieldRule(codeStyleProviders), + FunctionNode.createFunctionRule(codeStyleProviders), + DecoratorNode.createDecoratorRule(codeStyleProviders), + ) +} diff --git a/simpleast-core/src/main/java/com/discord/simpleast/core/parser/Parser.kt b/simpleast-core/src/main/java/com/discord/simpleast/core/parser/Parser.kt index f806188..ad6bb9c 100644 --- a/simpleast-core/src/main/java/com/discord/simpleast/core/parser/Parser.kt +++ b/simpleast-core/src/main/java/com/discord/simpleast/core/parser/Parser.kt @@ -131,4 +131,4 @@ private inline fun List.firstMapOrNull(predicate: (T) -> V?): V? { return found } return null -} \ No newline at end of file +} diff --git a/simpleast-core/src/test/java/com/discord/simpleast/code/CodeRulesTest.kt b/simpleast-core/src/test/java/com/discord/simpleast/code/CodeRulesTest.kt index 6facc79..3af6c32 100644 --- a/simpleast-core/src/test/java/com/discord/simpleast/code/CodeRulesTest.kt +++ b/simpleast-core/src/test/java/com/discord/simpleast/code/CodeRulesTest.kt @@ -66,7 +66,7 @@ class CodeRulesTest { \```test``` """.trimIndent(), TestState()) - ast.assertNodeContents>("`", "``", "test", "``", "`") + ast.assertNodeContents>("test") } @Test @@ -329,4 +329,4 @@ class CodeRulesTest { "and", "is", "NOT", "NULL", "Order By") } -} \ No newline at end of file +} diff --git a/simpleast-core/src/test/java/com/discord/simpleast/code/JavaScriptRulesTest.kt b/simpleast-core/src/test/java/com/discord/simpleast/code/JavaScriptRulesTest.kt index 3880e19..e0d8030 100644 --- a/simpleast-core/src/test/java/com/discord/simpleast/code/JavaScriptRulesTest.kt +++ b/simpleast-core/src/test/java/com/discord/simpleast/code/JavaScriptRulesTest.kt @@ -108,12 +108,12 @@ class JavaScriptRulesTest { ast.assertNodeContents>( "function test(T)", - "function () {", - "function* generator() {", - "static test() {", - "async fetch() {", - "get tokens() {", - "set constants() {") + "function ()", + "function* generator()", + "static test()", + "async fetch()", + "get tokens()", + "set constants()") } @Test @@ -162,7 +162,7 @@ class JavaScriptRulesTest { "while", "true", "for", "if", "false", "class", "try", "catch", - "finally", "return", "throw") + "throw", "finally", "return") } @Test diff --git a/simpleast-core/src/test/java/com/discord/simpleast/code/TypeScriptRulesTest.kt b/simpleast-core/src/test/java/com/discord/simpleast/code/TypeScriptRulesTest.kt new file mode 100644 index 0000000..5738c47 --- /dev/null +++ b/simpleast-core/src/test/java/com/discord/simpleast/code/TypeScriptRulesTest.kt @@ -0,0 +1,278 @@ +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 TypeScriptRulesTest { + + 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(""" + ```ts + /** Multiline + Comment + */ + foo.bar(); // Inlined + // Line comment + + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + """ + /** Multiline + Comment + */ + """.trimIndent(), + "// Inlined", + "// Line comment") + } + + @Test + fun strings() { + val ast = parser.parse(""" + ```ts + x = 'Hello'; + y = "world"; + log(`Hi`); + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "'Hello'", + "\"world\"", + "`Hi`") + } + @Test + fun stringsMultiline() { + val ast = parser.parse(""" + ```ts + text = ` + hello + world + `; + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + """ + ` + hello + world + ` + """.trimIndent(), + ) + } + + @Test + fun functions() { + val ast = parser.parse(""" + ```ts + function test(T) { + // Implementation + } + fn = function () {}; + function* generator() {} + static test() {} + async fetch() {} + get tokens() {} + set constants() {} + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "function test", + "function ", + "function* generator", + "static test", + "async fetch", + "get tokens", + "set constants") + } + + @Test + fun commentedFunction() { + val ast = parser.parse(""" + ```ts + /* + function test(T) { + throw new Error(); + } + */ + // function O() {} + log(x /* test var */); + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + """ + /* + function test(T) { + throw new Error(); + } + */ + """.trimIndent(), + "// function O() {}", + "/* test var */") + } + + @Test + fun keywords() { + val ast = parser.parse(""" + ```ts + while (true) {} + for (;;) {} + if (false) {} + class {} + try { + } catch (err) { + throw + } finally { + return; + } + ``` + """.trimIndent(), TestState()) + ast.assertNodeContents>( + "while", "true", + "for", "if", "false", + "class", "try", "catch", + "throw", "finally", "return") + } + + @Test + fun numbers() { + val ast = parser.parse(""" + ```ts + x = 0; + x += 69; + max(x, 420); + ``` + """.trimIndent(), TestState()) + ast.assertNodeContents>( + "0", "69", "420" + ) + } + + @Test + fun fields() { + val ast = parser.parse(""" + ```ts + var foo = x; + let bar = y; + const baz = z; + ``` + """.trimIndent(), TestState()) + ast.assertNodeContents>( + "var foo", + "let bar", + "const baz") + } + + @Test + fun classDef() { + val ast = parser.parse(""" + ```ts + class Bug {} + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "class Bug") + } + + @Test + fun regex() { + val ast = parser.parse(""" + ```ts + /(.*)/g + /[\$\{\}]/ + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>("/(.*)/g", """/[\$\{\}]/""") + } + + @Test + fun objectProperties() { + val ast = parser.parse(""" + ```ts + { foo: bar } + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>("{ foo:") + } + + @Test + fun types() { + val ast = parser.parse(""" + ```ts + string; + boolean; + number; + symbol; + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "string", "boolean", "number", "symbol") + } + + @Test + fun decorators() { + val ast = parser.parse(""" + ```ts + @sealed + @test + @internal + @wrap + @save() + @call() + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "@sealed", "@test", "@internal", + "@wrap", "@save", "@call") + } + + @Test + fun interfaces() { + val ast = parser.parse(""" + ```ts + interface Foo {} + interface _Bar {} + interface Baz_ {} + ``` + """.trimIndent(), TestState()) + + ast.assertNodeContents>( + "interface Foo", "interface _Bar", "interface Baz_") + } +}