├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.gradle.kts
├── buildSrc
├── .gitignore
├── build.gradle.kts
├── settings.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── Setup.kt
├── demo
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
├── release
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── xyz
│ │ └── wingio
│ │ └── syntakts
│ │ └── demo
│ │ └── ExampleInstrumentedTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── xyz
│ │ └── wingio
│ │ └── syntakts
│ │ └── demo
│ │ ├── AndroidTestActivity.kt
│ │ ├── MainActivity.kt
│ │ ├── TestSyntakts.kt
│ │ └── ui
│ │ └── theme
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── font
│ └── jetbrains_mono.ttf
│ ├── layout
│ └── activity_android_test.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── images
└── logo.png
├── settings.gradle.kts
├── syntakts-android
├── .gitignore
├── api
│ └── syntakts-android.api
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ └── xyz
│ └── wingio
│ └── syntakts
│ └── android
│ ├── ClickableMovementMethod.kt
│ ├── Syntakts.kt
│ ├── markdown
│ └── Markdown.kt
│ ├── spans
│ ├── AnnotationSpan.kt
│ ├── ClickableSpan.kt
│ └── SyntaktsStyleSpan.kt
│ ├── style
│ ├── AndroidFontResolver.kt
│ ├── Color.kt
│ └── SpannableStyledTextBuilder.kt
│ └── util
│ └── DimenUtil.kt
├── syntakts-compose-material3
├── .gitignore
├── api
│ ├── android
│ │ └── syntakts-compose-material3.api
│ └── jvm
│ │ └── syntakts-compose-material3.api
├── build.gradle.kts
└── src
│ └── commonMain
│ └── kotlin
│ └── xyz
│ └── wingio
│ └── syntakts
│ └── compose
│ └── material3
│ └── clickable
│ └── ClickableText.kt
├── syntakts-compose
├── .gitignore
├── api
│ ├── android
│ │ └── syntakts-compose.api
│ └── jvm
│ │ └── syntakts-compose.api
├── build.gradle.kts
└── src
│ └── commonMain
│ └── kotlin
│ └── xyz
│ └── wingio
│ └── syntakts
│ └── compose
│ ├── Syntakts.kt
│ ├── clickable
│ ├── ClickHandlerStore.kt
│ └── ClickableText.kt
│ └── style
│ ├── AnnotatedStyledTextBuilder.kt
│ ├── Color.kt
│ ├── ComposeFontResolver.kt
│ ├── FontStyle.kt
│ ├── FontWeight.kt
│ ├── TextDecoration.kt
│ └── TextUnit.kt
└── syntakts-core
├── .gitignore
├── api
├── android
│ └── syntakts-core.api
└── jvm
│ └── syntakts-core.api
├── build.gradle.kts
└── src
├── androidMain
└── kotlin
│ └── xyz
│ └── wingio
│ └── syntakts
│ └── util
│ └── Logger.android.kt
├── commonMain
└── kotlin
│ └── xyz
│ └── wingio
│ └── syntakts
│ ├── ParseException.kt
│ ├── Syntakts.kt
│ ├── markdown
│ └── Markdown.kt
│ ├── node
│ ├── ClickableNode.kt
│ ├── Node.kt
│ ├── NodeDsl.kt
│ ├── StyleNode.kt
│ └── TextNode.kt
│ ├── parser
│ ├── ParseSpec.kt
│ └── Rule.kt
│ ├── style
│ ├── Color.kt
│ ├── FontResolver.kt
│ ├── FontStyle.kt
│ ├── FontWeight.kt
│ ├── Style.kt
│ ├── StyledTextBuilder.kt
│ ├── TextDecoration.kt
│ └── TextUnit.kt
│ └── util
│ ├── Logger.kt
│ ├── MarkdownUtils.kt
│ ├── Stack.kt
│ ├── SynchronizedCache.kt
│ └── Utils.kt
├── commonTest
└── kotlin
│ └── SyntaktsTest.kt
└── jvmMain
└── kotlin
└── xyz
└── wingio
└── syntakts
└── util
└── Logger.jvm.kt
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build artifacts
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | paths-ignore:
8 | - '**.md'
9 | - '.idea/*'
10 | - 'LICENSE'
11 | pull_request:
12 | branches:
13 | - '*'
14 | paths-ignore:
15 | - '**.md'
16 | - '.idea/*'
17 | - 'LICENSE'
18 | workflow_dispatch:
19 |
20 | jobs:
21 | build:
22 | timeout-minutes: 60
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v2
26 |
27 | - name: Setup JDK 17
28 | uses: actions/setup-java@v2
29 | with:
30 | java-version: 17
31 | distribution: 'temurin'
32 | cache: 'gradle'
33 |
34 | - name: Setup Android SDK
35 | uses: android-actions/setup-android@v2.0.10
36 |
37 | - name: Setup Gradle
38 | uses: gradle/gradle-build-action@v2.4.2
39 |
40 | - name: Build artifacts
41 | run: |
42 | chmod +x gradlew
43 | ./gradlew publishToMavenLocal --no-daemon --stacktrace
44 |
45 | - name: Upload artifacts
46 | uses: actions/upload-artifact@v3
47 | with:
48 | name: artifacts
49 | path: ~/.m2/repository/
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | concurrency:
4 | group: "release"
5 | cancel-in-progress: true
6 |
7 | permissions:
8 | contents: write
9 |
10 | on:
11 | workflow_dispatch:
12 | inputs:
13 | version:
14 | required: true
15 | description: Release version
16 | default: "1.0.0"
17 |
18 | jobs:
19 | release:
20 | timeout-minutes: 60
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v2
24 |
25 | - name: Test version
26 | id: version
27 | run: |
28 | version=${{github.event.inputs.version}}
29 | if git show-ref --tags --quiet --verify -- "refs/tags/$version" >/dev/null; then
30 | echo "Git tag $version already exists, failing to publish";
31 | exit 1
32 | else
33 | echo "::set-output name=release_tag::$version"
34 | fi
35 |
36 | - name: Setup JDK 17
37 | uses: actions/setup-java@v2
38 | with:
39 | java-version: 17
40 | distribution: 'temurin'
41 | cache: 'gradle'
42 |
43 | - name: Setup Android SDK
44 | uses: android-actions/setup-android@v2.0.10
45 |
46 | - name: Setup Gradle
47 | uses: gradle/gradle-build-action@v2.4.2
48 |
49 | - name: Build and Maven publish
50 | env:
51 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
52 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
53 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
54 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}
55 | LIBRARY_VERSION: ${{ github.event.inputs.version }}
56 | run: |
57 | chmod +x gradlew
58 | ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache --no-daemon --stacktrace
59 |
60 | - name: Create Release
61 | uses: softprops/action-gh-release@v1
62 | with:
63 | tag_name: ${{ steps.version.outputs.release_tag }}
64 | generate_release_notes: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | captures/
13 | .externalNativeBuild/
14 | .cxx/
15 | *.apk
16 | output.json
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 | misc.xml
22 | deploymentTargetDropDown.xml
23 | render.experimental.xml
24 |
25 | # Keystore files
26 | *.jks
27 | *.keystore
28 |
29 | # Google Services (e.g. APIs or Firebase)
30 | google-services.json
31 |
32 | # Android Profiling
33 | *.hprof
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Wing
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 | ### Simple to use text parser and syntax highlighter for Kotlin Multiplatform
5 | [](https://central.sonatype.com/namespace/xyz.wingio.syntakts)
6 | [](https://github.com/wingio/syntakts/stargazers)
7 | [](https://github.com/sponsors/wingio)
8 |
9 | ---
10 |
11 |
12 | ## Setup
13 | ```kotlin
14 | implementation("xyz.wingio.syntakts:syntakts-core:$syntaktsVersion")
15 | // implementation("xyz.wingio.syntakts:syntakts-compose:$syntaktsVersion")
16 | // implementation("xyz.wingio.syntakts:syntakts-compose-material3:$syntaktsVersion")
17 | ```
18 |
19 | ## Use
20 | Syntakts can be set up through a simple DSL:
21 | ```kotlin
22 | val mySyntakts = syntakts {
23 | rule("@([A-z])") { result, context ->
24 | append(result.groupValues[1]) {
25 | color = Color.Yellow
26 | }
27 | }
28 | }
29 | ```
30 |
31 | We also provide MarkdownSyntakts and BasicMarkdownSyntakts, which has some default markdown rules
32 |
33 | ### Context
34 | Syntakts allows you to pass any class as context, this can be used to pass additional information for rendering.
35 | If you don't need to use context you can set it to Unit
36 |
37 | Example:
38 | ```kotlin
39 | data class Context(
40 | val userMap = mapOf("1234" to "Wing")
41 | )
42 |
43 | val mySytankts = syntakts {
44 | rule("<@([0-9]+)>") { result, context ->
45 | val username = context.userMap[result.groupValues[1]] ?: "Unknown"
46 | append("@$username") {
47 | color = Color.Yellow
48 | }
49 | }
50 | }
51 | ```
52 |
53 | ## Displaying
54 |
55 | ### Compose
56 | Artifact: `syntakts-compose`
57 |
58 | Syntakts uses AnnotatedStrings in order to display rendered text in Compose
59 |
60 | > [!NOTE]
61 | >
62 | > When creating a Syntakts instance in a composable we reccommend replacing `syntakts {}` with `rememberSyntakts {}`
63 |
64 | Example:
65 | ```kotlin
66 | @Composable
67 | fun SomeScreen() {
68 | val syntakts = rememberSyntakts { /* */ }
69 |
70 | Text(
71 | text = syntakts.rememberRendered("some input")
72 | )
73 | }
74 | ```
75 |
76 | ### Android
77 | Artifact: `syntakts-android`
78 |
79 | Syntakts uses SpannableStrings in order to display rendered text on Android
80 |
81 | Example:
82 | ```kotlin
83 | val syntakts = syntakts { /* */ }
84 |
85 | findViewById(R.id.my_text_view).render("some input", syntakts)
86 | ```
87 |
88 | #### Clickable
89 | Syntakts for Compose includes a ClickableText component that is neccessary in order to handle clickable text. The `syntakts-compose-material3` includes this component as well but adds support for Material 3 theming
90 |
91 | Syntakts for Android requires that the TextView have its movementMethod set to our ClickableMovementMethod
92 |
93 | ## Attribution
94 | Syntakts was heavily inspired by [SimpleAST](https://github.com/discord/SimpleAST), an unfortunately abandoned library that was once used in Discords android app
95 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | google()
5 | gradlePluginPortal()
6 | maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev" )
7 | }
8 |
9 | dependencies {
10 | classpath(libs.plugin.maven)
11 | classpath(libs.plugin.multiplatform.compose)
12 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0")
13 | }
14 | }
15 |
16 | allprojects {
17 | group = "xyz.wingio.syntakts"
18 | version = "1.0.0-SNAPSHOT"
19 | }
--------------------------------------------------------------------------------
/buildSrc/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /.gradle
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | google()
7 | mavenCentral()
8 | gradlePluginPortal()
9 | }
10 |
11 | dependencies {
12 | implementation(libs.plugin.android)
13 | implementation(libs.plugin.kotlin)
14 | implementation(libs.plugin.maven)
15 | }
--------------------------------------------------------------------------------
/buildSrc/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | versionCatalogs {
3 | create("libs") {
4 | from(files("../gradle/libs.versions.toml"))
5 | }
6 | }
7 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Setup.kt:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.BaseExtension
2 | import com.android.build.gradle.LibraryExtension
3 | import com.vanniktech.maven.publish.AndroidSingleVariantLibrary
4 | import com.vanniktech.maven.publish.JavaLibrary
5 | import com.vanniktech.maven.publish.JavadocJar
6 | import com.vanniktech.maven.publish.KotlinMultiplatform
7 | import org.gradle.api.Project
8 | import org.gradle.kotlin.dsl.findByType
9 | import org.gradle.api.provider.Property
10 | import org.gradle.kotlin.dsl.task
11 | import com.vanniktech.maven.publish.MavenPublishBaseExtension
12 | import com.vanniktech.maven.publish.Platform
13 | import com.vanniktech.maven.publish.SonatypeHost
14 | import org.gradle.jvm.tasks.Jar
15 |
16 | // Lets us use `=` for assignments
17 | private fun Property.assign(value: T) = set(value)
18 |
19 | fun Project.setupAndroid(name: String) {
20 | val androidExtension: LibraryExtension = extensions.findByType()
21 | ?: error("Could not found Android library plugin applied on module $name")
22 |
23 | androidExtension.apply {
24 | namespace = "xyz.wingio.${name.replace("-", ".")}"
25 | compileSdk = 34
26 | defaultConfig {
27 | minSdk = 21
28 | }
29 | }
30 | }
31 |
32 | @Suppress("UnstableApiUsage")
33 | fun Project.setup(
34 | libName: String,
35 | moduleName: String,
36 | moduleDescription: String,
37 | androidOnly: Boolean = false
38 | ) {
39 | setupAndroid(moduleName)
40 |
41 | val mavenPublishing = extensions.findByType() ?: error("Couldn't find maven publish plugin")
42 |
43 | mavenPublishing.apply {
44 | if(androidOnly) {
45 | configure(AndroidSingleVariantLibrary("release"))
46 | } else {
47 | configure(KotlinMultiplatform(JavadocJar.Empty()))
48 | }
49 | publishToMavenCentral(SonatypeHost.S01, automaticRelease = true)
50 | signAllPublications()
51 |
52 | coordinates("xyz.wingio.syntakts", moduleName, System.getenv("LIBRARY_VERSION") ?: version.toString())
53 |
54 | pom {
55 | name = libName
56 | description = moduleDescription
57 | inceptionYear = "2023"
58 | url = "https://github.com/wingio/syntakts"
59 |
60 | licenses {
61 | license {
62 | name = "MIT License"
63 | url = "https://opensource.org/license/mit/"
64 | }
65 | }
66 | developers {
67 | developer {
68 | id = "wingio"
69 | name = "Wing"
70 | url = "https://wingio.xyz"
71 | }
72 | }
73 | scm {
74 | url = "https://github.com/wingio/syntakts"
75 | connection = "scm:git:github.com/wingio/syntakts.git"
76 | developerConnection = "scm:git:ssh://github.com/wingio/syntakts.git"
77 | }
78 | }
79 | }
80 | }
--------------------------------------------------------------------------------
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/demo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed
2 | plugins {
3 | kotlin("android")
4 | id("com.android.application")
5 | alias(libs.plugins.compose.compiler)
6 | }
7 |
8 | android {
9 | namespace = "xyz.wingio.syntakts.demo"
10 | compileSdk = 34
11 |
12 | defaultConfig {
13 | applicationId = "xyz.wingio.syntakts.demo"
14 | minSdk = 21
15 | targetSdk = 34
16 | versionCode = 1
17 | versionName = "1.0"
18 |
19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
20 | vectorDrawables {
21 | useSupportLibrary = true
22 | }
23 | }
24 |
25 | buildTypes {
26 | release {
27 | isMinifyEnabled = false
28 | proguardFiles(
29 | getDefaultProguardFile("proguard-android-optimize.txt"),
30 | "proguard-rules.pro"
31 | )
32 | }
33 | }
34 | compileOptions {
35 | sourceCompatibility = JavaVersion.VERSION_1_8
36 | targetCompatibility = JavaVersion.VERSION_1_8
37 | }
38 | kotlinOptions {
39 | jvmTarget = "1.8"
40 | }
41 | buildFeatures {
42 | compose = true
43 | }
44 | packaging {
45 | resources {
46 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
47 | }
48 | }
49 | }
50 |
51 | dependencies {
52 | implementation(libs.bundles.compose)
53 | implementation(project(":syntakts-compose-material3"))
54 | implementation(project(":syntakts-android"))
55 | implementation(libs.appcompat)
56 | implementation(libs.material)
57 | implementation(libs.constraintlayout)
58 | }
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/demo/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "xyz.wingio.syntakts.demo",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.0",
16 | "outputFile": "demo-release.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/demo/src/androidTest/java/xyz/wingio/syntakts/demo/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("xyz.wingio.syntakts.demo", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/demo/src/main/java/xyz/wingio/syntakts/demo/AndroidTestActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo
2 |
3 | import android.os.Bundle
4 | import android.widget.TextView
5 | import androidx.activity.ComponentActivity
6 | import androidx.core.content.res.ResourcesCompat
7 | import xyz.wingio.syntakts.android.render
8 | import xyz.wingio.syntakts.android.style.DefaultFontResolver
9 |
10 | class AndroidTestActivity : ComponentActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContentView(R.layout.activity_android_test)
15 |
16 | DefaultFontResolver.register(
17 | "jetbrains mono" to ResourcesCompat.getFont(this, R.font.jetbrains_mono)!!
18 | )
19 |
20 | val testString = """
21 | # Header
22 | **Bold** *Italic* __Underline__ ~~Strikethrough~~ <@1234> :heart:
23 | `println("hi")`
24 | """.trimIndent()
25 |
26 | findViewById(R.id.test_text).render(
27 | text = testString,
28 | syntakts = TestSyntakts,
29 | context = Context(this),
30 | enableClickable = true
31 | )
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/demo/src/main/java/xyz/wingio/syntakts/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.rememberScrollState
13 | import androidx.compose.foundation.text.InlineTextContent
14 | import androidx.compose.foundation.verticalScroll
15 | import androidx.compose.material.icons.Icons
16 | import androidx.compose.material.icons.filled.Favorite
17 | import androidx.compose.material3.Button
18 | import androidx.compose.material3.Icon
19 | import androidx.compose.material3.LocalTextStyle
20 | import androidx.compose.material3.MaterialTheme
21 | import androidx.compose.material3.OutlinedTextField
22 | import androidx.compose.material3.Surface
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.mutableStateOf
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.runtime.setValue
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.platform.LocalContext
31 | import androidx.compose.ui.text.Placeholder
32 | import androidx.compose.ui.text.PlaceholderVerticalAlign
33 | import androidx.compose.ui.text.font.Font
34 | import androidx.compose.ui.text.font.FontFamily
35 | import androidx.compose.ui.unit.dp
36 | import xyz.wingio.syntakts.demo.ui.theme.SyntaktsTheme
37 |
38 | import xyz.wingio.syntakts.compose.material3.clickable.ClickableText
39 | import xyz.wingio.syntakts.compose.rememberRendered
40 | import xyz.wingio.syntakts.compose.style.DefaultFontResolver
41 |
42 | class MainActivity : ComponentActivity() {
43 |
44 | override fun onCreate(savedInstanceState: Bundle?) {
45 | super.onCreate(savedInstanceState)
46 | val atIntent = Intent(this, AndroidTestActivity::class.java)
47 |
48 | DefaultFontResolver.register(
49 | "jetbrains mono" to FontFamily(Font(R.font.jetbrains_mono))
50 | )
51 |
52 | setContent {
53 | SyntaktsTheme {
54 | var text by remember { mutableStateOf("**bold** *italic* __underline__ ~~strikethrough~~ `code`") }
55 |
56 | // A surface container using the 'background' color from the theme
57 | Surface(
58 | modifier = Modifier.fillMaxSize(),
59 | color = MaterialTheme.colorScheme.background
60 | ) {
61 | Column(
62 | verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
63 | horizontalAlignment = Alignment.CenterHorizontally,
64 | modifier = Modifier
65 | .padding(16.dp)
66 | .fillMaxSize()
67 | ) {
68 | ClickableText(
69 | text = TestSyntakts.rememberRendered(text, Context(LocalContext.current)),
70 | inlineContent = mapOf(
71 | "heart" to InlineTextContent(
72 | Placeholder(LocalTextStyle.current.fontSize, LocalTextStyle.current.fontSize, PlaceholderVerticalAlign.Center)
73 | ) {
74 | Icon(
75 | imageVector = Icons.Filled.Favorite,
76 | contentDescription = it
77 | )
78 | }
79 | ),
80 | modifier = Modifier
81 | .weight(1f)
82 | .fillMaxWidth()
83 | .verticalScroll(rememberScrollState())
84 | )
85 |
86 | OutlinedTextField(
87 | value = text,
88 | onValueChange = {
89 | text = it
90 | },
91 | modifier = Modifier.weight(1f).fillMaxWidth()
92 | )
93 | Button(
94 | onClick = { startActivity(atIntent) }
95 | ) {
96 | Text("Launch android test")
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
104 | }
105 |
106 |
--------------------------------------------------------------------------------
/demo/src/main/java/xyz/wingio/syntakts/demo/TestSyntakts.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo
2 |
3 | import android.content.Context as AndroidContext
4 | import android.widget.Toast
5 | import xyz.wingio.syntakts.compose.style.appendInlineContent
6 | import xyz.wingio.syntakts.markdown.addMarkdownRules
7 | import xyz.wingio.syntakts.style.Color
8 | import xyz.wingio.syntakts.style.Style
9 | import xyz.wingio.syntakts.syntakts
10 |
11 | val TestSyntakts = syntakts {
12 |
13 | rule("<@([0-9]+)>") { result, ctx ->
14 | val username = ctx.userMap[result.groupValues[1]] ?: "Unknown"
15 | appendClickable(
16 | "@$username",
17 | Style(
18 | color = Color.MAGENTA,
19 | background = Color.MAGENTA withOpacity 0.2f
20 | ),
21 | onLongClick = {
22 | Toast.makeText(ctx.androidContext, "Mention long clicked", Toast.LENGTH_SHORT).show()
23 | },
24 | onClick = {
25 | Toast.makeText(ctx.androidContext, "Mention clicked", Toast.LENGTH_SHORT).show()
26 | }
27 | )
28 | }
29 |
30 | rule(":heart:") { result, _ ->
31 | appendInlineContent(
32 | id = "heart",
33 | alternateText = result.value
34 | )
35 | }
36 |
37 | rule("(`+)([\\s\\S]*?[^`])\\1(?!`)") { result, _ ->
38 | append(result.groupValues[2]) {
39 | font = "jetbrains mono"
40 | }
41 | }
42 |
43 | addMarkdownRules()
44 | }
45 |
46 | data class Context(
47 | val androidContext: AndroidContext,
48 | val userMap: Map = mapOf("1234" to "Wing")
49 | )
--------------------------------------------------------------------------------
/demo/src/main/java/xyz/wingio/syntakts/demo/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/demo/src/main/java/xyz/wingio/syntakts/demo/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun SyntaktsTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/demo/src/main/java/xyz/wingio/syntakts/demo/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.demo.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/demo/src/main/res/font/jetbrains_mono.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/font/jetbrains_mono.ttf
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_android_test.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Syntakts
3 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/demo/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.5.1"
3 | compose-material3 = "1.2.1"
4 | compose-multiplatform = "1.6.11"
5 | compose-stable-marker = "1.0.2"
6 | junit = "4.13.2"
7 | kotlin = "2.0.0"
8 | kotlin-binary-compatibility = "0.13.2"
9 | uuid = "0.8.1"
10 | appcompat = "1.7.0"
11 | material = "1.12.0"
12 | constraintlayout = "2.1.4"
13 |
14 | [libraries]
15 | plugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" }
16 | plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
17 | plugin-maven = { module = "com.vanniktech:gradle-maven-publish-plugin", version = "0.25.3" }
18 | plugin-multiplatform-compose = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" }
19 |
20 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.13.1" }
21 | compose-activity = { group = "androidx.activity", name = "activity-compose", version = "1.9.0" }
22 | compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" }
23 | compose-stable-marker = { group = "com.github.skydoves", name = "compose-stable-marker", version.ref = "compose-stable-marker" }
24 | junit = { module = "junit:junit", version.ref = "junit" }
25 | kotlin-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version = "1.8.0" }
26 | uuid = { group = "com.benasher44", name = "uuid", version.ref = "uuid"}
27 | appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
28 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
29 | constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
30 |
31 | [plugins]
32 | binary-compatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlin-binary-compatibility" }
33 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
34 |
35 | [bundles]
36 | compose = ["compose-activity", "compose-material3"]
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Oct 10 18:14:35 EDT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or 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 UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wingio/syntakts/d2efac8efe21b6b3967c043c07ec60c0e5f357ce/images/logo.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "Syntakts"
18 |
19 | include(":demo")
20 |
21 | include(":syntakts-core")
22 |
23 | include(":syntakts-android")
24 | include(":syntakts-compose")
25 | include(":syntakts-compose-material3")
26 |
--------------------------------------------------------------------------------
/syntakts-android/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/syntakts-android/api/syntakts-android.api:
--------------------------------------------------------------------------------
1 | public final class xyz/wingio/syntakts/android/ClickableMovementMethod : android/text/method/LinkMovementMethod {
2 | public static final field Companion Lxyz/wingio/syntakts/android/ClickableMovementMethod$Companion;
3 | public static final field LONG_TOUCH_DURATION J
4 | public fun ()V
5 | public fun onTouchEvent (Landroid/widget/TextView;Landroid/text/Spannable;Landroid/view/MotionEvent;)Z
6 | }
7 |
8 | public final class xyz/wingio/syntakts/android/ClickableMovementMethod$Companion {
9 | }
10 |
11 | public final class xyz/wingio/syntakts/android/SyntaktsKt {
12 | public static final fun render (Landroid/widget/TextView;Ljava/lang/CharSequence;Lxyz/wingio/syntakts/Syntakts;Ljava/lang/Object;ZLxyz/wingio/syntakts/android/style/AndroidFontResolver;)V
13 | public static final fun render (Landroid/widget/TextView;Ljava/lang/CharSequence;Lxyz/wingio/syntakts/Syntakts;ZLxyz/wingio/syntakts/android/style/AndroidFontResolver;)V
14 | public static final fun render (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/CharSequence;Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;)Ljava/lang/CharSequence;
15 | public static final fun render (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/CharSequence;Ljava/lang/Object;Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;)Ljava/lang/CharSequence;
16 | public static synthetic fun render$default (Landroid/widget/TextView;Ljava/lang/CharSequence;Lxyz/wingio/syntakts/Syntakts;Ljava/lang/Object;ZLxyz/wingio/syntakts/android/style/AndroidFontResolver;ILjava/lang/Object;)V
17 | public static synthetic fun render$default (Landroid/widget/TextView;Ljava/lang/CharSequence;Lxyz/wingio/syntakts/Syntakts;ZLxyz/wingio/syntakts/android/style/AndroidFontResolver;ILjava/lang/Object;)V
18 | public static synthetic fun render$default (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/CharSequence;Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;ILjava/lang/Object;)Ljava/lang/CharSequence;
19 | public static synthetic fun render$default (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/CharSequence;Ljava/lang/Object;Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;ILjava/lang/Object;)Ljava/lang/CharSequence;
20 | }
21 |
22 | public final class xyz/wingio/syntakts/android/markdown/MarkdownKt {
23 | public static final fun renderBasicMarkdown (Landroid/widget/TextView;Ljava/lang/CharSequence;)V
24 | public static final fun renderMarkdown (Landroid/widget/TextView;Ljava/lang/CharSequence;)V
25 | }
26 |
27 | public final class xyz/wingio/syntakts/android/spans/AnnotationSpan {
28 | public fun (Ljava/lang/String;Ljava/lang/String;)V
29 | public final fun getAnnotation ()Ljava/lang/String;
30 | public final fun getTag ()Ljava/lang/String;
31 | }
32 |
33 | public class xyz/wingio/syntakts/android/spans/ClickableSpan : android/text/style/ClickableSpan {
34 | public fun (Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)V
35 | public fun onClick (Landroid/view/View;)V
36 | public final fun onLongClick (Landroid/view/View;)V
37 | public fun updateDrawState (Landroid/text/TextPaint;)V
38 | }
39 |
40 | public class xyz/wingio/syntakts/android/spans/SyntaktsStyleSpan : android/text/style/MetricAffectingSpan {
41 | public static final field Companion Lxyz/wingio/syntakts/android/spans/SyntaktsStyleSpan$Companion;
42 | public fun (Lxyz/wingio/syntakts/style/Style;Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;)V
43 | public final fun getContext ()Landroid/content/Context;
44 | public final fun getStyle ()Lxyz/wingio/syntakts/style/Style;
45 | public fun updateDrawState (Landroid/text/TextPaint;)V
46 | public fun updateMeasureState (Landroid/text/TextPaint;)V
47 | }
48 |
49 | public final class xyz/wingio/syntakts/android/spans/SyntaktsStyleSpan$Companion {
50 | public final fun apply (Landroid/text/TextPaint;Lxyz/wingio/syntakts/style/Style;Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;)V
51 | }
52 |
53 | public final class xyz/wingio/syntakts/android/style/AndroidFontResolver : xyz/wingio/syntakts/style/FontResolver {
54 | public fun ()V
55 | public fun registerFont (Ljava/lang/String;Landroid/graphics/Typeface;)V
56 | public synthetic fun registerFont (Ljava/lang/String;Ljava/lang/Object;)V
57 | public fun resolveFont (Ljava/lang/String;)Landroid/graphics/Typeface;
58 | public synthetic fun resolveFont (Ljava/lang/String;)Ljava/lang/Object;
59 | }
60 |
61 | public final class xyz/wingio/syntakts/android/style/AndroidFontResolverKt {
62 | public static final fun getDefaultFontResolver ()Lxyz/wingio/syntakts/android/style/AndroidFontResolver;
63 | }
64 |
65 | public final class xyz/wingio/syntakts/android/style/ColorKt {
66 | public static final fun fromAndroidColorLong (Lxyz/wingio/syntakts/style/Color$Companion;J)Lxyz/wingio/syntakts/style/Color;
67 | public static final fun toAndroidColorInt (Lxyz/wingio/syntakts/style/Color;)I
68 | public static final fun toSyntaktsColor (J)Lxyz/wingio/syntakts/style/Color;
69 | }
70 |
71 | public final class xyz/wingio/syntakts/android/style/SpannableStyledTextBuilder : xyz/wingio/syntakts/style/StyledTextBuilder {
72 | public fun (Landroid/content/Context;Lxyz/wingio/syntakts/android/style/AndroidFontResolver;)V
73 | public fun addAnnotation (Ljava/lang/String;Ljava/lang/String;II)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
74 | public synthetic fun addAnnotation (Ljava/lang/String;Ljava/lang/String;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
75 | public fun addAnnotation (Ljava/lang/String;Ljava/lang/String;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
76 | public fun addClickable (IILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
77 | public synthetic fun addClickable (IILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
78 | public fun addClickable (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
79 | public fun addStyle (IILkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
80 | public synthetic fun addStyle (IILkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
81 | public fun addStyle (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
82 | public fun addStyle (Lxyz/wingio/syntakts/style/Style;II)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
83 | public synthetic fun addStyle (Lxyz/wingio/syntakts/style/Style;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
84 | public fun addStyle (Lxyz/wingio/syntakts/style/Style;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
85 | public fun append (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
86 | public synthetic fun append (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
87 | public fun append (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
88 | public synthetic fun append (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
89 | public fun appendAnnotated (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
90 | public synthetic fun appendAnnotated (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
91 | public fun appendClickable (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/android/style/SpannableStyledTextBuilder;
92 | public synthetic fun appendClickable (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
93 | public fun build ()Ljava/lang/CharSequence;
94 | public synthetic fun build ()Ljava/lang/Object;
95 | public fun clear ()V
96 | public final fun getContext ()Landroid/content/Context;
97 | public fun getLength ()I
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/syntakts-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("android")
3 | id("com.android.library")
4 | id("com.vanniktech.maven.publish.base")
5 | alias(libs.plugins.binary.compatibility)
6 | }
7 |
8 | setup(
9 | libName = "Syntakts for Android",
10 | moduleName = "syntakts-android",
11 | moduleDescription = "Support for Syntakts rendering on Android",
12 | androidOnly = true
13 | )
14 |
15 | kotlin {
16 | jvmToolchain(17)
17 | explicitApi()
18 | }
19 |
20 | dependencies {
21 | api(project(":syntakts-core"))
22 | implementation(libs.androidx.core.ktx)
23 | implementation(libs.kotlin.coroutines.core)
24 |
25 | testImplementation(kotlin("test"))
26 | testImplementation(libs.junit)
27 | }
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/ClickableMovementMethod.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android
2 |
3 | import android.text.Spannable
4 | import android.text.method.LinkMovementMethod
5 | import android.view.MotionEvent
6 | import android.widget.TextView
7 | import kotlinx.coroutines.CoroutineScope
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.Job
10 | import kotlinx.coroutines.delay
11 | import kotlinx.coroutines.launch
12 | import xyz.wingio.syntakts.android.spans.ClickableSpan
13 |
14 | /**
15 | * Special [LinkMovementMethod] that can also process long clicks, only works with [ClickableSpan]
16 | */
17 | public class ClickableMovementMethod : LinkMovementMethod() {
18 |
19 | /**
20 | * Used to asynchronously measure time
21 | */
22 | private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)
23 |
24 | /**
25 | * How long the touch has lasted
26 | */
27 | private var downTime: Long = 0
28 |
29 | /**
30 | * The current long press job, can only keep track of one at a time
31 | */
32 | private var longPressJob: Job? = null
33 |
34 | override fun onTouchEvent(
35 | widget: TextView,
36 | buffer: Spannable,
37 | event: MotionEvent
38 | ): Boolean {
39 | val action = event.action
40 |
41 | // We only need to wait for these events
42 | if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
43 | var x = event.x.toInt()
44 | var y = event.y.toInt()
45 |
46 | x -= widget.totalPaddingLeft
47 | y -= widget.totalPaddingTop
48 |
49 | x += widget.scrollX
50 | y += widget.scrollY
51 |
52 | val layout = widget.layout
53 | val line = layout.getLineForVertical(y)
54 | val offset = layout.getOffsetForHorizontal(line, x.toFloat())
55 |
56 | val links = buffer.getSpans(
57 | /* start = */ offset,
58 | /* end = */ offset,
59 | /* type = */ ClickableSpan::class.java
60 | )
61 |
62 | if (links.isNotEmpty()) {
63 | val link = links[0] // Unlike Compose we can only do the first one
64 |
65 | if (action == MotionEvent.ACTION_DOWN) {
66 | downTime = System.currentTimeMillis()
67 |
68 | longPressJob = coroutineScope.launch {
69 | while (true) {
70 | delay(1) // Only check every millisecond
71 | val downDuration = System.currentTimeMillis() - downTime
72 | if (downDuration >= LONG_TOUCH_DURATION) {
73 | link.onLongClick(widget)
74 | break // Only fire once
75 | }
76 | }
77 | }
78 | }
79 |
80 | if (action == MotionEvent.ACTION_UP) {
81 | longPressJob?.cancel()
82 | longPressJob = null
83 | val downDuration = System.currentTimeMillis() - downTime
84 | if (downDuration < LONG_TOUCH_DURATION) {
85 | link.onClick(widget)
86 | }
87 | }
88 |
89 | return true
90 | }
91 | }
92 |
93 | return super.onTouchEvent(widget, buffer, event)
94 | }
95 |
96 | public companion object {
97 |
98 | /**
99 | * How long to wait before firing a long click event
100 | */
101 | public const val LONG_TOUCH_DURATION: Long = 500 // ms
102 |
103 | }
104 |
105 | }
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/Syntakts.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android
2 |
3 | import android.content.Context
4 | import android.widget.TextView
5 | import xyz.wingio.syntakts.Syntakts
6 | import xyz.wingio.syntakts.android.style.AndroidFontResolver
7 | import xyz.wingio.syntakts.android.style.DefaultFontResolver
8 | import xyz.wingio.syntakts.android.style.SpannableStyledTextBuilder
9 |
10 | /**
11 | * Parse and render the given [text] using the defined rules into a SpannableString
12 | *
13 | * @param text What to parse and render
14 | * @param context Additional information that nodes may need to render
15 | * @param androidContext Necessary for certain text measurements
16 | * @param fontResolver (optional) The [AndroidFontResolver] used to override fonts in this specific [TextView]
17 | * @return SpannableString as a [CharSequence]
18 | */
19 | public fun Syntakts.render(
20 | text: CharSequence,
21 | context: C,
22 | androidContext: Context,
23 | fontResolver: AndroidFontResolver = DefaultFontResolver
24 | ): CharSequence {
25 | val builder = SpannableStyledTextBuilder(androidContext, fontResolver)
26 | val nodes = parse(text)
27 | for (node in nodes) {
28 | node.render(builder, context)
29 | }
30 | return builder.build()
31 | }
32 |
33 | /**
34 | * Parse and render the given [text] using the defined rules into a SpannableString
35 | *
36 | * @param text What to parse and render
37 | * @param androidContext Necessary for certain text measurements
38 | * @param fontResolver (optional) The [AndroidFontResolver] used to override fonts in this specific [TextView]
39 | * @return SpannableString as a [CharSequence]
40 | */
41 | public fun Syntakts.render(
42 | text: CharSequence,
43 | androidContext: Context,
44 | fontResolver: AndroidFontResolver = DefaultFontResolver
45 | ): CharSequence = render(text, Unit, androidContext, fontResolver)
46 |
47 | /**
48 | * Parse and render the given [text] using the [syntakts] onto this [TextView]
49 | *
50 | * @param text What to parse and render
51 | * @param syntakts An instance of [Syntakts] with the desired rules
52 | * @param context Additional information that nodes may need to render
53 | * @param enableClickable (optional) Whether or not to process click and long click events
54 | * @param fontResolver (optional) The [AndroidFontResolver] used to override fonts in this specific [TextView]
55 | */
56 | public fun TextView.render(
57 | text: CharSequence,
58 | syntakts: Syntakts,
59 | context: C,
60 | enableClickable: Boolean = false,
61 | fontResolver: AndroidFontResolver = DefaultFontResolver
62 | ) {
63 | setText(syntakts.render(text, context, getContext(), fontResolver))
64 | if (enableClickable) {
65 | movementMethod = ClickableMovementMethod()
66 | }
67 | }
68 |
69 | /**
70 | * Parse and render the given [text] using the [syntakts] onto this [TextView]
71 | *
72 | * @param text What to parse and render
73 | * @param syntakts An instance of [Syntakts] with the desired rules
74 | * @param enableClickable (optional) Whether or not to process click and long click events
75 | * @param fontResolver (optional) The [AndroidFontResolver] used to override fonts in this specific [TextView]
76 | */
77 | public fun TextView.render(
78 | text: CharSequence,
79 | syntakts: Syntakts,
80 | enableClickable: Boolean = false,
81 | fontResolver: AndroidFontResolver = DefaultFontResolver
82 | ) {
83 | setText(syntakts.render(text, context, fontResolver))
84 | if (enableClickable) {
85 | movementMethod = ClickableMovementMethod()
86 | }
87 | }
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/markdown/Markdown.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.markdown
2 |
3 | import android.widget.TextView
4 | import xyz.wingio.syntakts.android.render
5 | import xyz.wingio.syntakts.markdown.BasicMarkdownSyntakts
6 | import xyz.wingio.syntakts.markdown.MarkdownSyntakts
7 |
8 | /**
9 | * Renders some Markdown to this [TextView]
10 | *
11 | * @param text The Markdown to be rendered, see [MarkdownSyntakts] for supported rules
12 | * @see [MarkdownSyntakts]
13 | */
14 | public fun TextView.renderMarkdown(text: CharSequence): Unit = render(text, MarkdownSyntakts)
15 |
16 | /**
17 | * Renders some basic Markdown rules to this [TextView]
18 | *
19 | * @param text The Markdown to be rendered, see [BasicMarkdownSyntakts] for supported rules
20 | * @see [BasicMarkdownSyntakts]
21 | */
22 | public fun TextView.renderBasicMarkdown(text: CharSequence): Unit = render(text, BasicMarkdownSyntakts)
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/spans/AnnotationSpan.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.spans
2 |
3 | /**
4 | * Represents an annotation applied to some text
5 | *
6 | * @param tag Used to distinguish annotations
7 | * @param annotation Annotation to attach
8 | */
9 | public class AnnotationSpan(
10 | public val tag: String,
11 | public val annotation: String
12 | )
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/spans/ClickableSpan.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.spans
2 |
3 | import android.text.TextPaint
4 | import android.text.style.ClickableSpan
5 | import android.view.View
6 |
7 | /**
8 | * [ClickableSpan] with the added ability to receive long clicks
9 | *
10 | * @param onClickListener Called when this span is clicked
11 | * @param onLongClickListener Called when this span is long clicked
12 | */
13 | public open class ClickableSpan(
14 | private val onClickListener: (() -> Unit)?,
15 | private val onLongClickListener: (() -> Unit)?
16 | ) : ClickableSpan() {
17 |
18 | override fun onClick(view: View) {
19 | onClickListener?.invoke()
20 | }
21 |
22 | /**
23 | * Performs the long click action associated with this span
24 | *
25 | * @param view A reference to the view that was clicked
26 | */
27 | public fun onLongClick(view: View) {
28 | onLongClickListener?.invoke()
29 | }
30 |
31 | override fun updateDrawState(ds: TextPaint) {
32 | // NO-OP
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/spans/SyntaktsStyleSpan.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.spans
2 |
3 | import android.content.Context
4 | import android.graphics.Typeface
5 | import android.text.TextPaint
6 | import android.text.style.MetricAffectingSpan
7 | import androidx.core.graphics.TypefaceCompat
8 | import xyz.wingio.syntakts.android.style.AndroidFontResolver
9 | import xyz.wingio.syntakts.android.style.toAndroidColorInt
10 | import xyz.wingio.syntakts.android.util.emToPx
11 | import xyz.wingio.syntakts.android.util.spToEm
12 | import xyz.wingio.syntakts.android.util.spToPx
13 | import xyz.wingio.syntakts.style.FontStyle
14 | import xyz.wingio.syntakts.style.Style
15 | import xyz.wingio.syntakts.style.TextDecoration
16 | import xyz.wingio.syntakts.style.TextUnit
17 | import kotlin.math.roundToInt
18 |
19 | /**
20 | * Span that applies a [Style]
21 | *
22 | * @param style The [Style] to apply
23 | * @param context Necessary for certain measurements
24 | * @param fontResolver Needed to resolve a [Typeface] from a font name
25 | */
26 | public open class SyntaktsStyleSpan(
27 | public val style: Style,
28 | public val context: Context,
29 | private val fontResolver: AndroidFontResolver
30 | ) : MetricAffectingSpan() {
31 |
32 | override fun updateDrawState(tp: TextPaint?) {
33 | apply(tp, style, context, fontResolver)
34 | }
35 |
36 | override fun updateMeasureState(textPaint: TextPaint) {
37 | apply(textPaint, style, context, fontResolver)
38 | }
39 |
40 | public companion object {
41 |
42 | /**
43 | * Applies a given [style] to a [paint]
44 | *
45 | * @param paint Information for how text can be displayed
46 | * @param style The [Style] to apply
47 | * @param context Necessary for certain measurements
48 | * @param fontResolver Needed to resolve a [Typeface] from a font name
49 | */
50 | public fun apply(paint: TextPaint?, style: Style, context: Context, fontResolver: AndroidFontResolver) {
51 | if (paint == null) return
52 |
53 | with(style) {
54 | color?.let { color ->
55 | paint.setColor(color.toAndroidColorInt())
56 | }
57 |
58 | background?.let { background ->
59 | paint.bgColor = background.toAndroidColorInt()
60 | }
61 |
62 | font?.let {
63 | fontResolver.resolveFont(it)?.let { typeface -> paint.setTypeface(typeface) }
64 | }
65 |
66 | if (fontSize !is TextUnit.Unspecified) {
67 | paint.textSize = when (fontSize.unit) {
68 | "sp" -> context.spToPx(fontSize.value)
69 | "em" -> emToPx(fontSize.value, paint.textSize)
70 | else -> paint.letterSpacing
71 | }
72 | }
73 |
74 | fontWeight?.let { fontWeight ->
75 | paint.typeface =
76 | TypefaceCompat.create(context, paint.typeface, fontWeight.weight, false)
77 | }
78 |
79 | paint.typeface = when {
80 | fontWeight != null && fontStyle != null -> TypefaceCompat.create(
81 | context,
82 | paint.typeface,
83 | fontWeight!!.weight,
84 | fontStyle!! == FontStyle.Italic
85 | )
86 |
87 | fontWeight != null -> TypefaceCompat.create(
88 | context,
89 | paint.typeface,
90 | fontWeight!!.weight,
91 | false
92 | )
93 |
94 | fontStyle != null -> TypefaceCompat.create(
95 | context,
96 | paint.typeface,
97 | Typeface.ITALIC
98 | )
99 |
100 | else -> paint.typeface
101 | }
102 |
103 | if (letterSpacing !is TextUnit.Unspecified) {
104 | paint.letterSpacing = when (letterSpacing.unit) {
105 | "sp" -> spToEm(letterSpacing.value, paint.textSize)
106 | "em" -> letterSpacing.value
107 | else -> paint.letterSpacing
108 | }
109 | }
110 |
111 | when (textDecoration) {
112 | TextDecoration.Underline -> paint.isUnderlineText = true
113 | TextDecoration.LineThrough -> paint.isStrikeThruText = true
114 | else -> {}
115 | }
116 |
117 | paragraphStyle?.let { paragraphStyle ->
118 | when (paragraphStyle.lineHeight.unit) {
119 | "sp" -> paint.applyLineHeight(context.spToPx(paragraphStyle.lineHeight.value))
120 | "em" -> paint.applyLineHeight(emToPx(paragraphStyle.lineHeight.value, paint.textSize))
121 | }
122 | }
123 | }
124 | }
125 |
126 | }
127 |
128 | }
129 |
130 | /**
131 | * Applies the given [lineHeight] to this paint
132 | *
133 | * @param lineHeight The line height for this text (in pixels)
134 | */
135 | internal fun TextPaint.applyLineHeight(lineHeight: Float) {
136 | val originHeight = fontMetricsInt.descent - fontMetricsInt.ascent
137 |
138 | if (originHeight <= 0) {
139 | return
140 | }
141 |
142 | val ratio: Float = lineHeight * 1.0f / originHeight
143 | fontMetricsInt.descent = (ratio * fontMetricsInt.descent).roundToInt()
144 | fontMetricsInt.ascent = (fontMetricsInt.descent - lineHeight).roundToInt()
145 | }
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/style/AndroidFontResolver.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.style
2 |
3 | import android.graphics.Typeface
4 | import xyz.wingio.syntakts.style.FontResolver
5 | import xyz.wingio.syntakts.style.Fonts
6 |
7 | /**
8 | * Keeps a record of [Typefaces][Typeface] with a plain font name
9 | *
10 | * For most cases it is reccommended to use [DefaultFontResolver] to register your fonts
11 | */
12 | public class AndroidFontResolver: FontResolver() {
13 |
14 | override val fontMap: MutableMap = mutableMapOf(
15 | Fonts.DEFAULT to Typeface.DEFAULT,
16 | Fonts.MONOSPACE to Typeface.MONOSPACE,
17 | "monospaced" to Typeface.MONOSPACE,
18 | Fonts.SERIF to Typeface.SERIF,
19 | Fonts.SANS_SERIF to Typeface.SANS_SERIF
20 | )
21 |
22 | /**
23 | * Gets a font in the [Typeface] format from a given name
24 | *
25 | * @param fontName Case-insensitive font name to look up
26 | */
27 | public override fun resolveFont(fontName: String): Typeface? {
28 | return fontMap[fontName.lowercase()]
29 | }
30 |
31 | /**
32 | * Registers a [Typeface] with the specified [name][fontName]
33 | *
34 | * @param fontName Case-insensitive name to use for the [font][platformFont]
35 | * @param platformFont The font to register with the [fontName]
36 | */
37 | override fun registerFont(fontName: String, platformFont: Typeface) {
38 | fontMap[fontName.lowercase()] = platformFont
39 | }
40 |
41 |
42 | }
43 |
44 | /**
45 | * Default [FontResolver] used when rendering
46 | */
47 | public val DefaultFontResolver: AndroidFontResolver = AndroidFontResolver()
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/style/Color.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.style
2 |
3 | import androidx.annotation.ColorInt
4 | import androidx.annotation.ColorLong
5 | import xyz.wingio.syntakts.style.Color
6 |
7 | /**
8 | * Convert a [Color] to an Android color int (0xAARRGGBB)
9 | */
10 | @ColorInt
11 | public fun Color.toAndroidColorInt(): Int {
12 | return (
13 | (alpha shl 24) or
14 | (red shl 16) or
15 | (green shl 8) or
16 | blue
17 | )
18 | }
19 |
20 | /**
21 | * Converts a color formatted [Long] to a [Color]
22 | */
23 | public fun @receiver:ColorLong Long.toSyntaktsColor(): Color {
24 | val alpha = shr(24) and 0xFF
25 | val red = shr(16) and 0xFF
26 | val green = shr(8) and 0xFF
27 | val blue = and(0xFF)
28 |
29 | return Color(
30 | red = red.toInt(),
31 | green = green.toInt(),
32 | blue = blue.toInt(),
33 | alpha = alpha.toInt()
34 | )
35 | }
36 |
37 | /**
38 | * Creates a [Color] from a color long
39 | */
40 | public fun Color.Companion.fromAndroidColorLong(@ColorLong colorLong: Long): Color =
41 | colorLong.toSyntaktsColor()
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/style/SpannableStyledTextBuilder.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.style
2 |
3 | import android.content.Context
4 | import android.text.Spannable
5 | import android.text.SpannableStringBuilder
6 | import xyz.wingio.syntakts.android.spans.AnnotationSpan
7 | import xyz.wingio.syntakts.android.spans.ClickableSpan
8 | import xyz.wingio.syntakts.android.spans.SyntaktsStyleSpan
9 | import xyz.wingio.syntakts.style.Style
10 | import xyz.wingio.syntakts.style.StyledTextBuilder
11 |
12 | /**
13 | * Instance of [StyledTextBuilder] that builds SpannableStrings
14 | *
15 | * @param context Used for certain measurements when applying styles
16 | * @param fontResolver Used to resolve a [android.graphics.Typeface] from a font name
17 | */
18 | public class SpannableStyledTextBuilder(
19 | public val context: Context,
20 | private val fontResolver: AndroidFontResolver
21 | ) : StyledTextBuilder {
22 | private val builder = SpannableStringBuilder()
23 |
24 | override val length: Int
25 | get() = builder.length
26 |
27 | override fun append(text: CharSequence, style: Style?): SpannableStyledTextBuilder {
28 | val i = length
29 | builder.append(text)
30 | style?.let {
31 | builder.setSpan(
32 | /* what = */ SyntaktsStyleSpan(style, context, fontResolver),
33 | /* start = */ i,
34 | /* end = */ length,
35 | /* flags = */ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
36 | )
37 |
38 | it.paragraphStyle?.let { paragraphStyle ->
39 | paragraphStyle.lineHeight
40 | }
41 | }
42 | return this
43 | }
44 |
45 | override fun append(
46 | text: CharSequence,
47 | style: Style.() -> Unit
48 | ): SpannableStyledTextBuilder {
49 | return append(text = text, style = Style().apply(style))
50 | }
51 |
52 | override fun appendClickable(
53 | text: CharSequence,
54 | style: Style?,
55 | onLongClick: (() -> Unit)?,
56 | onClick: () -> Unit
57 | ): SpannableStyledTextBuilder {
58 | val i = length
59 | append(text, style)
60 | builder.setSpan(
61 | /* what = */ ClickableSpan(
62 | onClickListener = onClick,
63 | onLongClickListener = onLongClick
64 | ),
65 | /* start = */ i,
66 | /* end = */ length,
67 | /* flags = */ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
68 | )
69 | return this
70 | }
71 |
72 | override fun appendAnnotated(
73 | text: CharSequence,
74 | tag: String,
75 | annotation: String
76 | ): SpannableStyledTextBuilder {
77 | val i = length
78 | append(text)
79 | addAnnotation(tag, annotation, i, length)
80 | return this
81 | }
82 |
83 | override fun addClickable(
84 | startIndex: Int,
85 | endIndex: Int,
86 | onLongClick: (() -> Unit)?,
87 | onClick: () -> Unit
88 | ): SpannableStyledTextBuilder {
89 | builder.setSpan(
90 | /* what = */ ClickableSpan(
91 | onClickListener = onClick,
92 | onLongClickListener = onLongClick
93 | ),
94 | /* start = */ startIndex,
95 | /* end = */ endIndex,
96 | /* flags = */ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
97 | )
98 | return this
99 | }
100 |
101 | override fun addStyle(
102 | style: Style,
103 | startIndex: Int,
104 | endIndex: Int
105 | ): SpannableStyledTextBuilder {
106 | builder.setSpan(
107 | /* what = */ SyntaktsStyleSpan(style, context, fontResolver),
108 | /* start = */ startIndex,
109 | /* end = */ endIndex,
110 | /* flags = */ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
111 | )
112 | return this
113 | }
114 |
115 | override fun addStyle(
116 | startIndex: Int,
117 | endIndex: Int,
118 | style: Style.() -> Unit
119 | ): SpannableStyledTextBuilder {
120 | builder.setSpan(
121 | /* what = */ SyntaktsStyleSpan(Style().apply(style), context, fontResolver),
122 | /* start = */ startIndex,
123 | /* end = */ endIndex,
124 | /* flags = */ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
125 | )
126 | return this
127 | }
128 |
129 | override fun addAnnotation(
130 | tag: String,
131 | annotation: String,
132 | startIndex: Int,
133 | endIndex: Int
134 | ): SpannableStyledTextBuilder {
135 | builder.setSpan(
136 | /* what = */ AnnotationSpan(tag, annotation),
137 | /* start = */ startIndex,
138 | /* end = */ endIndex,
139 | /* flags = */ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
140 | )
141 | return this
142 | }
143 |
144 | override fun clear() {
145 | builder.clear()
146 | }
147 |
148 | override fun build(): CharSequence {
149 | return builder
150 | }
151 |
152 | }
--------------------------------------------------------------------------------
/syntakts-android/src/main/kotlin/xyz/wingio/syntakts/android/util/DimenUtil.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.android.util
2 |
3 | import android.content.Context
4 | import android.util.TypedValue
5 |
6 | /**
7 | * Converts sp units to em
8 | *
9 | * @param textSize Size of the text, in pixels
10 | */
11 | internal fun spToEm(
12 | sp: Float,
13 | textSize: Float
14 | ) = sp / textSize
15 |
16 | /**
17 | * Converts em units to px
18 | *
19 | * @param textSize Size of the text, in pixels
20 | * @return Em size in pixels
21 | */
22 | internal fun emToPx(em: Float, textSize: Float): Float {
23 | return em * textSize
24 | }
25 |
26 | /**
27 | * Converts sp units to px
28 | *
29 | * @return sp size in pixels
30 | */
31 | internal fun Context.spToPx(sp: Float): Float {
32 | return TypedValue.applyDimension(
33 | TypedValue.COMPLEX_UNIT_SP,
34 | sp,
35 | resources.displayMetrics
36 | )
37 | }
--------------------------------------------------------------------------------
/syntakts-compose-material3/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/syntakts-compose-material3/api/android/syntakts-compose-material3.api:
--------------------------------------------------------------------------------
1 | public final class xyz/wingio/syntakts/compose/material3/clickable/ClickableTextKt {
2 | public static final fun ClickableText-QxQCc2s (Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;IJLandroidx/compose/ui/text/TextStyle;IZILjava/util/Map;Landroidx/compose/runtime/Composer;III)V
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/syntakts-compose-material3/api/jvm/syntakts-compose-material3.api:
--------------------------------------------------------------------------------
1 | public final class xyz/wingio/syntakts/compose/material3/clickable/ClickableTextKt {
2 | public static final fun ClickableText-QxQCc2s (Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;IJLandroidx/compose/ui/text/TextStyle;IZILjava/util/Map;Landroidx/compose/runtime/Composer;III)V
3 | }
4 |
5 |
--------------------------------------------------------------------------------
/syntakts-compose-material3/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | id("com.vanniktech.maven.publish.base")
6 | alias(libs.plugins.binary.compatibility)
7 | alias(libs.plugins.compose.compiler)
8 | }
9 |
10 | setup(
11 | libName = "Syntakts for Compose (Material 3)",
12 | moduleName = "syntakts-compose-material3",
13 | moduleDescription = "Adds Material 3 theming support to Syntakts components"
14 | )
15 |
16 | kotlin {
17 | androidTarget() {
18 | publishLibraryVariants("release")
19 | }
20 | jvm()
21 |
22 | jvmToolchain(17)
23 | explicitApi()
24 |
25 | sourceSets {
26 | commonMain {
27 | dependencies {
28 | api(project(":syntakts-compose"))
29 | api(compose.material3)
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/syntakts-compose-material3/src/commonMain/kotlin/xyz/wingio/syntakts/compose/material3/clickable/ClickableText.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.material3.clickable
2 |
3 | import androidx.compose.foundation.text.BasicText
4 | import androidx.compose.foundation.text.InlineTextContent
5 | import androidx.compose.material3.LocalContentColor
6 | import androidx.compose.material3.LocalTextStyle
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.takeOrElse
15 | import androidx.compose.ui.text.AnnotatedString
16 | import androidx.compose.ui.text.TextLayoutResult
17 | import androidx.compose.ui.text.TextStyle
18 | import androidx.compose.ui.text.font.FontFamily
19 | import androidx.compose.ui.text.font.FontStyle
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.text.style.TextDecoration
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.TextUnit
25 | import xyz.wingio.syntakts.compose.clickable.clickableText
26 |
27 | internal const val CLICKABLE_ANNOTATION_TAG: String = "xyz.wingio.syntakts.clickable"
28 | internal const val LONG_CLICKABLE_ANNOTATION_TAG: String = "xyz.wingio.syntakts.longclickable"
29 |
30 | /**
31 | * Basic Text component that enables support for Syntakts click handling and Material 3 theming
32 | *
33 | * @param text The rendered text
34 | * @param modifier Used to alter how the component looks or behaves
35 | * @param color Color of the text
36 | * @param fontSize Size of the text
37 | * @param fontWeight The thickness of the font glyphs
38 | * @param fontStyle Whether the font is italic or normal
39 | * @param fontFamily The specific fonts that should be used
40 | * @param letterSpacing Spacing between each character
41 | * @param textDecoration Used to draw a line over the text
42 | * @param textAlign How the text should be aligned
43 | * @param lineHeight Line height for the paragraph
44 | * @param style Styling configuration for the text
45 | * @param overflow How the text should handle visual overflow
46 | * @param softWrap Whether the text should break at soft line breaks
47 | * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary
48 | * @param inlineContent A map store composables that replaces certain ranges of the text
49 | */
50 | @Composable
51 | public fun ClickableText(
52 | text: AnnotatedString,
53 | modifier: Modifier = Modifier,
54 | color: Color = Color.Unspecified,
55 | fontSize: TextUnit = TextUnit.Unspecified,
56 | fontWeight: FontWeight? = null,
57 | fontStyle: FontStyle? = null,
58 | fontFamily: FontFamily? = null,
59 | letterSpacing: TextUnit = TextUnit.Unspecified,
60 | textDecoration: TextDecoration? = null,
61 | textAlign: TextAlign = TextAlign.Unspecified,
62 | lineHeight: TextUnit = TextUnit.Unspecified,
63 | style: TextStyle = LocalTextStyle.current,
64 | overflow: TextOverflow = TextOverflow.Clip,
65 | softWrap: Boolean = true,
66 | maxLines: Int = Int.MAX_VALUE,
67 | inlineContent: Map = mapOf()
68 | ) {
69 | val isClickable = text.getStringAnnotations(0, text.length).find {
70 | it.tag == CLICKABLE_ANNOTATION_TAG || it.tag == LONG_CLICKABLE_ANNOTATION_TAG
71 | } != null
72 | var layoutResult by remember { mutableStateOf(null) }
73 | val textColor = color.takeOrElse {
74 | style.color.takeOrElse {
75 | LocalContentColor.current
76 | }
77 | }
78 | TextStyle()
79 | val textStyle = style.merge(
80 | TextStyle(
81 | color = textColor,
82 | fontSize = fontSize,
83 | fontWeight = fontWeight,
84 | textAlign = textAlign,
85 | lineHeight = lineHeight,
86 | fontFamily = fontFamily,
87 | textDecoration = textDecoration,
88 | fontStyle = fontStyle,
89 | letterSpacing = letterSpacing
90 | )
91 | )
92 |
93 | BasicText(
94 | text = text,
95 | modifier = modifier.thenIf(isClickable) {
96 | clickableText(text, layoutResult)
97 | },
98 | style = textStyle,
99 | overflow = overflow,
100 | softWrap = softWrap,
101 | maxLines = maxLines,
102 | inlineContent = inlineContent,
103 | onTextLayout = {
104 | layoutResult = it
105 | }
106 | )
107 | }
108 |
109 | internal inline fun Modifier.thenIf(predicate: Boolean, block: Modifier.() -> Modifier): Modifier {
110 | return if (predicate) then(block()) else this
111 | }
--------------------------------------------------------------------------------
/syntakts-compose/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/syntakts-compose/api/android/syntakts-compose.api:
--------------------------------------------------------------------------------
1 | public final class xyz/wingio/syntakts/compose/SyntaktsKt {
2 | public static final fun rememberAsyncRendered (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/text/AnnotatedString;
3 | public static final fun rememberRendered (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/text/AnnotatedString;
4 | public static final fun rememberRendered (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/text/AnnotatedString;
5 | public static final fun rememberSyntakts (Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Lxyz/wingio/syntakts/Syntakts;
6 | public static final fun render (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;)Landroidx/compose/ui/text/AnnotatedString;
7 | public static synthetic fun render$default (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;ILjava/lang/Object;)Landroidx/compose/ui/text/AnnotatedString;
8 | }
9 |
10 | public final class xyz/wingio/syntakts/compose/clickable/ClickHandlerStore {
11 | public static final field $stable I
12 | public static final field INSTANCE Lxyz/wingio/syntakts/compose/clickable/ClickHandlerStore;
13 | public final fun add (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
14 | public final fun clearForBuilder (Ljava/lang/String;)V
15 | public final fun get (Ljava/lang/String;)Lkotlin/jvm/functions/Function0;
16 | }
17 |
18 | public final class xyz/wingio/syntakts/compose/clickable/ClickableTextKt {
19 | public static final fun ClickableText-QxQCc2s (Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;IJLandroidx/compose/ui/text/TextStyle;IZILjava/util/Map;Landroidx/compose/runtime/Composer;III)V
20 | public static final fun clickableText (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextLayoutResult;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/Modifier;
21 | }
22 |
23 | public final class xyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder : xyz/wingio/syntakts/style/StyledTextBuilder {
24 | public static final field $stable I
25 | public fun (Landroidx/compose/ui/text/AnnotatedString$Builder;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;)V
26 | public synthetic fun (Landroidx/compose/ui/text/AnnotatedString$Builder;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
27 | public fun (Ljava/lang/String;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;)V
28 | public fun addAnnotation (Ljava/lang/String;Ljava/lang/String;II)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
29 | public synthetic fun addAnnotation (Ljava/lang/String;Ljava/lang/String;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
30 | public fun addAnnotation (Ljava/lang/String;Ljava/lang/String;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
31 | public fun addClickable (IILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
32 | public fun addClickable (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
33 | public fun addStyle (IILkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
34 | public synthetic fun addStyle (IILkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
35 | public fun addStyle (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
36 | public fun addStyle (Lxyz/wingio/syntakts/style/Style;II)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
37 | public synthetic fun addStyle (Lxyz/wingio/syntakts/style/Style;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
38 | public fun addStyle (Lxyz/wingio/syntakts/style/Style;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
39 | public fun append (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
40 | public synthetic fun append (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
41 | public fun append (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
42 | public synthetic fun append (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
43 | public fun appendAnnotated (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
44 | public synthetic fun appendAnnotated (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
45 | public fun appendClickable (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
46 | public synthetic fun appendClickable (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
47 | public fun build ()Landroidx/compose/ui/text/AnnotatedString;
48 | public synthetic fun build ()Ljava/lang/Object;
49 | public fun clear ()V
50 | public fun getLength ()I
51 | }
52 |
53 | public final class xyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilderKt {
54 | public static final fun addInlineContent (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
55 | public static final fun addInlineContent (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
56 | public static final fun appendInlineContent (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;Ljava/lang/CharSequence;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
57 | public static synthetic fun appendInlineContent$default (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;Ljava/lang/CharSequence;ILjava/lang/Object;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
58 | }
59 |
60 | public final class xyz/wingio/syntakts/compose/style/ColorKt {
61 | public static final fun toComposeColor (Lxyz/wingio/syntakts/style/Color;)J
62 | public static final fun toSyntaktsColor-8_81llA (J)Lxyz/wingio/syntakts/style/Color;
63 | }
64 |
65 | public final class xyz/wingio/syntakts/compose/style/ComposeFontResolver : xyz/wingio/syntakts/style/FontResolver {
66 | public static final field $stable I
67 | public fun ()V
68 | public fun registerFont (Ljava/lang/String;Landroidx/compose/ui/text/font/FontFamily;)V
69 | public synthetic fun registerFont (Ljava/lang/String;Ljava/lang/Object;)V
70 | public fun resolveFont (Ljava/lang/String;)Landroidx/compose/ui/text/font/FontFamily;
71 | public synthetic fun resolveFont (Ljava/lang/String;)Ljava/lang/Object;
72 | }
73 |
74 | public final class xyz/wingio/syntakts/compose/style/ComposeFontResolverKt {
75 | public static final fun getDefaultFontResolver ()Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;
76 | }
77 |
78 | public final class xyz/wingio/syntakts/compose/style/FontStyleKt {
79 | public static final fun toComposeFontStyle (Lxyz/wingio/syntakts/style/FontStyle;)I
80 | public static final fun toComposeFontStyle-nzbMABs (I)Lxyz/wingio/syntakts/style/FontStyle;
81 | }
82 |
83 | public final class xyz/wingio/syntakts/compose/style/FontWeightKt {
84 | public static final fun toComposeFontWeight (Lxyz/wingio/syntakts/style/FontWeight;)Landroidx/compose/ui/text/font/FontWeight;
85 | public static final fun toSyntaktsFontWeight (Landroidx/compose/ui/text/font/FontWeight;)Lxyz/wingio/syntakts/style/FontWeight;
86 | }
87 |
88 | public final class xyz/wingio/syntakts/compose/style/TextDecorationKt {
89 | public static final fun toComposeTextDecoration (Lxyz/wingio/syntakts/style/TextDecoration;)Landroidx/compose/ui/text/style/TextDecoration;
90 | public static final fun toSyntaktsTextDecoration (Landroidx/compose/ui/text/style/TextDecoration;)Lxyz/wingio/syntakts/style/TextDecoration;
91 | }
92 |
93 | public final class xyz/wingio/syntakts/compose/style/TextUnitKt {
94 | public static final fun toComposeTextUnit (Lxyz/wingio/syntakts/style/TextUnit;)J
95 | public static final fun toSyntaktsTextUnit--R2X_6o (J)Lxyz/wingio/syntakts/style/TextUnit;
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/syntakts-compose/api/jvm/syntakts-compose.api:
--------------------------------------------------------------------------------
1 | public final class xyz/wingio/syntakts/compose/SyntaktsKt {
2 | public static final fun rememberAsyncRendered (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/text/AnnotatedString;
3 | public static final fun rememberRendered (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/text/AnnotatedString;
4 | public static final fun rememberRendered (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/text/AnnotatedString;
5 | public static final fun rememberSyntakts (Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)Lxyz/wingio/syntakts/Syntakts;
6 | public static final fun render (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;)Landroidx/compose/ui/text/AnnotatedString;
7 | public static synthetic fun render$default (Lxyz/wingio/syntakts/Syntakts;Ljava/lang/String;Ljava/lang/Object;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;ILjava/lang/Object;)Landroidx/compose/ui/text/AnnotatedString;
8 | }
9 |
10 | public final class xyz/wingio/syntakts/compose/clickable/ClickHandlerStore {
11 | public static final field $stable I
12 | public static final field INSTANCE Lxyz/wingio/syntakts/compose/clickable/ClickHandlerStore;
13 | public final fun add (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V
14 | public final fun clearForBuilder (Ljava/lang/String;)V
15 | public final fun get (Ljava/lang/String;)Lkotlin/jvm/functions/Function0;
16 | }
17 |
18 | public final class xyz/wingio/syntakts/compose/clickable/ClickableTextKt {
19 | public static final fun ClickableText-QxQCc2s (Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/font/FontWeight;Landroidx/compose/ui/text/font/FontStyle;Landroidx/compose/ui/text/font/FontFamily;JLandroidx/compose/ui/text/style/TextDecoration;IJLandroidx/compose/ui/text/TextStyle;IZILjava/util/Map;Landroidx/compose/runtime/Composer;III)V
20 | public static final fun clickableText (Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/AnnotatedString;Landroidx/compose/ui/text/TextLayoutResult;Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/Modifier;
21 | }
22 |
23 | public final class xyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder : xyz/wingio/syntakts/style/StyledTextBuilder {
24 | public static final field $stable I
25 | public fun (Landroidx/compose/ui/text/AnnotatedString$Builder;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;)V
26 | public synthetic fun (Landroidx/compose/ui/text/AnnotatedString$Builder;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
27 | public fun (Ljava/lang/String;Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;)V
28 | public fun addAnnotation (Ljava/lang/String;Ljava/lang/String;II)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
29 | public synthetic fun addAnnotation (Ljava/lang/String;Ljava/lang/String;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
30 | public fun addAnnotation (Ljava/lang/String;Ljava/lang/String;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
31 | public fun addClickable (IILkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
32 | public fun addClickable (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
33 | public fun addStyle (IILkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
34 | public synthetic fun addStyle (IILkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
35 | public fun addStyle (Lkotlin/ranges/IntRange;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
36 | public fun addStyle (Lxyz/wingio/syntakts/style/Style;II)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
37 | public synthetic fun addStyle (Lxyz/wingio/syntakts/style/Style;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
38 | public fun addStyle (Lxyz/wingio/syntakts/style/Style;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
39 | public fun append (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
40 | public synthetic fun append (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
41 | public fun append (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
42 | public synthetic fun append (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
43 | public fun appendAnnotated (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
44 | public synthetic fun appendAnnotated (Ljava/lang/CharSequence;Ljava/lang/String;Ljava/lang/String;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
45 | public fun appendClickable (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder;
46 | public synthetic fun appendClickable (Ljava/lang/CharSequence;Lxyz/wingio/syntakts/style/Style;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
47 | public fun build ()Landroidx/compose/ui/text/AnnotatedString;
48 | public synthetic fun build ()Ljava/lang/Object;
49 | public fun clear ()V
50 | public fun getLength ()I
51 | }
52 |
53 | public final class xyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilderKt {
54 | public static final fun addInlineContent (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;II)Lxyz/wingio/syntakts/style/StyledTextBuilder;
55 | public static final fun addInlineContent (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;Lkotlin/ranges/IntRange;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
56 | public static final fun appendInlineContent (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;Ljava/lang/CharSequence;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
57 | public static synthetic fun appendInlineContent$default (Lxyz/wingio/syntakts/style/StyledTextBuilder;Ljava/lang/String;Ljava/lang/CharSequence;ILjava/lang/Object;)Lxyz/wingio/syntakts/style/StyledTextBuilder;
58 | }
59 |
60 | public final class xyz/wingio/syntakts/compose/style/ColorKt {
61 | public static final fun toComposeColor (Lxyz/wingio/syntakts/style/Color;)J
62 | public static final fun toSyntaktsColor-8_81llA (J)Lxyz/wingio/syntakts/style/Color;
63 | }
64 |
65 | public final class xyz/wingio/syntakts/compose/style/ComposeFontResolver : xyz/wingio/syntakts/style/FontResolver {
66 | public static final field $stable I
67 | public fun ()V
68 | public fun registerFont (Ljava/lang/String;Landroidx/compose/ui/text/font/FontFamily;)V
69 | public synthetic fun registerFont (Ljava/lang/String;Ljava/lang/Object;)V
70 | public fun resolveFont (Ljava/lang/String;)Landroidx/compose/ui/text/font/FontFamily;
71 | public synthetic fun resolveFont (Ljava/lang/String;)Ljava/lang/Object;
72 | }
73 |
74 | public final class xyz/wingio/syntakts/compose/style/ComposeFontResolverKt {
75 | public static final fun getDefaultFontResolver ()Lxyz/wingio/syntakts/compose/style/ComposeFontResolver;
76 | }
77 |
78 | public final class xyz/wingio/syntakts/compose/style/FontStyleKt {
79 | public static final fun toComposeFontStyle (Lxyz/wingio/syntakts/style/FontStyle;)I
80 | public static final fun toComposeFontStyle-nzbMABs (I)Lxyz/wingio/syntakts/style/FontStyle;
81 | }
82 |
83 | public final class xyz/wingio/syntakts/compose/style/FontWeightKt {
84 | public static final fun toComposeFontWeight (Lxyz/wingio/syntakts/style/FontWeight;)Landroidx/compose/ui/text/font/FontWeight;
85 | public static final fun toSyntaktsFontWeight (Landroidx/compose/ui/text/font/FontWeight;)Lxyz/wingio/syntakts/style/FontWeight;
86 | }
87 |
88 | public final class xyz/wingio/syntakts/compose/style/TextDecorationKt {
89 | public static final fun toComposeTextDecoration (Lxyz/wingio/syntakts/style/TextDecoration;)Landroidx/compose/ui/text/style/TextDecoration;
90 | public static final fun toSyntaktsTextDecoration (Landroidx/compose/ui/text/style/TextDecoration;)Lxyz/wingio/syntakts/style/TextDecoration;
91 | }
92 |
93 | public final class xyz/wingio/syntakts/compose/style/TextUnitKt {
94 | public static final fun toComposeTextUnit (Lxyz/wingio/syntakts/style/TextUnit;)J
95 | public static final fun toSyntaktsTextUnit--R2X_6o (J)Lxyz/wingio/syntakts/style/TextUnit;
96 | }
97 |
98 |
--------------------------------------------------------------------------------
/syntakts-compose/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("org.jetbrains.compose")
5 | id("com.vanniktech.maven.publish.base")
6 | alias(libs.plugins.binary.compatibility)
7 | alias(libs.plugins.compose.compiler)
8 | }
9 |
10 | setup(
11 | libName = "Syntakts for Compose",
12 | moduleName = "syntakts-compose",
13 | moduleDescription = "Support for Syntakts rendering in Compose"
14 | )
15 |
16 | kotlin {
17 | androidTarget() {
18 | publishLibraryVariants("release")
19 | }
20 | jvm()
21 |
22 | jvmToolchain(17)
23 | explicitApi()
24 |
25 | sourceSets {
26 | commonMain {
27 | dependencies {
28 | api(project(":syntakts-core"))
29 | api(compose.runtime)
30 | api(compose.ui)
31 | api(compose.foundation)
32 | implementation(libs.uuid)
33 | }
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/Syntakts.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.DisposableEffect
5 | import androidx.compose.runtime.LaunchedEffect
6 | import androidx.compose.runtime.getValue
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.text.AnnotatedString
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.launch
13 | import xyz.wingio.syntakts.Syntakts
14 | import xyz.wingio.syntakts.compose.clickable.ClickHandlerStore
15 | import xyz.wingio.syntakts.compose.style.AnnotatedStyledTextBuilder
16 | import xyz.wingio.syntakts.compose.style.ComposeFontResolver
17 | import xyz.wingio.syntakts.compose.style.DefaultFontResolver
18 | import xyz.wingio.syntakts.syntakts
19 |
20 | /**
21 | * Remembered DSL for [Syntakts.Builder], the preferred way to create a [Syntakts] instance in compose
22 | *
23 | * Simple example:
24 | * ```
25 | * val syntakts = rememberSyntakts {
26 | * rule("@([A-z0-9_]+)") { result, context ->
27 | * append(result.value)
28 | * }
29 | * }
30 | * ```
31 | *
32 | * @see [rememberRendered]
33 | * @see [render]
34 | *
35 | * @param builder A lambda given an instance of [Syntakts.Builder], this is where you declare rules
36 | * @return [Syntakts]
37 | */
38 | @Composable
39 | public fun rememberSyntakts(builder: Syntakts.Builder.() -> Unit): Syntakts = remember { syntakts(builder) }
40 |
41 | /**
42 | * Parse and render the given [text] using the defined rules into an [AnnotatedString]
43 | * ---
44 | * Use [rememberRendered] when in a composable scope
45 | *
46 | * @see [rememberRendered]
47 | *
48 | * @param text What to parse and render
49 | * @param context Additional information that nodes may need to render
50 | * @param fontResolver Used to retrieve fonts from specified font names
51 | * @return [AnnotatedString] - The final rendered text to be used on a Text component
52 | */
53 | public fun Syntakts.render(
54 | text: String,
55 | context: C,
56 | fontResolver: ComposeFontResolver = DefaultFontResolver,
57 | builder: AnnotatedStyledTextBuilder = AnnotatedStyledTextBuilder(fontResolver = fontResolver)
58 | ): AnnotatedString {
59 | val nodes = parse(text)
60 | for (node in nodes) {
61 | node.render(builder, context)
62 | }
63 | return builder.build()
64 | }
65 |
66 | /**
67 | * Remember the rendered text, only updates when either [text] or [context] updates
68 | *
69 | * @see [rememberAsyncRendered]
70 | *
71 | * @param text What to parse and render
72 | * @param context Additional information that nodes may need to render
73 | * @param fontResolver Used to retrieve fonts from specified font names
74 | * @return [AnnotatedString] - The final rendered text to be used on a Text component
75 | */
76 | @Composable
77 | public fun Syntakts.rememberRendered(
78 | text: String,
79 | context: C,
80 | fontResolver: ComposeFontResolver = DefaultFontResolver
81 | ): AnnotatedString {
82 | val builder = remember { AnnotatedStyledTextBuilder(fontResolver = fontResolver) }
83 |
84 | LaunchedEffect(text, context) {
85 | builder.clear()
86 | ClickHandlerStore.clearForBuilder(builder.id)
87 | }
88 |
89 | DisposableEffect(Unit) {
90 | onDispose {
91 | ClickHandlerStore.clearForBuilder(builder.id)
92 | }
93 | }
94 |
95 | return remember(text, context, fontResolver) {
96 | render(text, context, fontResolver, builder)
97 | }
98 | }
99 |
100 | /**
101 | * Remember the rendered text, only updates when either [text] or [context] updates
102 | * ---
103 | * This will do the text parsing in a separate thread and update the text when the rendering finishes
104 | *
105 | * @param text What to parse and render
106 | * @param context Additional information that nodes may need to render
107 | * @param fontResolver Used to retrieve fonts from specified font names
108 | * @return [AnnotatedString] - The final rendered text to be used on a Text component
109 | */
110 | @Composable
111 | public fun Syntakts.rememberAsyncRendered(
112 | text: String,
113 | context: C,
114 | fontResolver: ComposeFontResolver = DefaultFontResolver
115 | ): AnnotatedString {
116 | var parsedText by remember { mutableStateOf(AnnotatedString(text)) }
117 | val builder = remember { AnnotatedStyledTextBuilder(fontResolver = fontResolver) }
118 |
119 | LaunchedEffect(text, context) {
120 | builder.clear()
121 | ClickHandlerStore.clearForBuilder(builder.id)
122 | launch(Dispatchers.IO) {
123 | parsedText = render(text, context, fontResolver, builder)
124 | }
125 | }
126 |
127 | DisposableEffect(Unit) {
128 | onDispose {
129 | ClickHandlerStore.clearForBuilder(builder.id)
130 | }
131 | }
132 |
133 | return parsedText
134 | }
135 |
136 | /**
137 | * Remember the rendered text, only updates when [text] updates
138 | *
139 | * @param text What to parse and render
140 | * @param fontResolver Used to retrieve fonts from specified font names
141 | * @return [AnnotatedString] - The final rendered text to be used on a Text or [ClickableText][xyz.wingio.syntakts.compose.clickable.ClickableText] component
142 | */
143 | @Composable
144 | public fun Syntakts.rememberRendered(
145 | text: String,
146 | fontResolver: ComposeFontResolver = DefaultFontResolver
147 | ): AnnotatedString = rememberRendered(text, Unit, fontResolver)
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/clickable/ClickHandlerStore.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.clickable
2 |
3 | import xyz.wingio.syntakts.compose.style.CLICKABLE_ID_SEPARATOR
4 |
5 | /**
6 | * Stores handlers for clickable annotations, this is only needed due to how compose handles text clicking
7 | */
8 | public object ClickHandlerStore {
9 |
10 | private val items: MutableMap* builder id */ String, MutableMap* handler id */ String, /* OnClick or OnLongClick */ () -> Unit>> = mutableMapOf()
11 |
12 | /**
13 | * Gets the handler for a given [id]
14 | *
15 | * @param id The id for the desired handler, must be two UUIDv4 strings separated by "||"
16 | */
17 | public operator fun get(id: String): (() -> Unit)? {
18 | if (!id.contains(CLICKABLE_ID_SEPARATOR)) return null
19 | val (builderId, handlerId) = id.split(CLICKABLE_ID_SEPARATOR, limit = 2)
20 | return items[builderId]?.get(handlerId)
21 | }
22 |
23 | /**
24 | * Adds a handler with a given [builderId] and [handlerId]
25 | *
26 | * @param builderId The id of the [AnnotatedStyledTextBuilder][xyz.wingio.syntakts.compose.style.AnnotatedStyledTextBuilder] that the clickable text comes from
27 | * @param handlerId The id of the specific click handler
28 | * @param handler Function to be called when specified text is clicked
29 | */
30 | public fun add(builderId: String, handlerId: String, handler: () -> Unit) {
31 | if(items.containsKey(builderId)) {
32 | items[builderId]!![handlerId] = handler
33 | } else {
34 | items[builderId] = mutableMapOf(handlerId to handler)
35 | }
36 | }
37 |
38 | /**
39 | * Clear all handlers for a builder
40 | *
41 | * @param builderId The id of the builder
42 | */
43 | public fun clearForBuilder(builderId: String) {
44 | items.remove(builderId)
45 | }
46 |
47 | }
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/clickable/ClickableText.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.clickable
2 |
3 | import androidx.compose.foundation.gestures.detectTapGestures
4 | import androidx.compose.foundation.text.BasicText
5 | import androidx.compose.foundation.text.InlineTextContent
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.setValue
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.geometry.Offset
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.takeOrElse
15 | import androidx.compose.ui.input.pointer.pointerInput
16 | import androidx.compose.ui.text.AnnotatedString
17 | import androidx.compose.ui.text.TextLayoutResult
18 | import androidx.compose.ui.text.TextStyle
19 | import androidx.compose.ui.text.font.FontFamily
20 | import androidx.compose.ui.text.font.FontStyle
21 | import androidx.compose.ui.text.font.FontWeight
22 | import androidx.compose.ui.text.style.TextAlign
23 | import androidx.compose.ui.text.style.TextDecoration
24 | import androidx.compose.ui.text.style.TextOverflow
25 | import androidx.compose.ui.unit.TextUnit
26 | import xyz.wingio.syntakts.compose.style.CLICKABLE_ANNOTATION_TAG
27 | import xyz.wingio.syntakts.compose.style.LONG_CLICKABLE_ANNOTATION_TAG
28 |
29 | /**
30 | * Basic Text component that enabled support for Syntakts click handling
31 | * ---
32 | * **Does not support Material theming out of the box**
33 | *
34 | * @param text The rendered text
35 | * @param modifier Used to alter how the component looks or behaves
36 | * @param color Color of the text
37 | * @param fontSize Size of the text
38 | * @param fontWeight The thickness of the font glyphs
39 | * @param fontStyle Whether the font is italic or normal
40 | * @param fontFamily The specific fonts that should be used
41 | * @param letterSpacing Spacing between each character
42 | * @param textDecoration Used to draw a line over the text
43 | * @param textAlign How the text should be aligned
44 | * @param lineHeight Line height for the paragraph
45 | * @param style Styling configuration for the text
46 | * @param overflow How the text should handle visual overflow
47 | * @param softWrap Whether the text should break at soft line breaks
48 | * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary
49 | * @param inlineContent A map store composables that replaces certain ranges of the text
50 | */
51 | @Composable
52 | public fun ClickableText(
53 | text: AnnotatedString,
54 | modifier: Modifier = Modifier,
55 | color: Color = Color.Unspecified,
56 | fontSize: TextUnit = TextUnit.Unspecified,
57 | fontWeight: FontWeight? = null,
58 | fontStyle: FontStyle? = null,
59 | fontFamily: FontFamily? = null,
60 | letterSpacing: TextUnit = TextUnit.Unspecified,
61 | textDecoration: TextDecoration? = null,
62 | textAlign: TextAlign = TextAlign.Unspecified,
63 | lineHeight: TextUnit = TextUnit.Unspecified,
64 | style: TextStyle = TextStyle(),
65 | overflow: TextOverflow = TextOverflow.Clip,
66 | softWrap: Boolean = true,
67 | maxLines: Int = Int.MAX_VALUE,
68 | inlineContent: Map = mapOf()
69 | ) {
70 | var layoutResult by remember { mutableStateOf(null) }
71 | val isClickable = text.getStringAnnotations(0, text.length).find {
72 | it.tag == CLICKABLE_ANNOTATION_TAG || it.tag == LONG_CLICKABLE_ANNOTATION_TAG
73 | } != null
74 |
75 | val textColor = color.takeOrElse {
76 | style.color.takeOrElse {
77 | Color.White
78 | }
79 | }
80 |
81 | val textStyle = style.merge(
82 | TextStyle(
83 | color = textColor,
84 | fontSize = fontSize,
85 | fontWeight = fontWeight,
86 | textAlign = textAlign,
87 | lineHeight = lineHeight,
88 | fontFamily = fontFamily,
89 | textDecoration = textDecoration,
90 | fontStyle = fontStyle,
91 | letterSpacing = letterSpacing
92 | )
93 | )
94 |
95 | BasicText(
96 | text = text,
97 | modifier = modifier.thenIf(isClickable) {
98 | clickableText(text, layoutResult)
99 | },
100 | style = textStyle,
101 | overflow = overflow,
102 | softWrap = softWrap,
103 | maxLines = maxLines,
104 | inlineContent = inlineContent,
105 | onTextLayout = {
106 | layoutResult = it
107 | }
108 | )
109 | }
110 |
111 | /**
112 | * Adds support for handling clickable text done with [AnnotatedStyledTextBuilder][xyz.wingio.syntakts.compose.style.AnnotatedStyledTextBuilder]
113 | *
114 | * @param annotatedString Rendered text, needed to get the annotations
115 | * @param textLayoutResult Required for properly handling annotations
116 | */
117 | @Composable
118 | public fun Modifier.clickableText(annotatedString: AnnotatedString, textLayoutResult: TextLayoutResult?): Modifier {
119 | fun handleAnnotation(offset: Offset, annotationTag: String) {
120 | val pos = textLayoutResult?.getOffsetForPosition(offset) ?: return
121 | annotatedString
122 | .getStringAnnotations(pos, pos)
123 | .filter { it.tag == annotationTag }
124 | .forEach {
125 | ClickHandlerStore[it.item]?.invoke()
126 | }
127 | }
128 |
129 | return pointerInput(textLayoutResult) {
130 | if(textLayoutResult != null) {
131 | detectTapGestures(
132 | onTap = { offset ->
133 | handleAnnotation(offset, CLICKABLE_ANNOTATION_TAG)
134 | },
135 | onLongPress = { offset ->
136 | handleAnnotation(offset, LONG_CLICKABLE_ANNOTATION_TAG)
137 | }
138 | )
139 | }
140 | }
141 | }
142 |
143 | internal inline fun Modifier.thenIf(predicate: Boolean, block: Modifier.() -> Modifier): Modifier {
144 | return if (predicate) then(block()) else this
145 | }
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/AnnotatedStyledTextBuilder.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.foundation.text.InlineTextContent
4 | import androidx.compose.foundation.text.appendInlineContent
5 | import androidx.compose.ui.text.AnnotatedString
6 | import androidx.compose.ui.text.ParagraphStyle
7 | import androidx.compose.ui.text.SpanStyle
8 | import androidx.compose.ui.text.style.LineBreak
9 | import androidx.compose.ui.text.style.LineHeightStyle
10 | import com.benasher44.uuid.uuid4
11 | import xyz.wingio.syntakts.compose.clickable.ClickHandlerStore
12 | import xyz.wingio.syntakts.style.Style
13 | import xyz.wingio.syntakts.style.StyledTextBuilder
14 |
15 | internal const val CLICKABLE_ANNOTATION_TAG: String = "xyz.wingio.syntakts.clickable"
16 | internal const val LONG_CLICKABLE_ANNOTATION_TAG: String = "xyz.wingio.syntakts.longclickable"
17 | internal const val CLICKABLE_ID_SEPARATOR: String = "||"
18 |
19 | /**
20 | * Instance of [StyledTextBuilder] that builds [AnnotatedString]s
21 | *
22 | * @param builder (Optional) Base [AnnotatedString.Builder] to use
23 | * @param fontResolver Resolves [FontFamilies][androidx.compose.ui.text.font.FontFamily] from font names
24 | */
25 | public class AnnotatedStyledTextBuilder(
26 | private var builder: AnnotatedString.Builder = AnnotatedString.Builder(),
27 | private val fontResolver: ComposeFontResolver
28 | ): StyledTextBuilder {
29 | internal val id = uuid4().toString()
30 |
31 | /**
32 | * Instance of [StyledTextBuilder] that builds [AnnotatedString]s
33 | *
34 | * @param text Initial text
35 | * @param fontResolver Resolves [FontFamilies][androidx.compose.ui.text.font.FontFamily] from font names
36 | */
37 | public constructor(text: String, fontResolver: ComposeFontResolver): this(AnnotatedString.Builder(text), fontResolver)
38 |
39 | override val length: Int
40 | get() = builder.length
41 |
42 | override fun append(text: CharSequence, style: Style?): AnnotatedStyledTextBuilder {
43 | val i = length
44 | builder.append(text)
45 |
46 | style?.let {
47 | builder.addStyle(it.toSpanStyle(), i, builder.length)
48 | it.applyParagraphStyle(i, builder.length)
49 | }
50 | return this
51 | }
52 |
53 | override fun append(text: CharSequence, style: Style.() -> Unit): AnnotatedStyledTextBuilder {
54 | return append(text, Style().apply(style))
55 | }
56 |
57 | override fun appendClickable(
58 | text: CharSequence,
59 | style: Style?,
60 | onLongClick: (() -> Unit)?,
61 | onClick: () -> Unit
62 | ): AnnotatedStyledTextBuilder {
63 | val i = length
64 |
65 | builder.append(text)
66 |
67 | // OnClick
68 | val onClickHandlerId = uuid4().toString()
69 | builder.addStringAnnotation(CLICKABLE_ANNOTATION_TAG, "$id$CLICKABLE_ID_SEPARATOR$onClickHandlerId", i, builder.length)
70 | ClickHandlerStore.add(id, onClickHandlerId, onClick)
71 |
72 | // OnLongClick
73 | onLongClick?.let {
74 | val onLongClickHandlerId = uuid4().toString()
75 | builder.addStringAnnotation(LONG_CLICKABLE_ANNOTATION_TAG, "$id$CLICKABLE_ID_SEPARATOR$onLongClickHandlerId", i, builder.length)
76 | ClickHandlerStore.add(id, onLongClickHandlerId, onLongClick)
77 | }
78 |
79 | style?.let {
80 | builder.addStyle(it.toSpanStyle(), i, builder.length)
81 | it.applyParagraphStyle(i, builder.length)
82 | }
83 |
84 | return this
85 | }
86 |
87 | override fun appendAnnotated(
88 | text: CharSequence,
89 | tag: String,
90 | annotation: String
91 | ): AnnotatedStyledTextBuilder {
92 | val i = length
93 | append(text)
94 | addAnnotation(tag, annotation, i, length)
95 | return this
96 | }
97 |
98 | override fun addClickable(
99 | startIndex: Int,
100 | endIndex: Int,
101 | onLongClick: (() -> Unit)?,
102 | onClick: () -> Unit
103 | ): StyledTextBuilder {
104 | // OnClick
105 | val onClickHandlerId = uuid4().toString()
106 | builder.addStringAnnotation(CLICKABLE_ANNOTATION_TAG, "$id$CLICKABLE_ID_SEPARATOR$onClickHandlerId", startIndex, endIndex)
107 | ClickHandlerStore.add(id, onClickHandlerId, onClick)
108 |
109 | // OnLongClick
110 | onLongClick?.let {
111 | val onLongClickHandlerId = uuid4().toString()
112 | builder.addStringAnnotation(LONG_CLICKABLE_ANNOTATION_TAG, "$id$CLICKABLE_ID_SEPARATOR$onLongClickHandlerId", startIndex, endIndex)
113 | ClickHandlerStore.add(id, onLongClickHandlerId, onLongClick)
114 | }
115 |
116 | return this
117 | }
118 |
119 | override fun addStyle(style: Style, startIndex: Int, endIndex: Int): AnnotatedStyledTextBuilder {
120 | builder.addStyle(style.toSpanStyle(), startIndex, endIndex)
121 | style.applyParagraphStyle(startIndex, endIndex)
122 | return this
123 | }
124 |
125 | override fun addStyle(startIndex: Int, endIndex: Int, style: Style.() -> Unit): AnnotatedStyledTextBuilder {
126 | val _style = Style().apply(style)
127 | builder.addStyle(_style.toSpanStyle(), startIndex, endIndex)
128 | _style.applyParagraphStyle(startIndex, endIndex)
129 | return this
130 | }
131 |
132 | override fun addAnnotation(
133 | tag: String,
134 | annotation: String,
135 | startIndex: Int,
136 | endIndex: Int
137 | ): AnnotatedStyledTextBuilder {
138 | builder.addStringAnnotation(tag, annotation, startIndex, endIndex)
139 | return this
140 | }
141 |
142 | override fun clear() {
143 | builder = AnnotatedString.Builder()
144 | }
145 |
146 | override fun build(): AnnotatedString = builder.toAnnotatedString()
147 |
148 | private fun Style.toSpanStyle(): SpanStyle {
149 | return SpanStyle(
150 | color = color.toComposeColor(),
151 | fontWeight = fontWeight?.toComposeFontWeight(),
152 | fontStyle = fontStyle?.toComposeFontStyle(),
153 | background = background.toComposeColor(),
154 | fontSize = fontSize.toComposeTextUnit(),
155 | letterSpacing = letterSpacing.toComposeTextUnit(),
156 | textDecoration = textDecoration?.toComposeTextDecoration(),
157 | fontFamily = font?.let(fontResolver::resolveFont)
158 | )
159 | }
160 |
161 | private fun Style.applyParagraphStyle(startIndex: Int, endIndex: Int) {
162 | paragraphStyle?.run {
163 | builder.addStyle(
164 | ParagraphStyle(
165 | lineHeight = lineHeight.toComposeTextUnit(),
166 | lineBreak = LineBreak.Heading,
167 | lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.Both)
168 | ),
169 | startIndex,
170 | endIndex
171 | )
172 | }
173 | }
174 |
175 | }
176 |
177 | /**
178 | * Used to insert composables into the text layout
179 | *
180 | * @param id The id used to look up the [InlineTextContent]
181 | * @param alternateText The text to be replaced by the inline content.
182 | */
183 | public fun StyledTextBuilder.appendInlineContent(
184 | id: String,
185 | alternateText: CharSequence = "\uFFFD"
186 | ): StyledTextBuilder {
187 | appendAnnotated(
188 | text = alternateText,
189 | tag = "androidx.compose.foundation.text.inlineContent",
190 | annotation = id
191 | )
192 | return this
193 | }
194 |
195 | /**
196 | * Used to insert composables into the text layout, replaces any text within the [range]
197 | *
198 | * @see appendInlineContent
199 | *
200 | * @param id The id used to look up the [InlineTextContent]
201 | * @param range (inclusive start, exclusive end) Where the content will be placed
202 | */
203 | public fun StyledTextBuilder.addInlineContent(
204 | id: String,
205 | range: IntRange
206 | ): StyledTextBuilder = addInlineContent(id, range.first, range.last + 1)
207 |
208 | /**
209 | * Used to insert composables into the text layout, replaces any text within the range of [startIndex] to [endIndex]]
210 | *
211 | * @see appendInlineContent
212 | *
213 | * @param id The id used to look up the [InlineTextContent]
214 | * @param startIndex (inclusive) Start of the replaced text
215 | * @param endIndex (exclusive) End of the replaced text
216 | */
217 | public fun StyledTextBuilder.addInlineContent(
218 | id: String,
219 | startIndex: Int,
220 | endIndex: Int,
221 | ): StyledTextBuilder {
222 | addAnnotation("androidx.compose.foundation.text.inlineContent", id, startIndex, endIndex)
223 | return this
224 | }
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/Color.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.ui.graphics.Color as ComposeColor
4 | import xyz.wingio.syntakts.style.Color
5 |
6 | /**
7 | * Converts a [Color] to it's compose representation
8 | */
9 | public fun Color?.toComposeColor(): ComposeColor {
10 | if(this == null || ignore) return ComposeColor.Unspecified
11 | return ComposeColor(red, green, blue, alpha)
12 | }
13 |
14 | /**
15 | * Converts a [Color][ComposeColor] to it's Syntakts representation
16 | */
17 | public fun ComposeColor.toSyntaktsColor(): Color = Color(
18 | red = (red * 255).toInt(),
19 | green = (green * 255).toInt(),
20 | blue = (blue * 255).toInt(),
21 | alpha = (alpha * 255).toInt()
22 | )
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/ComposeFontResolver.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.ui.text.font.FontFamily
4 | import xyz.wingio.syntakts.style.FontResolver
5 | import xyz.wingio.syntakts.style.Fonts
6 |
7 | /**
8 | * Resolves fonts into a [FontFamily] from a font name
9 | *
10 | * For most cases it is reccommended to use [DefaultFontResolver] to register your fonts
11 | */
12 | public class ComposeFontResolver: FontResolver() {
13 |
14 | override val fontMap: MutableMap = mutableMapOf(
15 | Fonts.DEFAULT to FontFamily.Default,
16 | Fonts.MONOSPACE to FontFamily.Monospace,
17 | "monospaced" to FontFamily.Monospace,
18 | Fonts.SERIF to FontFamily.Serif,
19 | Fonts.SANS_SERIF to FontFamily.SansSerif
20 | )
21 |
22 | /**
23 | * Gets a [FontFamily] from the given [fontName]
24 | *
25 | * @param fontName Case-insensitive name for the desired font
26 | */
27 | override fun resolveFont(fontName: String): FontFamily? {
28 | return fontMap[fontName.lowercase()]
29 | }
30 |
31 | /**
32 | * Registers a [FontFamily] with a specific [fontName]
33 | *
34 | * @param fontName Case-insensitive name for the [font][platformFont]
35 | * @param platformFont The [FontFamily] to assign to the provided [fontName]
36 | */
37 | override fun registerFont(fontName: String, platformFont: FontFamily) {
38 | fontMap[fontName.lowercase()] = platformFont
39 | }
40 |
41 | }
42 |
43 | /**
44 | * Default [FontResolver] used when rendering
45 | */
46 | public val DefaultFontResolver: ComposeFontResolver = ComposeFontResolver()
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/FontStyle.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.ui.text.font.FontStyle as ComposeFontStyle
4 | import xyz.wingio.syntakts.style.FontStyle
5 |
6 | /**
7 | * Converts a [FontStyle] to it's compose representation
8 | */
9 | public fun FontStyle.toComposeFontStyle(): ComposeFontStyle = when(this) {
10 | FontStyle.Normal -> ComposeFontStyle.Normal
11 | FontStyle.Italic -> ComposeFontStyle.Italic
12 | }
13 |
14 | /**
15 | * Converts a [ComposeFontStyle] to it's Syntakts representation
16 | */
17 | public fun ComposeFontStyle.toComposeFontStyle(): FontStyle = when(this) {
18 | ComposeFontStyle.Normal -> FontStyle.Normal
19 | ComposeFontStyle.Italic -> FontStyle.Italic
20 | else -> FontStyle.Normal
21 | }
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/FontWeight.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.ui.text.font.FontWeight as ComposeFontWeight
4 | import xyz.wingio.syntakts.style.FontWeight
5 |
6 | /**
7 | * Converts a [FontWeight] into its compose representation
8 | */
9 | public fun FontWeight.toComposeFontWeight(): ComposeFontWeight = ComposeFontWeight(weight)
10 |
11 | /**
12 | * Converts a [ComposeFontWeight] into its Syntakts representation
13 | */
14 | public fun ComposeFontWeight.toSyntaktsFontWeight(): FontWeight = FontWeight(weight)
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/TextDecoration.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.ui.text.style.TextDecoration as ComposeTextDecoration
4 | import xyz.wingio.syntakts.style.TextDecoration
5 |
6 | /**
7 | * Converts [TextDecoration] to its compose representation
8 | */
9 | public fun TextDecoration.toComposeTextDecoration(): ComposeTextDecoration = when(this) {
10 | TextDecoration.None -> ComposeTextDecoration.None
11 | TextDecoration.Underline -> ComposeTextDecoration.Underline
12 | TextDecoration.LineThrough -> ComposeTextDecoration.LineThrough
13 | }
14 |
15 | /**
16 | * Converts a [ComposeTextDecoration] to its Syntakts representation
17 | */
18 | public fun ComposeTextDecoration.toSyntaktsTextDecoration(): TextDecoration = when(this) {
19 | ComposeTextDecoration.None -> TextDecoration.None
20 | ComposeTextDecoration.Underline -> TextDecoration.Underline
21 | ComposeTextDecoration.LineThrough -> TextDecoration.LineThrough
22 | else -> TextDecoration.None
23 | }
--------------------------------------------------------------------------------
/syntakts-compose/src/commonMain/kotlin/xyz/wingio/syntakts/compose/style/TextUnit.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.compose.style
2 |
3 | import androidx.compose.ui.unit.TextUnitType
4 | import androidx.compose.ui.unit.em
5 | import androidx.compose.ui.unit.sp
6 | import xyz.wingio.syntakts.style.Em
7 | import xyz.wingio.syntakts.style.Sp
8 | import androidx.compose.ui.unit.TextUnit as ComposeTextUnit
9 | import xyz.wingio.syntakts.style.TextUnit
10 |
11 | /**
12 | * Converts a [TextUnit] into its compose representation
13 | */
14 | public fun TextUnit.toComposeTextUnit(): ComposeTextUnit = when(this) {
15 | is Sp -> value.sp
16 | is Em -> value.em
17 | else -> ComposeTextUnit.Unspecified
18 | }
19 |
20 | /**
21 | * Converts a [ComposeTextUnit] into its Syntakts representation
22 | */
23 | public fun ComposeTextUnit.toSyntaktsTextUnit(): TextUnit = when(type) {
24 | TextUnitType.Companion.Sp -> Sp(value)
25 | TextUnitType.Companion.Em -> Em(value)
26 | else -> TextUnit.Unspecified
27 | }
--------------------------------------------------------------------------------
/syntakts-core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/syntakts-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.library")
4 | id("com.vanniktech.maven.publish.base")
5 | alias(libs.plugins.binary.compatibility)
6 | }
7 |
8 | setup(
9 | libName = "Syntakts Core",
10 | moduleName = "syntakts-core",
11 | moduleDescription = "Easy to use text parsing and syntax highlighting library"
12 | )
13 |
14 | kotlin {
15 | androidTarget() {
16 | publishLibraryVariants("release")
17 | }
18 | jvm()
19 |
20 | jvmToolchain(17)
21 | explicitApi()
22 |
23 | sourceSets {
24 | commonMain {
25 | dependencies {
26 | compileOnly(libs.compose.stable.marker)
27 | implementation(libs.kotlin.coroutines.core)
28 | }
29 | }
30 |
31 | commonTest {
32 | dependencies {
33 | implementation(kotlin("test"))
34 | implementation(libs.junit)
35 | }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/syntakts-core/src/androidMain/kotlin/xyz/wingio/syntakts/util/Logger.android.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 | import android.util.Log
4 |
5 | internal actual class LoggerImpl actual constructor(
6 | private val tag: String
7 | ): Logger {
8 |
9 | override fun info(message: String) {
10 | Log.i(tag, message)
11 | }
12 |
13 | override fun debug(message: String) {
14 | Log.d(tag, message)
15 | }
16 |
17 | override fun warn(message: String, throwable: Throwable?) {
18 | Log.w(tag, message, throwable)
19 | }
20 |
21 | override fun error(message: String, throwable: Throwable?) {
22 | Log.e(tag, message, throwable)
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/ParseException.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts
2 |
3 | public class ParseException internal constructor (message: String, source: CharSequence?, cause: Throwable? = null)
4 | : RuntimeException("Error while parsing: $message \n Source: $source", cause)
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/Syntakts.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.node.Node
5 | import xyz.wingio.syntakts.node.node
6 | import xyz.wingio.syntakts.parser.ParseRule
7 | import xyz.wingio.syntakts.parser.ParseSpec
8 | import xyz.wingio.syntakts.parser.Rule
9 | import xyz.wingio.syntakts.parser.addTextRule
10 | import xyz.wingio.syntakts.style.StyledTextBuilder
11 | import xyz.wingio.syntakts.util.Logger
12 | import xyz.wingio.syntakts.util.LoggerImpl
13 | import xyz.wingio.syntakts.util.SynchronizedCache
14 | import xyz.wingio.syntakts.util.Stack
15 | import xyz.wingio.syntakts.util.firstMapOrNull
16 |
17 | /**
18 | * The base class used to parse any input string into AST [Node]s from as set of [Rule]s
19 | *
20 | * @param C The context passed to a [Node] when its getting rendered, can be any class
21 | */
22 | @Stable
23 | public class Syntakts internal constructor(
24 | private val rules: List>,
25 | @Deprecated("Use debugOptions instead")
26 | private val debug: Boolean = false,
27 | private val debugOptions: DebugOptions
28 | ){
29 |
30 | /**
31 | * Configures options for debugging
32 | *
33 | * @param enableLogging Whether or not logging is enabled
34 | * @param logger Used to log rule matches and parsing time
35 | * @param storeMetadata Whether to store some metadata in each node
36 | */
37 | @Stable
38 | public data class DebugOptions(
39 | var enableLogging: Boolean = false,
40 | var logger: Logger = LoggerImpl(tag = "Syntakts"),
41 | var storeMetadata: Boolean = false
42 | )
43 |
44 | /**
45 | * The class used to build an instance of [Syntakts], used within the [syntakts] DSL
46 | *
47 | * @param C The context passed to a [Node] when its getting rendered, can be any class
48 | */
49 | @Stable
50 | public class Builder {
51 |
52 | /**
53 | * The parsing rules that will be passed into a [Syntakts] when built
54 | */
55 | private val rules = mutableListOf>()
56 |
57 | /**
58 | * When enabled will log any rule misses and matches
59 | */
60 | @Deprecated("Use debugOptions instead")
61 | public var debug: Boolean = false
62 |
63 | /**
64 | * Options for debugging
65 | */
66 | public var debugOptions: DebugOptions = DebugOptions()
67 |
68 | /**
69 | * Create an instance of [Syntakts] with the currently defined rules
70 | */
71 | public fun build(): Syntakts {
72 | return Syntakts(rules, debug, debugOptions)
73 | }
74 |
75 | /**
76 | * Adds multiple rules
77 | *
78 | * @param rules The rules to add
79 | */
80 | public fun addRules(rules: Collection>) {
81 | this.rules.addAll(rules)
82 | }
83 |
84 | /**
85 | * Adds multiple rules
86 | *
87 | * @param rules The rules to add
88 | */
89 | public fun addRules(vararg rules: Rule) {
90 | this.rules.addAll(rules)
91 | }
92 |
93 | /**
94 | * Manually add an instance of a [Rule]
95 | *
96 | * @param rule The rule to add
97 | * @return [Builder] Used to chain methods together
98 | * @return [Builder] To allow for builder method chaining
99 | */
100 | public fun addRule(rule: Rule): Builder {
101 | rules.add(rule)
102 | return this
103 | }
104 |
105 | /**
106 | * Add a rule based on the specified [regex]
107 | *
108 | * ```
109 | * addRule("@([A-z]+)") { result ->
110 | * node {
111 | * append(result.value, style = Style(color = Color.CYAN))
112 | * }
113 | * }
114 | * ```
115 | *
116 | * @param regex The regex pattern used to define this rule
117 | * @param name (optional) The name of this rule
118 | * @param parse The callback to run when the pattern is found
119 | * @return [Builder] To allow for builder method chaining
120 | */
121 | public fun addRule(regex: String, name: String = "Unnamed Rule", parse: ParseRule): Builder = addRule(regex.toRegex(), name, parse)
122 |
123 | /**
124 | * Add a rule based on the specified [regex]
125 | *
126 | * ```
127 | * addRule("@([A-z]+)".toRegex()) { result ->
128 | * node {
129 | * append(result.value, style = Style(color = Color.CYAN))
130 | * }
131 | * }
132 | * ```
133 | *
134 | * @param regex The regex pattern used to define this rule
135 | * @param name (optional) The name of this rule
136 | * @param parse The callback to run when the pattern is found
137 | * @return [Builder] To allow for builder method chaining
138 | */
139 | public fun addRule(regex: Regex, name: String = "Unnamed Rule", parse: ParseRule): Builder {
140 | rules.add(Rule(regex, name, parse))
141 | return this
142 | }
143 |
144 | /**
145 | * Simplest way to add a rule based on the specified [regex], doesn't render any children or use any predefined nodes
146 | *
147 | * ```
148 | * rule("@([A-z]+)") { result, context ->
149 | * append(result.value, style = Style(color = Color.CYAN))
150 | * }
151 | * ```
152 | *
153 | * @param regex The regex pattern used to define this rule
154 | * @param name (optional) The name of this rule
155 | * @param render How to render the resulting node, see: [StyledTextBuilder]
156 | * @return [Builder] To allow for builder method chaining
157 | */
158 | public fun rule(regex: String, name: String = "Unnamed Rule", render: StyledTextBuilder<*>.(result: MatchResult, context: C) -> Unit): Builder = rule(regex.toRegex(), name, render)
159 |
160 | /**
161 | * Simplest way to add a rule based on the specified [regex], doesn't render any children or use any predefined nodes
162 | *
163 | * ```
164 | * rule("@([A-z]+)".toRegex()) { result, context ->
165 | * append(result.value, style = Style(color = Color.CYAN))
166 | * }
167 | * ```
168 | *
169 | * @param regex The regex pattern used to define this rule
170 | * @param name (optional) The name of this rule
171 | * @param render How to render the resulting node, see: [StyledTextBuilder]
172 | * @return [Builder] To allow for builder method chaining
173 | */
174 | public fun rule(regex: Regex, name: String = "Unnamed Rule", render: StyledTextBuilder<*>.(result: MatchResult, context: C) -> Unit): Builder {
175 | addRule(regex, name) { result -> node { context: C -> render(result, context) } }
176 | return this
177 | }
178 |
179 | /**
180 | * When enabled will log any rule misses and matches
181 | *
182 | * @param debug Whether debug mode is enabled
183 | * @return [Builder] To allow for builder method chaining
184 | */
185 | @Deprecated("Use debugOptions instead")
186 | public fun debug(debug: Boolean): Builder {
187 | debugOptions(enableLogging = debug)
188 | return this
189 | }
190 |
191 | /**
192 | * Configures options for debugging
193 | *
194 | * @param options Lambda for configuring options
195 | */
196 | public fun debugOptions(options: DebugOptions.() -> Unit): Builder {
197 | debugOptions.apply(options)
198 | return this
199 | }
200 |
201 | /**
202 | * Configures options for debugging
203 | *
204 | * @param enableLogging Whether or not logging is enabled
205 | * @param logger Used to log rule matches and parsing time
206 | * @param storeMetadata Whether to store some metadata in each node
207 | */
208 | // Make sure parameters here match the DebugOptions data class
209 | public fun debugOptions(
210 | enableLogging: Boolean = false,
211 | logger: Logger = LoggerImpl(tag = "Syntakts"),
212 | storeMetadata: Boolean = false
213 | ): Builder {
214 | debugOptions = DebugOptions(
215 | enableLogging = enableLogging,
216 | logger = logger,
217 | storeMetadata = storeMetadata
218 | )
219 | return this
220 | }
221 |
222 | /**
223 | * Set options for debugging
224 | *
225 | * @param options Options for debugging
226 | */
227 | public fun debugOptions(options: DebugOptions): Builder {
228 | debugOptions = options
229 | return this
230 | }
231 |
232 | }
233 |
234 | /**
235 | * Log a message to stdout with a defined prefix
236 | */
237 | private fun log(message: String) {
238 | if(debugOptions.enableLogging) debugOptions.logger.debug(message)
239 | }
240 |
241 | private val cache: SynchronizedCache = SynchronizedCache()
242 |
243 | /**
244 | * Parse an input using the specified [rules]
245 | *
246 | * @param text The text to parse
247 | * @return A list of [Node]s, used to render the final text
248 | * @throws ParseException when no matching rules could be found or when a rule fails to match
249 | */
250 | public fun parse(
251 | text: CharSequence
252 | ): List> {
253 | val start = System.currentTimeMillis()
254 | val remainingParses = Stack>()
255 | val rootNode = Node()
256 |
257 | var lastCapture: String? = null
258 |
259 | if(text.isNotEmpty()) {
260 | remainingParses.add(ParseSpec(rootNode, 0, text.length))
261 | }
262 |
263 | while (!remainingParses.isEmpty()) {
264 | val builder = remainingParses.pop()
265 |
266 | if (builder.startIndex >= builder.endIndex) {
267 | break
268 | }
269 |
270 | val inspectionSource = text.subSequence(builder.startIndex, builder.endIndex)
271 | val offset = builder.startIndex
272 |
273 | val (rule, matchResult) =
274 | rules.firstMapOrNull { rule ->
275 | val key = "${rule.regex}-$inspectionSource-$lastCapture"
276 |
277 | val matchResult = if(cache.hasKey(key))
278 | cache[key]
279 | else
280 | rule.match(inspectionSource, lastCapture).apply {
281 | if(cache.size > 10_000) cache.removeFirst()
282 | cache[key] = this
283 | }
284 |
285 | if (matchResult == null) {
286 | log("MISSED: ${rule._regex.pattern} in $inspectionSource")
287 | null
288 | } else {
289 | log("MATCHED: ${rule._regex.pattern} in $inspectionSource")
290 | rule to matchResult
291 | }
292 | }
293 | ?: throw ParseException("failed to find rule to match source", text)
294 |
295 | val matcherSourceEnd = matchResult.range.last + offset + 1
296 | val newBuilder = rule.parse(matchResult)
297 |
298 | if(debugOptions.storeMetadata) {
299 | newBuilder.root.setMetadata(rule.name, rule.regex, matchResult)
300 | }
301 |
302 | val parent = builder.root
303 | parent.addChild(newBuilder.root)
304 |
305 | // In case the last match didn't consume the rest of the source for this subtree,
306 | // make sure the rest of the source is consumed.
307 | if (matcherSourceEnd != builder.endIndex) {
308 | remainingParses.push(ParseSpec.createNonterminal(parent, matcherSourceEnd, builder.endIndex))
309 | }
310 |
311 | // We want to speak in terms of indices within the source string,
312 | // but the Rules only see the matchers in the context of the substring
313 | // being examined. Adding this offset addresses that issue.
314 | if (!newBuilder.isTerminal) {
315 | newBuilder.applyOffset(offset)
316 | remainingParses.push(newBuilder)
317 | }
318 |
319 | try {
320 | lastCapture = matchResult.groups[0]!!.value
321 | } catch (throwable: Throwable) {
322 | throw ParseException(message = "matcher found no matches", source = text, cause = throwable)
323 | }
324 | }
325 |
326 | val ast = rootNode.children?.toMutableList()
327 | log("Finished in ${System.currentTimeMillis() - start}ms")
328 | return ast ?: arrayListOf()
329 | }
330 |
331 | }
332 |
333 | /**
334 | * DSL for [Syntakts.Builder], the preferred way to set up a [Syntakts] instance
335 | *
336 | * Simple example:
337 | * ```
338 | * val syntakts = syntakts {
339 | * rule("@([A-z0-9_]+)") { result, context ->
340 | * append(result.value)
341 | * }
342 | * }
343 | * ```
344 | *
345 | * @param builder A lambda given an instance of [Syntakts.Builder], this is where you declare rules
346 | * @return [Syntakts]
347 | */
348 | public fun syntakts(builder: Syntakts.Builder.() -> Unit): Syntakts {
349 | val _builder = Syntakts.Builder().also(builder)
350 | _builder.addTextRule() // Add a plain text rule to match anything not caught by another rule
351 | return _builder.build()
352 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/markdown/Markdown.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.markdown
2 |
3 | import xyz.wingio.syntakts.Syntakts
4 | import xyz.wingio.syntakts.node.Node
5 | import xyz.wingio.syntakts.node.styleNode
6 | import xyz.wingio.syntakts.node.textNode
7 | import xyz.wingio.syntakts.parser.ParseSpec
8 | import xyz.wingio.syntakts.style.FontStyle
9 | import xyz.wingio.syntakts.style.FontWeight
10 | import xyz.wingio.syntakts.style.Style
11 | import xyz.wingio.syntakts.style.StyledTextBuilder
12 | import xyz.wingio.syntakts.style.TextDecoration
13 | import xyz.wingio.syntakts.syntakts
14 | import xyz.wingio.syntakts.util.hashCountToStyle
15 |
16 | private val ITALICS_REGEX = (
17 | // only match _s surrounding words.
18 | "\\b_" + "((?:__|\\\\[\\s\\S]|[^\\\\_])+?)_" + "\\b" +
19 | "|" +
20 | // Or match *s that are followed by a non-space:
21 | "^\\*(?=\\S)(" +
22 | // Match any of:
23 | // - `**`: so that bolds inside italics don't close the italics
24 | // - whitespace
25 | // - non-whitespace, non-* characters
26 | "(?:\\*\\*|\\s+(?:[^*\\s]|\\*\\*)|[^\\s*])+?" +
27 | // followed by a non-space, non-* then *
28 | ")\\*(?!\\*)"
29 | ).toRegex()
30 |
31 | private val BOLD_REGEX = "\\*\\*([\\s\\S]+?)\\*\\*(?!\\*)".toRegex()
32 |
33 | private val UNDERLINE_REGEX = "__([\\s\\S]+?)__(?!_)".toRegex()
34 |
35 | private val STRIKETHROUGH_REGEX = "~~([\\s\\S]+?)~~(?!~)".toRegex()
36 |
37 | private val HEADER_REGEX = "\\s*(#+)[ \\t](.+) *(?=\\n|\$)".toRegex()
38 |
39 |
40 | /**
41 | * Adds a simple bold rule
42 | *
43 | * Example: `**Some Text**` -> **Some Text**
44 | *
45 | * @return [Syntakts.Builder] To allow for builder method chaining
46 | */
47 | public fun Syntakts.Builder.addBoldRule(): Syntakts.Builder = addRule(BOLD_REGEX, name = "Bold") {
48 | styleNode(Style(fontWeight = FontWeight.Bold), it.groups[1]!!.range)
49 | }
50 |
51 | /**
52 | * Adds a simple italics rule
53 | *
54 | * Example: `*Some Text*` or `_Some Text_` -> *Some Text*
55 | *
56 | * @return [Syntakts.Builder] To allow for builder method chaining
57 | */
58 | public fun Syntakts.Builder.addItalicsRule(): Syntakts.Builder = addRule(ITALICS_REGEX, name = "Italics") {
59 | val asteriskMatch = it.groups[2]
60 | val range = if (asteriskMatch != null && asteriskMatch.value.isNotEmpty()) {
61 | asteriskMatch.range
62 | } else {
63 | it.groups[1]!!.range
64 | }
65 |
66 | styleNode(Style(fontStyle = FontStyle.Italic), range)
67 | }
68 |
69 | /**
70 | * Adds a simple underline rule
71 | *
72 | * Example: `__Some Text__`
73 | *
74 | * @return [Syntakts.Builder] To allow for builder method chaining
75 | */
76 | public fun Syntakts.Builder.addUnderlineRule(): Syntakts.Builder = addRule(UNDERLINE_REGEX, name = "Underline") {
77 | styleNode(Style(textDecoration = TextDecoration.Underline), it.groups[1]!!.range)
78 | }
79 |
80 | /**
81 | * Adds a simple strikethrough rule
82 | *
83 | * Example: `~~Some Text~~`
84 | *
85 | * @return [Syntakts.Builder] To allow for builder method chaining
86 | */
87 | public fun Syntakts.Builder.addStrikethroughRule(): Syntakts.Builder = addRule(STRIKETHROUGH_REGEX, name = "Strikethrough") {
88 | styleNode(Style(textDecoration = TextDecoration.LineThrough), it.groups[1]!!.range)
89 | }
90 |
91 | /**
92 | * Adds a simple header rule
93 | *
94 | * Example: `# Some Text`
95 | *
96 | * @return [Syntakts.Builder] To allow for builder method chaining
97 | */
98 | public fun Syntakts.Builder.addHeaderRule(): Syntakts.Builder = addRule(HEADER_REGEX, name = "Header") { result ->
99 | val hashCount = result.groups[1]!!.value.length
100 | val content = result.groups[2]!!
101 | ParseSpec.createNonterminal(
102 | object : Node.Parent() {
103 | override fun render(builder: StyledTextBuilder<*>, context: C) {
104 | val i = builder.length
105 | super.render(builder, context)
106 | builder.addStyle(hashCountToStyle(hashCount), i, builder.length)
107 | }
108 | },
109 | content.range.first,
110 | content.range.last + 1
111 | )
112 | }
113 |
114 | /**
115 | * Adds some basic markdown rules
116 | *
117 | * **Bold**: `**Some Text**` -> **Some Text**
118 | *
119 | * **Italics**: `*Some Text*` or `_Some Text_` -> *Some Text*
120 | *
121 | * **Underline**: `__Some Text__`
122 | *
123 | * **Strikethrough**: `~~Some Text~~`
124 | *
125 | * @see addMarkdownRules
126 | * @see addExtendedMarkdownRules
127 | */
128 | public fun Syntakts.Builder.addBasicMarkdownRules(): Syntakts.Builder {
129 | addStrikethroughRule()
130 | addUnderlineRule()
131 | addItalicsRule()
132 | addBoldRule()
133 | return this
134 | }
135 |
136 | /**
137 | * Adds more advanced markdown rules
138 | *
139 | * **Headers**: `# Header`
140 | *
141 | * @see addMarkdownRules
142 | * @see addBasicMarkdownRules
143 | */
144 | public fun Syntakts.Builder.addExtendedMarkdownRules(): Syntakts.Builder {
145 | addHeaderRule()
146 | return this
147 | }
148 |
149 | /**
150 | * Adds all markdown rules
151 | *
152 | * **Bold**: `**Some Text**` -> **Some Text**
153 | *
154 | * **Italics**: `*Some Text*` or `_Some Text_` -> *Some Text*
155 | *
156 | * **Underline**: `__Some Text__`
157 | *
158 | * **Strikethrough**: `~~Some Text~~`
159 | *
160 | * **Headers**: `# Header`
161 | *
162 | * @see addBasicMarkdownRules
163 | * @see addExtendedMarkdownRules
164 | */
165 | public fun Syntakts.Builder.addMarkdownRules(): Syntakts.Builder {
166 | addExtendedMarkdownRules()
167 | addBasicMarkdownRules()
168 | return this
169 | }
170 |
171 | /**
172 | * Instance of [Syntakts] with some basic markdown rules
173 | *
174 | * **Bold**: `**Some Text**` -> **Some Text**
175 | *
176 | * **Italics**: `*Some Text*` or `_Some Text_` -> *Some Text*
177 | *
178 | * **Underline**: `__Some Text__`
179 | *
180 | * **Strikethrough**: `~~Some Text~~`
181 | */
182 | public val BasicMarkdownSyntakts: Syntakts = syntakts {
183 | addBasicMarkdownRules()
184 | }
185 |
186 | /**
187 | * Instance of [Syntakts] with some markdown rules
188 | *
189 | * **Bold**: `**Some Text**` -> **Some Text**
190 | *
191 | * **Italics**: `*Some Text*` or `_Some Text_` -> *Some Text*
192 | *
193 | * **Underline**: `__Some Text__`
194 | *
195 | * **Strikethrough**: `~~Some Text~~`
196 | *
197 | * **Headers**: `# Header`
198 | */
199 | public val MarkdownSyntakts: Syntakts = syntakts {
200 | addMarkdownRules()
201 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/node/ClickableNode.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.node
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.parser.ParseSpec
5 | import xyz.wingio.syntakts.style.StyledTextBuilder
6 |
7 | /**
8 | * Node that marks its children as clickable
9 | *
10 | * @param onClick Callback for when the child nodes are clicked
11 | * @param onLongClick Callback for when the child nodes are long clicked
12 | */
13 | @Stable
14 | public class ClickableNode(
15 | public val onClick: () -> Unit,
16 | public val onLongClick: (() -> Unit)? = null
17 | ): Node.Parent() {
18 | override fun render(builder: StyledTextBuilder<*>, context: C) {
19 | val startIndex = builder.length
20 | super.render(builder, context)
21 |
22 | builder.addClickable(startIndex, builder.length, onLongClick, onClick)
23 | }
24 | }
25 |
26 | /**
27 | * Creates a simple [Node] that marks its children as clickable
28 | *
29 | * @param range (inclusive start, exclusive end) What part should be clickable
30 | * @param onLongClick Callback for when the child nodes are long clicked
31 | * @param onClick Method called when the children are clicked
32 | */
33 | public fun clickableNode(range: IntRange, onLongClick: (() -> Unit)? = null, onClick: () -> Unit): ParseSpec {
34 | return ParseSpec.createNonterminal(ClickableNode(onClick, onLongClick), range.first, range.last + 1)
35 | }
36 |
37 | /**
38 | * Creates a simple [Node] that marks its children as clickable
39 | *
40 | * @param startIndex (inclusive) Where the clickable area should start
41 | * @param endIndex (exclusive) Where the clickable area should end
42 | * @param onLongClick Callback for when the child nodes are long clicked
43 | * @param onClick Method called when the children are clicked
44 | */
45 | public fun clickableNode(startIndex: Int, endIndex: Int, onLongClick: (() -> Unit)? = null, onClick: () -> Unit): ParseSpec {
46 | return ParseSpec.createNonterminal(ClickableNode(onClick, onLongClick), startIndex, endIndex)
47 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/node/Node.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.node
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.style.StyledTextBuilder
5 |
6 | /**
7 | * Represents an AST node, this is what gets rendered
8 | *
9 | * @param _children Default child nodes
10 | * @param _metadata Metadata associated with this node, only present when the storeMetadata [debug option][xyz.wingio.syntakts.Syntakts.DebugOptions] is enabled
11 | * @param C (Context) Extra information to be used when rendering
12 | */
13 | @Stable
14 | public open class Node(
15 | private var _children: MutableCollection>? = null,
16 | private var _metadata: MetaData? = null
17 | ) {
18 |
19 | /**
20 | * Information to be stored alongside a [Node]
21 | *
22 | * @param ruleName Name of the rule that created this node
23 | * @param rule The pattern used to create this node
24 | * @param matchResult The matched text for this node
25 | */
26 | public data class MetaData(
27 | val ruleName: String,
28 | val rule: Regex,
29 | val matchResult: MatchResult
30 | )
31 |
32 | /**
33 | * Metadata associated with this node, only present when the storeMetadata [debug option][xyz.wingio.syntakts.Syntakts.DebugOptions] is enabled
34 | */
35 | public val metadata: MetaData?
36 | get() = _metadata
37 |
38 | /**
39 | * Child nodes
40 | */
41 | public val children: Collection>?
42 | get() = _children
43 |
44 | /**
45 | * Whether or not this node has any children
46 | */
47 | public val hasChildren: Boolean
48 | get() = children?.isNotEmpty() == true
49 |
50 | /**
51 | * Adds a child node
52 | *
53 | * @param child The new child node
54 | */
55 | public open fun addChild(child: Node) {
56 | _children = (_children ?: mutableListOf()).apply { add(child) }
57 | }
58 |
59 | /**
60 | * Set node metadata
61 | *
62 | * @param ruleName Name of the rule that created this node
63 | * @param rule The pattern used to create this node
64 | * @param matchResult The matched text for this node
65 | */
66 | public open fun setMetadata(
67 | ruleName: String,
68 | rule: Regex,
69 | matchResult: MatchResult
70 | ) {
71 | _metadata = MetaData(ruleName, rule, matchResult)
72 | }
73 |
74 | /**
75 | * Render this node
76 | *
77 | * @param builder Builder used to append text and apply styles
78 | * @param context Additional information used to help render this node
79 | */
80 | public open fun render(builder: StyledTextBuilder<*>, context: C) {}
81 |
82 | /**
83 | * Represents a node that only renders its children
84 | *
85 | * @param children Default child nodes
86 | * @param C (Context) Extra information to be used when rendering
87 | */
88 | public open class Parent(vararg children: Node?) : Node(children.mapNotNull { it }.toMutableList()) {
89 | override fun render(builder: StyledTextBuilder<*>, context: C) {
90 | children?.forEach { it.render(builder, context) }
91 | }
92 | }
93 |
94 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/node/NodeDsl.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.node
2 |
3 | import xyz.wingio.syntakts.parser.ParseSpec
4 | import xyz.wingio.syntakts.style.StyledTextBuilder
5 |
6 | /**
7 | * Creates a node that can contain children
8 | *
9 | * @param startIndex (inclusive) Where in the match to start parsing for children
10 | * @param endIndex (exclusive) Where in the match to stop parsing for children
11 | * @param renderer How to render the node
12 | * @return [ParseSpec] Describes how the [Node] should be processed
13 | */
14 | public fun nonTerminalNode(startIndex: Int, endIndex: Int, renderer: StyledTextBuilder<*>.(context: C) -> Unit): ParseSpec {
15 | return ParseSpec.createNonterminal(
16 | root = object : Node() {
17 | override fun render(builder: StyledTextBuilder<*>, context: C) {
18 | builder.renderer(context)
19 | }
20 | },
21 | startIndex = startIndex,
22 | endIndex = endIndex
23 | )
24 | }
25 |
26 | /**
27 | * Creates a node that can contain children
28 | *
29 | * @param range (inclusive start, exclusive end) Where in the match to look for children
30 | * @param renderer How to render the node
31 | * @return [ParseSpec] Describes how the [Node] should be processed
32 | */
33 | public fun nonTerminalNode(range: IntRange, renderer: StyledTextBuilder<*>.(context: C) -> Unit): ParseSpec
34 | = nonTerminalNode(range.first, range.last + 1, renderer)
35 |
36 | /**
37 | * Creates a node that can contain children
38 | *
39 | * @param startIndex (inclusive) Where in the match to start parsing for children
40 | * @param endIndex (exclusive) Where in the match to stop parsing for children
41 | * @param renderer How to render the node
42 | * @return [ParseSpec] Describes how the [Node] should be processed
43 | */
44 | public fun nodeWithChildren(startIndex: Int, endIndex: Int, renderer: StyledTextBuilder<*>.(context: C) -> Unit): ParseSpec
45 | = nonTerminalNode(startIndex, endIndex, renderer)
46 |
47 | /**
48 | * Creates a node that can contain children
49 | *
50 | * @param range (inclusive start, exclusive end) Where in the match to look for children
51 | * @param renderer How to render the node
52 | * @return [ParseSpec] Describes how the [Node] should be processed
53 | */
54 | public fun nodeWithChildren(range: IntRange, renderer: StyledTextBuilder<*>.(context: C) -> Unit): ParseSpec
55 | = nonTerminalNode(range, renderer)
56 |
57 | /**
58 | * Creates a node that does not contain children
59 | *
60 | * @see [node]
61 | *
62 | * @param renderer How to render the node
63 | * @return [ParseSpec] Describes how the [Node] should be processed
64 | */
65 | public fun terminalNode(renderer: StyledTextBuilder<*>.(context: C) -> Unit): ParseSpec {
66 | return ParseSpec.createTerminal(
67 | root = object : Node() {
68 | override fun render(builder: StyledTextBuilder<*>, context: C) {
69 | builder.renderer(context)
70 | }
71 | }
72 | )
73 | }
74 |
75 | /**
76 | * Creates a node that does not contain children
77 | *
78 | * @param renderer How to render the node
79 | * @return [ParseSpec] Describes how the [Node] should be processed
80 | */
81 | public fun node(renderer: StyledTextBuilder<*>.(context: C) -> Unit): ParseSpec
82 | = terminalNode(renderer)
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/node/StyleNode.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.node
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.parser.ParseSpec
5 | import xyz.wingio.syntakts.style.Style
6 | import xyz.wingio.syntakts.style.StyledTextBuilder
7 |
8 | /**
9 | * Node that applies a [style] to its children
10 | *
11 | * @param style The style to apply
12 | */
13 | @Stable
14 | public open class StyleNode(
15 | public val style: Style
16 | ): Node.Parent() {
17 | override fun render(builder: StyledTextBuilder<*>, context: C) {
18 | val startIndex = builder.length
19 | super.render(builder, context)
20 |
21 | builder.addStyle(style, startIndex, builder.length)
22 | }
23 | }
24 |
25 | /**
26 | * Creates a simple [Node] that applies a [style] to its children
27 | *
28 | * @param style The style to apply
29 | * @param range (inclusive start, exclusive end) Where this style should apply
30 | */
31 | public fun styleNode(style: Style, range: IntRange): ParseSpec {
32 | return ParseSpec.createNonterminal(StyleNode(style), range.first, range.last + 1)
33 | }
34 |
35 | /**
36 | * Creates a simple [Node] that applies a [style] to its children
37 | *
38 | * @param style The style to apply
39 | * @param startIndex (inclusive) Where the style should start
40 | * @param endIndex (exclusive) Where the style should end
41 | */
42 | public fun styleNode(style: Style, startIndex: Int, endIndex: Int): ParseSpec {
43 | return ParseSpec.createNonterminal(StyleNode(style), startIndex, endIndex)
44 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/node/TextNode.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.node
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.parser.ParseSpec
5 | import xyz.wingio.syntakts.style.StyledTextBuilder
6 |
7 | /**
8 | * [Node] that only appends plain text
9 | *
10 | * @param content The text to append
11 | */
12 | @Stable
13 | public open class TextNode(
14 | public val content: CharSequence
15 | ): Node() {
16 | override fun render(builder: StyledTextBuilder<*>, context: C) {
17 | builder.append(content)
18 | }
19 | }
20 |
21 | /**
22 | * Creates a simple [Node] that only appends some text
23 | *
24 | * @param string The text to append
25 | */
26 | public fun textNode(string: CharSequence): ParseSpec {
27 | return ParseSpec.createTerminal(TextNode(string))
28 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/parser/ParseSpec.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.parser
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.node.Node
5 |
6 | /**
7 | * Tells the parser how to best parse a [Node]
8 | *
9 | * For nonterminal subtrees, the provided root will be added to the main, and text between
10 | * [startIndex] (inclusive) and [endIndex] (exclusive) will continue to be parsed into [Node]s and
11 | * added as children under this root.
12 | *
13 | *
14 | * For terminal subtrees, the root will simply be added to the tree and no additional parsing will
15 | * take place on the text.
16 | *
17 | * @param C The context used to pass additional information to a [Node] when rendering
18 | */
19 | @Stable
20 | public class ParseSpec {
21 |
22 | public val root: Node
23 | public val isTerminal: Boolean
24 | public var startIndex: Int = 0
25 | public var endIndex: Int = 0
26 |
27 | internal constructor(root: Node, startIndex: Int, endIndex: Int) {
28 | this.root = root
29 | this.isTerminal = false
30 | this.startIndex = startIndex
31 | this.endIndex = endIndex
32 | }
33 |
34 | internal constructor(root: Node) {
35 | this.root = root
36 | this.isTerminal = true
37 | }
38 |
39 | internal fun applyOffset(offset: Int) {
40 | startIndex += offset
41 | endIndex += offset
42 | }
43 |
44 | public companion object {
45 |
46 | /**
47 | * Creates a nonterminal [ParseSpec]
48 | *
49 | * @param root The starting node
50 | * @param startIndex (inclusive) Where in the match to start parsing for children
51 | * @param endIndex (exclusive) Where in the match to stop parsing for children
52 | * @return [ParseSpec] Describes how the [Node] should be processed
53 | */
54 | public fun createNonterminal(root: Node, startIndex: Int, endIndex: Int): ParseSpec = ParseSpec(root, startIndex, endIndex)
55 |
56 | /**
57 | * Creates a terminal [ParseSpec]
58 | *
59 | * @param root The [Node] the [ParseSpec] will represent
60 | * @return [ParseSpec] Describes how the [Node] should be processed
61 | */
62 | public fun createTerminal(root: Node): ParseSpec = ParseSpec(root)
63 |
64 | }
65 |
66 | }
67 |
68 |
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/parser/Rule.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.parser
2 |
3 | import androidx.compose.runtime.Stable
4 | import xyz.wingio.syntakts.Syntakts
5 | import xyz.wingio.syntakts.node.node
6 | import xyz.wingio.syntakts.node.textNode
7 |
8 | @Stable
9 | public open class Rule(
10 | public val regex: Regex,
11 | public val name: String,
12 | public val parse: ParseRule
13 | ) {
14 |
15 | internal val _regex = "^${regex.pattern}".toRegex()
16 |
17 | public open fun match(inspectionSource: CharSequence, lastCapture: String?): MatchResult? {
18 | return _regex.find(inspectionSource)
19 | }
20 |
21 | }
22 |
23 | /**
24 | * Uses the result of the match to generate a [ParseSpec], usually done using [node]
25 | */
26 | @Stable
27 | public fun interface ParseRule {
28 |
29 | /**
30 | * Uses the result of the match to generate a [ParseSpec], usually done using [node]
31 | *
32 | * @param result The result of the regex match
33 | * @return [ParseSpec] representing the node to be rendered
34 | */
35 | public operator fun invoke(result: MatchResult): ParseSpec
36 |
37 | }
38 |
39 | private val PLAINTEXT_REGEX = """^[\s\S]+?(?=\b|[^0-9A-Za-z\s\u00c0-\uffff]|\n| {2,}\n|\w+:\S|$)""".toRegex()
40 |
41 | /**
42 | * Adds a plain text rule, used as a fallback in case no user defined rules catch anything
43 | *
44 | * @return [Syntakts.Builder] To allow for builder method chaining
45 | */
46 | public fun Syntakts.Builder.addTextRule(): Syntakts.Builder = addRule(PLAINTEXT_REGEX, name = "Plain Text") {
47 | textNode(it.value)
48 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/Color.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 | import kotlin.math.roundToInt
6 |
7 | /**
8 | * Multiplatform representation of a color
9 | *
10 | * @param red Number representing how much red this color has (0-255)
11 | * @param green Number representing how much red this color has (0-255)
12 | * @param blue Number representing how much red this color has (0-255)
13 | * @param alpha Number representing how much opacity this color has (0-255)
14 | * @param ignore Whether or not to ignore this color
15 | */
16 | @Stable
17 | @Immutable
18 | public data class Color(
19 | /* @IntRange(0, 255) */
20 | val red: Int,
21 | /* @IntRange(0, 255) */
22 | val green: Int,
23 | /* @IntRange(0, 255) */
24 | val blue: Int,
25 | /* @IntRange(0, 255) */
26 | val alpha: Int = 255,
27 | val ignore: Boolean = false
28 | ) {
29 |
30 | public companion object {
31 |
32 | public val BLACK: Color = Color(0, 0, 0)
33 | public val DARK_GRAY: Color = Color(68, 68, 68)
34 | public val GRAY: Color = Color(136, 136, 136)
35 | public val LIGHT_GRAY: Color = Color(204, 204, 204)
36 | public val WHITE: Color = Color(255, 255, 255)
37 |
38 | public val RED: Color = Color(255, 0, 0)
39 | public val GREEN: Color = Color(0, 255, 0)
40 | public val BLUE: Color = Color(0, 0, 255)
41 | public val YELLOW: Color = Color(255, 255, 0)
42 | public val CYAN: Color = Color(0, 255, 255)
43 | public val MAGENTA: Color = Color(255, 0, 255)
44 |
45 | public val TRANSPARENT: Color = Color(0, 0, 0, 0)
46 |
47 | /**
48 | * Refers to a nonexistent color
49 | */
50 | public val UNSPECIFIED: Color = Color(0, 0, 0, 0, ignore = true)
51 |
52 | }
53 |
54 | override fun toString(): String {
55 | val sb = StringBuilder("Color(\n")
56 | sb.append("\tred = $red,\n")
57 | sb.append("\tgreen = $green,\n")
58 | sb.append("\tblue = $blue,\n")
59 | sb.append("\talpha = $alpha,\n")
60 | sb.append("\thexCode = \"$hexCode\"\n")
61 | sb.append(")")
62 | return sb.toString()
63 | }
64 |
65 | /**
66 | * Hex color code representation (#RRGGBBAA)
67 | */
68 | val hexCode: String = StringBuilder("#").apply {
69 | append("%x".format(red).padStart(2, '0'))
70 | append("%x".format(green).padStart(2, '0'))
71 | append("%x".format(blue).padStart(2, '0'))
72 | append("%x".format(alpha).padStart(2, '0'))
73 | }.toString()
74 |
75 | /**
76 | * Apply a level of opacity to this [Color]
77 | */
78 | public infix fun alpha(alpha: Float): Color = withOpacity(alpha)
79 |
80 | /**
81 | * Apply a level of [opacity] to this [Color]
82 | */
83 | public infix fun withOpacity(opacity: Float): Color {
84 | assert(opacity in 0f..1f) { "Opacity must be a value between 0 and 1" }
85 | return copy(alpha = (255 * opacity).roundToInt())
86 | }
87 |
88 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/FontResolver.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | /**
4 | * Used to retrieve a platform or framework specific font representation from a font name
5 | */
6 | public abstract class FontResolver {
7 |
8 | protected abstract val fontMap: MutableMap
9 |
10 | public abstract fun resolveFont(fontName: String): T?
11 |
12 | public abstract fun registerFont(fontName: String, platformFont: T)
13 |
14 | /**
15 | * Bulk register fonts
16 | */
17 | public fun register(vararg fonts: Pair) {
18 | fontMap.putAll(fonts)
19 | }
20 |
21 | }
22 |
23 | /**
24 | * Default fonts included on most platforms
25 | */
26 | public object Fonts {
27 |
28 | /**
29 | * The default font for the system
30 | */
31 | public const val DEFAULT: String = "default"
32 |
33 | /**
34 | * Default monospaced font for the system
35 | */
36 | public const val MONOSPACE: String = "monospace"
37 |
38 | /**
39 | * Default serif font for the system
40 | */
41 | public const val SERIF: String = "serif"
42 |
43 | /**
44 | * Default sans-serif font for the system
45 | */
46 | public const val SANS_SERIF: String = "sans-serif"
47 |
48 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/FontStyle.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | /**
4 | * Variant of the font (Ex [italic][FontStyle.Italic])
5 | */
6 | public enum class FontStyle {
7 | Italic,
8 | Normal
9 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/FontWeight.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 |
6 | /**
7 | * Multiplatform representation of font weight
8 | *
9 | * @param weight Thickness of the glyphs, between 1 and 1000
10 | */
11 | @Stable
12 | @Immutable
13 | public data class FontWeight(
14 | public val weight: Int
15 | ) {
16 |
17 | public companion object {
18 | /**
19 | * [Thin]
20 | */
21 | @Stable
22 | public val W100: FontWeight = FontWeight(100)
23 |
24 | /**
25 | * [ExtraLight]
26 | */
27 | @Stable
28 | public val W200: FontWeight = FontWeight(200)
29 |
30 | /**
31 | * [Light]
32 | */
33 | @Stable
34 | public val W300: FontWeight = FontWeight(300)
35 |
36 | /**
37 | * [Normal], regular, default
38 | */
39 | @Stable
40 | public val W400: FontWeight = FontWeight(400)
41 |
42 | /**
43 | * [Medium]
44 | */
45 | @Stable
46 | public val W500: FontWeight = FontWeight(500)
47 |
48 | /**
49 | * [SemiBold]
50 | */
51 | @Stable
52 | public val W600: FontWeight = FontWeight(600)
53 |
54 | /**
55 | * [Bold]
56 | */
57 | @Stable
58 | public val W700: FontWeight = FontWeight(700)
59 |
60 | /**
61 | * [ExtraBold]
62 | */
63 | @Stable
64 | public val W800: FontWeight = FontWeight(800)
65 |
66 | /**
67 | * [Black]
68 | */
69 | @Stable
70 | public val W900: FontWeight = FontWeight(900)
71 |
72 | /**
73 | * Alias for [W100]
74 | */
75 | @Stable
76 | public val Thin: FontWeight = W100
77 |
78 | /**
79 | * Alias for [W200]
80 | */
81 | @Stable
82 | public val ExtraLight: FontWeight = W200
83 |
84 | /**
85 | * Alias for [W300]
86 | */
87 | @Stable
88 | public val Light: FontWeight = W300
89 |
90 | /**
91 | * Alias for [W400]
92 | */
93 | @Stable
94 | public val Normal: FontWeight = W400
95 |
96 | /**
97 | * Alias for [W500]
98 | */
99 | @Stable
100 | public val Medium: FontWeight = W500
101 |
102 | /**
103 | * Alias for [W600]
104 | */
105 | @Stable
106 | public val SemiBold: FontWeight = W600
107 |
108 | /**
109 | * Alias for [W700]
110 | */
111 | @Stable
112 | public val Bold: FontWeight = W700
113 |
114 | /**
115 | * Alias for [W800]
116 | */
117 | @Stable
118 | public val ExtraBold: FontWeight = W800
119 |
120 | /**
121 | * Alias for [W900]
122 | */
123 | @Stable
124 | public val Black: FontWeight = W900
125 |
126 | @Stable
127 | public val values: List = listOf(W100, W200, W300, W400, W500, W600, W700, W800, W900)
128 | }
129 |
130 | init {
131 | require(weight in 1..1000) {
132 | "Font weight must be between 1 and 1000, currently: $weight"
133 | }
134 | }
135 |
136 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/Style.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | import androidx.compose.runtime.Stable
4 |
5 | /**
6 | * Collection of properties that can be used to style text
7 | *
8 | * @param color Color to use for the text itself
9 | * @param background Color to use for the background of the text
10 | * @param fontSize The size of the text
11 | * @param fontWeight Thickness of the glyphs (Ex [bold][FontWeight.Bold])
12 | * @param fontStyle Variant of the font (Ex [italic][FontStyle.Italic])
13 | * @param letterSpacing Spacing between each character
14 | * @param paragraphStyle Set of styles for blocks of text, applying could separate the text as if a line feed was added
15 | * @param font The font or font family for the text, see [Fonts] for default values
16 | */
17 | @Stable
18 | public data class Style(
19 | var color: Color? = null,
20 | var background: Color? = null,
21 | var fontSize: TextUnit = TextUnit.Unspecified,
22 | var fontWeight: FontWeight? = null,
23 | var fontStyle: FontStyle? = null,
24 | var letterSpacing: TextUnit = TextUnit.Unspecified,
25 | var textDecoration: TextDecoration? = null,
26 | var paragraphStyle: ParagraphStyle? = null,
27 | var font: String? = null
28 | ) {
29 |
30 | /**
31 | * Apply paragraph styles
32 | *
33 | * @param styleBlock lambda that lets you easily set paragraph styles
34 | */
35 | public fun paragraph(styleBlock: ParagraphStyle.() -> Unit): Style {
36 | paragraphStyle = paragraphStyle?.apply(styleBlock) ?: ParagraphStyle().apply(styleBlock)
37 | return this
38 | }
39 |
40 | }
41 |
42 | /**
43 | * Set of styles for blocks of text, applying could separate the text as if a line feed was added
44 | *
45 | * @param lineHeight Line height for a paragraph in either [sp][Sp] or [em][Em]
46 | */
47 | @Stable
48 | public data class ParagraphStyle(
49 | var lineHeight: TextUnit = TextUnit.Unspecified
50 | )
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/StyledTextBuilder.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | import androidx.compose.runtime.Stable
4 |
5 | /**
6 | * Used to build styled text across various UI frameworks
7 | *
8 | * @param S The type that this builder uses when building, usually framework dependant
9 | */
10 | @Stable
11 | public interface StyledTextBuilder {
12 |
13 | /**
14 | * Length of the text
15 | */
16 | public val length: Int
17 |
18 | /**
19 | * Append some [text] with an optional [style]
20 | *
21 | * @param text The text to append
22 | * @param style Information used to style the [text]
23 | * @return [StyledTextBuilder] To allow for builder method chaining
24 | */
25 | public fun append(text: CharSequence, style: Style? = null): StyledTextBuilder
26 |
27 | /**
28 | * Append some [text] with a DSL for styling
29 | *
30 | * @param text The text to append
31 | * @param style Lambda used to style the [text]
32 | * @return [StyledTextBuilder] To allow for builder method chaining
33 | */
34 | public fun append(text: CharSequence, style: Style.() -> Unit): StyledTextBuilder
35 |
36 | /**
37 | * Appends some [text] that can be clicked
38 | *
39 | * @param text The text to append
40 | * @param style How to style the [text]
41 | * @param onLongClick Callback for when the child nodes are long clicked
42 | * @param onClick What to do when the [text] is clicked
43 | */
44 | public fun appendClickable(text: CharSequence, style: Style? = null, onLongClick: (() -> Unit)? = null, onClick: () -> Unit): StyledTextBuilder
45 |
46 | /**
47 | * Appends some [text] with an annotation
48 | *
49 | * @param text The text to append
50 | * @param tag Used to distinguish annotations
51 | * @param annotation The annotation to attach
52 | */
53 | public fun appendAnnotated(
54 | text: CharSequence,
55 | tag: String,
56 | annotation: String
57 | ): StyledTextBuilder
58 |
59 | /**
60 | * Will call [onClick] when anything within the given [range] is clicked
61 | *
62 | * @param range (inclusive start, exclusive end) Any clicked here will call [onClick]
63 | * @param onLongClick Callback for when the child nodes are long clicked
64 | * @param onClick Callback invoked when the text is clicked
65 | * @return [StyledTextBuilder] To allow for builder method chaining
66 | */
67 | public fun addClickable(range: IntRange, onLongClick: (() -> Unit)? = null, onClick: () -> Unit): StyledTextBuilder = addClickable(range.first, range.last + 1, onLongClick, onClick)
68 |
69 | /**
70 | * Will call [onClick] when anything between the [startIndex] and [endIndex] is clicked
71 | *
72 | * @param startIndex (inclusive) Start of the clickable area
73 | * @param endIndex (exclusive) End of the clickable area
74 | * @param onLongClick Callback for when the child nodes are long clicked
75 | * @param onClick Callback invoked when the text is clicked
76 | * @return [StyledTextBuilder] To allow for builder method chaining
77 | */
78 | public fun addClickable(startIndex: Int, endIndex: Int, onLongClick: (() -> Unit)? = null, onClick: () -> Unit): StyledTextBuilder
79 |
80 | /**
81 | * Apply a [Style] to a given [range]
82 | *
83 | * @param style The style to apply
84 | * @param range (inclusive start, exclusive end) Where the style should apply
85 | * @return [StyledTextBuilder] To allow for builder method chaining
86 | */
87 | public fun addStyle(style: Style, range: IntRange): StyledTextBuilder = addStyle(style, range.first, range.last + 1)
88 |
89 | /**
90 | * Apply a [Style] from [start][startIndex] to [end][endIndex]
91 | *
92 | * @param style The style to apply
93 | * @param startIndex (inclusive) Where to start applying the style
94 | * @param endIndex (exclusive) Where to finish applying the style
95 | * @return [StyledTextBuilder] To allow for builder method chaining
96 | */
97 | public fun addStyle(style: Style, startIndex: Int, endIndex: Int): StyledTextBuilder
98 |
99 | /**
100 | * Apply a [Style] to a given [range]
101 | *
102 | * @param range (inclusive start, exclusive end) Where the style should apply
103 | * @param style The style to apply
104 | * @return [StyledTextBuilder] To allow for builder method chaining
105 | */
106 | public fun addStyle(range: IntRange, style: Style.() -> Unit): StyledTextBuilder = addStyle(range.first, range.last + 1, style)
107 |
108 | /**
109 | * Apply a [Style] from [start][startIndex] to [end][endIndex]
110 | *
111 | * @param startIndex (inclusive) Where to start applying the style
112 | * @param endIndex (exclusive) Where to finish applying the style
113 | * @param style The style to apply
114 | * @return [StyledTextBuilder] To allow for builder method chaining
115 | */
116 | public fun addStyle(startIndex: Int, endIndex: Int, style: Style.() -> Unit): StyledTextBuilder
117 |
118 | /**
119 | * Apply an annotation to the text, this does not usually effect the appearance of the text
120 | *
121 | * @param tag Used to distinguish annotations
122 | * @param annotation Annotation to attach
123 | * @param range (inclusive start, exclusive end) Where the annotation should apply
124 | */
125 | public fun addAnnotation(
126 | tag: String,
127 | annotation: String,
128 | range: IntRange
129 | ): StyledTextBuilder = addAnnotation(tag, annotation, range.first, range.last + 1)
130 |
131 | /**
132 | * Apply an annotation to the text, this does not usually effect the appearance of the text
133 | *
134 | * @param tag Used to distinguish annotations
135 | * @param annotation Annotation to attach
136 | * @param startIndex (inclusive) Where to start applying the annotation
137 | * @param endIndex (exclusive) Where to finish applying the annotation
138 | */
139 | public fun addAnnotation(
140 | tag: String,
141 | annotation: String,
142 | startIndex: Int,
143 | endIndex: Int
144 | ): StyledTextBuilder
145 |
146 | /**
147 | * Clears all text and styles
148 | */
149 | public fun clear()
150 |
151 | /**
152 | * Builds the framework specific representation of rich text
153 | */
154 | public fun build(): S
155 |
156 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/TextDecoration.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | import androidx.compose.runtime.Immutable
4 |
5 | /**
6 | * Defines a line to be drawn through the text
7 | */
8 | public enum class TextDecoration {
9 | None,
10 |
11 | /**
12 | * Draws a line below the text
13 | */
14 | Underline,
15 |
16 | /**
17 | * Draws a line over the text
18 | */
19 | LineThrough;
20 |
21 | public companion object {
22 |
23 | /**
24 | * Draws a line over the text, alias of [LineThrough]
25 | */
26 | public val StrikeThrough: TextDecoration = LineThrough
27 |
28 | }
29 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/style/TextUnit.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.style
2 |
3 | import androidx.compose.runtime.Immutable
4 | import androidx.compose.runtime.Stable
5 |
6 | /**
7 | * Framework independent representation of a text unit (sp and em)
8 | * @see sp
9 | * @see em
10 | */
11 | @Immutable
12 | public open class TextUnit(public open val value: Float, public val unit: String) {
13 | /**
14 | * Represents an unspecified text unit, typically used to pass through to a fallback
15 | */
16 | @Immutable
17 | public object Unspecified : TextUnit(0f, "")
18 | }
19 |
20 | @Immutable
21 | public class Sp(value: Float): TextUnit(value, "sp")
22 |
23 | @Immutable
24 | public class Em(value: Float): TextUnit(value, "em")
25 |
26 | public val Int.sp: Sp
27 | get() = Sp(this.toFloat())
28 |
29 | public val Int.em: Em
30 | get() = Em(this.toFloat())
31 |
32 | public val Float.sp: Sp
33 | get() = Sp(this)
34 |
35 | public val Float.em: Em
36 | get() = Em(this)
37 |
38 | public val Double.sp: Sp
39 | get() = Sp(this.toFloat())
40 |
41 | public val Double.em: Em
42 | get() = Em(this.toFloat())
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/util/Logger.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 | public interface Logger {
4 |
5 | /**
6 | * Prints a message to the console
7 | *
8 | * @param message Message to be printed
9 | */
10 | public fun info(message: String)
11 |
12 | /**
13 | * Prints a message to the console at a higher log level
14 | *
15 | * @param message Message to be printed
16 | */
17 | public fun debug(message: String)
18 |
19 | /**
20 | * Prints a warning to the console, optionally with an error
21 | *
22 | * @param message Message to be printed
23 | * @param throwable Error to show alongside warning
24 | */
25 | public fun warn(message: String, throwable: Throwable? = null)
26 |
27 | /**
28 | * Prints an error to the console
29 | *
30 | * @param message Message to be printed
31 | * @param throwable Error to show alongside [message]
32 | */
33 | public fun error(message: String, throwable: Throwable? = null)
34 |
35 | }
36 |
37 | /**
38 | * Uses [println] on jvm and Log on Android
39 | *
40 | * @param tag Printed alongside each message for better locating
41 | */
42 | internal expect class LoggerImpl internal constructor(tag: String): Logger
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/util/MarkdownUtils.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 | import xyz.wingio.syntakts.style.FontWeight
4 | import xyz.wingio.syntakts.style.Style
5 | import xyz.wingio.syntakts.style.em
6 | import xyz.wingio.syntakts.style.sp
7 |
8 | internal fun hashCountToStyle(hashCount: Int): Style {
9 | return when(hashCount) {
10 | 1 -> Style(
11 | fontWeight = FontWeight.SemiBold,
12 | fontSize = 2.2.em
13 | )
14 | 2 -> Style(
15 | fontWeight = FontWeight.SemiBold,
16 | fontSize = 1.7.em
17 | )
18 | 3 -> Style(
19 | fontWeight = FontWeight.SemiBold,
20 | fontSize = 1.5.em
21 | )
22 | 4 -> Style(
23 | fontWeight = FontWeight.SemiBold,
24 | fontSize = 1.4.em
25 | )
26 | 5 -> Style(
27 | fontWeight = FontWeight.SemiBold,
28 | fontSize = 1.2.em
29 | )
30 | else -> Style(
31 | fontWeight = FontWeight.SemiBold,
32 | fontSize = 1.1.em
33 | )
34 | }.paragraph { lineHeight = 1.1f.em }
35 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/util/Stack.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 |
4 | /**
5 | * Wrapper around MutableList that lets us use it as a stack, where you can push and pop elements
6 | */
7 | internal class Stack(private val items: MutableList = mutableListOf()) : MutableList by items {
8 |
9 | fun push(item: E): E {
10 | add(item)
11 | return item
12 | }
13 |
14 | fun pop(): E {
15 | return removeLast()
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/util/SynchronizedCache.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.launch
6 | import kotlinx.coroutines.sync.Mutex
7 | import kotlinx.coroutines.sync.withLock
8 |
9 | /**
10 | * Thread safe cache store, all writes are restricted with a [Mutex] to prevent [ConcurrentModificationException]s
11 | */
12 | public class SynchronizedCache {
13 |
14 | private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate)
15 | private val mutex: Mutex = Mutex(false)
16 | private val cache: MutableMap = mutableMapOf()
17 |
18 | /**
19 | * Number of cached entities
20 | */
21 | public val size: Int get() = cache.size
22 |
23 | public operator fun set(key: K, value: V?) {
24 | scope.launch {
25 | mutex.withLock {
26 | cache[key] = value
27 | }
28 | }
29 | }
30 |
31 | public operator fun get(key: K): V? {
32 | return cache[key]
33 | }
34 |
35 | /**
36 | * Returns true if this cache already contains an element with the given [key]
37 | */
38 | public fun hasKey(key: K): Boolean {
39 | return cache.containsKey(key)
40 | }
41 |
42 | /**
43 | * Removes the first element of this cache
44 | */
45 | public fun removeFirst() {
46 | scope.launch {
47 | mutex.withLock {
48 | cache.remove(cache.keys.firstOrNull())
49 | }
50 | }
51 | }
52 |
53 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonMain/kotlin/xyz/wingio/syntakts/util/Utils.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 | internal inline fun List.firstMapOrNull(predicate: (T) -> V?): V? {
4 | for (element in this) {
5 | @Suppress("UnnecessaryVariable") // wants to inline, but it's unreadable that way
6 | val found = predicate(element) ?: continue
7 | return found
8 | }
9 | return null
10 | }
--------------------------------------------------------------------------------
/syntakts-core/src/commonTest/kotlin/SyntaktsTest.kt:
--------------------------------------------------------------------------------
1 | import org.junit.Test
2 | import xyz.wingio.syntakts.markdown.MarkdownSyntakts
3 | import xyz.wingio.syntakts.node.StyleNode
4 | import xyz.wingio.syntakts.style.Color
5 | import xyz.wingio.syntakts.style.Style
6 | import xyz.wingio.syntakts.syntakts
7 |
8 | const val sampleText = "Just **some** *sample* @text"
9 |
10 | class SyntaktsTest {
11 |
12 | @Test
13 | fun `Parse plain text`() {
14 | val syntakts = syntakts { }
15 | val parsedNodes = syntakts.parse(sampleText)
16 | assert(parsedNodes.isNotEmpty())
17 | }
18 |
19 | @Test
20 | fun `Parse text with markdown`() {
21 | val parsedNodes = MarkdownSyntakts.parse(sampleText)
22 | assert(parsedNodes.isNotEmpty())
23 | assert(parsedNodes.any { it is StyleNode })
24 | }
25 |
26 | @Test
27 | fun `Parse text with custom rules`() {
28 | val syntakts = syntakts {
29 | rule("@([A-z]+)") { result, _ ->
30 | append(result.value, Style(color = Color.GRAY))
31 | }
32 | }
33 | val parsedNodes = syntakts.parse(sampleText)
34 | assert(parsedNodes.isNotEmpty())
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/syntakts-core/src/jvmMain/kotlin/xyz/wingio/syntakts/util/Logger.jvm.kt:
--------------------------------------------------------------------------------
1 | package xyz.wingio.syntakts.util
2 |
3 | internal actual class LoggerImpl actual constructor(
4 | private val tag: String
5 | ): Logger {
6 |
7 | override fun info(message: String) {
8 | println("I: [$tag] $message")
9 | }
10 |
11 | override fun debug(message: String) {
12 | println("D: [$tag] $message")
13 | }
14 |
15 | override fun warn(message: String, throwable: Throwable?) {
16 | println("""
17 | W: [$tag] $message ${
18 | throwable?.let {
19 | "\n${it.stackTraceToString()}"
20 | }
21 | }
22 | """.trimIndent())
23 | }
24 |
25 | override fun error(message: String, throwable: Throwable?) {
26 | System.err.println("""
27 | E: [$tag] $message ${
28 | throwable?.let {
29 | "\n${it.stackTraceToString()}"
30 | }
31 | }
32 | """.trimIndent())
33 | }
34 |
35 | }
--------------------------------------------------------------------------------