├── .DS_Store ├── .gitignore ├── .kotlin └── metadata │ └── kotlinTransformedMetadataLibraries │ ├── org.jetbrains.kotlin-kotlin-stdlib-2.0.0-commonMain-2bbUHA.klib │ ├── org.jetbrains.kotlin-kotlin-test-2.0.0-annotationsCommonMain-24eTFQ.klib │ └── org.jetbrains.kotlin-kotlin-test-2.0.0-assertionsCommonMain-24eTFQ.klib ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── publish-root.gradle ├── sample ├── build.gradle.kts ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ └── Main.kt ├── settings.gradle.kts └── src ├── commonMain └── kotlin │ └── dev │ └── snipme │ └── highlights │ ├── Highlights.kt │ ├── HighlightsResultListener.kt │ ├── internal │ ├── CodeAnalyzer.kt │ ├── CodeComparator.kt │ ├── Extensions.kt │ ├── SyntaxTokens.kt │ └── locator │ │ ├── AnnotationLocator.kt │ │ ├── CommentLocator.kt │ │ ├── KeywordLocator.kt │ │ ├── LinkLocator.kt │ │ ├── MarkLocator.kt │ │ ├── MultilineCommentLocator.kt │ │ ├── NumericLiteralLocator.kt │ │ ├── PunctuationLocator.kt │ │ ├── StringLocator.kt │ │ └── TokenLocator.kt │ └── model │ ├── CodeHighlight.kt │ ├── CodeStructure.kt │ ├── SyntaxLanguage.kt │ ├── SyntaxTheme.kt │ └── SyntaxThemes.kt └── commonTest └── kotlin └── dev └── snipme └── highlights └── internal ├── CodeAnalyzerTest.kt ├── CodeComparatorTest.kt ├── CodeSamples.kt ├── ExtensionsKtTest.kt ├── HighlightsTest.kt ├── SyntaxTokensTest.kt ├── TestExtensions.kt ├── language ├── CommentTest.kt ├── JavaTest.kt └── KotlinTest.kt └── locator ├── AnnotationLocatorTest.kt ├── CommentLocatorTest.kt ├── KeywordLocatorTest.kt ├── LinkLocatorTest.kt ├── MarkLocatorTest.kt ├── MultilineCommentLocatorTest.kt ├── NumericLiteralLocatorTest.kt ├── PunctuationLocatorTest.kt ├── StringLocatorTest.kt └── TokenLocatorTest.kt /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnipMeDev/Highlights/8437fb3fa708a49d5938e738d321d7a7fa05b19a/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | **/build/ 3 | !src/**/build/ 4 | /.kotlin/ 5 | 6 | # IDE files 7 | .idea 8 | /.idea/* 9 | 10 | # Ignore local settings 11 | local.properties 12 | */local.properties 13 | 14 | # Ignore Gradle GUI config 15 | gradle-app.setting 16 | 17 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 18 | !gradle-wrapper.jar 19 | 20 | # Avoid ignore Gradle wrappper properties 21 | !gradle-wrapper.properties 22 | 23 | # Cache of project 24 | .gradletasknamecache 25 | 26 | # Eclipse Gradle plugin generated files 27 | # Eclipse Core 28 | .project 29 | # JDT-specific (Eclipse Java Development Tools) 30 | .classpath 31 | # MacOS files 32 | *.DS_Store 33 | -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-stdlib-2.0.0-commonMain-2bbUHA.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnipMeDev/Highlights/8437fb3fa708a49d5938e738d321d7a7fa05b19a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-stdlib-2.0.0-commonMain-2bbUHA.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-annotationsCommonMain-24eTFQ.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnipMeDev/Highlights/8437fb3fa708a49d5938e738d321d7a7fa05b19a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-annotationsCommonMain-24eTFQ.klib -------------------------------------------------------------------------------- /.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-assertionsCommonMain-24eTFQ.klib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnipMeDev/Highlights/8437fb3fa708a49d5938e738d321d7a7fa05b19a/.kotlin/metadata/kotlinTransformedMetadataLibraries/org.jetbrains.kotlin-kotlin-test-2.0.0-assertionsCommonMain-24eTFQ.klib -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.0.0] 2 | 3 | ### Added 4 | - serialization for public models 5 | - XCFramework output 6 | - `getHighlightsAsync()` with handy `DefaultHighlightsResultListener` adapter 7 | - `getByName()` function for `SyntaxThemes` 8 | 9 | ### Changed 10 | - Kotlin version to 2.0.20 11 | - direct snapshot field access to `getSnapshot()` function 12 | 13 | ## [0.9.3] 14 | 15 | ### Fixed 16 | - strings in comment locating 17 | 18 | ## [0.9.2] 19 | 20 | ### Fixed 21 | - partial analysis for same length strings 22 | 23 | ## [0.9.1] 24 | 25 | ### Fixed 26 | - loading local.properties 27 | - splitting javascript keywords 28 | 29 | ## [0.9.0] 30 | 31 | ### Added 32 | - Atom One theme. Thanks Nek-12! 33 | - languages snippet tests 34 | 35 | ### Fixed 36 | - keyword highlighted in oneline comment 37 | - keyword highlighted inside other word 38 | 39 | ### Changed 40 | - Kotlin version to 1.9.23 41 | 42 | ## [0.8.1] 43 | 44 | ### Added 45 | - Dart keywords 46 | - TypeScript keywords 47 | - GO keywords 48 | - PHP keywords 49 | - supported languages list to README 50 | 51 | ### Fixed 52 | - all current keywords 53 | 54 | ## [0.8.0] 55 | 56 | ### Changed 57 | - Kotlin version to 1.9.22 58 | 59 | ### Fixed 60 | - scientific notation numbers highlight length 61 | - redundant keyword highlights in strings and comments 62 | - ambiguous nested forEach returns 63 | 64 | ## [0.7.1] 65 | 66 | ### Fixed 67 | - unclosed string notation during input 68 | 69 | ## [0.7.0] 70 | 71 | ### Added 72 | - `key` field to `SyntaxTheme` model 73 | - `getNames()` function to `SyntaxThemes` 74 | - `SyntaxTheme.useDark(darkMode: Boolean)` extension to `SyntaxThemes` 75 | 76 | ### Changed 77 | - static theme constructors names in `SyntaxTheme` 78 | 79 | ## [0.6.0] 80 | 81 | ### Added 82 | - support for other non-mobile targets 83 | 84 | ### Changed 85 | - project maven description 86 | - Kotlin version to 1.9.0 87 | 88 | ## [0.5.0] 89 | 90 | ### Changed 91 | - publication script to add pom and java doc to all targets 92 | 93 | ## [0.4.2] 94 | 95 | ### Changed 96 | - version everywhere to 0.4.2 97 | 98 | ## [0.4.1] 99 | 100 | ### Added 101 | - more sections to README 102 | 103 | ## [0.4.0] 104 | 105 | ### Changed 106 | - snapshot is moved to Highlights to keep it with each instance 107 | 108 | ## [0.3.1] 109 | 110 | ### Added 111 | - README code usage examples 112 | - folder with sample 113 | - README installation section 114 | 115 | ## [0.3.0] 116 | 117 | ### Added 118 | - public static `themes` and `languages` functions for easier accessing 119 | all predefined values 120 | 121 | ## [0.2.1] 122 | 123 | ### Added 124 | - this CHANGELOG file to hopefully serve as an evolving example of a 125 | standardized open source project CHANGELOG 126 | 127 | ## [0.2.0] 128 | 129 | ### Added 130 | - own native algorithm for `indicesOf` function 131 | - KMM targets (iosArm64, iosX64, iosSimulatorArm64, jvm) 132 | 133 | ### Changed 134 | - Java target to Kotlin 135 | - escaped comment indicator characters to the regular ones 136 | 137 | ## [0.1.0] 138 | 139 | ### Added 140 | - token locators and other internal core components 141 | - unit tests 142 | - public API interface 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![highlights_banner_opaque](https://github.com/SnipMeDev/Highlights/assets/8405055/e123ce0f-6f58-451a-9e0a-893c0809b909) 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/dev.snipme/highlights)](https://mvnrepository.com/artifact/dev.snipme) 4 | [![Kotlin](https://img.shields.io/badge/kotlin-2.0.20-blue.svg?logo=kotlin)](http://kotlinlang.org) 5 | [![GitHub License](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) 6 | 7 | # Highlights 8 | Kotlin Multiplatform syntax highlighting engine. 9 | 10 | ## Installation ⬇️ 11 | ```shell 12 | repositories { 13 | mavenCentral() 14 | } 15 | ``` 16 | 17 | ```shell 18 | implementation("dev.snipme:highlights:1.0.0") 19 | ``` 20 | 21 | ## Features ✨ 22 | - Code component analysis (Keyword, comment, etc.) 23 | - Multiple syntax languages (Java, Swift, Kotlin, C, ...) 24 | - Themes 25 | - Text bolding (emphasis) 26 | - Result caching and support for incremental changes 27 | - Written in pure Kotlin, so available for many platforms 📱 💻 🖥️ 28 | - Sync or async mode 29 | 30 | ## Support ☕ 31 | Kotlin Multiplatform is a fresh environment and developing for it is neither fast nor easy 🥲 32 | 33 | If you feel that any of our project has saved you a time or effort, then consider supporting us via: 34 | [🧋 Buy Me A Coffee](https://bmc.link/SnipMeDev) 35 | 36 | ## Usage ✍️ 37 | 38 | > 💡 As each Highlights instance caches code analysis, it is recommended to re-use the same instance for small code changes. 39 | 40 | To start, simply put any code snippet in the default builder 41 | 42 | ```kotlin 43 | Highlights.default().apply { 44 | setCode("public class ExampleClass {}") 45 | // Keywords = [public, class], Marks = [{, }] 46 | getCodeStructure() 47 | // BoldHighlight, ColorHighlight 48 | getHighlights() 49 | } 50 | ``` 51 | 52 | There is also a possibility to handle result asynchronously 53 | 54 | ```kotlin 55 | highlights.getHighlightsAsync( 56 | object : DefaultHighlightsResultListener() { 57 | // onStart 58 | // onError 59 | // onCancel 60 | override fun onSuccess(result: List) { 61 | emitResult(highlights) 62 | } 63 | } 64 | ) 65 | ``` 66 | 67 | You can also set language, theme and phrase emphasis. 68 | Language and theme has impact on the ColorHighlight and emphasis is represented by the BoldHighlight. 69 | 70 | ```kotlin 71 | Highlights.Builder() 72 | .code("public class ExampleClass {}") 73 | .theme(SyntaxThemes.monokai()) 74 | .language(SyntaxLanguage.JAVA) 75 | .emphasis(PhraseLocation(13, 25)) // ExampleClass 76 | .build() 77 | .run { 78 | getHighlights() 79 | } 80 | ``` 81 | 82 | More advance usage of this library is shown [here](/sample). 83 | 84 | ## Languages 🌍 85 | 86 | `C`, 87 | `C++`, 88 | `DART`, 89 | `JAVA`, 90 | `KOTLIN`, 91 | `RUST`, 92 | `C#`, 93 | `COFFEESCRIPT`, 94 | `JAVASCRIPT`, 95 | `PERL`, 96 | `PYTHON`, 97 | `RUBY`, 98 | `SHELL`, 99 | `SWIFT`, 100 | `TYPESCRIPT`, 101 | `GO`, 102 | `PHP` 103 | 104 | ## Themes 🖌️ 105 | 106 | The library comes with predefined syntax coloring themes available in `SyntaxThemes`: 107 | 108 | ### Dark 🌚 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 130 | 142 | 154 | 166 | 178 | 191 | 192 |
DarculaMonokaiNotepadMatrixPastelAtom One
119 | 120 | - ![#EDEDED](https://placehold.co/15x15/EDEDED/EDEDED.png) Code 121 | - ![#CC7832](https://placehold.co/15x15/CC7832/CC7832.png) Keyword 122 | - ![#6A8759](https://placehold.co/15x15/6A8759/6A8759.png) String 123 | - ![#6897BB](https://placehold.co/15x15/6897BB/6897BB.png) Literal 124 | - ![#909090](https://placehold.co/15x15/909090/909090.png) Comment 125 | - ![#BBB529](https://placehold.co/15x15/BBB529/BBB529.png) Metadata 126 | - ![#629755](https://placehold.co/15x15/629755/629755.png) Multiline Comment 127 | - ![#CC7832](https://placehold.co/15x15/CC7832/CC7832.png) Punctuation 128 | - ![#EDEDED](https://placehold.co/15x15/EDEDED/EDEDED.png) Mark 129 | 131 | 132 | - ![#F8F8F2](https://placehold.co/15x15/F8F8F2/F8F8F2.png) Code 133 | - ![#F92672](https://placehold.co/15x15/F92672/F92672.png) Keyword 134 | - ![#E6DB74](https://placehold.co/15x15/E6DB74/E6DB74.png) String 135 | - ![#AE81FF](https://placehold.co/15x15/AE81FF/AE81FF.png) Literal 136 | - ![#FD971F](https://placehold.co/15x15/FD971F/FD971F.png) Comment 137 | - ![#B8F4B8](https://placehold.co/15x15/B8F4B8/B8F4B8.png) Metadata 138 | - ![#FD971F](https://placehold.co/15x15/FD971F/FD971F.png) Multiline Comment 139 | - ![#F8F8F2](https://placehold.co/15x15/F8F8F2/F8F8F2.png) Punctuation 140 | - ![#F8F8F2](https://placehold.co/15x15/F8F8F2/F8F8F2.png) Mark 141 | 143 | 144 | - ![#000080](https://placehold.co/15x15/000080/000080.png) Code 145 | - ![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png) Keyword 146 | - ![#808080](https://placehold.co/15x15/808080/808080.png) String 147 | - ![#FF8000](https://placehold.co/15x15/FF8000/FF8000.png) Literal 148 | - ![#008000](https://placehold.co/15x15/008000/008000.png) Comment 149 | - ![#000080](https://placehold.co/15x15/000080/000080.png) Metadata 150 | - ![#008000](https://placehold.co/15x15/008000/008000.png) Multiline Comment 151 | - ![#AA2C8C](https://placehold.co/15x15/AA2C8C/AA2C8C.png) Punctuation 152 | - ![#AA2C8C](https://placehold.co/15x15/AA2C8C/AA2C8C.png) Mark 153 | 155 | 156 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Code 157 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Keyword 158 | - ![#269926](https://placehold.co/15x15/269926/269926.png) String 159 | - ![#39E639](https://placehold.co/15x15/39E639/39E639.png) Literal 160 | - ![#67E667](https://placehold.co/15x15/67E667/67E667.png) Comment 161 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Metadata 162 | - ![#67E667](https://placehold.co/15x15/67E667/67E667.png) Multiline Comment 163 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Punctuation 164 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Mark 165 | 167 | 168 | - ![#DFDEE0](https://placehold.co/15x15/DFDEE0/DFDEE0.png) Code 169 | - ![#729FCF](https://placehold.co/15x15/729FCF/729FCF.png) Keyword 170 | - ![#93CF55](https://placehold.co/15x15/93CF55/93CF55.png) String 171 | - ![#8AE234](https://placehold.co/15x15/8AE234/8AE234.png) Literal 172 | - ![#888A85](https://placehold.co/15x15/888A85/888A85.png) Comment 173 | - ![#5DB895](https://placehold.co/15x15/5DB895/5DB895.png) Metadata 174 | - ![#888A85](https://placehold.co/15x15/888A85/888A85.png) Multiline Comment 175 | - ![#CB956D](https://placehold.co/15x15/CB956D/CB956D.png) Punctuation 176 | - ![#CB956D](https://placehold.co/15x15/CB956D/CB956D.png) Mark 177 | 179 | 180 | - ![#DFDEE0](https://placehold.co/15x15/BBBBBB/BBBBBB.png) Code 181 | - ![#729FCF](https://placehold.co/15x15/D55FDE/D55FDE.png) Keyword 182 | - ![#93CF55](https://placehold.co/15x15/89CA78/89CA78.png) String 183 | - ![#8AE234](https://placehold.co/15x15/D19A66/D19A66.png) Literal 184 | - ![#888A85](https://placehold.co/15x15/5C6370/5C6370.png) Comment 185 | - ![#5DB895](https://placehold.co/15x15/E5C07B/E5C07B.png) Metadata 186 | - ![#888A85](https://placehold.co/15x15/5C6370/5C6370.png) Multiline Comment 187 | - ![#CB956D](https://placehold.co/15x15/EF596F/EF596F.png) Punctuation 188 | - ![#CB956D](https://placehold.co/15x15/2BBAC5/2BBAC5.png) Mark 189 | 190 |
193 | 194 | ### Light 🌞 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 216 | 228 | 240 | 252 | 264 | 277 | 278 |
DarculaMonokaiNotepadMatrixPastelAtom One
205 | 206 | - ![#121212](https://placehold.co/15x15/121212/121212.png) Code 207 | - ![#CC7832](https://placehold.co/15x15/CC7832/CC7832.png) Keyword 208 | - ![#6A8759](https://placehold.co/15x15/6A8759/6A8759.png) String 209 | - ![#6897BB](https://placehold.co/15x15/6897BB/6897BB.png) Literal 210 | - ![#909090](https://placehold.co/15x15/909090/909090.png) Comment 211 | - ![#BBB529](https://placehold.co/15x15/BBB529/BBB529.png) Metadata 212 | - ![#629755](https://placehold.co/15x15/629755/629755.png) Multiline Comment 213 | - ![#CC7832](https://placehold.co/15x15/CC7832/CC7832.png) Punctuation 214 | - ![#121212](https://placehold.co/15x15/121212/121212.png) Mark 215 | 217 | 218 | - ![#07070D](https://placehold.co/15x15/07070D/07070D.png) Code 219 | - ![#F92672](https://placehold.co/15x15/F92672/F92672.png) Keyword 220 | - ![#E6DB74](https://placehold.co/15x15/E6DB74/E6DB74.png) String 221 | - ![#AE81FF](https://placehold.co/15x15/AE81FF/AE81FF.png) Literal 222 | - ![#FD971F](https://placehold.co/15x15/FD971F/FD971F.png) Comment 223 | - ![#B8F4B8](https://placehold.co/15x15/B8F4B8/B8F4B8.png) Metadata 224 | - ![#FD971F](https://placehold.co/15x15/FD971F/FD971F.png) Multiline Comment 225 | - ![#07070D](https://placehold.co/15x15/07070D/07070D.png) Punctuation 226 | - ![#07070D](https://placehold.co/15x15/07070D/07070D.png) Mark 227 | 229 | 230 | - ![#000080](https://placehold.co/15x15/000080/000080.png) Code 231 | - ![#0000FF](https://placehold.co/15x15/0000FF/0000FF.png) Keyword 232 | - ![#808080](https://placehold.co/15x15/808080/808080.png) String 233 | - ![#FF8000](https://placehold.co/15x15/FF8000/FF8000.png) Literal 234 | - ![#008000](https://placehold.co/15x15/008000/008000.png) Comment 235 | - ![#000080](https://placehold.co/15x15/000080/000080.png) Metadata 236 | - ![#008000](https://placehold.co/15x15/008000/008000.png) Multiline Comment 237 | - ![#AA2C8C](https://placehold.co/15x15/AA2C8C/AA2C8C.png) Punctuation 238 | - ![#AA2C8C](https://placehold.co/15x15/AA2C8C/AA2C8C.png) Mark 239 | 241 | 242 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Code 243 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Keyword 244 | - ![#269926](https://placehold.co/15x15/269926/269926.png) String 245 | - ![#39E639](https://placehold.co/15x15/39E639/39E639.png) Literal 246 | - ![#67E667](https://placehold.co/15x15/67E667/67E667.png) Comment 247 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Metadata 248 | - ![#67E667](https://placehold.co/15x15/67E667/67E667.png) Multiline Comment 249 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Punctuation 250 | - ![#008500](https://placehold.co/15x15/008500/008500.png) Mark 251 | 253 | 254 | - ![#20211F](https://placehold.co/15x15/20211F/20211F.png) Code 255 | - ![#729FCF](https://placehold.co/15x15/729FCF/729FCF.png) Keyword 256 | - ![#93CF55](https://placehold.co/15x15/93CF55/93CF55.png) String 257 | - ![#8AE234](https://placehold.co/15x15/8AE234/8AE234.png) Literal 258 | - ![#888A85](https://placehold.co/15x15/888A85/888A85.png) Comment 259 | - ![#5DB895](https://placehold.co/15x15/5DB895/5DB895.png) Metadata 260 | - ![#888A85](https://placehold.co/15x15/888A85/888A85.png) Multiline Comment 261 | - ![#CB956D](https://placehold.co/15x15/CB956D/CB956D.png) Punctuation 262 | - ![#CB956D](https://placehold.co/15x15/CB956D/CB956D.png) Mark 263 | 265 | 266 | - ![#DFDEE0](https://placehold.co/15x15/383A42/383A42.png) Code 267 | - ![#729FCF](https://placehold.co/15x15/A626A4/A626A4.png) Keyword 268 | - ![#93CF55](https://placehold.co/15x15/50A14F/50A14F.png) String 269 | - ![#8AE234](https://placehold.co/15x15/986801/986801.png) Literal 270 | - ![#888A85](https://placehold.co/15x15/A1A1A1/A1A1A1.png) Comment 271 | - ![#5DB895](https://placehold.co/15x15/C18401/C18401.png) Metadata 272 | - ![#888A85](https://placehold.co/15x15/A1A1A1/A1A1A1.png) Multiline Comment 273 | - ![#CB956D](https://placehold.co/15x15/E45649/E45649.png) Punctuation 274 | - ![#CB956D](https://placehold.co/15x15/526FFF/526FFF.png) Mark 275 | 276 |
279 | 280 | You can also prepare your own themes and use them. Just create the `SyntaxTheme` class: 281 | 282 | ```kotlin 283 | SyntaxTheme( 284 | key = "MY_THEME", 285 | code = 0xEDEDED, 286 | keyword = 0xCC7832, 287 | string = 0x6A8759, 288 | literal = 0x6897BB, 289 | comment = 0x909090, 290 | metadata = 0xBBB529, 291 | multilineComment = 0x629755, 292 | punctuation = 0xCC7832, 293 | mark = 0xEDEDED 294 | ) 295 | ``` 296 | 297 | ## Popular uses 🙌 298 | 299 | If your project uses this code, please write me or add your info 300 | 301 | 302 | 303 | 304 | 305 | 306 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 |
TypeName
Library 307 | KodeView 308 |
Application SnippLog
Application FlowMVI Sample
319 | 320 | ## TODO 🚧 321 | - [X] Migrate some lists to sets 322 | - [X] Optimize code analysis 323 | - [ ] Add more themes and languages 324 | - [ ] Support italic and underline text style 325 | 326 | ## Contribution 💻 327 | Any form of support is very welcomed. 328 | Bugs, problems and new feature requests should be placed in the `Issues` tab with proper labeling. 329 | New feature can be also submitted via `Pull Requests`. 330 | Then make sure: 331 | - Current and added tests have passed 332 | - CHANGELOG and README have been updated 333 | - New code matches library's vision and code style 334 | 335 | License 🖋️ 336 | ======= 337 | 338 | Copyright 2023-2024 Tomasz Kądziołka. 339 | 340 | Licensed under the Apache License, Version 2.0 (the "License"); 341 | you may not use this file except in compliance with the License. 342 | You may obtain a copy of the License at 343 | 344 | http://www.apache.org/licenses/LICENSE-2.0 345 | 346 | Unless required by applicable law or agreed to in writing, software 347 | distributed under the License is distributed on an "AS IS" BASIS, 348 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 349 | See the License for the specific language governing permissions and 350 | limitations under the License. 351 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework 2 | 3 | apply(from = "publish-root.gradle") 4 | 5 | plugins { 6 | kotlin("multiplatform") version "2.0.20" 7 | kotlin("plugin.serialization") version "2.0.20" 8 | id("maven-publish") 9 | id("io.github.gradle-nexus.publish-plugin") version "1.3.0" 10 | id("signing") 11 | } 12 | 13 | group = "dev.snipme" 14 | version = "1.0.0" 15 | 16 | kotlin { 17 | // Android 18 | jvm { 19 | compilations.all { 20 | kotlinOptions.jvmTarget = "1.8" 21 | } 22 | withJava() 23 | testRuns["test"].executionTask.configure { 24 | useJUnitPlatform() 25 | } 26 | } 27 | // iOS 28 | 29 | val xcf = XCFramework() 30 | val iosTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64()) 31 | 32 | iosTargets.forEach { 33 | it.binaries.framework { 34 | baseName = "highlights" 35 | xcf.add(this) 36 | } 37 | } 38 | // Desktop 39 | mingwX64() 40 | linuxX64() 41 | linuxArm64() 42 | macosX64() 43 | macosArm64() 44 | // Web 45 | js { 46 | browser() 47 | nodejs() 48 | } 49 | wasmJs() 50 | // Dependencies 51 | sourceSets { 52 | val commonMain by getting { 53 | dependencies { 54 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") 55 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") 56 | } 57 | } 58 | 59 | val commonTest by getting { 60 | dependencies { 61 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0") 62 | implementation(kotlin("test")) 63 | } 64 | } 65 | } 66 | } 67 | 68 | publishing { 69 | val emptyJar = tasks.register("emptyJar") { 70 | archiveAppendix.set("empty") 71 | } 72 | 73 | publications.forEach { 74 | val publication = it as? MavenPublication ?: return@forEach 75 | 76 | publication.artifact(emptyJar) { 77 | classifier = "javadoc" 78 | } 79 | 80 | publication.pom.withXml { 81 | val root = asNode() 82 | root.appendNode("name", project.name) 83 | root.appendNode( 84 | "description", 85 | "Kotlin Multiplatform syntax highlighting engine." 86 | ) 87 | root.appendNode("url", "https://github.com/SnipMeDev/Highlights") 88 | 89 | root.appendNode("licenses").apply { 90 | appendNode("license").apply { 91 | appendNode("name", "The Apache Software License, Version 2.0") 92 | appendNode("url", "https://www.apache.org/licenses/LICENSE-2.0.txt") 93 | appendNode("distribution", "repo") 94 | } 95 | } 96 | 97 | root.appendNode("developers").apply { 98 | appendNode("developer").apply { 99 | appendNode("id", "tkadziolka") 100 | appendNode("name", "Tomasz Kądziołka") 101 | appendNode("email", "kontakt@tkadziolka.pl") 102 | } 103 | } 104 | 105 | root.appendNode("scm").apply { 106 | appendNode( 107 | "connection", 108 | "scm:git:ssh://git@github.com:SnipMeDev/Highlights.git" 109 | ) 110 | appendNode( 111 | "developerConnection", 112 | "scm:git:ssh://git@github.org:SnipMeDev/Highlights.git", 113 | ) 114 | appendNode("url", "https://github.com/SnipMeDev/Highlights") 115 | } 116 | } 117 | } 118 | } 119 | 120 | signing { 121 | useInMemoryPgpKeys( 122 | rootProject.ext["signing.keyId"] as String, 123 | rootProject.ext["signing.key"] as String, 124 | rootProject.ext["signing.password"] as String 125 | ) 126 | sign(publishing.publications) 127 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.js.compiler=ir -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SnipMeDev/Highlights/8437fb3fa708a49d5938e738d321d7a7fa05b19a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /publish-root.gradle: -------------------------------------------------------------------------------- 1 | // Create variables with empty default values 2 | ext["signing.keyId"] = '' 3 | ext["signing.password"] = '' 4 | ext["signing.key"] = '' 5 | ext["ossrhUsername"] = '' 6 | ext["ossrhPassword"] = '' 7 | ext["sonatypeStagingProfileId"] = '' 8 | 9 | Properties p = new Properties() 10 | def localPropertiesFile = file("local.properties") 11 | // Read local.properties file if it exists 12 | if (localPropertiesFile.exists()) { 13 | p.load(localPropertiesFile.newInputStream()) 14 | } 15 | p.each { name, value -> ext[name] = value } 16 | 17 | // Set up Sonatype repository 18 | nexusPublishing { 19 | repositories { 20 | sonatype { 21 | stagingProfileId = sonatypeStagingProfileId 22 | username = ossrhUsername 23 | password = ossrhPassword 24 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) 25 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") version "2.0.20" 5 | application 6 | } 7 | 8 | group = "dev.snipme" 9 | version = "1.0-SNAPSHOT" 10 | 11 | repositories { 12 | mavenCentral() 13 | mavenLocal() 14 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") 15 | } 16 | 17 | dependencies { 18 | implementation("dev.snipme:highlights:1.0.0") 19 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") 20 | 21 | testImplementation(kotlin("test")) 22 | } 23 | 24 | tasks.test { 25 | useJUnitPlatform() 26 | } 27 | 28 | tasks.withType { 29 | kotlinOptions.jvmTarget = "1.8" 30 | } 31 | 32 | application { 33 | mainClass.set("MainKt") 34 | } -------------------------------------------------------------------------------- /sample/gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /sample/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /sample/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "sample" -------------------------------------------------------------------------------- /sample/src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import dev.snipme.highlights.DefaultHighlightsResultListener 2 | import dev.snipme.highlights.Highlights 3 | import dev.snipme.highlights.model.BoldHighlight 4 | import dev.snipme.highlights.model.CodeHighlight 5 | import dev.snipme.highlights.model.PhraseLocation 6 | import dev.snipme.highlights.model.SyntaxLanguage 7 | import dev.snipme.highlights.model.SyntaxThemes 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.runBlocking 10 | import kotlinx.coroutines.suspendCancellableCoroutine 11 | 12 | val sampleClass = """ 13 | @Serializable 14 | public class ExampleClass { 15 | // Single-line comment 16 | 17 | /* Multi-line 18 | * comment */ 19 | 20 | // Class variables 21 | private int x; 22 | protected float y; 23 | public static final String MESSAGE = "Hello!"; 24 | 25 | // Constructor 26 | public ExampleClass() { 27 | this.x = 0; 28 | this.y = 0.0f; 29 | } 30 | 31 | // Method with parameters and return type 32 | public static int calculateSum(int a, int b) { 33 | int sum = a + b; 34 | return sum; 35 | } 36 | } 37 | """.trimIndent() 38 | 39 | fun main() { 40 | runBlocking { 41 | val highlights = Highlights.Builder() 42 | .code(sampleClass) 43 | .theme(SyntaxThemes.monokai()) 44 | .language(SyntaxLanguage.JAVA) 45 | .build() 46 | 47 | val syncResult = runSync(highlights) 48 | println("Sync count with emphasis: ${syncResult.size}") 49 | 50 | launch { 51 | suspendCancellableCoroutine { continuation -> 52 | runAsync(highlights) { asyncResult -> 53 | assert(syncResult == asyncResult) 54 | println("Async count: ${asyncResult.size}") 55 | continuation.resumeWith(Result.success(Unit)) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | fun runSync(highlights: Highlights): List { 63 | println("### HIGHLIGHTS ###") 64 | println() 65 | 66 | println("Available languages:") 67 | println("${SyntaxLanguage.getNames()}") 68 | println() 69 | 70 | println("Available themes:") 71 | println("${SyntaxThemes.getNames()}") 72 | println() 73 | 74 | println("This is a sample class:") 75 | println(sampleClass) 76 | println() 77 | 78 | val structure = highlights.getCodeStructure() 79 | 80 | println("After analysis there has been found:") 81 | println("${structure.printStructure(sampleClass)}") 82 | println() 83 | 84 | val newInstance = highlights.getBuilder() 85 | .emphasis(PhraseLocation(0, 13)) 86 | .build() 87 | 88 | println("The emphasis was put on the word:") 89 | val result = newInstance.getHighlights() 90 | val emphasisLocation = result 91 | .filterIsInstance() 92 | .first() 93 | .location 94 | 95 | println(sampleClass.substring(emphasisLocation.start, emphasisLocation.end)) 96 | 97 | return result 98 | } 99 | 100 | fun runAsync( 101 | highlights: Highlights, 102 | emitResult: (List) -> Unit, 103 | ) { 104 | println() 105 | println("### ASYNC HIGHLIGHTS ###") 106 | 107 | highlights.getHighlightsAsync( 108 | object : DefaultHighlightsResultListener() { 109 | // onStart 110 | // onError 111 | // onCancel 112 | override fun onSuccess(result: List) { 113 | emitResult(result) 114 | } 115 | } 116 | ) 117 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | 16 | rootProject.name = "highlights" -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/Highlights.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights 2 | 3 | import dev.snipme.highlights.internal.CodeAnalyzer 4 | import dev.snipme.highlights.internal.CodeSnapshot 5 | import dev.snipme.highlights.internal.onCancel 6 | import dev.snipme.highlights.model.BoldHighlight 7 | import dev.snipme.highlights.model.CodeHighlight 8 | import dev.snipme.highlights.model.CodeStructure 9 | import dev.snipme.highlights.model.ColorHighlight 10 | import dev.snipme.highlights.model.PhraseLocation 11 | import dev.snipme.highlights.model.SyntaxLanguage 12 | import dev.snipme.highlights.model.SyntaxTheme 13 | import dev.snipme.highlights.model.SyntaxThemes 14 | import kotlinx.coroutines.CoroutineExceptionHandler 15 | import kotlinx.coroutines.CoroutineScope 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.cancelChildren 18 | import kotlinx.coroutines.ensureActive 19 | import kotlinx.coroutines.launch 20 | 21 | class Highlights private constructor( 22 | private var code: String, 23 | private val language: SyntaxLanguage, 24 | private val theme: SyntaxTheme, 25 | private var emphasisLocations: List 26 | ) { 27 | private var coroutineScope = CoroutineScope(Dispatchers.Default) 28 | 29 | private var snapshot: CodeSnapshot? = null 30 | 31 | companion object { 32 | fun default() = fromBuilder(Builder()) 33 | 34 | fun fromBuilder(builder: Builder) = builder.build() 35 | 36 | fun themes(darkMode: Boolean) = SyntaxThemes.themes(darkMode) 37 | 38 | fun languages() = SyntaxLanguage.values().toList() 39 | } 40 | 41 | data class Builder( 42 | var code: String = "", 43 | var language: SyntaxLanguage = SyntaxLanguage.DEFAULT, 44 | var theme: SyntaxTheme = SyntaxThemes.default(), 45 | var emphasisLocations: List = emptyList(), 46 | ) { 47 | fun code(code: String) = apply { this.code = code } 48 | fun language(language: SyntaxLanguage) = apply { this.language = language } 49 | fun theme(theme: SyntaxTheme) = apply { this.theme = theme } 50 | fun emphasis(vararg locations: PhraseLocation) = 51 | apply { this.emphasisLocations = locations.toList() } 52 | 53 | fun build() = Highlights(code, language, theme, emphasisLocations) 54 | } 55 | 56 | fun setCode(code: String) { 57 | this.code = code 58 | } 59 | 60 | fun setEmphasis(vararg locations: PhraseLocation) { 61 | this.emphasisLocations = locations.toList() 62 | } 63 | 64 | fun getCodeStructure(): CodeStructure { 65 | val structure = CodeAnalyzer.analyze(code, language, snapshot) 66 | snapshot = CodeSnapshot(code, structure, language) 67 | return structure 68 | } 69 | 70 | fun getHighlights(): List { 71 | val structure = getCodeStructure() 72 | return constructHighlights(structure) 73 | } 74 | 75 | fun getHighlightsAsync(listener: HighlightsResultListener) = with(coroutineScope) { 76 | try { 77 | val errorHandler = CoroutineExceptionHandler { _, exception -> 78 | listener.onError(exception) 79 | } 80 | 81 | coroutineContext.cancelChildren() 82 | launch(errorHandler) { 83 | listener.onStart() 84 | ensureActive() 85 | val structure = getCodeStructure() 86 | ensureActive() 87 | val highlights = constructHighlights(structure) 88 | listener.onSuccess(highlights) 89 | }.also { it.onCancel { listener.onCancel() } } 90 | } catch (exception: Exception) { 91 | listener.onError(exception) 92 | } 93 | } 94 | 95 | fun getBuilder() = Builder(code, language, theme, emphasisLocations) 96 | 97 | fun getCode() = code 98 | 99 | fun getLanguage() = language 100 | 101 | fun getTheme() = theme 102 | 103 | fun getEmphasis() = emphasisLocations 104 | 105 | fun getSnapshot() = snapshot 106 | 107 | fun clearSnapshot() { 108 | snapshot = null 109 | } 110 | 111 | private fun constructHighlights(structure: CodeStructure): List = 112 | mutableListOf().apply { 113 | structure.marks.forEach { add(ColorHighlight(it, theme.mark)) } 114 | structure.punctuations.forEach { add(ColorHighlight(it, theme.punctuation)) } 115 | structure.keywords.forEach { add(ColorHighlight(it, theme.keyword)) } 116 | structure.strings.forEach { add(ColorHighlight(it, theme.string)) } 117 | structure.literals.forEach { add(ColorHighlight(it, theme.literal)) } 118 | structure.annotations.forEach { add(ColorHighlight(it, theme.metadata)) } 119 | structure.comments.forEach { add(ColorHighlight(it, theme.comment)) } 120 | structure.multilineComments.forEach { add(ColorHighlight(it, theme.multilineComment)) } 121 | emphasisLocations.forEach { add(BoldHighlight(it)) } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/HighlightsResultListener.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights 2 | 3 | import dev.snipme.highlights.model.CodeHighlight 4 | 5 | interface HighlightsResultListener { 6 | fun onStart() 7 | fun onSuccess(result: List) 8 | fun onError(exception: Throwable) 9 | fun onCancel() 10 | } 11 | 12 | abstract class DefaultHighlightsResultListener : HighlightsResultListener { 13 | override fun onStart() {} 14 | override fun onSuccess(result: List) {} 15 | override fun onError(exception: Throwable) {} 16 | override fun onCancel() {} 17 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/CodeAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.ALL_KEYWORDS 4 | import dev.snipme.highlights.internal.SyntaxTokens.COFFEE_SCRIPT_KEYWORDS 5 | import dev.snipme.highlights.internal.SyntaxTokens.CPP_KEYWORDS 6 | import dev.snipme.highlights.internal.SyntaxTokens.CSHARP_KEYWORDS 7 | import dev.snipme.highlights.internal.SyntaxTokens.C_KEYWORDS 8 | import dev.snipme.highlights.internal.SyntaxTokens.DART_KEYWORDS 9 | import dev.snipme.highlights.internal.SyntaxTokens.GO_KEYWORDS 10 | import dev.snipme.highlights.internal.SyntaxTokens.JAVASCRIPT_KEYWORDS 11 | import dev.snipme.highlights.internal.SyntaxTokens.JAVA_KEYWORDS 12 | import dev.snipme.highlights.internal.SyntaxTokens.KOTLIN_KEYWORDS 13 | import dev.snipme.highlights.internal.SyntaxTokens.PERL_KEYWORDS 14 | import dev.snipme.highlights.internal.SyntaxTokens.PHP_KEYWORDS 15 | import dev.snipme.highlights.internal.SyntaxTokens.PYTHON_KEYWORDS 16 | import dev.snipme.highlights.internal.SyntaxTokens.RUBY_KEYWORDS 17 | import dev.snipme.highlights.internal.SyntaxTokens.RUST_KEYWORDS 18 | import dev.snipme.highlights.internal.SyntaxTokens.SH_KEYWORDS 19 | import dev.snipme.highlights.internal.SyntaxTokens.SWIFT_KEYWORDS 20 | import dev.snipme.highlights.internal.SyntaxTokens.TYPESCRIPT_KEYWORDS 21 | import dev.snipme.highlights.internal.locator.AnnotationLocator 22 | import dev.snipme.highlights.internal.locator.CommentLocator 23 | import dev.snipme.highlights.internal.locator.KeywordLocator 24 | import dev.snipme.highlights.internal.locator.MarkLocator 25 | import dev.snipme.highlights.internal.locator.MultilineCommentLocator 26 | import dev.snipme.highlights.internal.locator.NumericLiteralLocator 27 | import dev.snipme.highlights.internal.locator.PunctuationLocator 28 | import dev.snipme.highlights.internal.locator.StringLocator 29 | import dev.snipme.highlights.model.CodeStructure 30 | import dev.snipme.highlights.model.SyntaxLanguage 31 | import dev.snipme.highlights.model.SyntaxLanguage.C 32 | import dev.snipme.highlights.model.SyntaxLanguage.COFFEESCRIPT 33 | import dev.snipme.highlights.model.SyntaxLanguage.CPP 34 | import dev.snipme.highlights.model.SyntaxLanguage.CSHARP 35 | import dev.snipme.highlights.model.SyntaxLanguage.DART 36 | import dev.snipme.highlights.model.SyntaxLanguage.DEFAULT 37 | import dev.snipme.highlights.model.SyntaxLanguage.GO 38 | import dev.snipme.highlights.model.SyntaxLanguage.JAVA 39 | import dev.snipme.highlights.model.SyntaxLanguage.JAVASCRIPT 40 | import dev.snipme.highlights.model.SyntaxLanguage.KOTLIN 41 | import dev.snipme.highlights.model.SyntaxLanguage.PERL 42 | import dev.snipme.highlights.model.SyntaxLanguage.PHP 43 | import dev.snipme.highlights.model.SyntaxLanguage.PYTHON 44 | import dev.snipme.highlights.model.SyntaxLanguage.RUBY 45 | import dev.snipme.highlights.model.SyntaxLanguage.RUST 46 | import dev.snipme.highlights.model.SyntaxLanguage.SHELL 47 | import dev.snipme.highlights.model.SyntaxLanguage.SWIFT 48 | import dev.snipme.highlights.model.SyntaxLanguage.TYPESCRIPT 49 | 50 | data class CodeSnapshot( 51 | val code: String, 52 | val structure: CodeStructure, 53 | val language: SyntaxLanguage, 54 | ) 55 | 56 | internal object CodeAnalyzer { 57 | fun analyze( 58 | code: String, 59 | language: SyntaxLanguage = DEFAULT, 60 | snapshot: CodeSnapshot? = null, 61 | ): CodeStructure = 62 | when { 63 | snapshot == null -> analyzeFull(code, language) 64 | language != snapshot.language -> analyzeFull(code, language) 65 | code != snapshot.code -> analyzePartial(snapshot, code) 66 | else -> snapshot.structure 67 | } 68 | 69 | private fun analyzeFull(code: String, language: SyntaxLanguage): CodeStructure { 70 | return analyzeForLanguage(code, language) 71 | } 72 | 73 | private fun analyzePartial(codeSnapshot: CodeSnapshot, code: String): CodeStructure { 74 | val difference = CodeComparator.difference(codeSnapshot.code, code) 75 | val structure = when (difference) { 76 | is CodeDifference.Increase -> { 77 | val newStructure = analyzeForLanguage(difference.change, codeSnapshot.language) 78 | codeSnapshot.structure + newStructure.move(codeSnapshot.code.length + 1) 79 | } 80 | 81 | is CodeDifference.Decrease -> { 82 | val newStructure = analyzeForLanguage(difference.change, codeSnapshot.language) 83 | val lengthDifference = codeSnapshot.code.length - difference.change.length 84 | codeSnapshot.structure - newStructure.move(lengthDifference) 85 | } 86 | 87 | is CodeDifference.Full -> analyzeForLanguage(code, codeSnapshot.language) 88 | 89 | CodeDifference.None -> return codeSnapshot.structure 90 | } 91 | 92 | return structure 93 | } 94 | 95 | private fun analyzeForLanguage(code: String, language: SyntaxLanguage) = 96 | when (language) { 97 | DEFAULT -> analyzeCodeWithKeywords(code, ALL_KEYWORDS) 98 | C -> analyzeCodeWithKeywords(code, C_KEYWORDS) 99 | CPP -> analyzeCodeWithKeywords(code, CPP_KEYWORDS) 100 | JAVA -> analyzeCodeWithKeywords(code, JAVA_KEYWORDS) 101 | KOTLIN -> analyzeCodeWithKeywords(code, KOTLIN_KEYWORDS) 102 | RUST -> analyzeCodeWithKeywords(code, RUST_KEYWORDS) 103 | CSHARP -> analyzeCodeWithKeywords(code, CSHARP_KEYWORDS) 104 | COFFEESCRIPT -> analyzeCodeWithKeywords(code, COFFEE_SCRIPT_KEYWORDS) 105 | JAVASCRIPT -> analyzeCodeWithKeywords(code, JAVASCRIPT_KEYWORDS) 106 | PERL -> analyzeCodeWithKeywords(code, PERL_KEYWORDS) 107 | PYTHON -> analyzeCodeWithKeywords(code, PYTHON_KEYWORDS) 108 | RUBY -> analyzeCodeWithKeywords(code, RUBY_KEYWORDS) 109 | SHELL -> analyzeCodeWithKeywords(code, SH_KEYWORDS) 110 | SWIFT -> analyzeCodeWithKeywords(code, SWIFT_KEYWORDS) 111 | DART -> analyzeCodeWithKeywords(code, DART_KEYWORDS) 112 | TYPESCRIPT -> analyzeCodeWithKeywords(code, TYPESCRIPT_KEYWORDS) 113 | GO -> analyzeCodeWithKeywords(code, GO_KEYWORDS) 114 | PHP -> analyzeCodeWithKeywords(code, PHP_KEYWORDS) 115 | } 116 | 117 | private fun analyzeCodeWithKeywords(code: String, keywords: Set): CodeStructure { 118 | val comments = CommentLocator.locate(code) 119 | val multiLineComments = MultilineCommentLocator.locate(code) 120 | val commentRanges = (comments + multiLineComments).toRangeSet() 121 | 122 | val strings = StringLocator.locate(code, commentRanges) 123 | val plainTextRanges = (comments + multiLineComments + strings).toRangeSet() 124 | 125 | // TODO Apply ignored ranges to other locators 126 | return CodeStructure( 127 | marks = MarkLocator.locate(code), 128 | punctuations = PunctuationLocator.locate(code), 129 | keywords = KeywordLocator.locate(code, keywords, plainTextRanges), 130 | strings = strings, 131 | literals = NumericLiteralLocator.locate(code), 132 | comments = comments, 133 | multilineComments = multiLineComments, 134 | annotations = AnnotationLocator.locate(code), 135 | incremental = false, 136 | ) 137 | } 138 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/CodeComparator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | private const val WORDS_DELIMITER = " " 4 | 5 | internal sealed class CodeDifference { 6 | data class Increase(val change: String) : CodeDifference() 7 | data class Decrease(val change: String) : CodeDifference() 8 | data object Full : CodeDifference() 9 | data object None : CodeDifference() 10 | } 11 | 12 | internal object CodeComparator { 13 | fun difference(current: String, updated: String): CodeDifference { 14 | val currentWords = current.tokenize().map { it.trim() } 15 | val updatedWords = updated.tokenize().map { it.trim() } 16 | 17 | return when { 18 | currentWords.size == updatedWords.size -> 19 | if (currentWords == updatedWords) CodeDifference.None 20 | else CodeDifference.Full 21 | 22 | currentWords.size < updatedWords.size -> CodeDifference.Increase( 23 | findDifference( 24 | currentWords, 25 | updatedWords, 26 | isIncrease = true 27 | ) 28 | ) 29 | 30 | else -> CodeDifference.Decrease( 31 | findDifference( 32 | currentWords, 33 | updatedWords, 34 | isIncrease = false 35 | ) 36 | ) 37 | } 38 | } 39 | 40 | private fun findDifference( 41 | current: List, 42 | updated: List, 43 | isIncrease: Boolean, 44 | ): String { 45 | val differentWords = if (isIncrease) { 46 | updated - current 47 | } else { 48 | current - updated 49 | } 50 | 51 | return differentWords.joinToString(WORDS_DELIMITER) 52 | } 53 | 54 | private fun String.tokenize() = 55 | this.split("\n").map { it.split(WORDS_DELIMITER) }.flatten() 56 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/Extensions.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import dev.snipme.highlights.model.CodeHighlight 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlinx.serialization.encodeToString 6 | import kotlinx.serialization.json.Json 7 | import kotlinx.coroutines.Job 8 | import kotlin.coroutines.cancellation.CancellationException 9 | 10 | fun List.toJson(): String { 11 | return Json.encodeToString>(this) 12 | } 13 | 14 | fun String.phraseLocationSetFromJson(): Set { 15 | return Json.decodeFromString(this) 16 | } 17 | 18 | inline operator fun Set.get(i: Int): E? { 19 | this.forEachIndexed { index, t -> 20 | if (i == index) return t 21 | } 22 | 23 | return null 24 | } 25 | 26 | fun String.indicesOf( 27 | phrase: String, 28 | ): Set { 29 | val indices = mutableSetOf() 30 | 31 | // No found 32 | val startIndexOf = indexOf(phrase, 0) 33 | if (startIndexOf < 0) { 34 | return emptySet() 35 | } 36 | 37 | indices.add(startIndexOf) 38 | 39 | // The found is the only one 40 | if (startIndexOf == (lastIndex - phrase.length)) { 41 | return indices 42 | } 43 | 44 | var startingIndex = indexOf(phrase, startIndexOf + phrase.length) 45 | 46 | while (startingIndex > 0) { 47 | indices.add(startingIndex) 48 | startingIndex = indexOf(phrase, startingIndex + phrase.length) 49 | } 50 | 51 | return indices 52 | } 53 | 54 | fun Char.isNewLine(): Boolean { 55 | val stringChar = this.toString() 56 | return stringChar == "\n" || stringChar == "\r" || stringChar == "\r\n" 57 | } 58 | 59 | fun String.lengthToEOF(start: Int = 0): Int { 60 | if (all { it.isNewLine().not() }) return length - start 61 | var endIndex = start 62 | while (this.getOrNull(endIndex)?.isNewLine()?.not() == true) { 63 | endIndex++ 64 | } 65 | return endIndex - start 66 | } 67 | 68 | // Sometimes keyword can be found in the middle of word. 69 | // This returns information if index points only to the keyword 70 | fun String.isIndependentPhrase( 71 | code: String, 72 | index: Int, 73 | ): Boolean { 74 | if (index == code.lastIndex) return true 75 | if (code.length == this.length) return true 76 | 77 | val charBefore = code[maxOf(index - 1, 0)] 78 | val charAfter = code[minOf(index + this.length, code.lastIndex)] 79 | 80 | if (index == 0) { 81 | return charAfter.isDigit().not() && charAfter.isLetter().not() 82 | } 83 | 84 | return charBefore.isLetter().not() && 85 | charAfter.isDigit().not() && (charAfter == code.last() || charAfter.isLetter().not()) 86 | } 87 | 88 | fun Set.toRangeSet(): Set = 89 | this.map { IntRange(it.start, it.end) }.toSet() 90 | 91 | operator fun IntRange.contains(range: IntRange): Boolean { 92 | return range.first >= this.first && range.last <= this.last 93 | } 94 | 95 | fun Job.onCancel(block: () -> Unit) { 96 | invokeOnCompletion { 97 | if (it is CancellationException) { 98 | block() 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/SyntaxTokens.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | internal object SyntaxTokens { 4 | 5 | val C_KEYWORDS = setOf( 6 | "auto", "break", "case", "char", "const", "continue", "default", "do", "double", "else", 7 | "enum", "extern", "float", "for", "goto", "if", "int", "long", "register", "return", 8 | "short", "signed", "sizeof", "static", "struct", "switch", "typedef", "union", "unsigned", 9 | "void", "volatile", "while" 10 | ) 11 | 12 | val CPP_KEYWORDS = setOf( 13 | "asm", "auto", "bool", "break", "case", "catch", "char", "class", "const", "const_cast", 14 | "continue", "default", "delete", "do", "double", "dynamic_cast", "else", "enum", 15 | "explicit", "export", "extern", "false", "float", "for", "friend", "goto", "if", "inline", 16 | "int", "long", "mutable", "namespace", "new", "operator", "private", "protected", "public", 17 | "register", "reinterpret_cast", "return", "short", "signed", "sizeof", "static", 18 | "static_cast", "struct", "switch", "template", "this", "throw", "true", "try", "typedef", 19 | "typeid", "typename", "union", "unsigned", "using", "virtual", "void", "volatile", 20 | "wchar_t", "while" 21 | ) 22 | 23 | val JAVA_KEYWORDS = setOf( 24 | "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", 25 | "const", "continue", "default", "do", "double", "else", "enum", "extends", "final", 26 | "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", 27 | "interface", "long", "native", "new", "null", "package", "private", "protected", "public", 28 | "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", "throw", 29 | "throws", "transient", "try", "void", "volatile", "while" 30 | ) 31 | 32 | val KOTLIN_KEYWORDS = setOf( 33 | "actual", "abstract", "annotation", "as", "as?", "break", "by", "catch", "class", 34 | "companion", "const", "constructor", "continue", "coroutine", "crossinline", "data", 35 | "delegate", "dynamic", "do", "else", "enum", "expect", "external", "false", "final", 36 | "finally", "for", "fun", "get", "if", "import", "in", "!in", "infix", "inline", "interface", 37 | "internal", "is", "!is", "lazy", "lateinit", "native", "null", "object", "open", "operator", 38 | "out", "override", "package", "private", "protected", "public", "reified", "return", "sealed", 39 | "set", "super", "suspend", "tailrec", "this", "throw", "true", "try", "typealias", "typeof", 40 | "val", "var", "vararg", "when", "while", "yield" 41 | ) 42 | 43 | val RUST_KEYWORDS = setOf( 44 | "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", 45 | "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", 46 | "mut", "pub", "ref", "return", "Self", "self", "static", "struct", "super", "trait", "true", 47 | "type", "union", "unsafe", "use", "where", "while", "abstract", "become", "box", "do", 48 | "final", "macro", "override", "priv", "try", "typeof", "unsized", "virtual", "yield" 49 | ) 50 | 51 | val CSHARP_KEYWORDS = setOf( 52 | "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", "checked", 53 | "class", "const", "continue", "decimal", "default", "delegate", "do", "double", "else", 54 | "enum", "event", "explicit", "extern", "false", "finally", "fixed", "float", "for", 55 | "foreach", "goto", "if", "implicit", "in", "int", "interface", "internal", "is", "lock", 56 | "long", "namespace", "new", "null", "object", "operator", "out", "override", "params", 57 | "private", "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", "short", 58 | "sizeof", "stackalloc", "static", "string", "struct", "switch", "this", "throw", "true", 59 | "try", "typeof", "uint", "ulong", "unchecked", "unsafe", "ushort", "using", "virtual", 60 | "void", "volatile", "while" 61 | ) 62 | 63 | val COFFEE_SCRIPT_KEYWORDS = setOf( 64 | "=", "->", "Infinity", "NaN", "and", "arguments", "await", "break", "by", "case", "catch", 65 | "class", "continue", "debugger", "delete", "defer", "default", "do", "else", "export", 66 | "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", "is", 67 | "isnt", "let", "loop", "new", "no", "not", "null", "of", "on", "or", "package", "return", 68 | "super", "switch", "this", "throw", "true", "try", "typeof", "unless", "undefined", "var", 69 | "wait", "when", "with", "yield" 70 | ) 71 | 72 | val JAVASCRIPT_KEYWORDS = setOf( 73 | "async", "await", "boolean", "break", "case", "catch", "class", "const", "continue", 74 | "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "false", 75 | "finally", "for", "function", "if", "implements", "import", "in", "instanceof", "interface", 76 | "let", "new", "null", "package", "private", "protected", "public", "return", "super", 77 | "switch", "this", "throw", "true", "try", "typeof", "var", "void", "while", "with", "yield" 78 | ) 79 | 80 | 81 | val PERL_KEYWORDS = setOf( 82 | "__DATA__", "__END__", "__FILE__", "__LINE__", "__PACKAGE__", "and", "cmp", "continue", 83 | "do", "else", "elsif", "eq", "eval", "for", "foreach", "goto", "gt", "if", "last", "last", 84 | "le", "lt", "my", "ne", "next", "no", "not", "or", "package", "redo", "ref", "return", "sub", 85 | "unless", "until", "use", "while", "xor" 86 | ) 87 | 88 | val PYTHON_KEYWORDS = setOf( 89 | "False", "True", "and", "as", "assert", "async", "await", "break", "class", "continue", 90 | "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", 91 | "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", 92 | "with", "yield" 93 | ) 94 | 95 | val RUBY_KEYWORDS = setOf( 96 | "__ENCODING__", "__END__", "__FILE__", "__LINE__", "BEGIN", "END", "alias", "and", "begin", 97 | "break", "case", "class", "def", "defined?", "do", "else", "elsif", "end", "ensure", "false", 98 | "for", "if", "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", "return", 99 | "self", "super", "then", "true", "undef", "unless", "until", "when", "while", "yield" 100 | ) 101 | 102 | val SH_KEYWORDS = setOf( 103 | "alias", "bg", "bind", "break", "builtin", "caller", "cd", "command", "compgen", "complete", 104 | "compopt", "continue", "declare", "dirs", "disown", "echo", "enable", "eval", "exec", "exit", 105 | "export", "fc", "fg", "getopts", "hash", "help", "history", "jobs", "kill", "let", "local", 106 | "logout", "popd", "printf", "pushd", "pwd", "read", "readonly", "return", "set", "shift", 107 | "shopt", "source", "suspend", "test" 108 | ) 109 | 110 | val SWIFT_KEYWORDS = setOf( 111 | "_", "associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", 112 | "import", "init", "inout", "internal", "let", "open", "operator", "private", 113 | "precedencegroup", "protocol", "public", "rethrows", "static", "struct", "subscript", 114 | "typealias", "var", "break", "case", "catch", "continue", "default", "defer", "do", "else", 115 | "fallthrough", "for", "guard", "if", "in", "repeat", "return", "throw", "switch", "where", 116 | "while", "Any", "as", "await", "catch", "false", "is", "nil", "rethrows", "self", "Self", 117 | "super", "throw", "throws", "true", "try", "#available", "#colorLiteral", 118 | "#else", "#elseif", "#endif", "#fileLiteral", "#if", "#imageLiteral", "#keyPath", 119 | "#selector", "#sourceLocation", "#unavailable", "associativity", "convenience", "didSet", 120 | "dynamic", "final", "get", "indirect", "infix", "lazy", "left", "mutating", "none", 121 | "nonmutating", "optional", "override", "postfix", "precedence", "prefix", "Protocol", 122 | "required", "right", "set", "some", "Type", "unowned", "weak", "willSet" 123 | ) 124 | 125 | val DART_KEYWORDS = setOf( 126 | "abstract", "as", "assert", "async", "await", "base", "break", "case", "catch", "class", 127 | "const", "continue", "covariant", "default", "deferred", "do", "dynamic", "else", "enum", 128 | "export", "extends", "external", "factory", "false", "final", "finally", "for", "get", "if", 129 | "implements", "import", "in", "interface", "is", "late", "library", "mixin", "new", "null", 130 | "on", "operator", "part", "required", "rethrow", "return", "sealed", "set", "show", "static", 131 | "super", "switch", "this", "throw", "true", "try", "var", "void", "when", "with", "while", 132 | "yield" 133 | ) 134 | 135 | val GO_KEYWORDS = setOf( 136 | "break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", 137 | "false", "for", "func", "go", "goto", "if", "import", "interface", "map", "package", "range", 138 | "return", "select", "struct", "switch", "type", "var", "true" 139 | ) 140 | 141 | val PHP_KEYWORDS = setOf( 142 | "__halt_compiler", "abstract", "and", "array", "as", "break", "callable", "case", "catch", 143 | "class", "clone", "const", "continue", "declare", "default", "die", "do", "echo", "else", 144 | "elseif", "empty", "enddeclare", "endfor", "endforeach", "endif", "endswitch", "endwhile", 145 | "eval", "exit", "extends", "final", "finally", "fn", "for", "foreach", "function", "global", 146 | "goto", "if", "implements", "include", "include_once", "instanceof", "insteadof", "interface", 147 | "isset", "list", "match", "new", "or", "print", "private", "protected", "public", "require", 148 | "require_once", "return", "static", "switch", "throw", "trait", "try", "unset", "use", "var", 149 | "while", "xor", "yield" 150 | ) 151 | 152 | val TYPESCRIPT_KEYWORDS = setOf( 153 | "abstract", "as", "asserts", "await", "break", "case", "catch", "class", "const", 154 | "constructor", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", 155 | "extends", "false", "finally", "for", "from", "function", "get", "if", "implements", "import", 156 | "in", "infer", "instanceof", "interface", "is", "keyof", "let", "module", "namespace", "new", 157 | "null", "number", "object", "package", "private", "protected", "public", "readonly", "require", 158 | "global", "return", "set", "static", "string", "super", "switch", "this", "throw", "true", 159 | "try", "type", "typeof", "undefined", "unique", "unknown", "var", "void", "while", "with", 160 | "yield" 161 | ) 162 | 163 | val ALL_KEYWORDS = (C_KEYWORDS + CPP_KEYWORDS + JAVA_KEYWORDS + KOTLIN_KEYWORDS + 164 | RUST_KEYWORDS + CSHARP_KEYWORDS + COFFEE_SCRIPT_KEYWORDS + JAVASCRIPT_KEYWORDS + 165 | PERL_KEYWORDS + PYTHON_KEYWORDS + RUBY_KEYWORDS + SH_KEYWORDS + 166 | SWIFT_KEYWORDS + DART_KEYWORDS + GO_KEYWORDS + PHP_KEYWORDS + 167 | TYPESCRIPT_KEYWORDS).toSet() 168 | 169 | // TODO Migrate to list of chars 170 | val TOKEN_DELIMITERS = 171 | listOf(" ", ",", ".", ":", ";", "(", ")", "=", "{", "}", "<", ">", "\r", "\n") 172 | val STRING_DELIMITERS = listOf("\'", "\"", "\"\"\"") 173 | val COMMENT_DELIMITERS = listOf("//", "#") 174 | 175 | // TODO Add support for other other languages like Dart or Python 176 | val MULTILINE_COMMENT_DELIMITERS = listOf(Pair("/*", "*/")) 177 | val PUNCTUATION_CHARACTERS = listOf(",", ".", ":", ";") 178 | val MARK_CHARACTERS = listOf("(", ")", "=", "{", "}", "<", ">", "-", "+", "[", "]", "|", "&") 179 | } 180 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/AnnotationLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS 5 | import dev.snipme.highlights.internal.indicesOf 6 | 7 | internal object AnnotationLocator { 8 | 9 | fun locate(code: String): Set { 10 | val foundAnnotations = emptyList() 11 | val locations = mutableSetOf() 12 | code.split(*TOKEN_DELIMITERS.toTypedArray()) 13 | .asSequence() 14 | .filter { it.isNotEmpty() } 15 | .filter { foundAnnotations.contains(it).not() } 16 | .filter { it.contains('@') } 17 | .forEach { annotation -> 18 | code.indicesOf(annotation).forEach { phraseStartIndex -> 19 | val symbolLocation = annotation.indexOf('@') 20 | val startIndex = phraseStartIndex + symbolLocation 21 | 22 | locations.add( 23 | PhraseLocation( 24 | startIndex, 25 | startIndex + annotation.length - symbolLocation 26 | ) 27 | ) 28 | } 29 | } 30 | 31 | return locations.toSet() 32 | } 33 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/CommentLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.COMMENT_DELIMITERS 4 | import dev.snipme.highlights.internal.indicesOf 5 | import dev.snipme.highlights.internal.lengthToEOF 6 | import dev.snipme.highlights.model.PhraseLocation 7 | 8 | internal object CommentLocator { 9 | 10 | fun locate(code: String): Set { 11 | val locations = mutableListOf() 12 | val indices = mutableListOf() 13 | COMMENT_DELIMITERS.forEach { delimiter -> 14 | indices.addAll(code.indicesOf(delimiter)) 15 | } 16 | 17 | indices.forEach { start -> 18 | val end = start + code.lengthToEOF(start) 19 | locations.add(PhraseLocation(start, end)) 20 | } 21 | 22 | return locations.toSet() 23 | } 24 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/KeywordLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS 4 | import dev.snipme.highlights.internal.indicesOf 5 | import dev.snipme.highlights.internal.isIndependentPhrase 6 | import dev.snipme.highlights.model.PhraseLocation 7 | 8 | internal object KeywordLocator { 9 | 10 | fun locate( 11 | code: String, 12 | keywords: Set, 13 | ignoreRanges: Set = emptySet(), 14 | ): Set { 15 | val locations = mutableSetOf() 16 | val foundKeywords = findKeywords(code, keywords) 17 | 18 | foundKeywords.forEach { keyword -> 19 | val indices = code 20 | .indicesOf(keyword) 21 | .filterNot { index -> ignoreRanges.any { index in it } } 22 | .filter { keyword.isIndependentPhrase(code, it) } 23 | 24 | indices.forEach { index -> 25 | locations.add(PhraseLocation(index, index + keyword.length)) 26 | } 27 | } 28 | 29 | return locations 30 | } 31 | 32 | private fun findKeywords(code: String, keywords: Set): Set = 33 | TOKEN_DELIMITERS.toTypedArray().let { delimiters -> 34 | code.split(*delimiters, ignoreCase = true) // Split into words 35 | .asSequence() // Reduce amount of operations 36 | .filter { it.isNotBlank() } // Remove empty 37 | .map { it.trim() } // Remove whitespaces from phrase 38 | .map { it.lowercase() } // Standardize 39 | .filter { it in keywords } // Get supported 40 | .toSet() // Filter duplicates 41 | } 42 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/LinkLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS 4 | import dev.snipme.highlights.model.PhraseLocation 5 | 6 | private val EXCLUDED_URL_CHARACTERS = listOf(":", ".", "=") 7 | 8 | internal object LinkLocator { 9 | fun locate(code: String): List = 10 | code.split(*TOKEN_DELIMITERS.minus(EXCLUDED_URL_CHARACTERS).toTypedArray()) 11 | .filter { isUrl(it) } 12 | .map { 13 | val start = code.indexOf(it) 14 | val end = start + it.length 15 | PhraseLocation(start, end) 16 | } 17 | 18 | private fun isUrl(phrase: String): Boolean = 19 | phrase.startsWith("http://") || phrase.startsWith("https://") 20 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MarkLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.MARK_CHARACTERS 4 | import dev.snipme.highlights.internal.indicesOf 5 | import dev.snipme.highlights.model.PhraseLocation 6 | 7 | internal object MarkLocator { 8 | fun locate(code: String): Set { 9 | val locations = mutableListOf() 10 | code.asSequence() 11 | .toSet() 12 | .filter { it.toString() in MARK_CHARACTERS } 13 | .forEach { 14 | code.indicesOf(it.toString()).forEach { index -> 15 | locations.add(PhraseLocation(index, index + 1)) 16 | } 17 | } 18 | 19 | return locations.toSet() 20 | } 21 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/MultilineCommentLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.MULTILINE_COMMENT_DELIMITERS 4 | import dev.snipme.highlights.internal.indicesOf 5 | import dev.snipme.highlights.model.PhraseLocation 6 | 7 | private const val START_INDEX = 0 8 | 9 | internal object MultilineCommentLocator { 10 | 11 | fun locate(code: String): Set { 12 | val locations = mutableListOf() 13 | val comments = mutableListOf>() 14 | val startIndices = mutableListOf() 15 | val endIndices = mutableListOf() 16 | 17 | MULTILINE_COMMENT_DELIMITERS.forEach { commentBlock -> 18 | val (prefix, postfix) = commentBlock 19 | startIndices.addAll(code.indicesOf(prefix)) 20 | endIndices.addAll(code.indicesOf(postfix).map { it + (postfix.length) }) 21 | } 22 | 23 | val endIndex = minOf(startIndices.size, endIndices.size) -1 24 | for (i in START_INDEX..endIndex) { 25 | comments.add(Pair(startIndices[i], endIndices[i])) 26 | } 27 | 28 | comments.forEach { 29 | val (start, end) = it 30 | locations.add(PhraseLocation(start, end)) 31 | } 32 | 33 | return locations.toSet() 34 | } 35 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/NumericLiteralLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS 4 | import dev.snipme.highlights.internal.indicesOf 5 | import dev.snipme.highlights.model.PhraseLocation 6 | 7 | private val NUMBER_START_CHARACTERS = listOf('-', '.') 8 | private val NUMBER_TYPE_CHARACTERS = listOf('e', 'u', 'f', 'l') 9 | private val HEX_NUMBER_CHARACTERS = listOf('a', 'b', 'c', 'd', 'e', 'f') 10 | private val NUMBER_SPECIAL_CHARACTERS = listOf('_') 11 | 12 | internal object NumericLiteralLocator { 13 | 14 | fun locate(code: String): Set { 15 | return findDigitIndices(code) 16 | } 17 | 18 | private fun findDigitIndices(code: String): Set { 19 | val foundPhrases = mutableSetOf() 20 | val locations = mutableSetOf() 21 | 22 | val delimiters = TOKEN_DELIMITERS.filterNot { it == "." }.toTypedArray() 23 | 24 | code.split(*delimiters) // Separate words 25 | .asSequence() // Manipulate on given word separately 26 | .filterNot { foundPhrases.contains(it) } 27 | .filter { it.isNotBlank() } // Filter spaces and others 28 | .filter { 29 | it.first().isDigit() || (NUMBER_START_CHARACTERS.contains(it.first()) 30 | && it.getOrNull(1)?.isDigit() == true) 31 | } // Find start of literals 32 | .forEach { number -> 33 | // For given literal find all occurrences 34 | val indices = code.indicesOf(number) 35 | for (startIndex in indices) { 36 | // TODO Correct this and publish 37 | if (code.isFullNumber(number, startIndex).not()) return@forEach 38 | // Omit in the middle of text, probably variable name (this100) 39 | if (code.isNumberFirstIndex(startIndex).not()) return@forEach 40 | // Add matching occurrence to the output locations 41 | val length = calculateNumberLength(number.lowercase()) 42 | locations.add(PhraseLocation(startIndex, startIndex + length)) 43 | } 44 | 45 | foundPhrases.add(number) 46 | } 47 | 48 | return locations.toSet() 49 | } 50 | 51 | // Returns if given index is the beginning of word (there is no letter before) 52 | private fun String.isNumberFirstIndex(index: Int): Boolean { 53 | if (index < 0) return false 54 | if (index == 0) return true 55 | val positionBefore = maxOf(index - 1, 0) 56 | val charBefore = getOrNull(positionBefore) ?: return false 57 | 58 | return TOKEN_DELIMITERS.contains(charBefore.toString()) 59 | } 60 | 61 | private fun String.isFullNumber(number: String, startIndex: Int): Boolean { 62 | val numberEndingIndex = startIndex + number.length 63 | if (numberEndingIndex >= lastIndex) return true 64 | val numberEnding = getOrNull(numberEndingIndex) ?: return false 65 | 66 | return TOKEN_DELIMITERS.contains(numberEnding.toString()) 67 | } 68 | 69 | private fun calculateNumberLength(number: String): Int { 70 | val letters = number.filter { it.isLetter() } 71 | 72 | if (number.startsWith("0x")) { 73 | return getLengthOfSubstringFor(number) { 74 | it.isDigit() || HEX_NUMBER_CHARACTERS.contains(it) 75 | } 76 | } 77 | 78 | if (number.contains("0b")) { 79 | return getLengthOfSubstringFor(number) { 80 | it == '0' || it == '1' 81 | } 82 | } 83 | 84 | // Highlight only 4f when e.g. number is like 4fff 85 | if (NUMBER_TYPE_CHARACTERS.any { letters.contains(it) }) { 86 | var length = 1 // Single letter 87 | length += number.count { it.isDigit() } 88 | length += number.count { NUMBER_START_CHARACTERS.contains(it) } 89 | length += number.count { NUMBER_SPECIAL_CHARACTERS.contains(it) } 90 | if ("e+" in number) length++ 91 | return length 92 | } 93 | 94 | return number.filter { 95 | it.isDigit() || 96 | NUMBER_START_CHARACTERS.contains(it) || 97 | NUMBER_TYPE_CHARACTERS.contains(it) || 98 | NUMBER_SPECIAL_CHARACTERS.contains(it) 99 | 100 | }.length 101 | } 102 | 103 | private fun getLengthOfSubstringFor(number: String, condition: (Char) -> Boolean): Int { 104 | var hexSequenceLength = 2 105 | run loop@{ 106 | number.substring(startIndex = hexSequenceLength).forEach { 107 | if (condition(it)) { 108 | hexSequenceLength++ 109 | } else { 110 | return@loop 111 | } 112 | } 113 | } 114 | 115 | return hexSequenceLength 116 | } 117 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/PunctuationLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.PUNCTUATION_CHARACTERS 4 | import dev.snipme.highlights.internal.SyntaxTokens.TOKEN_DELIMITERS 5 | import dev.snipme.highlights.internal.indicesOf 6 | import dev.snipme.highlights.model.PhraseLocation 7 | 8 | internal object PunctuationLocator { 9 | fun locate(code: String): Set { 10 | val locations = mutableSetOf() 11 | code.asSequence() 12 | .map { it.toString().trim() } 13 | .filter { it in TOKEN_DELIMITERS } 14 | .filter { it.isNotBlank() } 15 | .filter { it in PUNCTUATION_CHARACTERS } 16 | .forEach { 17 | val indices = code.indicesOf(it) 18 | for (index in indices) { 19 | if (code[index].isWhitespace()) return@forEach 20 | locations.add(PhraseLocation(index, index + 1)) 21 | } 22 | } 23 | 24 | return locations.toSet() 25 | } 26 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/StringLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.SyntaxTokens.STRING_DELIMITERS 4 | import dev.snipme.highlights.internal.contains 5 | import dev.snipme.highlights.internal.indicesOf 6 | import dev.snipme.highlights.model.PhraseLocation 7 | 8 | private const val START_INDEX = 0 9 | private const val TWO_ELEMENTS = 2 10 | private const val QUOTE_ENDING_POSITION = 1 11 | 12 | internal object StringLocator { 13 | 14 | fun locate( 15 | code: String, 16 | ignoreRanges: Set = emptySet(), 17 | ): Set = findStrings(code, ignoreRanges) 18 | 19 | private fun findStrings( 20 | code: String, 21 | ignoreRanges: Set, 22 | ): Set { 23 | val locations = mutableSetOf() 24 | 25 | // Find index of each string delimiter like " or ' or """ 26 | STRING_DELIMITERS.forEach { 27 | var textIndices = mutableListOf() 28 | textIndices += code.indicesOf(it) 29 | 30 | // Exclude positions basing on ignoreRanges 31 | textIndices = textIndices.filter { index -> 32 | val textRange = IntRange(index, index + QUOTE_ENDING_POSITION) 33 | ignoreRanges.none { ignored -> textRange in ignored } 34 | }.toMutableList() 35 | 36 | // For given indices find words between 37 | for (i in START_INDEX..textIndices.lastIndex step TWO_ELEMENTS) { 38 | if (textIndices.getOrNull(i + 1) == null) continue 39 | 40 | // Skip unwanted phrases 41 | val textRange = IntRange(textIndices[i], textIndices[i + 1]) 42 | if (ignoreRanges.any { ignored -> textRange in ignored }) 43 | continue 44 | 45 | locations.add( 46 | PhraseLocation( 47 | textIndices[i], 48 | textIndices[i + 1] + QUOTE_ENDING_POSITION 49 | ) 50 | ) 51 | } 52 | } 53 | 54 | return locations 55 | } 56 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/internal/locator/TokenLocator.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | import dev.snipme.highlights.internal.SyntaxTokens 5 | import dev.snipme.highlights.internal.indicesOf 6 | import dev.snipme.highlights.internal.isIndependentPhrase 7 | 8 | internal object TokenLocator { 9 | fun locate(code: String): List { 10 | val locations = mutableSetOf() 11 | code.split(*SyntaxTokens.TOKEN_DELIMITERS.toTypedArray()) // Separate words 12 | .asSequence() // Manipulate on given word separately 13 | .filter { it.isNotBlank() } // Filter spaces and others 14 | .forEach { token -> 15 | code.indicesOf(token) 16 | .filter { token.isIndependentPhrase(code, it) } 17 | .forEach { startIndex -> 18 | locations.add(PhraseLocation(startIndex, startIndex + token.length)) 19 | } 20 | } 21 | 22 | return locations.toList() 23 | } 24 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/model/CodeHighlight.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.model 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlinx.serialization.encodeToString 5 | import kotlinx.serialization.json.Json 6 | 7 | @Serializable 8 | sealed class CodeHighlight { 9 | abstract val location: PhraseLocation 10 | } 11 | 12 | @Serializable 13 | data class BoldHighlight(override val location: PhraseLocation) : CodeHighlight() 14 | 15 | @Serializable 16 | data class ColorHighlight( 17 | override val location: PhraseLocation, 18 | val rgb: Int 19 | ) : CodeHighlight() 20 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/model/CodeStructure.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class PhraseLocation(val start: Int, val end: Int) 7 | 8 | @Serializable 9 | data class CodeStructure( 10 | val marks: Set, 11 | val punctuations: Set, 12 | val keywords: Set, 13 | val strings: Set, 14 | val literals: Set, 15 | val comments: Set, 16 | val multilineComments: Set, 17 | val annotations: Set, 18 | val incremental: Boolean, 19 | ) { 20 | fun move(position: Int) = 21 | CodeStructure( 22 | marks = marks.map { 23 | it.copy(start = it.start + position, end = it.end + position) 24 | }.toSet(), 25 | punctuations = punctuations.map { 26 | it.copy( 27 | start = it.start + position, 28 | end = it.end + position 29 | ) 30 | }.toSet(), 31 | keywords = keywords.map { 32 | it.copy( 33 | start = it.start + position, 34 | end = it.end + position 35 | ) 36 | }.toSet(), 37 | strings = strings.map { 38 | it.copy(start = it.start + position, end = it.end + position) 39 | }.toSet(), 40 | literals = literals.map { 41 | it.copy( 42 | start = it.start + position, 43 | end = it.end + position 44 | ) 45 | }.toSet(), 46 | comments = comments.map { 47 | it.copy( 48 | start = it.start + position, 49 | end = it.end + position 50 | ) 51 | }.toSet(), 52 | multilineComments = multilineComments.map { 53 | it.copy( 54 | start = it.start + position, 55 | end = it.end + position 56 | ) 57 | }.toSet(), 58 | annotations = annotations.map { 59 | it.copy( 60 | start = it.start + position, 61 | end = it.end + position 62 | ) 63 | }.toSet(), 64 | incremental = true, 65 | ) 66 | 67 | operator fun plus(new: CodeStructure): CodeStructure = 68 | CodeStructure( 69 | marks = marks + new.marks, 70 | punctuations = punctuations + new.punctuations, 71 | keywords = keywords + new.keywords, 72 | strings = strings + new.strings, 73 | literals = literals + new.literals, 74 | comments = comments + new.comments, 75 | multilineComments = multilineComments + new.multilineComments, 76 | annotations = annotations + new.annotations, 77 | incremental = true, 78 | ) 79 | 80 | operator fun minus(new: CodeStructure): CodeStructure = 81 | CodeStructure( 82 | marks = marks - new.marks, 83 | punctuations = punctuations - new.punctuations, 84 | keywords = keywords - new.keywords, 85 | strings = strings - new.strings, 86 | literals = literals - new.literals, 87 | comments = comments - new.comments, 88 | multilineComments = multilineComments - new.multilineComments, 89 | annotations = annotations - new.annotations, 90 | incremental = true, 91 | ) 92 | 93 | fun printStructure(code: String) { 94 | print("marks = ${marks.join(code)}") 95 | print("punctuations = ${punctuations.join(code)}") 96 | print("keywords = ${keywords.join(code)}") 97 | print("strings = ${strings.join(code)}") 98 | print("literals = ${literals.join(code)}") 99 | print("comments = ${comments.join(code)}") 100 | print("multilineComments = ${multilineComments.join(code)}") 101 | print("annotations = ${annotations.join(code)}") 102 | } 103 | 104 | private fun Set.join(code: String) = 105 | this.joinToString(separator = " ") { code.substring(it.start, it.end) } + "\n" 106 | } 107 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxLanguage.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.model 2 | 3 | enum class SyntaxLanguage { 4 | DEFAULT, 5 | C, 6 | CPP, 7 | DART, 8 | JAVA, 9 | KOTLIN, 10 | RUST, 11 | CSHARP, 12 | COFFEESCRIPT, 13 | JAVASCRIPT, 14 | PERL, 15 | PYTHON, 16 | RUBY, 17 | SHELL, 18 | SWIFT, 19 | TYPESCRIPT, 20 | GO, 21 | PHP; 22 | 23 | companion object { 24 | fun getNames(): List = values().map { 25 | it.name 26 | .lowercase() 27 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 28 | } 29 | 30 | fun getByName(name: String): SyntaxLanguage? = 31 | entries.find { it.name.equals(name, ignoreCase = true) } 32 | } 33 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxTheme.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.model 2 | 3 | data class SyntaxTheme( 4 | val key: String, 5 | val code: Int, 6 | val keyword: Int, 7 | val string: Int, 8 | val literal: Int, 9 | val comment: Int, 10 | val metadata: Int, 11 | val multilineComment: Int, 12 | val punctuation: Int, 13 | val mark: Int 14 | ) { 15 | companion object { 16 | fun simple(key: String, code: Int, string: Int, accent: Int, value: Int) = SyntaxTheme( 17 | key = key, 18 | code = code, 19 | keyword = accent, 20 | string = string, 21 | literal = value, 22 | comment = string, 23 | metadata = value, 24 | multilineComment = string, 25 | punctuation = accent, 26 | mark = code 27 | ) 28 | 29 | fun basic(key: String, code: Int, string: Int, accent: Int, value: Int, comment: Int) = 30 | SyntaxTheme( 31 | key = key, 32 | code = code, 33 | keyword = accent, 34 | string = string, 35 | literal = value, 36 | comment = comment, 37 | metadata = code, 38 | multilineComment = comment, 39 | punctuation = accent, 40 | mark = code 41 | ) 42 | } 43 | } -------------------------------------------------------------------------------- /src/commonMain/kotlin/dev/snipme/highlights/model/SyntaxThemes.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.model 2 | 3 | private const val DARCULA_KEY = "darcula" 4 | private const val MONOKAI_KEY = "monokai" 5 | private const val NOTEPAD_KEY = "notepad" 6 | private const val MATRIX_KEY = "matrix" 7 | private const val PASTEL_KEY = "pastel" 8 | private const val ATOM_ONE_KEY = "atomone" 9 | 10 | object SyntaxThemes { 11 | 12 | val dark = mapOf( 13 | DARCULA_KEY to SyntaxTheme( 14 | key = DARCULA_KEY, 15 | code = 0xEDEDED, 16 | keyword = 0xCC7832, 17 | string = 0x6A8759, 18 | literal = 0x6897BB, 19 | comment = 0x909090, 20 | metadata = 0xBBB529, 21 | multilineComment = 0x629755, 22 | punctuation = 0xCC7832, 23 | mark = 0xEDEDED 24 | ), 25 | MONOKAI_KEY to SyntaxTheme( 26 | key = MONOKAI_KEY, 27 | code = 0xF8F8F2, 28 | keyword = 0xF92672, 29 | string = 0xE6DB74, 30 | literal = 0xAE81FF, 31 | comment = 0xFD971F, 32 | metadata = 0xB8F4B8, 33 | multilineComment = 0xFD971F, 34 | punctuation = 0xF8F8F2, 35 | mark = 0xF8F8F2 36 | ), 37 | NOTEPAD_KEY to SyntaxTheme( 38 | key = NOTEPAD_KEY, 39 | code = 0x000080, 40 | keyword = 0x0000FF, 41 | string = 0x808080, 42 | literal = 0xFF8000, 43 | comment = 0x008000, 44 | metadata = 0x000080, 45 | multilineComment = 0x008000, 46 | punctuation = 0xAA2C8C, 47 | mark = 0xAA2C8C 48 | ), 49 | MATRIX_KEY to SyntaxTheme( 50 | key = MATRIX_KEY, 51 | code = 0x008500, 52 | keyword = 0x008500, 53 | string = 0x269926, 54 | literal = 0x39E639, 55 | comment = 0x67E667, 56 | metadata = 0x008500, 57 | multilineComment = 0x67E667, 58 | punctuation = 0x008500, 59 | mark = 0x008500 60 | ), 61 | PASTEL_KEY to SyntaxTheme( 62 | key = PASTEL_KEY, 63 | code = 0xDFDEE0, 64 | keyword = 0x729FCF, 65 | string = 0x93CF55, 66 | literal = 0x8AE234, 67 | comment = 0x888A85, 68 | metadata = 0x5DB895, 69 | multilineComment = 0x888A85, 70 | punctuation = 0xCB956D, 71 | mark = 0xCB956D 72 | ), 73 | ATOM_ONE_KEY to SyntaxTheme( 74 | key = ATOM_ONE_KEY, 75 | code = 0xBBBBBB, 76 | keyword = 0xD55FDE, 77 | string = 0x89CA78, 78 | literal = 0xD19A66, 79 | comment = 0x5C6370, 80 | metadata = 0xE5C07B, 81 | multilineComment = 0x5C6370, 82 | punctuation = 0xEF596F, 83 | mark = 0x2BBAC5 84 | ) 85 | ) 86 | 87 | val light = mapOf( 88 | DARCULA_KEY to SyntaxTheme( 89 | key = DARCULA_KEY, 90 | code = 0x121212, 91 | keyword = 0xCC7832, 92 | string = 0x6A8759, 93 | literal = 0x6897BB, 94 | comment = 0x909090, 95 | metadata = 0xBBB529, 96 | multilineComment = 0x629755, 97 | punctuation = 0xCC7832, 98 | mark = 0x121212 99 | ), 100 | MONOKAI_KEY to SyntaxTheme( 101 | key = MONOKAI_KEY, 102 | code = 0x07070D, 103 | keyword = 0xF92672, 104 | string = 0xE6DB74, 105 | literal = 0xAE81FF, 106 | comment = 0xFD971F, 107 | metadata = 0xB8F4B8, 108 | multilineComment = 0xFD971F, 109 | punctuation = 0x07070D, 110 | mark = 0x07070D 111 | ), 112 | NOTEPAD_KEY to SyntaxTheme( 113 | key = NOTEPAD_KEY, 114 | code = 0x000080, 115 | keyword = 0x0000FF, 116 | string = 0x808080, 117 | literal = 0xFF8000, 118 | comment = 0x008000, 119 | metadata = 0x000080, 120 | multilineComment = 0x008000, 121 | punctuation = 0xAA2C8C, 122 | mark = 0xAA2C8C 123 | ), 124 | MATRIX_KEY to SyntaxTheme( 125 | key = MATRIX_KEY, 126 | code = 0x008500, 127 | keyword = 0x008500, 128 | string = 0x269926, 129 | literal = 0x39E639, 130 | comment = 0x67E667, 131 | metadata = 0x008500, 132 | multilineComment = 0x67E667, 133 | punctuation = 0x008500, 134 | mark = 0x008500 135 | ), 136 | PASTEL_KEY to SyntaxTheme( 137 | key = PASTEL_KEY, 138 | code = 0x20211F, 139 | keyword = 0x729FCF, 140 | string = 0x93CF55, 141 | literal = 0x8AE234, 142 | comment = 0x888A85, 143 | metadata = 0x5DB895, 144 | multilineComment = 0x888A85, 145 | punctuation = 0xCB956D, 146 | mark = 0xCB956D 147 | ), 148 | ATOM_ONE_KEY to SyntaxTheme( 149 | key = ATOM_ONE_KEY, 150 | code = 0x383A42, 151 | keyword = 0xA626A4, 152 | string = 0x50A14F, 153 | literal = 0x986801, 154 | comment = 0xA1A1A1, 155 | metadata = 0xC18401, 156 | multilineComment = 0xA1A1A1, 157 | punctuation = 0xE45649, 158 | mark = 0x526FFF, 159 | ) 160 | 161 | ) 162 | 163 | fun themes(darkMode: Boolean = false) = if (darkMode) dark else light 164 | 165 | fun default(darkMode: Boolean = false) = themes(darkMode)[DARCULA_KEY]!! 166 | 167 | fun darcula(darkMode: Boolean = false) = themes(darkMode)[DARCULA_KEY]!! 168 | fun monokai(darkMode: Boolean = false) = themes(darkMode)[MONOKAI_KEY]!! 169 | fun notepad(darkMode: Boolean = false) = themes(darkMode)[NOTEPAD_KEY]!! 170 | fun matrix(darkMode: Boolean = false) = themes(darkMode)[MATRIX_KEY]!! 171 | fun pastel(darkMode: Boolean = false) = themes(darkMode)[PASTEL_KEY]!! 172 | fun atom(darkMode: Boolean = false) = themes(darkMode)[ATOM_ONE_KEY]!! 173 | 174 | fun getNames(darkMode: Boolean = false): List { 175 | val source = if (darkMode) dark else light 176 | 177 | return source.map { 178 | it.key 179 | .lowercase() 180 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 181 | } 182 | } 183 | 184 | fun getByName(name: String, darkMode: Boolean = false): SyntaxTheme? { 185 | val source = if (darkMode) dark else light 186 | return source[name.lowercase()] 187 | } 188 | 189 | fun SyntaxTheme.useDark(darkMode: Boolean) = if (darkMode) dark[key] else light[key] 190 | } 191 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/CodeAnalyzerTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | import dev.snipme.highlights.model.SyntaxLanguage 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class CodeAnalyzerTest { 9 | 10 | @Test 11 | fun `Returns structure of code analyzed first time`() { 12 | val testCode = """ 13 | /** a */ 14 | // b 15 | class C extends {} 16 | "d"; 17 | @E 18 | ... 19 | 123.00f 20 | """.trimIndent() 21 | 22 | val result = CodeAnalyzer.analyze(testCode) 23 | 24 | assertEquals( 25 | setOf( 26 | PhraseLocation(30, 31), 27 | PhraseLocation(31, 32) 28 | ), 29 | result.marks 30 | ) 31 | 32 | assertEquals( 33 | setOf( 34 | PhraseLocation(36, 37), 35 | PhraseLocation(41, 42), 36 | PhraseLocation(42, 43), 37 | PhraseLocation(43, 44), 38 | PhraseLocation(48, 49), 39 | ), 40 | result.punctuations 41 | ) 42 | 43 | assertEquals( 44 | setOf( 45 | PhraseLocation(14, 19), 46 | PhraseLocation(22, 29) 47 | ), 48 | result.keywords 49 | ) 50 | 51 | assertEquals( 52 | setOf( 53 | PhraseLocation(33, 36), 54 | ), 55 | result.strings 56 | ) 57 | 58 | assertEquals( 59 | setOf( 60 | PhraseLocation(45, 52), 61 | ), 62 | result.literals 63 | ) 64 | 65 | assertEquals( 66 | setOf( 67 | PhraseLocation(9, 13), 68 | ), 69 | result.comments 70 | ) 71 | 72 | assertEquals( 73 | setOf( 74 | PhraseLocation(0, 8), 75 | ), 76 | result.multilineComments 77 | ) 78 | 79 | assertEquals( 80 | setOf( 81 | PhraseLocation(38, 40), 82 | ), 83 | result.annotations 84 | ) 85 | 86 | assertEquals(false, result.incremental) 87 | } 88 | 89 | @Test 90 | fun `Returns incremental structure of code analyzed second time`() { 91 | val testCode = """ 92 | /** a */ 93 | // b 94 | class C extends {} 95 | "d"; 96 | @E 97 | ... 98 | 123.00f 99 | """.trimIndent() 100 | 101 | val firstResult = CodeAnalyzer.analyze(testCode) 102 | assertEquals(false, firstResult.incremental) 103 | 104 | val secondTestCode = """ 105 | /** a */ 106 | // b 107 | class C extends {} 108 | "d"; 109 | @E 110 | ... 111 | 123.00f 112 | 113 | class 114 | """.trimIndent() 115 | 116 | val snapshot = CodeSnapshot(testCode, firstResult, SyntaxLanguage.DEFAULT) 117 | 118 | val result = CodeAnalyzer.analyze(secondTestCode, snapshot = snapshot) 119 | assertEquals(true, result.incremental) 120 | 121 | assertEquals( 122 | setOf( 123 | PhraseLocation(30, 31), 124 | PhraseLocation(31, 32), 125 | ), 126 | result.marks 127 | ) 128 | 129 | assertEquals( 130 | setOf( 131 | PhraseLocation(36, 37), 132 | PhraseLocation(41, 42), 133 | PhraseLocation(42, 43), 134 | PhraseLocation(43, 44), 135 | PhraseLocation(48, 49), 136 | ), 137 | result.punctuations 138 | ) 139 | 140 | assertEquals( 141 | setOf( 142 | PhraseLocation(14, 19), 143 | PhraseLocation(22, 29), 144 | ), 145 | result.keywords 146 | ) 147 | 148 | assertEquals( 149 | setOf( 150 | PhraseLocation(33, 36), 151 | ), 152 | result.strings 153 | ) 154 | 155 | assertEquals( 156 | setOf( 157 | PhraseLocation(45, 52), 158 | ), 159 | result.literals 160 | ) 161 | 162 | assertEquals( 163 | setOf( 164 | PhraseLocation(9, 13), 165 | ), 166 | result.comments 167 | ) 168 | 169 | assertEquals( 170 | setOf( 171 | PhraseLocation(0, 8), 172 | ), 173 | result.multilineComments 174 | ) 175 | 176 | assertEquals( 177 | setOf( 178 | PhraseLocation(38, 40), 179 | ), 180 | result.annotations 181 | ) 182 | } 183 | 184 | @Test 185 | fun `Returns incremental structure of decreased code analyzed second time`() { 186 | val testCode = """ 187 | /** a */ 188 | // b 189 | class C extends {} 190 | "d"; 191 | @E 192 | ... 193 | 123.00f 194 | """.trimIndent() 195 | 196 | val firstResult = CodeAnalyzer.analyze(testCode) 197 | assertEquals(false, firstResult.incremental) 198 | 199 | val secondTestCode = """ 200 | /** a */ 201 | // b 202 | class 203 | """.trimIndent() 204 | 205 | val snapshot = CodeSnapshot(testCode, firstResult, SyntaxLanguage.DEFAULT) 206 | 207 | val result = CodeAnalyzer.analyze(secondTestCode, snapshot = snapshot) 208 | assertEquals(true, result.incremental) 209 | 210 | assertEquals( 211 | emptySet(), 212 | result.marks 213 | ) 214 | 215 | assertEquals( 216 | emptySet(), 217 | result.punctuations 218 | ) 219 | 220 | assertEquals( 221 | setOf( 222 | PhraseLocation(14, 19), 223 | ), 224 | result.keywords 225 | ) 226 | 227 | assertEquals( 228 | emptySet(), 229 | result.strings 230 | ) 231 | 232 | assertEquals( 233 | emptySet(), 234 | result.literals 235 | ) 236 | 237 | assertEquals( 238 | setOf( 239 | PhraseLocation(9, 13), 240 | ), 241 | result.comments 242 | ) 243 | 244 | assertEquals( 245 | setOf( 246 | PhraseLocation(0, 8), 247 | ), 248 | result.multilineComments 249 | ) 250 | 251 | assertEquals( 252 | emptySet(), 253 | result.annotations 254 | ) 255 | } 256 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/CodeComparatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | internal class CodeComparatorTest { 7 | 8 | @Test 9 | fun `Returns none difference for empty phrases`() { 10 | val currentCode = "" 11 | val newCode = "" 12 | 13 | val result = CodeComparator.difference(currentCode, newCode) 14 | 15 | assertEquals(CodeDifference.None, result) 16 | } 17 | 18 | @Test 19 | fun `Returns none difference for blank phrases`() { 20 | val currentCode = " " 21 | val newCode = " " 22 | 23 | val result = CodeComparator.difference(currentCode, newCode) 24 | 25 | assertEquals(CodeDifference.None, result) 26 | } 27 | 28 | @Test 29 | fun `Returns none difference for whitespace phrases`() { 30 | val currentCode = "\r" 31 | val newCode = "\r\r" 32 | 33 | val result = CodeComparator.difference(currentCode, newCode) 34 | 35 | assertEquals(CodeDifference.None, result) 36 | } 37 | 38 | @Test 39 | fun `Returns none difference for the same phrases`() { 40 | val currentCode = "@ABCD" 41 | val newCode = "@ABCD" 42 | 43 | val result = CodeComparator.difference(currentCode, newCode) 44 | 45 | assertEquals(CodeDifference.None, result) 46 | } 47 | 48 | @Test 49 | fun `Returns increase difference for the longer new phrase`() { 50 | val currentCode = "@ABCD" 51 | val newCode = "@ABCD abcd dd ee" 52 | 53 | val result = CodeComparator.difference(currentCode, newCode) 54 | 55 | assertEquals(CodeDifference.Increase("abcd dd ee"), result) 56 | } 57 | 58 | @Test 59 | fun `Returns decrease difference for the shorter new phrase`() { 60 | val currentCode = "@ABCD abcd dd" 61 | val newCode = "@ABCD" 62 | 63 | val result = CodeComparator.difference(currentCode, newCode) 64 | 65 | assertEquals(CodeDifference.Decrease("abcd dd"), result) 66 | } 67 | 68 | @Test 69 | fun `Returns full difference for the mixed new phrase`() { 70 | val currentCode = "@ABCD abcd dd ee" 71 | val newCode = "@ABCD abcd ee dd" 72 | 73 | val result = CodeComparator.difference(currentCode, newCode) 74 | 75 | assertEquals(CodeDifference.Full, result) 76 | } 77 | 78 | @Test 79 | fun `Returns full difference for the char change in token`() { 80 | val currentCode = "const foo = 'bar';" 81 | 82 | val newCode = "const foo = 'baz';" 83 | 84 | val result = CodeComparator.difference(currentCode, newCode) 85 | 86 | assertEquals(CodeDifference.Full, result) 87 | } 88 | 89 | @Test 90 | fun `Returns difference for the char addition in single token`() { 91 | val currentCode = "const foo = 'bar';" 92 | 93 | val newCode = "const foo = 'barrr';" 94 | 95 | val result = CodeComparator.difference(currentCode, newCode) 96 | 97 | assertEquals(CodeDifference.Full, result) 98 | } 99 | 100 | @Test 101 | fun `Returns difference for the char subtraction in single token`() { 102 | val currentCode = "const foo = 'barrr';" 103 | 104 | val newCode = "const foo = 'bar';" 105 | 106 | val result = CodeComparator.difference(currentCode, newCode) 107 | 108 | assertEquals(CodeDifference.Full, result) 109 | } 110 | 111 | @Test 112 | fun `Returns only difference for complex code change`() { 113 | val currentCode = """ 114 | /** a */ 115 | // b 116 | class C extends {} 117 | "d"; 118 | @E 119 | ... 120 | 123.00f 121 | """.trimIndent() 122 | 123 | val newCode = """ 124 | /** a */ 125 | // b 126 | class C extends {} 127 | "d"; 128 | @E 129 | ... 130 | 123.00f 131 | 132 | field.forEach { } 133 | """.trimIndent() 134 | 135 | val result = CodeComparator.difference(currentCode, newCode) 136 | 137 | assertEquals(CodeDifference.Increase(" field.forEach { }"), result) 138 | } 139 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/ExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | internal class ExtensionsKtTest { 7 | 8 | class IndicesOfTest { 9 | 10 | @Test 11 | fun `Returns empty result of phrase that is not present`() { 12 | val text = "b c d" 13 | val phrase = "//a" 14 | 15 | val result = text.indicesOf(phrase) 16 | 17 | assertEquals(setOf(), result) 18 | } 19 | 20 | @Test 21 | fun `Returns index of phrase at start`() { 22 | val text = "//a b c d" 23 | val phrase = "//a" 24 | 25 | val result = text.indicesOf(phrase) 26 | 27 | assertEquals(setOf(0), result) 28 | } 29 | 30 | @Test 31 | fun `Returns index of phrase at end`() { 32 | val text = "/a b c d //a" 33 | val phrase = "//a" 34 | 35 | val result = text.indicesOf(phrase) 36 | 37 | assertEquals(setOf(9), result) 38 | } 39 | 40 | @Test 41 | fun `Returns index of phrase at start and end`() { 42 | val text = "//a b c d //a" 43 | val phrase = "//a" 44 | 45 | val result = text.indicesOf(phrase) 46 | 47 | assertEquals(setOf(0, 10), result) 48 | } 49 | 50 | @Test 51 | fun `Returns all indices of phrase at end`() { 52 | val text = "//a /a b //ac d //a" 53 | val phrase = "//a" 54 | 55 | val result = text.indicesOf(phrase) 56 | 57 | assertEquals(setOf(0, 9, 16), result) 58 | } 59 | 60 | @Test 61 | fun `Returns all indices of special phrase`() { 62 | val text = "//a /a b /** d //a" 63 | val phrase = "/**" 64 | 65 | val result = text.indicesOf(phrase) 66 | 67 | assertEquals(setOf(9), result) 68 | } 69 | } 70 | 71 | class IsIndependentPhraseTest { 72 | 73 | @Test 74 | fun `Returns true for phrase at start`() { 75 | val code = "class " 76 | val index = 0 77 | 78 | val result = "class".isIndependentPhrase(code, index) 79 | 80 | assertEquals(true, result) 81 | } 82 | 83 | @Test 84 | fun `Returns false for wrong phrase at start`() { 85 | val code = "classa " 86 | val index = 1 87 | 88 | val result = "class".isIndependentPhrase(code, index) 89 | 90 | assertEquals(false, result) 91 | } 92 | 93 | @Test 94 | fun `Returns true for phrase at end`() { 95 | val code = "asas class" 96 | val index = 5 97 | 98 | val result = "class".isIndependentPhrase(code, index) 99 | 100 | assertEquals(true, result) 101 | } 102 | 103 | @Test 104 | fun `Returns false for wrong phrase at end`() { 105 | val code = "asas classa" 106 | val index = 6 107 | 108 | val result = "class".isIndependentPhrase(code, index) 109 | 110 | assertEquals(false, result) 111 | } 112 | 113 | @Test 114 | fun `Returns false for phrase with wrong prefix at end`() { 115 | val code = "asas aclass" 116 | val index = 6 117 | 118 | val result = "class".isIndependentPhrase(code, index) 119 | 120 | assertEquals(false, result) 121 | } 122 | 123 | @Test 124 | fun `Returns false for phrase at start other phrase`() { 125 | val code = "valuable" 126 | val index = 0 127 | 128 | val result = "val".isIndependentPhrase(code, index) 129 | 130 | assertEquals(false, result) 131 | } 132 | 133 | @Test 134 | fun `Returns false for phrase at end other phrase`() { 135 | val code = "inval" 136 | val index = 2 137 | 138 | val result = "val".isIndependentPhrase(code, index) 139 | 140 | assertEquals(false, result) 141 | } 142 | 143 | @Test 144 | fun `Returns false for phrase inside other phrase`() { 145 | val code = "invaluable" 146 | val index = 2 147 | 148 | val result = "val".isIndependentPhrase(code, index) 149 | 150 | assertEquals(false, result) 151 | } 152 | 153 | @Test 154 | fun `Returns true for phrase is whole code`() { 155 | val code = "class" 156 | val index = 0 157 | 158 | val result = "class".isIndependentPhrase(code, index) 159 | 160 | assertEquals(true, result) 161 | } 162 | 163 | @Test 164 | fun `Returns true for phrase is with new line`() { 165 | val code = "class\n" 166 | val index = 0 167 | 168 | val result = "class".isIndependentPhrase(code, index) 169 | 170 | assertEquals(true, result) 171 | } 172 | 173 | @Test 174 | fun `Returns true for phrase is with new line and space`() { 175 | val code = """ 176 | class 177 | """ 178 | val index = 3 179 | 180 | val result = "class".isIndependentPhrase(code, index) 181 | 182 | assertEquals(true, result) 183 | } 184 | 185 | @Test 186 | fun `Returns true for phrase that starts with number`() { 187 | val code = "9class" 188 | val index = 1 189 | 190 | val result = "class".isIndependentPhrase(code, index) 191 | 192 | assertEquals(true, result) 193 | } 194 | 195 | @Test 196 | fun `Returns false for phrase that ends with number`() { 197 | val code = "class9" 198 | val index = 0 199 | 200 | val result = "class".isIndependentPhrase(code, index) 201 | 202 | assertEquals(false, result) 203 | } 204 | 205 | @Test 206 | fun `Returns false for phrase that is between numbers`() { 207 | val code = "9class9" 208 | val index = 1 209 | 210 | val result = "class".isIndependentPhrase(code, index) 211 | 212 | assertEquals(false, result) 213 | } 214 | 215 | @Test 216 | fun `Returns true for phrase that starts special character`() { 217 | val code = ".class" 218 | val index = 1 219 | 220 | val result = "class".isIndependentPhrase(code, index) 221 | 222 | assertEquals(true, result) 223 | } 224 | 225 | @Test 226 | fun `Returns true for phrase that ends with special character`() { 227 | val code = "class." 228 | val index = 0 229 | 230 | val result = "class".isIndependentPhrase(code, index) 231 | 232 | assertEquals(true, result) 233 | } 234 | 235 | @Test 236 | fun `Returns true for phrase that is between special characters`() { 237 | val code = ".class." 238 | val index = 1 239 | 240 | val result = "class".isIndependentPhrase(code, index) 241 | 242 | assertEquals(true, result) 243 | } 244 | } 245 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/HighlightsTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import dev.snipme.highlights.Highlights 4 | import dev.snipme.highlights.HighlightsResultListener 5 | import dev.snipme.highlights.model.CodeHighlight 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.ExperimentalCoroutinesApi 8 | import kotlinx.coroutines.launch 9 | import kotlinx.coroutines.suspendCancellableCoroutine 10 | import kotlinx.coroutines.test.StandardTestDispatcher 11 | import kotlinx.coroutines.test.resetMain 12 | import kotlinx.coroutines.test.runTest 13 | import kotlinx.coroutines.test.setMain 14 | import kotlin.test.AfterTest 15 | import kotlin.test.BeforeTest 16 | import kotlin.test.Test 17 | import kotlin.test.assertTrue 18 | import kotlin.time.Duration 19 | import kotlin.time.TimeSource.Monotonic.markNow 20 | import kotlin.time.measureTime 21 | 22 | @OptIn(ExperimentalCoroutinesApi::class) 23 | class HighlightsTest { 24 | private val testDispatcher = StandardTestDispatcher() 25 | 26 | @BeforeTest 27 | fun setup() { 28 | Dispatchers.setMain(testDispatcher) 29 | } 30 | 31 | @AfterTest 32 | fun tearDown() { 33 | Dispatchers.resetMain() 34 | } 35 | 36 | @Test 37 | fun `returns list of code highlights for sync call`() { 38 | val default = Highlights.default().apply { 39 | setCode(longJavaCode) 40 | } 41 | 42 | val highlights = default.getHighlights() 43 | assertTrue { highlights.isNotEmpty() } 44 | } 45 | 46 | @Test 47 | fun `returns error for exception during analysis`() = runTest { 48 | val default = Highlights.default().apply { 49 | setCode(longJavaCode) 50 | } 51 | 52 | var error: Throwable? = null 53 | suspendCancellableCoroutine { continuation -> 54 | invokeHighlightsRequest( 55 | default, 56 | onStart = { throw IllegalStateException() }, 57 | onError = { 58 | error = it 59 | continuation.resume(Unit) {} 60 | }, 61 | ) 62 | } 63 | 64 | assertTrue { error != null } 65 | } 66 | 67 | @Test 68 | fun `cancels first analysis when second is invoked`() = runTest { 69 | val default = Highlights.default().apply { 70 | setCode(longJavaCode) 71 | } 72 | 73 | val results = mutableListOf>() 74 | 75 | suspendCancellableCoroutine { continuation -> 76 | invokeHighlightsRequest( 77 | default, 78 | onSuccess = { results.add(it) }, 79 | onStart = { 80 | invokeHighlightsRequest(default) { 81 | results.add(it) 82 | continuation.resume(Unit) {} 83 | } 84 | }, 85 | ) 86 | } 87 | 88 | assertTrue { results.size == 1 } 89 | } 90 | 91 | @Test 92 | fun `returns list of code highlights asynchronously`() = runTest { 93 | val default = Highlights.default().apply { 94 | setCode(longJavaCode) 95 | } 96 | 97 | val result = suspendCancellableCoroutine { continuation -> 98 | invokeHighlightsRequest(default) { 99 | continuation.resume(it) {} 100 | } 101 | } 102 | 103 | assertTrue { result.isNotEmpty() } 104 | } 105 | 106 | @Test 107 | fun `returns asynchronous results one by one`() = runTest { 108 | val default = Highlights.default().apply { 109 | setCode(longJavaCode) 110 | } 111 | 112 | var result1: List 113 | val time1 = measureTime { 114 | result1 = suspendCancellableCoroutine { continuation -> 115 | invokeHighlightsRequest(default) { 116 | continuation.resume(it) {} 117 | } 118 | } 119 | } 120 | println("Time1: ${time1.inWholeMilliseconds} ms") 121 | assertTrue { result1.isNotEmpty() } 122 | 123 | default.setCode(longJavaCode.replace("static", "statac")) 124 | 125 | var result2: List 126 | val time2 = measureTime { 127 | result2 = suspendCancellableCoroutine { continuation -> 128 | invokeHighlightsRequest(default) { 129 | continuation.resume(it) {} 130 | } 131 | } 132 | } 133 | println("Time2: ${time2.inWholeMilliseconds} ms") 134 | assertTrue { result2.isNotEmpty() } 135 | } 136 | } 137 | 138 | @OptIn(ExperimentalCoroutinesApi::class) 139 | class HighlightsCancellationTest { 140 | private val testDispatcher = StandardTestDispatcher() 141 | 142 | @BeforeTest 143 | fun setup() { 144 | Dispatchers.setMain(testDispatcher) 145 | } 146 | 147 | @AfterTest 148 | fun tearDown() { 149 | Dispatchers.resetMain() 150 | } 151 | 152 | @Test 153 | fun `returns immediately result from second invocation`() = runTest { 154 | val default = Highlights.default().apply { 155 | setCode(longJavaCode) 156 | } 157 | 158 | var time1 = Duration.ZERO 159 | var time2 = Duration.ZERO 160 | 161 | val job1 = launch { 162 | suspendCancellableCoroutine { c -> 163 | invokeAndMeasureTime(default, "#1") { 164 | time1 = it 165 | c.resume(Unit) {} 166 | println("Time1: ${it.inWholeMilliseconds} ms") 167 | } 168 | } 169 | } 170 | 171 | val job2 = launch { 172 | suspendCancellableCoroutine { c -> 173 | invokeAndMeasureTime(default, "#2") { 174 | time2 = it 175 | c.resume(Unit) {} 176 | println("Time2: ${it.inWholeMilliseconds} ms") 177 | } 178 | } 179 | } 180 | 181 | job1.join() 182 | job2.join() 183 | assertTrue { time1.inWholeMilliseconds < time2.inWholeMilliseconds } 184 | } 185 | } 186 | 187 | private fun invokeAndMeasureTime( 188 | highlights: Highlights, 189 | name: String, 190 | onFinish: (Duration) -> Unit = {} 191 | ) { 192 | var result: Duration? = null 193 | val now = markNow() 194 | 195 | fun updateFirstTime() { 196 | if (result == null) { 197 | result = now.elapsedNow() 198 | onFinish(result!!) 199 | } 200 | } 201 | 202 | highlights.clearSnapshot() 203 | invokeHighlightsRequest( 204 | highlights, 205 | onStart = { println("Start $name"); }, 206 | onCancel = { println("Cancel $name"); updateFirstTime() }, 207 | onError = { println("Error $name: $it"); updateFirstTime() }, 208 | onSuccess = { println("Success $name"); updateFirstTime() }, 209 | ) 210 | } 211 | 212 | private fun invokeHighlightsRequest( 213 | highlights: Highlights, 214 | onStart: () -> Unit = {}, 215 | onCancel: () -> Unit = {}, 216 | onError: (Throwable) -> Unit = {}, 217 | onSuccess: (List) -> Unit = {} 218 | ) { 219 | highlights.getHighlightsAsync(object : HighlightsResultListener { 220 | override fun onStart() = onStart() 221 | override fun onSuccess(result: List) = onSuccess(result) 222 | override fun onError(exception: Throwable) = onError(exception) 223 | override fun onCancel() = onCancel() 224 | }) 225 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/SyntaxTokensTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertTrue 5 | 6 | class SyntaxTokensTest { 7 | @Test 8 | fun `Returns all keywords without whitespaces`() { 9 | val hasWhitespace = SyntaxTokens.ALL_KEYWORDS.filter { keyword -> 10 | keyword.any { it.isWhitespace() } 11 | } 12 | assertTrue(hasWhitespace.isEmpty()) 13 | } 14 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/TestExtensions.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | 5 | internal fun List.printResults(code: String) { 6 | this.forEach { 7 | println(code.substring(it.start, it.end)) 8 | } 9 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/language/CommentTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.language 2 | 3 | import dev.snipme.highlights.Highlights 4 | import dev.snipme.highlights.model.SyntaxLanguage 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class CommentTest { 9 | 10 | @Test 11 | fun test() { 12 | val code = """ 13 | /** 14 | * Class's start 15 | */ 16 | class PagesComponent( 17 | context: ComponentContext, 18 | container: () -> PagesContainer, // inject using DI or create 19 | ) : ComponentContext by context, 20 | PagesStore by context.retainedStore(factory = container) { 21 | 22 | 'a' 23 | 24 | private val navigator = PagesNavigation() 25 | 26 | val pages = childPages( 27 | source = navigator, 28 | serializer = PageConfig.serializer(), 29 | initialPages = { Pages(items = List(5) { PageConfig(it) }, selectedIndex = 0) }, 30 | childFactory = ::PageComponent, 31 | ) 32 | 33 | "b" 34 | 35 | init { 36 | // subscribe to the store following the component 's lifecycle 37 | subscribe { 38 | actions.collect { action -> 39 | when (action) { 40 | is SelectPage -> navigator.select(action.index) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | class PageComponent( 48 | page: PageConfig, 49 | context: ComponentContext, 50 | ) : ComponentContext by context, 51 | Container { 52 | 53 | override val store = store(PageState(page.page), coroutineScope()) { 54 | 55 | `c` 56 | 57 | // state keeper will preserve the store's state 58 | keepState(context.stateKeeper, PageState.serializer()) 59 | reduce { intent -> 60 | when (intent) { 61 | is ClickedIncrementCounter -> updateState { 62 | copy(counter = counter + 1) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | """.trimIndent() 69 | 70 | val result = Highlights.Builder( 71 | language = SyntaxLanguage.KOTLIN, 72 | code = code 73 | ).build().getCodeStructure() 74 | 75 | assertEquals(2, result.strings.size) 76 | } 77 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/language/JavaTest.kt: -------------------------------------------------------------------------------- 1 | import dev.snipme.highlights.Highlights 2 | import dev.snipme.highlights.model.SyntaxLanguage 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class JavaTest { 7 | 8 | @Test 9 | fun test() { 10 | val code = """ 11 | this.class.abcd ) new 12 | """.trimIndent() 13 | 14 | val result = Highlights.Builder( 15 | language = SyntaxLanguage.JAVA, 16 | code = code 17 | ).build().getCodeStructure() 18 | 19 | assertEquals(3, result.keywords.size) 20 | } 21 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/language/KotlinTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.language 2 | 3 | import dev.snipme.highlights.Highlights 4 | import dev.snipme.highlights.model.SyntaxLanguage 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class KotlinTest { 9 | 10 | @Test 11 | fun test() { 12 | val code = """ 13 | val intent = 0 14 | copy(input = input(intent.value)) // highlights "value" 15 | 16 | // calling configure() is equivalent to: 17 | 18 | val new = 1 19 | 20 | val initialPages = 0 21 | 22 | // Visibility modifiers 23 | internal val internalVar: Int = 10 24 | private fun privateFunction() { 25 | println("This is a private function") 26 | } 27 | 28 | // Data types 29 | val number: Int = 42 30 | val pi: Double = 3.14 31 | val isValid: Boolean = true 32 | """.trimIndent() 33 | 34 | val result = Highlights.Builder( 35 | language = SyntaxLanguage.KOTLIN, 36 | code = code 37 | ).build().getCodeStructure() 38 | 39 | assertEquals(11, result.keywords.size) 40 | } 41 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/AnnotationLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class AnnotationLocatorTest { 9 | 10 | @Test 11 | fun `Returns proper location for annotation`() { 12 | val testCode = "@ABCD" 13 | 14 | val result = AnnotationLocator.locate(testCode) 15 | 16 | assertEquals(1, result.size) 17 | assertEquals(PhraseLocation(0, 5), result.first()) 18 | } 19 | 20 | @Test 21 | fun `Returns proper location for annotation inside the code`() { 22 | val testCode = """ 23 | import com.test.example 24 | 25 | @Serialization 26 | class {} 27 | """.trimIndent() 28 | 29 | val result = AnnotationLocator.locate(testCode) 30 | 31 | assertEquals(1, result.size) 32 | assertEquals(PhraseLocation(25, 39), result.first()) 33 | } 34 | 35 | @Test 36 | fun `Returns proper location for multiple annotations`() { 37 | val testCode = """ 38 | import com.test.example 39 | 40 | @Serialization 41 | @Test 42 | class {} 43 | """.trimIndent() 44 | 45 | val result = AnnotationLocator.locate(testCode) 46 | 47 | assertEquals(2, result.size) 48 | assertEquals(PhraseLocation(25, 39), result[0]) 49 | assertEquals(PhraseLocation(40, 45), result[1]) 50 | } 51 | 52 | @Test 53 | fun `Returns proper locations for annotation in code between`() { 54 | val testCode = """ 55 | import com.test.example 56 | 57 | @Serialization 58 | class { 59 | 60 | @override 61 | fun test() {} 62 | 63 | @SerialName("name") 64 | fun test2() {} 65 | } 66 | """.trimIndent() 67 | 68 | val result = AnnotationLocator.locate(testCode) 69 | 70 | assertEquals(3, result.size) 71 | assertEquals(PhraseLocation(25, 39), result[0]) 72 | assertEquals(PhraseLocation(53, 62), result[1]) 73 | assertEquals(PhraseLocation(90, 101), result[2]) 74 | } 75 | 76 | @Test 77 | fun `Returns location in scope of pharse with annotation`() { 78 | val testCode = """ 79 | import com.test.example 80 | 81 | @override 82 | fun test() { 83 | this@AnnotationTest 84 | } 85 | 86 | @override 87 | fun test2() {} 88 | } 89 | """.trimIndent() 90 | 91 | val result = AnnotationLocator.locate(testCode) 92 | 93 | assertEquals(3, result.size) 94 | assertEquals(PhraseLocation(25, 34), result[0]) 95 | assertEquals(PhraseLocation(79, 88), result[1]) 96 | assertEquals(PhraseLocation(56, 71), result[2]) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/CommentLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | 8 | internal class CommentLocatorTest { 9 | 10 | @Test 11 | fun `Returns location of full line of comment`() { 12 | val testCode = """ 13 | // This is code 14 | class 15 | """.trimIndent() 16 | 17 | val result = CommentLocator.locate(testCode) 18 | 19 | assertEquals(1, result.size) 20 | assertEquals(PhraseLocation(0, 15), result.first()) 21 | } 22 | 23 | @Test 24 | fun `Returns location of inline comment`() { 25 | val testCode = """ 26 | class // This is code 27 | """.trimIndent() 28 | 29 | val result = CommentLocator.locate(testCode) 30 | 31 | assertEquals(1, result.size) 32 | assertEquals(PhraseLocation(6, 21), result.first()) 33 | } 34 | 35 | @Test 36 | fun `Returns location of whole comment with single apostrophe`() { 37 | val testCode = "// the component's lifecycle" 38 | 39 | val result = CommentLocator.locate(testCode) 40 | 41 | assertEquals(1, result.size) 42 | assertEquals(PhraseLocation(0, 28), result.first()) 43 | } 44 | 45 | @Test 46 | fun `Returns location of whole comment with multiple quotations`() { 47 | val testCode = """// "This" 'is a' comment`s quote""" 48 | 49 | val result = CommentLocator.locate(testCode) 50 | 51 | assertEquals(1, result.size) 52 | assertEquals(PhraseLocation(0, 32), result.first()) 53 | } 54 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/KeywordLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class KeywordLocatorTest { 9 | 10 | @Test 11 | fun `Returns empty list for no keywords`() { 12 | val testCode = "class NewClass" 13 | val keywords = setOf() 14 | val expectedResult = emptySet() 15 | 16 | val result = KeywordLocator.locate(testCode, keywords) 17 | 18 | assertEquals(expectedResult, result) 19 | } 20 | 21 | @Test 22 | fun `Returns location of first found keyword`() { 23 | val testCode = "class NewClass" 24 | val keywords = setOf("static", "new", "class") 25 | 26 | val result = KeywordLocator.locate(testCode, keywords) 27 | 28 | assertEquals(1, result.size) 29 | assertEquals(PhraseLocation(0, 5), result.first()) 30 | } 31 | 32 | @Test 33 | fun `Returns location of all found keyword in line`() { 34 | val testCode = "class NewClass extends { }" 35 | val keywords = setOf("static", "new", "class", "extends") 36 | 37 | val result = KeywordLocator.locate(testCode, keywords) 38 | 39 | assertEquals(2, result.size) 40 | assertEquals(PhraseLocation(0, 5), result[0]) 41 | assertEquals(PhraseLocation(15, 22), result[1]) 42 | } 43 | 44 | @Test 45 | fun `Returns location of all keyword next to each other`() { 46 | val testCode = "this.class.abcd ) new" 47 | val keywords = setOf("this", "new", "class") 48 | 49 | val result = KeywordLocator.locate(testCode, keywords) 50 | 51 | assertEquals(3, result.size) 52 | assertEquals(PhraseLocation(0, 4), result[0]) 53 | assertEquals(PhraseLocation(5, 10), result[1]) 54 | assertEquals(PhraseLocation(18, 21), result[2]) 55 | } 56 | 57 | @Test 58 | fun `Returns location of word that is proper keyword only`() { 59 | val testCode = "class 1class 1 class1 class& %class aclass" 60 | val keywords = setOf("this", "new", "class") 61 | 62 | val result = KeywordLocator.locate(testCode, keywords) 63 | 64 | assertEquals(4, result.size) 65 | assertEquals(PhraseLocation(0, 5), result[0]) 66 | assertEquals(PhraseLocation(7, 12), result[1]) 67 | assertEquals(PhraseLocation(22, 27), result[2]) 68 | assertEquals(PhraseLocation(30, 35), result[3]) 69 | } 70 | 71 | @Test 72 | fun `Returns location of all keywords`() { 73 | val testCode = """ 74 | class Example extends Sample { 75 | static class Example2 {} 76 | } 77 | """.trimIndent() 78 | val keywords = setOf("static", "class", "extends") 79 | 80 | val result = KeywordLocator.locate(testCode, keywords) 81 | 82 | assertEquals(4, result.size) 83 | assertEquals(PhraseLocation(0, 5), result[0]) 84 | assertEquals(PhraseLocation(42, 47), result[1]) 85 | assertEquals(PhraseLocation(14, 21), result[2]) 86 | assertEquals(PhraseLocation(35, 41), result[3]) 87 | } 88 | 89 | @Test 90 | fun `Not returns location of keyword inside phrase`() { 91 | val testCode = """ 92 | aclassa 93 | """.trimIndent() 94 | val keywords = setOf("static", "class", "extends") 95 | 96 | val result = KeywordLocator.locate(testCode, keywords) 97 | 98 | assertEquals(0, result.size) 99 | } 100 | 101 | @Test 102 | fun `Not returns location of keyword inside phrase from start`() { 103 | val testCode = """ 104 | val intent = 0 105 | """.trimIndent() 106 | val keywords = setOf("int") 107 | 108 | val result = KeywordLocator.locate(testCode, keywords) 109 | 110 | assertEquals(0, result.size) 111 | } 112 | 113 | @Test 114 | fun `Not returns keywords from single comment`() { 115 | val testCode = """ 116 | // This class is static and should extend another class 117 | """.trimIndent() 118 | val keywords = setOf("static", "class", "extends") 119 | 120 | val result = KeywordLocator.locate(testCode, keywords, setOf(IntRange(0, 55))) 121 | 122 | assertEquals(0, result.size) 123 | } 124 | 125 | @Test 126 | fun `Not returns keywords from multiline comment`() { 127 | val testCode = """ 128 | /* 129 | This class is static and should extend another class 130 | */ 131 | """.trimIndent() 132 | val keywords = setOf("static", "class", "extends") 133 | 134 | val result = KeywordLocator.locate(testCode, keywords, setOf(IntRange(0, 56))) 135 | 136 | assertEquals(0, result.size) 137 | } 138 | 139 | @Test 140 | fun `Not returns keywords from string`() { 141 | val testCode = """ 142 | "This class is static and should extend another class" 143 | """.trimIndent() 144 | val keywords = setOf("static", "class", "extends") 145 | 146 | val result = KeywordLocator.locate(testCode, keywords, setOf(IntRange(0, 54))) 147 | 148 | assertEquals(0, result.size) 149 | } 150 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/LinkLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | import kotlin.test.assertEquals 5 | import kotlin.test.Test 6 | 7 | internal class LinkLocatorTest { 8 | 9 | @Test 10 | fun `Returns location of HTTP protocol address`() { 11 | val testCode = "// http://" 12 | 13 | val result = LinkLocator.locate(testCode) 14 | 15 | assertEquals(1, result.size) 16 | assertEquals(PhraseLocation(3, 10), result.first()) 17 | } 18 | 19 | @Test 20 | fun `Returns location of HTTPS protocol address`() { 21 | val testCode = "// https://" 22 | 23 | val result = LinkLocator.locate(testCode) 24 | 25 | assertEquals(1, result.size) 26 | assertEquals(PhraseLocation(3, 11), result.first()) 27 | } 28 | 29 | @Test 30 | fun `Returns location of link between other tokens`() { 31 | val testCode = "// This is a https://www.example.com link" 32 | 33 | val result = LinkLocator.locate(testCode) 34 | 35 | assertEquals(1, result.size) 36 | assertEquals(PhraseLocation(13, 36), result.first()) 37 | } 38 | 39 | @Test 40 | fun `Returns location of full URL link`() { 41 | val testCode = "// This is a https://www.example.com/test?a=b&c=d%3A# link" 42 | 43 | val result = LinkLocator.locate(testCode) 44 | 45 | assertEquals(1, result.size) 46 | assertEquals(PhraseLocation(13, 53), result.first()) 47 | } 48 | 49 | @Test 50 | fun `Returns location of full URL in longer phrase`() { 51 | val testCode = """ 52 | val description = "more you can read [here](https://www.example.com)" 53 | 54 | 55 | """.trimIndent() 56 | 57 | val result = LinkLocator.locate(testCode) 58 | 59 | assertEquals(1, result.size) 60 | assertEquals(PhraseLocation(44, 67), result.first()) 61 | } 62 | 63 | @Test 64 | fun `Not returns location of malformed URL link`() { 65 | val testCode = "// This is a https:/www.example.com/test?a=b&c=d%3A# link" 66 | 67 | val result = LinkLocator.locate(testCode) 68 | 69 | assertEquals(0, result.size) 70 | } 71 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/MarkLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.assertEquals 6 | import kotlin.test.Test 7 | 8 | internal class MarkLocatorTest { 9 | @Test 10 | fun `Returns location of mark characters`() { 11 | val testCode = """ 12 | ( = { + - | ] & > 13 | """.trimIndent() 14 | 15 | val result = MarkLocator.locate(testCode) 16 | 17 | assertEquals(9, result.size) 18 | assertEquals(PhraseLocation(0, 1), result[0]) 19 | assertEquals(PhraseLocation(2, 3), result[1]) 20 | assertEquals(PhraseLocation(4, 5), result[2]) 21 | assertEquals(PhraseLocation(6, 7), result[3]) 22 | assertEquals(PhraseLocation(8, 9), result[4]) 23 | assertEquals(PhraseLocation(10, 11), result[5]) 24 | assertEquals(PhraseLocation(12, 13), result[6]) 25 | assertEquals(PhraseLocation(14, 15), result[7]) 26 | assertEquals(PhraseLocation(16, 17), result[8]) 27 | } 28 | 29 | @Test 30 | fun `Returns multiple locations of the same mark`() { 31 | val testCode = """ 32 | , ); ), 33 | """.trimIndent() 34 | 35 | val result = MarkLocator.locate(testCode) 36 | 37 | assertEquals(2, result.size) 38 | assertEquals(PhraseLocation(2, 3), result[0]) 39 | assertEquals(PhraseLocation(5, 6), result[1]) 40 | } 41 | 42 | @Test 43 | fun `Returns locations of the mark next to each other`() { 44 | val testCode = """ 45 | })& 46 | """.trimIndent() 47 | 48 | val result = MarkLocator.locate(testCode) 49 | 50 | assertEquals(3, result.size) 51 | assertEquals(PhraseLocation(0, 1), result[0]) 52 | assertEquals(PhraseLocation(1, 2), result[1]) 53 | assertEquals(PhraseLocation(2, 3), result[2]) 54 | } 55 | 56 | @Test 57 | fun `Returns locations of the mark next between tokens`() { 58 | val testCode = """ 59 | {(class)}, 60 | """.trimIndent() 61 | 62 | val result = MarkLocator.locate(testCode) 63 | 64 | assertEquals(4, result.size) 65 | assertEquals(PhraseLocation(0, 1), result[0]) 66 | assertEquals(PhraseLocation(1, 2), result[1]) 67 | assertEquals(PhraseLocation(7, 8), result[2]) 68 | assertEquals(PhraseLocation(8, 9), result[3]) 69 | } 70 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/MultilineCommentLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class MultilineCommentLocatorTest { 9 | 10 | @Test 11 | fun `Returns location of one-line comment`() { 12 | val testCode = """ 13 | /* Comment */ 14 | /*** Longer comment **/ 15 | '''Different comment''' 16 | "ABCD" 17 | // ABCD 18 | /// ABCD 19 | """.trimIndent() 20 | 21 | val result = MultilineCommentLocator.locate(testCode) 22 | 23 | assertEquals(2, result.size) 24 | assertEquals(PhraseLocation(0, 13), result[0]) 25 | assertEquals(PhraseLocation(14, 37), result[1]) 26 | } 27 | 28 | @Test 29 | fun `Returns location of multi-line comment`() { 30 | val testCode = """ 31 | /*** 32 | Comment 33 | */ 34 | """.trimIndent() 35 | 36 | val result = MultilineCommentLocator.locate(testCode) 37 | 38 | assertEquals(1, result.size) 39 | assertEquals(PhraseLocation(0, 16), result[0]) 40 | } 41 | 42 | @Test 43 | fun `Returns full location of complex comment`() { 44 | val testCode = complexComment.trimIndent() 45 | 46 | val result = MultilineCommentLocator.locate(testCode) 47 | 48 | assertEquals(1, result.size) 49 | assertEquals(PhraseLocation(0, 8747), result[0]) 50 | } 51 | } 52 | 53 | private val complexComment = 54 | """ 55 | /** 56 | * {@code Activity} which displays a fullscreen Flutter UI. 57 | * 58 | *

{@code FlutterActivity} is the simplest and most direct way to integrate Flutter within an 59 | * Android app. 60 | * 61 | *

FlutterActivity responsibilities 62 | * 63 | *

{@code FlutterActivity} maintains the following responsibilities: 64 | * 65 | *

    66 | *
  • Displays an Android launch screen. 67 | *
  • Displays a Flutter splash screen. 68 | *
  • Configures the status bar appearance. 69 | *
  • Chooses the Dart execution app bundle path, entrypoint and entrypoint arguments. 70 | *
  • Chooses Flutter's initial route. 71 | *
  • Renders {@code Activity} transparently, if desired. 72 | *
  • Offers hooks for subclasses to provide and configure a {@link 73 | * io.flutter.embedding.engine.FlutterEngine}. 74 | *
  • Save and restore instance state, see {@code #shouldRestoreAndSaveState()}; 75 | *
76 | * 77 | *

Dart entrypoint, entrypoint arguments, initial route, and app bundle path 78 | * 79 | *

The Dart entrypoint executed within this {@code Activity} is "main()" by default. To change 80 | * the entrypoint that a {@code FlutterActivity} executes, subclass {@code FlutterActivity} and 81 | * override {@link #getDartEntrypointFunctionName()}. For non-main Dart entrypoints to not be 82 | * tree-shaken away, you need to annotate those functions with {@code @pragma('vm:entry-point')} in 83 | * Dart. 84 | * 85 | *

The Dart entrypoint arguments will be passed as a list of string to Dart's entrypoint 86 | * function. It can be passed using a {@link NewEngineIntentBuilder} via {@link 87 | * NewEngineIntentBuilder#dartEntrypointArgs}. 88 | * 89 | *

The Flutter route that is initially loaded within this {@code Activity} is "/". The initial 90 | * route may be specified explicitly by passing the name of the route as a {@code String} in {@link 91 | * FlutterActivityLaunchConfigs#EXTRA_INITIAL_ROUTE}, e.g., "my/deep/link". 92 | * 93 | *

The initial route can each be controlled using a {@link NewEngineIntentBuilder} via {@link 94 | * NewEngineIntentBuilder#initialRoute}. 95 | * 96 | *

The app bundle path, Dart entrypoint, Dart entrypoint arguments, and initial route can also be 97 | * controlled in a subclass of {@code FlutterActivity} by overriding their respective methods: 98 | * 99 | *

    100 | *
  • {@link #getAppBundlePath()} 101 | *
  • {@link #getDartEntrypointFunctionName()} 102 | *
  • {@link #getDartEntrypointArgs()} 103 | *
  • {@link #getInitialRoute()} 104 | *
105 | * 106 | *

The Dart entrypoint and app bundle path are not supported as {@code Intent} parameters since 107 | * your Dart library entrypoints are your private APIs and Intents are invocable by other processes. 108 | * 109 | *

Using a cached FlutterEngine 110 | * 111 | *

{@code FlutterActivity} can be used with a cached {@link 112 | * io.flutter.embedding.engine.FlutterEngine} instead of creating a new one. Use {@link 113 | * #withCachedEngine(String)} to build a {@code FlutterActivity} {@code Intent} that is configured 114 | * to use an existing, cached {@link io.flutter.embedding.engine.FlutterEngine}. {@link 115 | * io.flutter.embedding.engine.FlutterEngineCache} is the cache that is used to obtain a given 116 | * cached {@link io.flutter.embedding.engine.FlutterEngine}. You must create and put a {@link 117 | * io.flutter.embedding.engine.FlutterEngine} into the {@link 118 | * io.flutter.embedding.engine.FlutterEngineCache} yourself before using the {@link 119 | * #withCachedEngine(String)} builder. An {@code IllegalStateException} will be thrown if a cached 120 | * engine is requested but does not exist in the cache. 121 | * 122 | *

When using a cached {@link io.flutter.embedding.engine.FlutterEngine}, that {@link 123 | * io.flutter.embedding.engine.FlutterEngine} should already be executing Dart code, which means 124 | * that the Dart entrypoint and initial route have already been defined. Therefore, {@link 125 | * CachedEngineIntentBuilder} does not offer configuration of these properties. 126 | * 127 | *

It is generally recommended to use a cached {@link io.flutter.embedding.engine.FlutterEngine} 128 | * to avoid a momentary delay when initializing a new {@link 129 | * io.flutter.embedding.engine.FlutterEngine}. The two exceptions to using a cached {@link 130 | * FlutterEngine} are: 131 | * 132 | *

    133 | *
  • When {@code FlutterActivity} is the first {@code Activity} displayed by the app, because 134 | * pre-warming a {@link io.flutter.embedding.engine.FlutterEngine} would have no impact in 135 | * this situation. 136 | *
  • When you are unsure when/if you will need to display a Flutter experience. 137 | *
138 | * 139 | *

See https://flutter.dev/docs/development/add-to-app/performance for additional performance 140 | * explorations on engine loading. 141 | * 142 | *

The following illustrates how to pre-warm and cache a {@link 143 | * io.flutter.embedding.engine.FlutterEngine}: 144 | * 145 | *

{@code
146 |          * // Create and pre-warm a FlutterEngine.
147 |          * FlutterEngine flutterEngine = new FlutterEngine(context);
148 |          * flutterEngine.getDartExecutor().executeDartEntrypoint(DartEntrypoint.createDefault());
149 |          *
150 |          * // Cache the pre-warmed FlutterEngine in the FlutterEngineCache.
151 |          * FlutterEngineCache.getInstance().put("my_engine", flutterEngine);
152 |          * }
153 | * 154 | *

Alternatives to FlutterActivity 155 | * 156 | *

If Flutter is needed in a location that cannot use an {@code Activity}, consider using a 157 | * {@link FlutterFragment}. Using a {@link FlutterFragment} requires forwarding some calls from an 158 | * {@code Activity} to the {@link FlutterFragment}. 159 | * 160 | *

If Flutter is needed in a location that can only use a {@code View}, consider using a {@link 161 | * FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an {@code 162 | * Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a {@code Fragment}. 163 | * 164 | *

Launch Screen and Splash Screen 165 | * 166 | *

{@code FlutterActivity} supports the display of an Android "launch screen" as well as a 167 | * Flutter-specific "splash screen". The launch screen is displayed while the Android application 168 | * loads. It is only applicable if {@code FlutterActivity} is the first {@code Activity} displayed 169 | * upon loading the app. After the launch screen passes, a splash screen is optionally displayed. 170 | * The splash screen is displayed for as long as it takes Flutter to initialize and render its first 171 | * frame. 172 | * 173 | *

Use Android themes to display a launch screen. Create two themes: a launch theme and a normal 174 | * theme. In the launch theme, set {@code windowBackground} to the desired {@code Drawable} for the 175 | * launch screen. In the normal theme, set {@code windowBackground} to any desired background color 176 | * that should normally appear behind your Flutter content. In most cases this background color will 177 | * never be seen, but for possible transition edge cases it is a good idea to explicitly replace the 178 | * launch screen window background with a neutral color. 179 | * 180 | *

Do not change aspects of system chrome between a launch theme and normal theme. Either define 181 | * both themes to be fullscreen or not, and define both themes to display the same status bar and 182 | * navigation bar settings. To adjust system chrome once the Flutter app renders, use platform 183 | * channels to instruct Android to do so at the appropriate time. This will avoid any jarring visual 184 | * changes during app startup. 185 | * 186 | *

In the AndroidManifest.xml, set the theme of {@code FlutterActivity} to the defined launch 187 | * theme. In the metadata section for {@code FlutterActivity}, defined the following reference to 188 | * your normal theme: 189 | * 190 | *

{@code } 192 | * 193 | *

With themes defined, and AndroidManifest.xml updated, Flutter displays the specified launch 194 | * screen until the Android application is initialized. 195 | * 196 | *

Flutter also requires initialization time. To specify a splash screen for Flutter 197 | * initialization, subclass {@code FlutterActivity} and override {@link #provideSplashScreen()}. See 198 | * {@link SplashScreen} for details on implementing a splash screen. 199 | * 200 | *

Flutter ships with a splash screen that automatically displays the exact same {@code 201 | * windowBackground} as the launch theme discussed previously. To use that splash screen, include 202 | * the following metadata in AndroidManifest.xml for this {@code FlutterActivity}: 203 | * 204 | *

{@code } 206 | * 207 | *

Alternative Activity {@link FlutterFragmentActivity} is also available, which 208 | * is similar to {@code FlutterActivity} but it extends {@code FragmentActivity}. You should use 209 | * {@code FlutterActivity}, if possible, but if you need a {@code FragmentActivity} then you should 210 | * use {@link FlutterFragmentActivity}. 211 | */ 212 | // A number of methods in this class have the same implementation as FlutterFragmentActivity. These 213 | // methods are duplicated for readability purposes. Be sure to replicate any change in this class in 214 | // FlutterFragmentActivity, too. 215 | """.trimIndent() -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/NumericLiteralLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.assertEquals 6 | import kotlin.test.Test 7 | 8 | internal class NumericLiteralLocatorTest { 9 | 10 | @Test 11 | fun `Returns location of single digit`() { 12 | val testCode = "val one = 1" 13 | 14 | val result = NumericLiteralLocator.locate(testCode) 15 | 16 | assertEquals(1, result.size) 17 | assertEquals(PhraseLocation(10, 11), result.first()) 18 | } 19 | 20 | @Test 21 | fun `Returns location of many digits number`() { 22 | val testCode = "val one = 123" 23 | 24 | val result = NumericLiteralLocator.locate(testCode) 25 | 26 | assertEquals(1, result.size) 27 | assertEquals(PhraseLocation(10, 13), result.first()) 28 | } 29 | 30 | @Test 31 | fun `Returns location of decimal number`() { 32 | val testCode = "val one = 123.456" 33 | 34 | val result = NumericLiteralLocator.locate(testCode) 35 | 36 | assertEquals(1, result.size) 37 | assertEquals(PhraseLocation(10, 17), result.first()) 38 | } 39 | 40 | // Detailed description of underscore literal 41 | // https://www.educative.io/answers/what-are-the-underscores-in-numeric-literals-in-java 42 | @Test 43 | fun `Returns location of proper underscore number`() { 44 | val testCode = "val one = 1_23__456".trimIndent() 45 | 46 | val result = NumericLiteralLocator.locate(testCode) 47 | 48 | assertEquals(1, result.size) 49 | assertEquals(PhraseLocation(10, 19), result.first()) 50 | } 51 | 52 | @Test 53 | fun `Returns location of floating number`() { 54 | val testCode = """ 55 | val one = 123.456f 56 | val two = .2f 57 | val three = 12F 58 | """.trimIndent() 59 | 60 | val result = NumericLiteralLocator.locate(testCode) 61 | 62 | assertEquals(3, result.size) 63 | assertEquals(PhraseLocation(10, 18), result[0]) 64 | assertEquals(PhraseLocation(29, 32), result[1]) 65 | assertEquals(PhraseLocation(45, 48), result[2]) 66 | } 67 | 68 | @Test 69 | fun `Returns location of number with letter`() { 70 | val testCode = """ 71 | val one = 123456L 72 | val two = 2L 73 | val three = 12l 74 | val floating = 1.2f 75 | val longFloat = 1.22fff 76 | val zero = zero100 77 | 12e + 13u 78 | """.trimIndent() 79 | 80 | val result = NumericLiteralLocator.locate(testCode) 81 | 82 | assertEquals(7, result.size) 83 | assertEquals(PhraseLocation(10, 17), result[0]) 84 | assertEquals(PhraseLocation(28, 30), result[1]) 85 | assertEquals(PhraseLocation(43, 46), result[2]) 86 | assertEquals(PhraseLocation(62, 66), result[3]) 87 | assertEquals(PhraseLocation(83, 88), result[4]) 88 | assertEquals(PhraseLocation(110, 113), result[5]) 89 | assertEquals(PhraseLocation(116, 119), result[6]) 90 | } 91 | 92 | @Test 93 | fun `Returns location of hex number`() { 94 | val testCode = """ 95 | 0x 96 | 0x0 97 | 0xabcdef 98 | 0x1ab2cdef 99 | 0xabcdefg 100 | 0xabgcdef 101 | """.trimIndent() 102 | 103 | val result = NumericLiteralLocator.locate(testCode) 104 | 105 | assertEquals(6, result.size) 106 | assertEquals(PhraseLocation(0, 2), result[0]) 107 | assertEquals(PhraseLocation(3, 6), result[1]) 108 | assertEquals(PhraseLocation(7, 15), result[2]) 109 | assertEquals(PhraseLocation(16, 26), result[3]) 110 | assertEquals(PhraseLocation(27, 35), result[4]) 111 | assertEquals(PhraseLocation(37, 41), result[5]) 112 | } 113 | 114 | @Test 115 | fun `Returns location of binary number`() { 116 | val testCode = """ 117 | 0b 118 | 0b0 119 | 0b1 120 | 0b001 121 | 0b9001 122 | """.trimIndent() 123 | 124 | val result = NumericLiteralLocator.locate(testCode) 125 | 126 | assertEquals(5, result.size) 127 | assertEquals(PhraseLocation(0, 2), result[0]) 128 | assertEquals(PhraseLocation(3, 6), result[1]) 129 | assertEquals(PhraseLocation(7, 10), result[2]) 130 | assertEquals(PhraseLocation(11, 16), result[3]) 131 | assertEquals(PhraseLocation(17, 19), result[4]) 132 | } 133 | 134 | @Test 135 | fun `Not returns location of the literal characters only`() { 136 | val testCode = """ 137 | .. 138 | LL 139 | xx 140 | """.trimIndent() 141 | 142 | val result = NumericLiteralLocator.locate(testCode) 143 | 144 | assertEquals(0, result.size) 145 | } 146 | 147 | @Test 148 | fun `Returns whole location of the all scientific notations`() { 149 | val testCode = """ 150 | 1e+10 151 | 100e100 152 | 0.11E-10 153 | 123.456E+10 154 | 100_00E10 155 | 12e+1000 156 | """.trimIndent() 157 | 158 | val result = NumericLiteralLocator.locate(testCode) 159 | 160 | assertEquals(6, result.size) 161 | assertEquals(PhraseLocation(0, 5), result[0]) 162 | assertEquals(PhraseLocation(6, 13), result[1]) 163 | assertEquals(PhraseLocation(14, 22), result[2]) 164 | assertEquals(PhraseLocation(23, 34), result[3]) 165 | assertEquals(PhraseLocation(35, 44), result[4]) 166 | assertEquals(PhraseLocation(45, 53), result[5]) 167 | } 168 | 169 | @Test 170 | fun `Returns only proper length number with letter`() { 171 | val testCode = """ 172 | 12e+1000 173 | 12.dp 174 | 12f.d 175 | -2b 176 | 12sss 177 | 0b10000 178 | 13.22f 179 | """.trimIndent() 180 | 181 | val result = NumericLiteralLocator.locate(testCode) 182 | 183 | assertEquals(7, result.size) 184 | assertEquals(PhraseLocation(0, 8), result[0]) 185 | assertEquals(PhraseLocation(9, 12), result[1]) 186 | assertEquals(PhraseLocation(15, 19), result[2]) 187 | assertEquals(PhraseLocation(21, 23), result[3]) 188 | assertEquals(PhraseLocation(25, 27), result[4]) 189 | assertEquals(PhraseLocation(31, 38), result[5]) 190 | assertEquals(PhraseLocation(39, 45), result[6]) 191 | } 192 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/PunctuationLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class PunctuationLocatorTest { 9 | 10 | @Test 11 | fun `Returns location of punctuation characters`() { 12 | val testCode = """ 13 | , . : ; 14 | """.trimIndent() 15 | 16 | val result = PunctuationLocator.locate(testCode) 17 | 18 | assertEquals(4, result.size) 19 | assertEquals(PhraseLocation(0, 1), result[0]) 20 | assertEquals(PhraseLocation(2, 3), result[1]) 21 | assertEquals(PhraseLocation(5, 6), result[2]) 22 | assertEquals(PhraseLocation(8, 9), result[3]) 23 | } 24 | 25 | @Test 26 | fun `Returns multiple locations of the same punctuation`() { 27 | val testCode = """ 28 | , ); ), 29 | """.trimIndent() 30 | 31 | val result = PunctuationLocator.locate(testCode) 32 | 33 | assertEquals(3, result.size) 34 | assertEquals(PhraseLocation(0, 1), result[0]) 35 | assertEquals(PhraseLocation(6, 7), result[1]) 36 | assertEquals(PhraseLocation(3, 4), result[2]) 37 | } 38 | 39 | @Test 40 | fun `Returns locations of the punctuation next to each other`() { 41 | val testCode = """ 42 | ,,, 43 | """.trimIndent() 44 | 45 | val result = PunctuationLocator.locate(testCode) 46 | 47 | assertEquals(3, result.size) 48 | assertEquals(PhraseLocation(0, 1), result[0]) 49 | assertEquals(PhraseLocation(1, 2), result[1]) 50 | assertEquals(PhraseLocation(2, 3), result[2]) 51 | } 52 | 53 | @Test 54 | fun `Returns locations of the punctuation next between tokens`() { 55 | val testCode = """ 56 | ,,,class, 57 | """.trimIndent() 58 | 59 | val result = PunctuationLocator.locate(testCode) 60 | 61 | assertEquals(4, result.size) 62 | assertEquals(PhraseLocation(0, 1), result[0]) 63 | assertEquals(PhraseLocation(1, 2), result[1]) 64 | assertEquals(PhraseLocation(2, 3), result[2]) 65 | assertEquals(PhraseLocation(8, 9), result[3]) 66 | } 67 | 68 | @Test 69 | fun `Returns locations of the punctuation next to strings`() { 70 | val testCode = """ 71 | "a"; 72 | "b"; 73 | """.trimIndent() 74 | 75 | val result = PunctuationLocator.locate(testCode) 76 | 77 | assertEquals(2, result.size) 78 | assertEquals(PhraseLocation(3, 4), result[0]) 79 | assertEquals(PhraseLocation(8, 9), result[1]) 80 | } 81 | 82 | @Test 83 | fun `Not returns locations of the non punctuation characters`() { 84 | val testCode = """ 85 | /** a */ 86 | "b"; 87 | """.trimIndent() 88 | 89 | val result = PunctuationLocator.locate(testCode) 90 | 91 | assertEquals(1, result.size) 92 | assertEquals(PhraseLocation(12, 13), result[0]) 93 | } 94 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/StringLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.internal.get 4 | import dev.snipme.highlights.model.PhraseLocation 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | internal class StringLocatorTest { 9 | @Test 10 | fun `Returns location of full string`() { 11 | val testCode = """ 12 | "a" 13 | """.trimIndent() 14 | 15 | val result = StringLocator.locate(testCode) 16 | 17 | assertEquals(1, result.size) 18 | assertEquals(PhraseLocation(0, 3), result[0]) 19 | } 20 | 21 | @Test 22 | fun `Returns location of character phrase`() { 23 | val testCode = """ 24 | 'a' 25 | """.trimIndent() 26 | 27 | val result = StringLocator.locate(testCode) 28 | 29 | assertEquals(1, result.size) 30 | assertEquals(PhraseLocation(0, 3), result[0]) 31 | } 32 | 33 | @Test 34 | fun `Returns location of escaped character phrase`() { 35 | val testCode = """ 36 | "\\" 37 | """.trimIndent() 38 | 39 | val result = StringLocator.locate(testCode) 40 | 41 | assertEquals(1, result.size) 42 | assertEquals(PhraseLocation(0, 4), result[0]) 43 | } 44 | 45 | @Test 46 | fun `Returns location of multiple same strings`() { 47 | val testCode = """ 48 | val a = "a" 49 | val b = "a" 50 | """.trimIndent() 51 | 52 | val result = StringLocator.locate(testCode) 53 | 54 | assertEquals(2, result.size) 55 | assertEquals(PhraseLocation(8, 11), result[0]) 56 | assertEquals(PhraseLocation(20, 23), result[1]) 57 | } 58 | 59 | @Test 60 | fun `Returns strings from simplest to most complex`() { 61 | val testCode = """ 62 | val b = "a" 63 | val a = 'a' 64 | """.trimIndent() 65 | 66 | val result = StringLocator.locate(testCode) 67 | 68 | assertEquals(2, result.size) 69 | assertEquals(PhraseLocation(20, 23), result[0]) 70 | assertEquals(PhraseLocation(8, 11), result[1]) 71 | } 72 | 73 | @Test 74 | fun `No returns location of unclosed string phrase`() { 75 | val testCode = """ 76 | val b = "a 77 | val a = 'a 78 | """.trimIndent() 79 | 80 | val result = StringLocator.locate(testCode) 81 | 82 | assertEquals(0, result.size) 83 | } 84 | 85 | @Test 86 | fun `Returns location only of closed string phrase`() { 87 | val testCode = """ 88 | val b = 'a 89 | val a = "a" 90 | """.trimIndent() 91 | 92 | val result = StringLocator.locate(testCode) 93 | 94 | assertEquals(1, result.size) 95 | assertEquals(PhraseLocation(19, 22), result[0]) 96 | } 97 | 98 | @Test 99 | fun `NOT returns location of escaped string phrase`() { 100 | val testCode = """ 101 | val b = "a\"" 102 | val a = 'a\'' 103 | """.trimIndent() 104 | 105 | val result = StringLocator.locate(testCode) 106 | 107 | assertEquals(2, result.size) 108 | assertEquals(PhraseLocation(8, 12), result[1]) 109 | assertEquals(PhraseLocation(22, 26), result[0]) 110 | } 111 | } -------------------------------------------------------------------------------- /src/commonTest/kotlin/dev/snipme/highlights/internal/locator/TokenLocatorTest.kt: -------------------------------------------------------------------------------- 1 | package dev.snipme.highlights.internal.locator 2 | 3 | import dev.snipme.highlights.model.PhraseLocation 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | internal class TokenLocatorTest { 8 | 9 | @Test 10 | fun `Returns location of all tokens`() { 11 | val testCode = """ 12 | val pair = ('a', 12, true) 13 | """.trimIndent() 14 | 15 | val result = TokenLocator.locate(testCode) 16 | 17 | assertEquals(5, result.size) 18 | assertEquals(PhraseLocation(0, 3), result[0]) 19 | assertEquals(PhraseLocation(4, 8), result[1]) 20 | assertEquals(PhraseLocation(12, 15), result[2]) 21 | assertEquals(PhraseLocation(17, 19), result[3]) 22 | assertEquals(PhraseLocation(21, 25), result[4]) 23 | } 24 | 25 | @Test 26 | fun `Returns location of all tokens in right order`() { 27 | val testCode = """ 28 | zzz yy xx 29 | """.trimIndent() 30 | 31 | val result = TokenLocator.locate(testCode) 32 | 33 | assertEquals(3, result.size) 34 | assertEquals(PhraseLocation(0, 3), result[0]) 35 | assertEquals(PhraseLocation(4, 6), result[1]) 36 | assertEquals(PhraseLocation(7, 9), result[2]) 37 | } 38 | } --------------------------------------------------------------------------------