From 683ac0cbfaf2cf5f6b41a52977c2bfbad6026a63 Mon Sep 17 00:00:00 2001 From: Wing <44992537+wingio@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:51:43 -0500 Subject: [PATCH] Replace SimplerAST with Syntakts --- app/build.gradle.kts | 1 + .../xyz/wingio/dimett/ast/AnnotatedBuilder.kt | 48 ---- .../java/xyz/wingio/dimett/ast/Renderer.kt | 93 +++---- .../main/java/xyz/wingio/dimett/ast/Rules.kt | 253 ++++++------------ .../wingio/dimett/ast/nodes/ClickableNode.kt | 30 --- .../xyz/wingio/dimett/ast/nodes/EmojiNode.kt | 29 -- .../wingio/dimett/ast/nodes/MentionNode.kt | 34 --- .../ast/rendercontext/DefaultRenderContext.kt | 15 +- .../xyz/wingio/dimett/ui/components/Text.kt | 42 +-- .../wingio/dimett/ui/widgets/posts/Poll.kt | 12 +- .../wingio/dimett/ui/widgets/posts/Post.kt | 40 +-- .../dimett/ui/widgets/posts/PostAuthor.kt | 16 +- .../java/xyz/wingio/dimett/utils/TextUtils.kt | 96 ++++--- gradle/libs.versions.toml | 6 +- 14 files changed, 238 insertions(+), 477 deletions(-) delete mode 100644 app/src/main/java/xyz/wingio/dimett/ast/AnnotatedBuilder.kt delete mode 100644 app/src/main/java/xyz/wingio/dimett/ast/nodes/ClickableNode.kt delete mode 100644 app/src/main/java/xyz/wingio/dimett/ast/nodes/EmojiNode.kt delete mode 100644 app/src/main/java/xyz/wingio/dimett/ast/nodes/MentionNode.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2abdc09..1a2acfb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,6 +82,7 @@ dependencies { implementation(libs.bundles.voyager) implementation(libs.pullrefresh) + implementation(libs.syntakts) ksp(libs.androidx.room.compiler) } \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/AnnotatedBuilder.kt b/app/src/main/java/xyz/wingio/dimett/ast/AnnotatedBuilder.kt deleted file mode 100644 index 70082c2..0000000 --- a/app/src/main/java/xyz/wingio/dimett/ast/AnnotatedBuilder.kt +++ /dev/null @@ -1,48 +0,0 @@ -package xyz.wingio.dimett.ast - -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.ParagraphStyle -import androidx.compose.ui.text.SpanStyle -import com.materii.simplerast.core.text.RichTextBuilder -import com.materii.simplerast.core.text.StyleInclusion - -class AnnotatedBuilder(initial: String) : RichTextBuilder { - - private var builder = AnnotatedString.Builder(initial) - override val length: Int get() = builder.length - - override fun append(text: Char) { - builder.append(text) - } - - override fun append(text: CharSequence) { - builder.append(text.toString()) - } - - override fun get(index: Int): Char { - return builder.toAnnotatedString()[index] - } - - override fun getChars(start: Int, end: Int, destination: CharArray, offset: Int) {} - override fun insert(where: Int, text: CharSequence) {} - - override fun setStyle(style: Any, start: Int, end: Int, inclusion: StyleInclusion) { - when (style) { - is SpanStyle -> builder.addStyle(style, start, end) - is ParagraphStyle -> builder.addStyle(style, start, end) - is StringAnnotation -> builder.addStringAnnotation(style.tag, style.text, start, end) - } - } - - override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { - return builder.toString().subSequence(startIndex, endIndex) - } - - fun build() = builder.toAnnotatedString() - -} - -data class StringAnnotation( - val tag: String, - val text: String -) \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/Renderer.kt b/app/src/main/java/xyz/wingio/dimett/ast/Renderer.kt index e3f6478..109e6cc 100644 --- a/app/src/main/java/xyz/wingio/dimett/ast/Renderer.kt +++ b/app/src/main/java/xyz/wingio/dimett/ast/Renderer.kt @@ -2,60 +2,55 @@ package xyz.wingio.dimett.ast import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import com.materii.simplerast.core.node.Node -import com.materii.simplerast.core.parser.Parser +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalUriHandler import xyz.wingio.dimett.ast.rendercontext.DefaultRenderContext +import xyz.wingio.syntakts.Syntakts +import xyz.wingio.syntakts.compose.rememberAsyncRendered +import xyz.wingio.syntakts.markdown.addBasicMarkdownRules +import xyz.wingio.syntakts.syntakts -object Renderer { - private val stringParser = createStringParser() - private val regularParser = createRegularParser() +@Composable +fun Syntakts.render( + text: String, + emojiMap: Map = emptyMap(), + mentionMap: Map = emptyMap(), + actionHandler: (String) -> Unit = {} +) = rememberAsyncRendered( + text = text, + context = rememberRenderContext(emojiMap, mentionMap, actionHandler) +) - private fun Parser, Any>.render( - text: String, - renderContext: DefaultRenderContext - ) = AnnotatedBuilder("").apply { - for (node in parse( - text - .replace("\uD83C\uDFF3\u200D⚧", "\uD83C\uDFF3️\u200D⚧️") // trans flag fix - .replace( - "\uD83C\uDFF3\u200D\uD83C\uDF08", - "\uD83C\uDFF3️\u200D\uD83C\uDF08" - ), // pride flag fix - "" - )) - node.render(this, renderContext) - } +@Composable +fun rememberRenderContext( + emojiMap: Map, + mentionMap: Map, + actionHandler: (String) -> Unit = {} +): DefaultRenderContext { + val uriHandler = LocalUriHandler.current + val linkColor = MaterialTheme.colorScheme.primary - private fun createStringParser() = - Parser, Any>(false).apply { -// addRule(hyperlinkRule) - addRule(urlRule) - addRule(clickableRule) - addRule(unicodeEmojiRule) - addRules(basicRules) - } + return remember(emojiMap, mentionMap, uriHandler, linkColor, actionHandler) { + DefaultRenderContext(emojiMap, mentionMap, linkColor, uriHandler, actionHandler) + } +} - private fun createRegularParser() = - Parser, Any>(false).apply { - addRule(hashtagRule) - addRule(mentionRule) - addRule(urlRule) - addRule(emojiRule) - addRule(unicodeEmojiRule) - addRule(plainTextRule) - } +val DefaultSyntakts = syntakts { + addHashtagRule() + addMentionRule() + addUrlRule() + addEmojiRule() + addUnicodeEmojiRule() +} - @Composable - fun renderString( - text: String, - renderContext: DefaultRenderContext = DefaultRenderContext.BLANK.copy(linkColor = MaterialTheme.colorScheme.secondary) - ) = - stringParser.render(text, renderContext) +val StringSyntakts = syntakts { + addUrlRule() + addClickableRule() + addBasicMarkdownRules() + addUnicodeEmojiRule() +} - @Composable - fun render(text: String, emojiMap: Map, mentionMap: Map) = - regularParser.render( - text, - DefaultRenderContext(emojiMap, mentionMap, MaterialTheme.colorScheme.secondary) - ) +val EmojiSyntakts = syntakts { + addEmojiRule() + addUnicodeEmojiRule() } \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/Rules.kt b/app/src/main/java/xyz/wingio/dimett/ast/Rules.kt index fdafc17..5bfc133 100644 --- a/app/src/main/java/xyz/wingio/dimett/ast/Rules.kt +++ b/app/src/main/java/xyz/wingio/dimett/ast/Rules.kt @@ -1,196 +1,115 @@ package xyz.wingio.dimett.ast -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration -import com.materii.simplerast.core.node.Node -import com.materii.simplerast.core.parser.ParseSpec -import com.materii.simplerast.core.parser.Parser -import com.materii.simplerast.core.parser.Rule -import com.materii.simplerast.core.simple.CoreMarkdownRules -import com.materii.simplerast.code.CodeRules -import xyz.wingio.dimett.ast.nodes.ClickableNode -import xyz.wingio.dimett.ast.nodes.EmojiNode -import xyz.wingio.dimett.ast.nodes.MentionNode import xyz.wingio.dimett.ast.rendercontext.DefaultRenderContext import xyz.wingio.dimett.utils.EmojiUtils -import java.util.regex.Matcher -import java.util.regex.Pattern +import xyz.wingio.syntakts.Syntakts +import xyz.wingio.syntakts.compose.style.appendInlineContent +import xyz.wingio.syntakts.compose.style.toSyntaktsColor +import xyz.wingio.syntakts.style.Style const val urlRegex = "https?:\\/\\/([\\w+?]+\\.[\\w+]+)([a-zA-Z0-9\\~\\!\\@\\#\\\$\\%\\^\\&\\*\\(\\)_\\-\\=\\+\\\\\\/\\?\\.\\:\\;\\'\\,]*)?" -val urlRule = object : Rule, Any>( - Pattern.compile("^$urlRegex") -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val url = matcher.group(0) - return ParseSpec.createNonterminal( - ClickableNode( - text = url, - tag = "URL", - annotation = url - ), - Unit, - matcher.start(1), - matcher.end(1), - ) - } -} +const val hyperlinkRegex = + "\\[(.+?)\\]\\(($urlRegex)\\)" -val hyperlinkRule = object : Rule, Any>( - Pattern.compile("^\\[(.+?)\\]\\(($urlRegex)\\)") -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val text = matcher.group(1) - val url = matcher.group(2) - return ParseSpec.createNonterminal( - ClickableNode( - text = text, - tag = "URL", - annotation = url - ), - Unit, - matcher.start(1), - matcher.end(1), - ) +const val clickableRegex = + "\\[(.+?)\\]\\{(.+?)\\}" + +const val emojiRegex = + ":(.+?):" + +const val mentionRegex = + "@(\\S+?)\\b(@(\\S+)\\b)?" + +const val hashtagRegex = + "#(.+?)\\b" + +fun Syntakts.Builder.addUrlRule() { + rule(urlRegex) { result, context -> + appendClickable( + result.value, + style = Style(color = context.linkColor.toSyntaktsColor()) + ) { + context.uriHandler.openUri(result.value) + } } } -val clickableRule = object : Rule, Any>( - Pattern.compile("^\\[(.+?)\\]\\{(.+?)\\}") -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val text = matcher.group(1) - val clickAction = matcher.group(2) - return ParseSpec.createNonterminal( - ClickableNode( - text = text, - tag = "CLICKABLE", - annotation = clickAction - ), - Unit, - matcher.start(1), - matcher.end(1), - ) +fun Syntakts.Builder.addHyperlinkRule() { + rule(hyperlinkRegex) { result, context -> + appendClickable( + result.groupValues[1], // Text + style = Style(color = context.linkColor.toSyntaktsColor()) + ) { + context.uriHandler.openUri(result.groupValues[1]) // Url + } } } -val emojiRule = object : Rule, Any>( - Pattern.compile("^:(.+?):") -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val emoji = matcher.group(1) - return ParseSpec.createNonterminal( - EmojiNode( - name = emoji, - custom = true - ), - Unit, - matcher.start(1), - matcher.end(1), - ) +fun Syntakts.Builder.addClickableRule() { + rule(clickableRegex) { result, context -> + appendClickable( + result.groupValues[1], // Text + style = Style(color = context.linkColor.toSyntaktsColor()) + ) { + context.clickActionHandler(result.groupValues[2]) // Click action + } } } -val unicodeEmojiRule = object : Rule, Any>( - Pattern.compile( - EmojiUtils.regex, - Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CASE - ) -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val emoji = matcher.group(1) - return ParseSpec.createTerminal( - EmojiNode( - name = emoji, - custom = false - ), - Unit - ) +fun Syntakts.Builder.addEmojiRule() { + rule(emojiRegex) { result, context -> + val name = result.groupValues[1] + val url = context.emojiMap[name] + if (url == null) { + append(result.value) + } else { + appendInlineContent( + id = "emote", + alternateText = url + ) + } } } -val mentionRule = object : Rule, Any>( - Pattern.compile("^@(\\w+?)\\b") -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val username = matcher.group(1) - return ParseSpec.createNonterminal( - MentionNode(username), - Unit, - matcher.start(1), - matcher.end(1), +fun Syntakts.Builder.addUnicodeEmojiRule() { + rule(EmojiUtils.regex.toRegex(RegexOption.IGNORE_CASE)) { result, _ -> + val emoji = result.groupValues[1] + appendInlineContent( + id = "emoji", + alternateText = emoji ) } } -val hashtagRule = object : Rule, Any>( - Pattern.compile("^#(.+?)\\b") -) { - override fun parse( - matcher: Matcher, - parser: Parser, Any>, - state: Any - ): ParseSpec { - val text = matcher.group(0) - val tag = matcher.group(1) - return ParseSpec.createNonterminal( - ClickableNode( - text = text, - tag = "CLICKABLE", - annotation = "hashtag:$tag" - ), - Unit, - matcher.start(1), - matcher.end(1), - ) +fun Syntakts.Builder.addMentionRule() { + rule(mentionRegex) { result, context -> + val mentionUsername = result.groupValues[1] + val mention = context.mentionMap[mentionUsername] + + if (mention != null) { + appendClickable( + "@$mentionUsername", // Text + style = Style(color = context.linkColor.toSyntaktsColor()) + ) { + context.clickActionHandler("mention:$mention") // Click action + } + } else { + append(result.value) + } } } -val plainTextRule = CoreMarkdownRules.createTextRule() -val boldRule = CoreMarkdownRules.createBoldRule { - SpanStyle(fontWeight = FontWeight.Bold) -} -val italicsRule = CoreMarkdownRules.createItalicsRule { - SpanStyle(fontStyle = FontStyle.Italic) -} -val strikethroughRule = CoreMarkdownRules.createStrikethroughRule { - SpanStyle(textDecoration = TextDecoration.LineThrough) -} -val underlineRule = CoreMarkdownRules.createUnderlineRule { - SpanStyle(textDecoration = TextDecoration.Underline) -} +fun Syntakts.Builder.addHashtagRule() { + rule(hashtagRegex) { result, context -> + val tag = result.groupValues[1] -val basicRules = listOf, Any>>( - boldRule, - italicsRule, - strikethroughRule, - underlineRule, - plainTextRule -) \ No newline at end of file + appendClickable( + result.value, // Text + style = Style(color = context.linkColor.toSyntaktsColor()) + ) { + context.clickActionHandler("hashtag:$tag") // Click action + } + } +} \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/nodes/ClickableNode.kt b/app/src/main/java/xyz/wingio/dimett/ast/nodes/ClickableNode.kt deleted file mode 100644 index 0e35b25..0000000 --- a/app/src/main/java/xyz/wingio/dimett/ast/nodes/ClickableNode.kt +++ /dev/null @@ -1,30 +0,0 @@ -package xyz.wingio.dimett.ast.nodes - -import androidx.compose.ui.text.SpanStyle -import com.materii.simplerast.core.node.Node -import com.materii.simplerast.core.text.RichTextBuilder -import com.materii.simplerast.core.text.StyleInclusion -import xyz.wingio.dimett.ast.StringAnnotation -import xyz.wingio.dimett.ast.rendercontext.DefaultRenderContext - -class ClickableNode(val text: String, private val tag: String, private val annotation: String) : - Node() { - override fun render(builder: RichTextBuilder, renderContext: DefaultRenderContext) { - val length = builder.length - builder.append(text) - builder.setStyle( - StringAnnotation(tag, annotation), - length, - builder.length, - StyleInclusion.InclusiveInclusive - ) - builder.setStyle( - SpanStyle( - color = renderContext.linkColor - ), - length, - builder.length, - StyleInclusion.InclusiveInclusive - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/nodes/EmojiNode.kt b/app/src/main/java/xyz/wingio/dimett/ast/nodes/EmojiNode.kt deleted file mode 100644 index d1dbe82..0000000 --- a/app/src/main/java/xyz/wingio/dimett/ast/nodes/EmojiNode.kt +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.wingio.dimett.ast.nodes - -import com.materii.simplerast.core.node.Node -import com.materii.simplerast.core.text.RichTextBuilder -import com.materii.simplerast.core.text.StyleInclusion -import xyz.wingio.dimett.ast.StringAnnotation -import xyz.wingio.dimett.ast.rendercontext.DefaultRenderContext - -class EmojiNode(val name: String, private val custom: Boolean) : Node() { - override fun render(builder: RichTextBuilder, renderContext: DefaultRenderContext) { - val length = builder.length - val url = renderContext.emojiMap[name] - if (url != null || !custom) { - builder.append(if (custom) url!! else name) - builder.setStyle( - StringAnnotation( - "androidx.compose.foundation.text.inlineContent", - if (custom) "emote" else "emoji" - ), - start = length, - end = builder.length, - inclusion = StyleInclusion.InclusiveInclusive - ) - } else { - builder.append(":$name:") - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/nodes/MentionNode.kt b/app/src/main/java/xyz/wingio/dimett/ast/nodes/MentionNode.kt deleted file mode 100644 index 3d4fcf5..0000000 --- a/app/src/main/java/xyz/wingio/dimett/ast/nodes/MentionNode.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.wingio.dimett.ast.nodes - -import androidx.compose.ui.text.SpanStyle -import com.materii.simplerast.core.node.Node -import com.materii.simplerast.core.text.RichTextBuilder -import com.materii.simplerast.core.text.StyleInclusion -import xyz.wingio.dimett.ast.StringAnnotation -import xyz.wingio.dimett.ast.rendercontext.DefaultRenderContext - -class MentionNode(private val username: String) : Node() { - - override fun render(builder: RichTextBuilder, renderContext: DefaultRenderContext) { - val length = builder.length - val mention = renderContext.mentionMap[username] - builder.append("@$username") - if (mention != null) { - builder.setStyle( - StringAnnotation("CLICKABLE", "mention:$mention"), - length, - builder.length, - StyleInclusion.InclusiveInclusive - ) - builder.setStyle( - SpanStyle( - color = renderContext.linkColor - ), - length, - builder.length, - StyleInclusion.InclusiveInclusive - ) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ast/rendercontext/DefaultRenderContext.kt b/app/src/main/java/xyz/wingio/dimett/ast/rendercontext/DefaultRenderContext.kt index 6d10268..bc74eab 100644 --- a/app/src/main/java/xyz/wingio/dimett/ast/rendercontext/DefaultRenderContext.kt +++ b/app/src/main/java/xyz/wingio/dimett/ast/rendercontext/DefaultRenderContext.kt @@ -1,17 +1,12 @@ package xyz.wingio.dimett.ast.rendercontext import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.UriHandler data class DefaultRenderContext( val emojiMap: Map, val mentionMap: Map, - val linkColor: Color -) { - companion object { - val BLANK = DefaultRenderContext( - emojiMap = emptyMap(), - mentionMap = emptyMap(), - linkColor = Color.Blue - ) - } -} \ No newline at end of file + val linkColor: Color, + val uriHandler: UriHandler, + val clickActionHandler: (String) -> Unit +) \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ui/components/Text.kt b/app/src/main/java/xyz/wingio/dimett/ui/components/Text.kt index ed38b6e..042b2a1 100644 --- a/app/src/main/java/xyz/wingio/dimett/ui/components/Text.kt +++ b/app/src/main/java/xyz/wingio/dimett/ui/components/Text.kt @@ -1,20 +1,13 @@ package xyz.wingio.dimett.ui.components -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -25,10 +18,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import xyz.wingio.dimett.utils.inlineContent import xyz.wingio.dimett.utils.toAnnotatedString +import xyz.wingio.syntakts.compose.material3.clickable.ClickableText @Composable fun Text( text: String, + modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontWeight: FontWeight? = null, @@ -42,7 +37,6 @@ fun Text( overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, - modifier: Modifier = Modifier ) = Text( text = text.toAnnotatedString(), color = color, @@ -64,6 +58,7 @@ fun Text( @Composable fun Text( text: AnnotatedString, + modifier: Modifier = Modifier, color: Color = Color.Unspecified, fontSize: TextUnit = TextUnit.Unspecified, fontWeight: FontWeight? = null, @@ -77,34 +72,13 @@ fun Text( overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, - inlineContent: Map = inlineContent(), - modifier: Modifier = Modifier, - clickHandler: ((String) -> Unit) = {} + inlineContent: Map = inlineContent(style), ) { - val canBeClicked = text.getStringAnnotations(0, text.length).any { - listOf("URL", "CLICKABLE").contains(it.tag) - } - val uriHandler = LocalUriHandler.current val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.current } } - val layoutResult = remember { mutableStateOf(null) } - val pressIndicator = if (canBeClicked) Modifier.pointerInput(clickHandler) { - detectTapGestures { offset -> - layoutResult.value?.let { layoutResult -> - val pos = layoutResult.getOffsetForPosition(offset) - text.getStringAnnotations(pos, pos).firstOrNull() - ?.let { - when (it.tag) { - "URL" -> uriHandler.openUri(it.item) - "CLICKABLE" -> clickHandler(it.item) - } - } - } - } - } else Modifier val textStyle = style.merge( TextStyle( @@ -120,17 +94,13 @@ fun Text( ) ) - BasicText( + ClickableText( text = text, - modifier = modifier - .then(pressIndicator), + modifier = modifier, style = textStyle, overflow = overflow, softWrap = softWrap, maxLines = maxLines, - onTextLayout = { - layoutResult.value = it - }, inlineContent = inlineContent ) } \ No newline at end of file diff --git a/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Poll.kt b/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Poll.kt index a40fec2..010a394 100644 --- a/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Poll.kt +++ b/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Poll.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -26,7 +25,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.unit.dp import xyz.wingio.dimett.R -import xyz.wingio.dimett.ast.Renderer +import xyz.wingio.dimett.ast.EmojiSyntakts +import xyz.wingio.dimett.ast.render import xyz.wingio.dimett.rest.dto.Poll import xyz.wingio.dimett.ui.components.Text import xyz.wingio.dimett.utils.getString @@ -50,7 +50,6 @@ fun Poll( ) { for (i in poll.options.indices) { val option = poll.options[i] - val text = Renderer.render(option.title, emojiMap, emptyMap()).build() val voted = selected.contains(i) val vote = { if (!poll.multiple) { @@ -79,15 +78,15 @@ fun Poll( ) { ProvideTextStyle(MaterialTheme.typography.bodyLarge) { Text( - text = text, + text = EmojiSyntakts.render(option.title, emojiMap, emptyMap()), maxLines = 1, modifier = Modifier .basicMarquee() .fillMaxWidth() - .weight(0.75f) + .weight(1f) ) } - Spacer(modifier = Modifier.weight(1f)) + RadioButton( selected = voted, onClick = { @@ -99,6 +98,7 @@ fun Poll( ) } } + OutlinedButton( onClick = { onVote(poll.id, selected) diff --git a/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Post.kt b/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Post.kt index 718b779..024edb1 100644 --- a/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Post.kt +++ b/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/Post.kt @@ -17,7 +17,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import xyz.wingio.dimett.R -import xyz.wingio.dimett.ast.Renderer +import xyz.wingio.dimett.ast.DefaultSyntakts +import xyz.wingio.dimett.ast.EmojiSyntakts +import xyz.wingio.dimett.ast.render import xyz.wingio.dimett.rest.dto.post.Post import xyz.wingio.dimett.ui.components.Text import xyz.wingio.dimett.ui.widgets.attachments.Attachments @@ -70,12 +72,11 @@ fun Post( ) { if (post.boosted != null) { val str = stringResource(R.string.post_user_boosted, post.author.displayName) - val text = Renderer.render(str, post.author.emojis.toEmojiMap(), emptyMap()).build() PostInfoBar( icon = Icons.Outlined.Repeat, iconDescription = R.string.cd_boosted, - text = text + text = EmojiSyntakts.render(str, post.author.emojis.toEmojiMap(), emptyMap()) ) } @@ -93,33 +94,32 @@ fun Post( it.id == repliedTo }?.let { mention -> Text( - text = getString(R.string.post_replying_to, mention.username), + text = getString(R.string.post_replying_to, mention.username) { + if (it == "onUserClick") onMentionClick(mention.id) + }, style = MaterialTheme.typography.labelMedium, color = LocalContentColor.current.copy(alpha = 0.5f) - ) { - if (it == "onUserClick") onMentionClick(mention.id) - } + ) } } _post.content?.let { val content = processPostContent(_post) if (content.isNotEmpty()) { - val text = Renderer.render( - content, - _post.emojis.toEmojiMap(), - _post.mentions.toMentionMap() - ).build() Text( - text = text, + text = DefaultSyntakts.render( + content, + _post.emojis.toEmojiMap(), + _post.mentions.toMentionMap() + ) { annotation -> + val (key, value) = annotation.split(':') + when (key) { + "mention" -> onMentionClick(value) + "hashtag" -> onHashtagClick(value) + } + }, style = MaterialTheme.typography.bodyMedium - ) { annotation -> - val (key, value) = annotation.split(':') - when (key) { - "mention" -> onMentionClick(value) - "hashtag" -> onHashtagClick(value) - } - } + ) } } diff --git a/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/PostAuthor.kt b/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/PostAuthor.kt index ed2cb4d..bf6d37e 100644 --- a/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/PostAuthor.kt +++ b/app/src/main/java/xyz/wingio/dimett/ui/widgets/posts/PostAuthor.kt @@ -11,7 +11,6 @@ import androidx.compose.material.icons.outlined.SmartToy import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProvideTextStyle import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -20,7 +19,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import xyz.wingio.dimett.R -import xyz.wingio.dimett.ast.Renderer +import xyz.wingio.dimett.ast.EmojiSyntakts +import xyz.wingio.dimett.ast.render import xyz.wingio.dimett.ui.components.BadgedItem import xyz.wingio.dimett.ui.components.Text @@ -33,8 +33,6 @@ fun PostAuthor( bot: Boolean, onAvatarClick: () -> Unit = {} ) { - val authorText = Renderer.render(displayName, emojis, emptyMap()).build() - Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) @@ -57,12 +55,12 @@ fun PostAuthor( .clickable(onClick = onAvatarClick) ) } + Column { - ProvideTextStyle(value = MaterialTheme.typography.labelLarge) { - Text( - text = authorText - ) - } + Text( + text = EmojiSyntakts.render(displayName, emojis, emptyMap()), + style = MaterialTheme.typography.labelLarge + ) Text( text = "@$acct", style = MaterialTheme.typography.labelMedium, diff --git a/app/src/main/java/xyz/wingio/dimett/utils/TextUtils.kt b/app/src/main/java/xyz/wingio/dimett/utils/TextUtils.kt index aaf680c..16d8fdf 100644 --- a/app/src/main/java/xyz/wingio/dimett/utils/TextUtils.kt +++ b/app/src/main/java/xyz/wingio/dimett/utils/TextUtils.kt @@ -6,63 +6,87 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage -import xyz.wingio.dimett.ast.Renderer +import xyz.wingio.dimett.ast.StringSyntakts +import xyz.wingio.dimett.ast.rendercontext.DefaultRenderContext +import xyz.wingio.syntakts.Syntakts +import xyz.wingio.syntakts.compose.rememberRendered @Composable -fun inlineContent(): Map { - val emoteSize = (LocalTextStyle.current.fontSize.value + 2f).sp +fun inlineContent( + textStyle: TextStyle = LocalTextStyle.current +): Map { + val emoteSize = remember(textStyle) { (textStyle.fontSize.value + 2f).sp } val ctx = LocalContext.current - return mapOf( - "emote" to InlineTextContent( - placeholder = Placeholder( - width = emoteSize, - height = (emoteSize.value - 2f).sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { emoteUrl -> - AsyncImage( - model = emoteUrl, - contentDescription = null, - modifier = Modifier.size(emoteSize.value.dp) - ) - }, - "emoji" to InlineTextContent( - placeholder = Placeholder( - width = emoteSize, - height = (emoteSize.value - 2f).sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { emoji -> - val emojiImage = BitmapDrawable(EmojiUtils.emojis[emoji]?.let { - ctx.getResId(it) - }?.let { ctx.resources.openRawResource(it) }).bitmap.asImageBitmap() - Image( - bitmap = emojiImage, - contentDescription = null, - modifier = Modifier.size(emoteSize.value.dp) - ) - }, - ) + return remember(emoteSize, ctx) { + mapOf( + "emote" to InlineTextContent( + placeholder = Placeholder( + width = emoteSize, + height = (emoteSize.value - 2f).sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { emoteUrl -> + AsyncImage( + model = emoteUrl, + contentDescription = null, + modifier = Modifier.size(emoteSize.value.dp) + ) + }, + + "emoji" to InlineTextContent( + placeholder = Placeholder( + width = emoteSize, + height = (emoteSize.value - 2f).sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { emoji -> + val emojiImage = BitmapDrawable(EmojiUtils.emojis[emoji]?.let { + ctx.getResId(it) + }?.let { ctx.resources.openRawResource(it) }).bitmap.asImageBitmap() + + Image( + bitmap = emojiImage, + contentDescription = null, + modifier = Modifier.size(emoteSize.value.dp) + ) + }, + ) + } } @Composable fun getString( @StringRes string: Int, - vararg args: Any + vararg args: Any, + syntakts: Syntakts = StringSyntakts, + actionHandler: (String) -> Unit = {} ): AnnotatedString { val _string = stringResource(string, *args) - return Renderer.renderString(_string).build() + return syntakts.rememberRendered( + text = _string, + context = DefaultRenderContext( + emojiMap = emptyMap(), + mentionMap = emptyMap(), + linkColor = MaterialTheme.colorScheme.primary, + uriHandler = LocalUriHandler.current, + clickActionHandler = actionHandler + ) + ) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 06ed49e..22460b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ lifecycle-runtime-ktx = "2.6.2" media3 = "1.2.0" paging-compose = "3.2.1" room = "2.6.0" -simpleast-core = "master-SNAPSHOT" +syntakts = "1.0.0-rc05" voyager = "1.0.0-rc07" [libraries] @@ -53,7 +53,7 @@ ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" } ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" } pullrefresh = { group = "dev.materii.pullrefresh", name = "pullrefresh", version = "1.1.0" } -simpleast-core = { group = "com.github.MateriiApps.SimplerAST", name = "simpleast-core", version.ref = "simpleast-core" } +syntakts = { group = "xyz.wingio.syntakts", name = "syntakts-compose-material3", version.ref = "syntakts" } voyager-koin = { group = "cafe.adriel.voyager", name = "voyager-koin", version.ref = "voyager" } voyager-navigator = { group = "cafe.adriel.voyager", name = "voyager-navigator", version.ref = "voyager" } voyager-tab-navigator = { group = "cafe.adriel.voyager", name = "voyager-tab-navigator", version.ref = "voyager" } @@ -64,7 +64,7 @@ accompanist = ["accompanist-systemuicontroller", "accompanist-pager-indicators"] androidx = ["core-ktx", "androidx-browser", "androidx-lifecycle-runtime-ktx", "androidx-activity-compose", "androidx-paging-compose", "androidx-emoji2"] coil = ["coil-compose", "coil", "coil-gif"] compose = ["compose-ui", "compose-ui-graphics", "compose-ui-tooling-preview", "compose-material3", "compose-material-icons-extended"] -github = ["compose-image-blurhash", "simpleast-core"] +github = ["compose-image-blurhash"] koin = ["koin-core", "koin-androidx-compose", "koin-android"] kotlinx = ["kotlinx-datetime"] ktor = ["ktor-client-cio", "ktor-client-content-negotiation", "ktor-client-core", "ktor-client-logging", "ktor-serialization-kotlinx-json"]