diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt index bbab146da..41a89a151 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/images/Base64Fetcher.kt @@ -30,8 +30,8 @@ import coil3.fetch.Fetcher import coil3.fetch.ImageFetchResult import coil3.key.Keyer import coil3.request.Options -import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.base64Image.toBitmap +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import com.vitorpamplona.quartz.nip01Core.core.toHexKey import com.vitorpamplona.quartz.utils.sha256.sha256 diff --git a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt index 9340c5d08..c5b8f2ca3 100644 --- a/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt +++ b/commons/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImageBitmap.kt @@ -24,6 +24,7 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.util.Base64 fun Base64Image.toBitmap(content: String): Bitmap { diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/ExpandableTextCutOffCalculator.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/GalleryParser.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/MediaContentModels.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt similarity index 86% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt index 5e9ddefbb..b54a9c760 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/Patterns.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.commons.richtext -import java.util.regex.Pattern - /** * Pattern constants for email and phone validation. * These replace android.util.Patterns for KMP compatibility. @@ -30,16 +28,21 @@ object Patterns { /** * Email address pattern from RFC 5322... From android.util.Patterns. */ - val EMAIL_ADDRESS: Pattern = - Pattern.compile( + val EMAIL_ADDRESS: Regex = + Regex( "[a-zA-Z0-9+._%-]{1,256}@[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}(\\.[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25})+", ) /** * Phone number pattern - matches common phone formats. */ - val PHONE: Pattern = - Pattern.compile( + val PHONE: Regex = + Regex( "^[+]?[(]?[0-9]{1,4}[)]?[-\\s./0-9]*\$", ) + + val BASE64_IMAGE: Regex = + Regex( + "data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|")});base64,([a-zA-Z0-9+/]+={0,2})", + ) } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt similarity index 88% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt index 243a07957..f51662962 100644 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParser.kt @@ -22,7 +22,6 @@ package com.vitorpamplona.amethyst.commons.richtext import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetectorOptions -import com.vitorpamplona.amethyst.commons.base64Image.Base64Image import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder import com.vitorpamplona.quartz.experimental.inlineMetadata.Nip54InlineMetadata import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists @@ -45,8 +44,8 @@ import kotlinx.collections.immutable.toPersistentList import java.net.MalformedURLException import java.net.URISyntaxException import java.net.URL -import java.util.regex.Pattern import kotlin.coroutines.cancellation.CancellationException +import kotlin.text.iterator class RichTextParser { fun createMediaContent( @@ -108,7 +107,7 @@ class RichTextParser { return urls.mapNotNullTo(LinkedHashSet(urls.size)) { if (it.originalUrl.contains("@")) { - if (Patterns.EMAIL_ADDRESS.matcher(it.originalUrl).matches()) { + if (Patterns.EMAIL_ADDRESS.matches(it.originalUrl)) { null } else { it.originalUrl @@ -201,7 +200,7 @@ class RichTextParser { }.toImmutableList() } - private fun isNumber(word: String) = numberPattern.matcher(word).matches() + private fun isNumber(word: String) = numberPattern.matches(word) private fun isPhoneNumberChar(c: Char): Boolean = when (c) { @@ -225,7 +224,7 @@ class RichTextParser { return isPotentialNumber } - fun isDate(word: String): Boolean = shortDatePattern.matcher(word).matches() || longDatePattern.matcher(word).matches() + fun isDate(word: String): Boolean = shortDatePattern.matches(word) || longDatePattern.matches(word) private fun isArabic(text: String): Boolean = text.any { it in '\u0600'..'\u06FF' || it in '\u0750'..'\u077F' } @@ -239,7 +238,9 @@ class RichTextParser { ): Segment { if (word.isEmpty()) return RegularTextSegment(word) - if (word.startsWith("data:image/") && Base64Image.isBase64(word)) return Base64Segment(word) + if (word.startsWith("data:image/")) { + if (Patterns.BASE64_IMAGE.matches(word)) return Base64Segment(word) + } if (images.contains(word)) return ImageSegment(word) @@ -260,25 +261,22 @@ class RichTextParser { if (EmojiCoder.isCoded(word)) return SecretEmoji(word) if (word.contains("@")) { - if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) return EmailSegment(word) + if (Patterns.EMAIL_ADDRESS.matches(word)) return EmailSegment(word) } if (startsWithNIP19Scheme(word)) return BechSegment(word) if (isPotentialPhoneNumber(word) && !isDate(word)) { - if (Patterns.PHONE.matcher(word).matches()) return PhoneSegment(word) + if (Patterns.PHONE.matches(word)) return PhoneSegment(word) } val indexOfPeriod = word.indexOf(".") if (indexOfPeriod > 0 && indexOfPeriod < word.length - 1) { // periods cannot be the last one - val schemelessMatcher = noProtocolUrlValidator.matcher(word) - if (schemelessMatcher.find()) { - val url = schemelessMatcher.group(1) // url - val additionalChars = schemelessMatcher.group(4).ifEmpty { null } // additional chars - val pattern = - """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" - .toRegex(RegexOption.IGNORE_CASE) - if (pattern.find(word) != null && url != null) { + val schemelessMatcher = noProtocolUrlValidator.find(word) + if (schemelessMatcher != null) { + val url = schemelessMatcher.groups[1]?.value // url + val additionalChars = schemelessMatcher.groups[4]?.value?.ifEmpty { null } // additional chars + if (additionalUrlSchema.find(word) != null && url != null) { return SchemelessUrlSegment(word, url, additionalChars) } } @@ -292,12 +290,11 @@ class RichTextParser { tags: ImmutableListOfLists, ): Segment { // First #[n] - - val matcher = tagIndex.matcher(word) try { - if (matcher.find()) { - val index = matcher.group(1)?.toInt() - val suffix = matcher.group(2) + val matcher = tagIndex.find(word) + if (matcher != null) { + val index = matcher.groups[1]?.value?.toInt() + val suffix = matcher.groups[2]?.value if (index != null && index >= 0 && index < tags.lists.size) { val tag = tags.lists[index] @@ -317,13 +314,12 @@ class RichTextParser { } // Second #Amethyst - val hashtagMatcher = hashTagsPattern.matcher(word) - try { - if (hashtagMatcher.find()) { - val hashtag = hashtagMatcher.group(1) + val hashtagMatcher = hashTagsPattern.find(word) + if (hashtagMatcher != null) { + val hashtag = hashtagMatcher.groups[1]?.value if (hashtag != null) { - return HashTagSegment(word, hashtag, hashtagMatcher.group(2).ifEmpty { null }) + return HashTagSegment(word, hashtag, hashtagMatcher.groups[2]?.value?.ifEmpty { null }) } } } catch (e: Exception) { @@ -335,22 +331,26 @@ class RichTextParser { } companion object { - val longDatePattern: Pattern = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$") - val shortDatePattern: Pattern = Pattern.compile("^\\d{2}-\\d{2}-\\d{2}$") - val numberPattern: Pattern = Pattern.compile("^(-?[\\d.]+)([a-zA-Z%]*)$") + val longDatePattern: Regex = Regex("^\\d{4}-\\d{2}-\\d{2}$") + val shortDatePattern: Regex = Regex("^\\d{2}-\\d{2}-\\d{2}$") + val numberPattern: Regex = Regex("^(-?[\\d.]+)([a-zA-Z%]*)$") // Android9 seems to have an issue starting this regex. val noProtocolUrlValidator = try { - Pattern.compile( + Regex( "(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+[^\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}])*\\/?)(.*)", ) } catch (e: Exception) { - Pattern.compile( + Regex( "(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)", ) } + val additionalUrlSchema = + """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""" + .toRegex(RegexOption.IGNORE_CASE) + val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?" .toRegex(RegexOption.IGNORE_CASE) @@ -361,9 +361,9 @@ class RichTextParser { val imageExtensions = imageExt + imageExt.map { it.uppercase() } val videoExtensions = videoExt + videoExt.map { it.uppercase() } - val tagIndex = Pattern.compile("\\#\\[([0-9]+)\\](.*)") - val hashTagsPattern: Pattern = - Pattern.compile("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", Pattern.CASE_INSENSITIVE) + val tagIndex = Regex("\\#\\[([0-9]+)\\](.*)") + val hashTagsPattern: Regex = + Regex("#([^\\s!@#\$%^&*()=+./,\\[{\\]};:'\"?><]+)(.*)", RegexOption.IGNORE_CASE) val acceptedNIP19schemes = listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1", "nembed") + @@ -436,6 +436,6 @@ class RichTextParser { } } - fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matcher(url).matches() + fun isUrlWithoutScheme(url: String) = noProtocolUrlValidator.matches(url) } } diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt similarity index 100% rename from commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt rename to commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/richtext/RichTextParserSegments.kt diff --git a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt b/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt deleted file mode 100644 index ac7bef89c..000000000 --- a/commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64Image.kt +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.commons.base64Image - -import com.vitorpamplona.amethyst.commons.richtext.RichTextParser -import java.util.regex.Pattern - -object Base64Image { - val pattern: Pattern = - Pattern.compile( - "data:image/(${RichTextParser.imageExtensions.joinToString(separator = "|")});base64,([a-zA-Z0-9+/]+={0,2})", - ) - - fun isBase64(content: String): Boolean { - val matcher = pattern.matcher(content) - return matcher.find() - } -} diff --git a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt index 6c523a33e..50fc059a2 100644 --- a/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt +++ b/commons/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/base64Image/Base64ImagePlatform.jvm.kt @@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.commons.base64Image import com.vitorpamplona.amethyst.commons.blurhash.PlatformImage import com.vitorpamplona.amethyst.commons.blurhash.toPlatformImage +import com.vitorpamplona.amethyst.commons.richtext.Base64Image import java.io.ByteArrayInputStream import java.util.Base64 import javax.imageio.ImageIO