Migrates to AGP 9
This commit is contained in:
+130
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 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.compose
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TextFieldValueExtensionTest {
|
||||
@Test
|
||||
fun testInsertTwoCharsStart() {
|
||||
val current = TextFieldValue("ab", selection = TextRange(0, 0))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("http://a.b ab", next.text)
|
||||
assertEquals(TextRange(10, 10), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsMiddle() {
|
||||
val current = TextFieldValue("ab", selection = TextRange(1, 1))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(12, 12), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsEnd() {
|
||||
val current = TextFieldValue("ab", selection = TextRange(2, 2))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("ab http://a.b", next.text)
|
||||
assertEquals(TextRange(13, 13), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertOneCharStart() {
|
||||
val current = TextFieldValue("a", selection = TextRange(0, 0))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("http://a.b a", next.text)
|
||||
assertEquals(TextRange(10, 10), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertOneCharEnd() {
|
||||
val current = TextFieldValue("a", selection = TextRange(1, 1))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b", next.text)
|
||||
assertEquals(TextRange(12, 12), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsWithSpaceStart() {
|
||||
val current = TextFieldValue("a b", selection = TextRange(1, 1))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(12, 12), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsWithSpaceEnd() {
|
||||
val current = TextFieldValue("a b", selection = TextRange(2, 2))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(12, 12), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsWithThreeSpaceStart() {
|
||||
val current = TextFieldValue("a b", selection = TextRange(1, 1))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(12, 12), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsWithThreeSpaceMiddle() {
|
||||
val current = TextFieldValue("a b", selection = TextRange(2, 2))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(12, 12), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertTwoCharsWithThreeSpaceEnd() {
|
||||
val current = TextFieldValue("a b", selection = TextRange(3, 3))
|
||||
val next = current.insertUrlAtCursor("http://a.b")
|
||||
|
||||
assertEquals("a http://a.b b", next.text)
|
||||
assertEquals(TextRange(13, 13), next.selection)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testInsertAfterNewLine() {
|
||||
val current = TextFieldValue("ab\n\n", selection = TextRange(4, 4))
|
||||
val next = current.insertUrlAtCursor("https://i.nostr.build/zdMW4.jpg")
|
||||
|
||||
assertEquals("ab\n\nhttps://i.nostr.build/zdMW4.jpg", next.text)
|
||||
assertEquals(TextRange(35, 35), next.selection)
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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.preview
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoder
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.BlurHashDecoderOld
|
||||
import com.vitorpamplona.amethyst.commons.blurhash.toBlurhash
|
||||
import org.junit.Assert
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class BlurhashTest {
|
||||
val warmHex = "[45#Y7_2^-xt%OSb%4S0-qt0xbotaRInV|M{RlD~M{M_IVIUNHM{M{M{M{RjNGRkoyj]o[t8tPt8"
|
||||
val testHex = "|NHL-]~pabocs+jDM{j?of4T9ZR+WBWZbdR-WCog04ITn\$t6t6t6t6oJoLZ}?bIUWBs:M{WCogRjs:s+o#R+WBoft7axWBx]IV%LogM{t5xaWBay%KRjxus.WCNGWWt7j[j]s+R-S5ofjYV@j[ofD%t8RPoJt7t7R*WCof"
|
||||
|
||||
@Test
|
||||
fun testAspectRatioWarm() {
|
||||
Assert.assertEquals(0.44444445f, BlurHashDecoder.aspectRatio(warmHex)!!, 0.001f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecoderWarm() {
|
||||
val aspectRatio = BlurHashDecoder.aspectRatio(warmHex) ?: 1.0f
|
||||
|
||||
val bmp1 = BlurHashDecoderOld.decode(warmHex, 100, (100 * (1 / aspectRatio)).roundToInt())
|
||||
val bmp2 = BlurHashDecoder.decodeKeepAspectRatio(warmHex, 100)
|
||||
|
||||
assertTrue(bmp1!!.sameAs(bmp2!!.bitmap))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecoderWarm25Pixels() {
|
||||
val aspectRatio = BlurHashDecoder.aspectRatio(warmHex) ?: 1.0f
|
||||
|
||||
val bmp1 = BlurHashDecoderOld.decode(warmHex, 25, (25 * (1 / aspectRatio)).roundToInt())
|
||||
val bmp2 = BlurHashDecoder.decodeKeepAspectRatio(warmHex, 25)
|
||||
|
||||
assertTrue(bmp1!!.sameAs(bmp2!!.bitmap))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAspectRatioTest() {
|
||||
Assert.assertEquals(1.0f, BlurHashDecoder.aspectRatio(testHex)!!)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDecoderTest() {
|
||||
val aspectRatio = BlurHashDecoder.aspectRatio(testHex) ?: 1.0f
|
||||
|
||||
val bmp1 = BlurHashDecoderOld.decode(testHex, 100, (100 * (1 / aspectRatio)).roundToInt())
|
||||
val bmp2 = BlurHashDecoder.decodeKeepAspectRatio(testHex, 100)
|
||||
|
||||
assertTrue(bmp1!!.sameAs(bmp2!!.bitmap))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBlack() {
|
||||
assertEquals("U00000fQfQfQfQfQfQfQfQfQfQfQfQfQfQfQ", load("/black.png").toBlurhash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test1x1() {
|
||||
assertEquals("U~TSUA~q~q~q~q~q~q~q~q~q~q~q~q~q~q~q", load("/1x1.png").toBlurhash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWhite() {
|
||||
assertEquals("U2TSUA~qfQ~q~qj[fQj[fQfQfQfQ~qj[fQj[", load("/white.png").toBlurhash())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLorikeet() {
|
||||
val blurhash = load("/lorikeet.jpg").toBlurhash()
|
||||
assertEquals("rFDcT@_LNs#:-pyBnhRRE2Z~MyX5VuV@WUo{xta\$9]RQw[OXS}rrWFXSw|OsxaxWNHSwn~M}NGaK%0RkM}w{xto|jGs+Sh-Tj]W?wJnjXSxGs.NI", blurhash)
|
||||
}
|
||||
|
||||
private fun load(filename: String): Bitmap =
|
||||
javaClass.getResourceAsStream(filename).use { inputStream ->
|
||||
BitmapFactory.decodeStream(inputStream)
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* 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.preview
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class MetaTagsParserTest {
|
||||
@Test
|
||||
fun testParse() {
|
||||
val input =
|
||||
"""<html>
|
||||
| <head>
|
||||
| <meta charset="utf-8">
|
||||
| <meta http-equiv="content-type" content="text/html; charset=utf-8">
|
||||
| <meta property="og:title" content=title>
|
||||
| <meta property="og:description" content='description'>
|
||||
| <meta property="og:image" content="https://example.com/img/foo.png">
|
||||
| <!-- edge cases -->
|
||||
| <meta
|
||||
| name="newline"
|
||||
| content="newline"
|
||||
| >
|
||||
| <meta name="space before gt" >
|
||||
| <meta name ="space before =">
|
||||
| <meta name= "space after =">
|
||||
| <META NAME="CAPITAL">
|
||||
| <meta name="character reference" content="<meta>">
|
||||
| <meta name="attr value with end of head doesn't harm" content="<head>bang!</head>">
|
||||
| <meta name="ignore tags with duplicated attr" name="dup">
|
||||
| </head>
|
||||
| <body>
|
||||
| <meta name="ignore meta tags in body">
|
||||
| </body>
|
||||
|</html>
|
||||
""".trimMargin()
|
||||
|
||||
val exp =
|
||||
listOf(
|
||||
listOf("charset" to "utf-8"),
|
||||
listOf("http-equiv" to "content-type", "content" to "text/html; charset=utf-8"),
|
||||
listOf("property" to "og:title", "content" to "title"),
|
||||
listOf("property" to "og:description", "content" to "description"),
|
||||
listOf("property" to "og:image", "content" to "https://example.com/img/foo.png"),
|
||||
listOf("name" to "newline", "content" to "newline"),
|
||||
listOf("name" to "space before gt"),
|
||||
listOf("name" to "space before ="),
|
||||
listOf("name" to "space after ="),
|
||||
listOf("name" to "CAPITAL"),
|
||||
listOf("name" to "character reference", "content" to "<meta>"),
|
||||
listOf("name" to "attr value with end of head doesn't harm", "content" to "<head>bang!</head>"),
|
||||
)
|
||||
|
||||
val metaTags = MetaTagsParser.parse(input).toList()
|
||||
println(metaTags)
|
||||
assertEquals(exp.size, metaTags.size)
|
||||
metaTags.zip(exp).forEach { (meta, expAttrs) ->
|
||||
expAttrs.forEach { (name, expValue) ->
|
||||
assertEquals(expValue, meta.attr(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testParseWithAdditionalTags() {
|
||||
val input =
|
||||
"""<html>
|
||||
| <head>
|
||||
| <meta data-preact-helmet property="og:title" content="Uma conversa com Antonio Vargas">
|
||||
| </head>
|
||||
|</html>
|
||||
""".trimMargin()
|
||||
|
||||
val exp =
|
||||
listOf(
|
||||
listOf("property" to "og:title", "content" to "Uma conversa com Antonio Vargas"),
|
||||
)
|
||||
|
||||
val metaTags = MetaTagsParser.parse(input).toList()
|
||||
println(metaTags)
|
||||
assertEquals(exp.size, metaTags.size)
|
||||
metaTags.zip(exp).forEach { (meta, expAttrs) ->
|
||||
expAttrs.forEach { (name, expValue) ->
|
||||
assertEquals(expValue, meta.attr(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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.richtext
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExpandableTextCutOffCalculatorTest {
|
||||
val shortPost: String =
|
||||
"Goldman Sachs Predicts Modest Gains for S&P 500 in 2024 ( #409962b2 , v0.09)"
|
||||
|
||||
val longPost: String =
|
||||
"Goldman Sachs Predicts Modest Gains for S&P 500 in 2024 ( #409962b2 , v0.09)\n" +
|
||||
"\n" +
|
||||
"data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIiA/Pgo8c3ZnIHdpZHRoPSIzMjAiIGhlaWdodD0iNjUiIHZpZXdCb3g9IjAgMCAzMjAgNjUiIGZpbGw9ImJsYWNrIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aAogICAgZmlsbC1ydWxlPSJldmVub2RkIgogICAgY2xpcC1ydWxlPSJldmVub2RkIgogICAgZD0iTTguNTk4MjggNDIuMTU1OEM3LjgxMjgxIDQ5LjA1OTkgNS45ODMyNCA0OC44Njg4IDMuNzYzMDYgNDkuNDMzOUMxLjc1MjU1IDQ5Ljk0MzggMi41MTk5MyA1MS4xMjcyIDMuODI2OTIgNTAuOTQwMkM0LjMxMjI1IDUwLjg3MDcgNy45MDU0MSA1MC41MDE4IDEwLjM2MTkgNTAuNTAxOEMxMy44MDI4IDUwLjUwMTggMTYuOTI1NiA1MC44MTM1IDE4LjYwOTMgNTAuODEzNUMxOS4zMTE4IDUwLjgxMzUgMjAuMjMyNCA0OS44NzU0IDE4LjUzMTYgNDkuNDMzOUMxNi41NjQ3IDQ4LjkyNiAxMS43MzkxIDQ5LjQzMzkgMTEuMjc4MyA0MS4xNTEzQzEwLjk0MyAzNS4wOTE0IDExLjUwMTggMTQuMzcxMSAxMS41MDE4IDE0LjM3MTFDMTEuNTAxOCAxMi45MjQxIDExLjcxMzYgMTIuNzA1NCAxMS45OTM1IDEyLjcwNTRDMTIuNDgyIDEyLjcwNTQgMTIuOTk1IDEzLjUyMTkgMTMuNDY5NyAxNC4wODE5TDQzLjQ5NzUgNTAuNzQ5MUM0Ni42NjcgNTQuMzA1MyA0Ni44NjM5IDUzLjIzNzQgNDYuOCA0OS42NjA4QzQ2LjU5MTQgMzguMTE4MyA0Ni45NjI5IDYuMjYxMjQgNDYuOTYyOSA2LjI2MTI0QzQ3LjA1NDQgLTAuMDA4MTc1MTYgNTIuOTQyMyAyLjUzNzQgNTIuOTQyMyAwLjY1NDAyM0M1Mi45NDIzIDAuMjE4Njg5IDUyLjY2MjMgMCA1Mi4yNDE5IDBDNTAuNTU2IDAgNDUuNzA3IDAuNTA0ODI0IDQzLjMyMDggMC41MDQ4MjRDNDEuMDcxOSAwLjUwNDgyNCAzOS43NDE1IDAuMTc4ODM0IDM3LjYzNTIgMC4xNzg4MzRDMzcuMjE2OSAwLjE3ODgzNCAzNi44NTYxIDAuMzk3NTIzIDM2Ljg2MzUgMC45MDU0MTJDMzYuODk1NSAyLjkzNzk5IDQ0LjQ0NjggLTAuNDEzODc0IDQ0LjE4NSA5Ljk0MDEyTDQ0LjI1OTUgMzkuOTM2M0M0NC4yNTk1IDQwLjUxNDcgNDQuMjE5MSA0MS42NTUxIDQzLjIwNjkgMzkuOTM2M0wxMS43Njc4IDEuOTA3OTFDMTEuMzQ1MyAxLjQwMTA0IDEwLjcxNTIgMC4yNDExNzEgOS45NDE0NiAwLjI0MTE3MUM4Ljg4ODg0IDAuMjQxMTcxIDUuNDA5NTcgMC41MDQ4MjQgNC4zNTY5NSAwLjUwNDgyNEMzLjIzNDA5IDAuNTA0ODI0IDIuMTgwNDEgMC4yMTU2MjMgMS4wNTc1NSAwLjIxNTYyM0MwLjcwNzM4OSAwLjIxNTYyMyAtMC4wNjEwNTI5IDAuNTA3ODg5IDAuMDAzODcwODYgMC45MzkxMzZDMC4zOTg3MzUgMy40NzY1NCA4LjYzMTI4IDAuNDY3MDEyIDguNzY0MzIgMTIuMzI2M0M4Ljc2NDMyIDEyLjMyNjMgOC45MzU2NyAzOS4xNzcgOC41OTgyOCA0Mi4xNTU4WiIKICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMy4yMjQgNS43NDIxOSkiCiAgLz4KPHBhdGgKICAgIGZpbGwtcnVsZT0iZXZlbm9kZCIKICAgIGNsaXAtcnVsZT0iZXZlbm9kZCIKICAgIGQ9Ik01LjY1NTE4IDAuMDU3NjM2MkM0LjcwOSAwLjMzMzU1MiAzLjIwNDA0IDEuNTI1MSAyLjU4NTY3IDEuOTM4OTdDMS44NzM2NCAyLjQxODI1IDEuODg4NTQgMy4yMTAyMyAyLjI1MzYgMy44ODU3MUM0LjAyMjUxIDcuMTQ4NjcgNS43NzExOSAxMi41MzQxIDEuNDA2NCAxOS4wMUMwLjY4Nzk4MSAyMC4wNzU4IC0wLjQyMzE3MiAyMC4zODk2IDAuMTY2NDYzIDIwLjgzQzAuNzg4MDI4IDIxLjI5NiAyLjc5MTA4IDE5Ljk5MSAzLjYyNjU4IDE5LjE5N0M4LjMzNTE1IDE0Ljc0MDQgMTAuMDMzOCA3LjE0ODY3IDcuMzUzODQgMS42MjYyN0M2LjgwODkxIDAuNTAzMTg5IDYuNTg2NDYgLTAuMjE0MTkyIDUuNjU1MTggMC4wNTc2MzYyWiIKICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3NS4wNzQpIgogIC8+CjxwYXRoCiAgICBmaWxsLXJ" +
|
||||
"1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMS4zODg4NiA0OC4zMTk3QzEuNjc5NDIgNDkuNjE0NSA3LjgwMzU0IDUyLjE0NzggMTMuMjI2MyA1Mi4xNDc4QzI5Ljg2NyA1Mi4xNDc4IDMzLjgwMjggNDMuNDM1IDMzLjgwMjggMzcuNDAwNkMzMy44MDI4IDE4LjMyMzYgNS41Njc0IDI1LjMwNTMgNS41Njc0IDExLjc5NTZDNS41Njc0IDUuMTg4OTUgMTAuODg4IDIuMzg0ODMgMTYuNjc4OSAyLjM4NDgzQzIzLjEzMyAyLjM4NDgzIDI4LjMzOTcgNi4zNzMzNSAzMC4yNDM3IDExLjI1NzFDMzAuNDg5NiAxMS44OTI3IDMwLjgwNjggMTMuMzI5NSAzMS42MTI1IDEzLjMyOTVDMzIuMzQ3OSAxMy4zMjk1IDMyLjE3NjUgMTIuODExNCAzMi4xMDYzIDEyLjIzNkwzMS4wNTU4IDIuMTk3ODJDMzAuOTgzNCAxLjQwNTg0IDMwLjYxOTQgMS4xOTEyNCAzMC4xNzY3IDEuMTkxMjRDMjkuNDQ0NCAxLjE5MTI0IDI4Ljk5ODUgMi40NzE2OSAyNy42Mjg3IDIuMjIwM0MyNi42MTk3IDIuMDM2MzYgMjQuODQ5NyAwLjA2NDA3MyAxNy4yNjk2IDAuMDAwNzE0NTNDOC44MzcgLTAuMDY4Nzc1NCAxLjY1MDY5IDQuOTMyNDUgMS42NTA2OSAxNC4wNTcxQzEuNjUwNjkgMzIuMTI5NiAyOS4wOTc1IDI1LjIyNTYgMjkuMjMxNiAzOC41MzA4QzI5LjI5OTcgNDYuMDczNSAyNC4yOTc0IDQ5Ljk1MDcgMTcuMzMwMyA0OS45NTA3QzcuMjE3MSA0OS45NTA3IDIuNzU1NDYgNDIuNDIyMyAxLjg3ODQ1IDM5LjExODRDMS41MTAyIDM3LjYxMDEgMS4zNTI2OCAzNi43OTc3IDAuNTY3MjA2IDM2LjkyMjNDLTAuMTU0NDA1IDM3LjAzODggLTAuMDg0MTU5NyAzNy44NDYxIDAuMjEwNjU4IDM5LjI4M0MwLjIxMDY1OCAzOS4yODMgMS4xNTc5MSA0Mi4zODE0IDEuMzg4ODYgNDguMzE5N1oiCiAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyODUuNDk1IDQuNDUxMTcpIgogIC8+CjxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMjYuMjA1OCA1My4yMTI5QzQyLjg5OTcgNTMuMjEyOSA1NC41MDUxIDQyLjU1NTQgNTQuNTA1MSAyNi4wMTg5QzU0LjUwNTEgMTAuODAyNiA0Mi43NTE3IDAgMjcuNDc5OCAwQzEyLjEzMDEgMCAwIDExLjA5NjkgMCAyNi4zODY3QzAgNDEuODkzMiA5Ljk1NTY4IDUzLjIxMjkgMjYuMjA1OCA1My4yMTI5Wk0yNS42NTYgMi45MDgyQzQwLjI1NDMgMi45MDgyIDQ5LjA0NTYgMTUuNDMzOCA0OS4wNDU2IDI4LjgwOTVDNDkuMDQ1NiA0Mi40MDkxIDM5Ljg1NzMgNTAuNzI2NCAyOC45MjU2IDUwLjcyNjRDMTQuMDk5NiA1MC43MjY0IDUuNDcyMTcgMzcuODQ5NCA1LjQ3MjE3IDI0LjI1MzlDNS40NzIxNyAxMi42Mzk5IDEzLjA3NjggMi45MDgyIDI1LjY1NiAyLjkwODJaIgogICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTcyLjI1NiA0LjgyODEyKSIKICAvPgo8cGF0aAogICAgZmlsbC1ydWxlPSJldmVub2RkIgogICAgY2xpcC1ydWxlPSJldmVub2RkIgogICAgZD0iTTI3LjA4MDYgMjUuMzkwMkMzMS44MjIyIDIzLjM3MDkgMzYuMjcyMSAxOC44MTkzIDM2LjI3MjEgMTMuMTkwNkMzNi4yNzIxIC0wLjczODAxNiAyMC42MTU5IDAuMDA2OTU2NTEgMTkuNTMxNCAwLjAwNjk1NjUxQzE2LjAwNjQgMC4wMDY5NTY1MSAxMS40MzgzIDAuNjU2ODkxIDcuNTA0NTQgMC42NTY4OTFDNC4wNDk3NSAwLjY1Njg5MSAyLjU3NjczIDAuMjc1NzE5IDEuMzU3MDEgMC4yNzU3MTlDMC41NDI4MDUgMC4yNzU3MTkgMCAwLjcwOTAwOSAwIDEuMTQyM0MwIDMuNTk1OTEgNy44NDE5MyAtMC4wNTAyNzAzIDcuNjQyOTEgNy42NThWNDQuMzkyNkM3LjY0MjkxIDQ5Ljg3NTEgMy42OTIxNCA0OS40MjM1IDIuODQzODcgNTAuMDQ4OUMyLjQ4NzMyIDUwLjMxMzUgMi4xMDk0OSA1MS4zMDQ4IDIuNTE2MDYgNTEuMzA0OEM1LjA5MDY2IDUxLjMwNDggNy45ODAzIDUwLjk5MzEgMTAuNTU2IDUwLjk5MzFDMTYuMTgyIDUwLjk5MzEgMTkuNjMwNCA1MS4yNjE5" +
|
||||
"IDIwLjIxNjggNTEuNDIxM0MyMS4yMDc3IDUxLjY4MjkgMjEuNjY0MyA1MC40OTAzIDIwLjU1MzEgNTAuMTEzM0MxOS4xOTUxIDQ5LjY1MzQgMTMuMTAyOSA1MC40MjggMTIuOTk3NSA0NC40NjUyVjI5LjIzNzdDMTIuOTk3NSAyOC41MTQyIDEyLjU5MzEgMjcuMjUxMSAxNC43NjExIDI3LjIxNjNDMjEuNTk0MSAyNy4xMTExIDIxLjQ2NDIgMjYuNjExNCAyMi42MzkyIDI4LjMwNDdDMjIuNjM5MiAyOC4zMDQ3IDM1LjY4MDQgNDMuNjcwMSA0Mi43NzMgNTAuMDgxNkM0OC45Njk1IDU1LjY4NDcgNTYuNjIzMSA1OS41ODMzIDYxLjU2NDcgNTkuODE3M0M2OC4yMzE2IDYwLjEyNyA3MS44MjQ4IDU5LjA0OTkgNzEuODI0OCA1OC4xODMzQzcxLjgyNDggNTYuNDUyMiA2Ni4wMDgzIDYxLjMyMjYgNTAuMTk0NSA0OS4xNDc2QzQwLjY0NzYgNDEuOCAyOC45MDE3IDI4LjAzMjggMjcuMDgwNiAyNS43NjYyQzI2LjkzMjcgMjUuNTg3NCAyNy4wOTU1IDI1LjQxNzggMjcuMDgwNiAyNS4zOTAyWk0xMy4wMDY1IDQuOTcyNUMxMy4wMDY1IDIuMTU3MTMgMTQuNjEyNSAyLjQ1ODYgMTcuMDUzIDIuNDU4NkMyMy42OTc2IDIuNDU4NiAzMC4zOTAxIDUuNjQxODUgMzAuMzkwMSAxNC40NDY2QzMwLjM5MDEgMjMuMzk2NSAyMy4zMDI3IDI0Ljc2MzggMjAuMzg4NiAyNC43NjM4QzE3LjIwMzEgMjQuNzYzOCAxMi45OTkgMjUuMTI0NiAxMi45OTkgMjMuNzU0MkwxMy4wMDY1IDQuOTcyNVoiCiAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjYuODg1IDUuMTI4OTEpIgogIC8+CjxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMjYuOTkyMyAyNS4yOTlDMzEuODM2MSAyMy4yNzg2IDM2LjkwNDQgMTguNTcxNyAzNi45MDQ0IDEyLjk0M0MzNi45MDQ0IC0wLjk4NjYyMyAyMC4xMDUxIDAuMDE1ODcxNiAxOC45OTUgMC4wMTU4NzE2QzE1LjM5NjUgMC4wMTU4NzE2IDExLjMxMjcgMC42NTk2NzUgNy4yOTcwNCAwLjY1OTY3NUMzLjc2NjY4IDAuNjU5Njc1IDIuNjMxMDUgMC4yODk3NDQgMS4zODQ3MiAwLjI4OTc0NEMwLjU1NDU1IDAuMjg5NzQ0IC0wLjAwNTI4NCAwLjcyMzAzNCAzLjc2MTI5ZS0wNSAxLjE1NjMyQzAuMDEwNjgwOCAzLjE3NTYyIDcuNTU5OTMgLTAuMDQ3NDg3MyA3LjQzNzUzIDcuNjU5NzZWNDQuMzk1NEM3LjQ2MDk1IDUwLjA1MTcgMy4zNDQxNCA0OS44NjM2IDEuOTA3MzEgNDkuODYzNkMwLjIyNzgwMyA0OS44NjM2IDIuNjU5NzggNTEuNDY1IDMuMDc0ODcgNTEuNDY1QzUuNzA0ODEgNTEuNDY1IDcuNzcxNzMgNTAuODA0OCAxMC40MDI3IDUwLjgwNDhDMTYuMTQ4IDUwLjgwNDggMjAuMDE2OCA1MS4zODQyIDIwLjYzMDkgNTEuNDg0NEMyMi4yMzI3IDUxLjc0NCAyMi41Mjc1IDUwLjU4ODIgMjAuOTg5NiA1MC4yNDE3QzE4LjUwNjUgNDkuNjc2NiAxMi45NTA3IDQ5LjczNzkgMTIuOTE4OCA0My43NTM2TDEyLjkwNiAyOS4yMzk0QzEyLjkwNiAyOC41MTcgMTIuNDkwOSAyNy4yMTkxIDE0LjcwNTggMjcuMjE5MUMyMi40NTgzIDI3LjIxOTEgMjEuODY0NCAyNi44NDYxIDI0LjM1NiAzMC44ODg4TDMyLjk0NjIgNDMuNjM2MUMzNS41NzgyIDQ3Ljc1MDMgMzguMjc2MyA1MS40MzAyIDQzLjMyOTcgNTEuNDMwMkM0My45NTM0IDUxLjQzMDIgNDguNzEwOSA1MS41MjIyIDQ4LjY2NzMgNTAuNjU1NkM0OC42MjI2IDQ5Ljc3ODggNDkuNTUwNyA0OS4yOTk1IDQ3LjUyMzEgNDkuNjEyMkM0NS44NjQ5IDQ5Ljg3MDggNDIuNjk1NCA0OC43MjUyIDM5LjQ1NDUgNDMuNTY0NkwyNi45NjA0IDI1LjczOTRDMjYuNzYzNSAyNS42MTI3IDI2Ljk5MjMgMjUuMjk5IDI2Ljk5MjMgMjUuMjk5Wk0xMy4wNDkzIDUuMDY3MTlDMTMuMDQ5MyAyLjI1Mzg3IDE0LjUwNTMgMi41MDUyNiAxNi45OTU4IDIuMzk2OTRDMjQuMjMyMiAyLjA4NTI2IDMwLjc2NzEgNS4zOTIxNiAzMC43NjcxIDE0LjE5NTlDMzAuNzY3MSAyMy4xNDQ4IDI0LjIwMjQgMjQuNzM3OSAyMS4yMjY1I" +
|
||||
"DI0LjczNzlDMTcuOTcwNyAyNC43Mzc5IDEzLjA1NjggMjUuMTY5MiAxMy4wNTY4IDIzLjc5NzhMMTMuMDQ5MyA1LjA2NzE5WiIKICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgzLjE4MjEgNS4xMjY5NSkiCiAgLz4KPHBhdGgKICAgIGZpbGwtcnVsZT0iZXZlbm9kZCIKICAgIGNsaXAtcnVsZT0iZXZlbm9kZCIKICAgIGQ9Ik0zOC4xMTAyIDQyLjAxNDhDMzguODAzMSA0NC4wMTU3IDM5LjM5NDkgNDYuMzI5MyA0MC4wMjkyIDQ3LjgzODdDNDEuNDAyMiA1MS4xMDU3IDM3Ljg3NCA1Mi4yOTYzIDM2LjUwMiA1Mi4xNjk1QzM1LjM5MDkgNTIuMDY5NCAzNS42NTA2IDUzLjk4ODUgMzcuNjI0OSA1My42NDYyQzM5LjQ2NzIgNTMuMzI1MyA0MS44MTUxIDUzLjExMTcgNDMuNjkwNSA1My4xMTE3QzQ2LjUzMzMgNTMuMTExNyA0OC45MjggNTMuNjQ2MiA1MS43NzI5IDUzLjY0NjJDNTIuMjU2MSA1My42NDYyIDUzLjY4NzcgNTIuNDgzMyA1Mi40NDc3IDUyLjE2OTVDNTEuNTY4NiA1MS45NDg4IDQ4LjkxOTUgNTEuOTgwNSA0Ny4zMTY2IDQ5LjI1ODFDNDUuNzc1NSA0Ni42MzQ5IDQ0LjY5NzMgNDMuNjU2IDQzLjY1NjQgNDAuOTE0MkwyOC43NDg0IDIuNzQwNzZDMjguNTQwOSAyLjE0ODA2IDI4LjA1NTYgMCAyNy4yOTI0IDBDMjYuMzkyIDAgMjYuMTg0NSAxLjE4NTQyIDIyLjA5MzIgMTAuNTIxNkw1LjUxOTU4IDQ4LjMxMThDMy43MTQ0OSA1Mi41MzY0IDAgNTEuMTUzOCAwIDUyLjcxMDFDMCA1My4xNTU3IDAuNjY0MTM4IDUzLjY0NjIgMS4wODM0OCA1My42NDYyQzIuODg0MzIgNTMuNjQ2MiA0Ljk1NjU1IDUzLjE3NjEgNi44Mjc2MyA1My4xNzYxQzguNzcwMDMgNTMuMTc2MSAxMC43MTM1IDUzLjI0NzcgMTMuMjY2OCA1My42NTIzQzE0Ljk5ODQgNTMuOTI1MiAxNS40MDA4IDUyLjUwNzggMTMuODg4NCA1Mi4xNjk1QzExLjkyNzkgNTEuNzMxMSA4Ljk4NjA4IDUwLjg3NDggOS40MDExNyA0Ny42NDQ1QzkuOTY2MzIgNDMuMjU5NSAxMy45ODUyIDM1LjU3ODggMTMuOTg1MiAzNS41Nzg4QzE0LjY4MDIgMzQuMDI0NSAxNi4zMDEyIDMzLjUwODQgMTcuNjE2NyAzMy41MDg0SDMyLjg3NDhDMzQuODE1MSAzMy41MDg0IDM1LjE5NzIgMzQuMjMyIDM1Ljg5MjIgMzUuOTM3NUwzOC4xMTAyIDQyLjAxNDhaTTI1LjAwNzYgMTAuNTIwM0MyNS40MjI3IDkuNjMxMjUgMjUuNzY5NyA5LjYzMTI1IDI2LjExNjYgMTAuNTIwM0wzMy4zMzE3IDI5LjM4NzhDMzMuNjc4NyAzMC4zNTM1IDMzLjc0ODkgMzAuODcxNyAzMi4yMjE2IDMwLjg3MTdIMTcuNzEyN0MxNi44MTIzIDMwLjg3MTcgMTYuMzI0OSAzMC41NzUzIDE2LjgxMjMgMjkuNTM5MUwyNS4wMDc2IDEwLjUyMDNaIgogICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzUuMzMwMSAyLjk0NTMxKSIKICAvPgo8cGF0aAogICAgZmlsbC1ydWxlPSJldmVub2RkIgogICAgY2xpcC1ydWxlPSJldmVub2RkIgogICAgZD0iTTYuMjIzNzYgNDIuODUxOEM2LjIyMzc2IDQ4LjQ3NTMgMy43MTcyOCA0OS4xNDI2IDIuOTEwNTIgNDkuMzYyNEMxLjU5Mzk2IDQ5Ljc5MzYgMC4xNjY2OTggNDkuNDI2NyAwLjI5NDQxNyA1MC42Nzc2QzAuMzYzNTk4IDUxLjMyMzQgMi4xMjI5MiA1MS4yMTUxIDIuNDg3OTkgNTEuMjE1MUM1Ljg1NzY0IDUxLjIxNTEgNy45NzQ1OCA1MS4wNTU3IDExLjM0MjEgNTEuMDU1N0MxMy44MzA1IDUxLjA1NTcgMjAuMDE0MiA1MS42ODQxIDIyLjY0NzMgNTEuNjg0MUMzNi45MjEgNTEuNjg0MSAzOS45NTAxIDQxLjMyNzEgMzkuOTY4MSAzNS45MjczQzQwLjAwMDEgMjUuNjY1MyAyOC40NjYgMjQuMTQ4OCAyOC40NjYgMjMuMDY3NkMyOC40NjYgMjIuNTYzOCAzNS42NTQ0IDE5LjgyIDM1LjY1NDQgMTIuNDY0MkMzNS42NTQ0IDcuMTI1NzggMzIuMTMzNyAwLjE1NzM3NCAxOS4yNTExIDAuMTU3Mzc0QzEzLjM5NjIgMC4xNTczNzQgMTIuMjExNiAwLjY5MDgxMSA5LjI4MjYzIDAuNjkwODExQzYuMzU1NzQgMC42OTA4MTEgMi4yNTE3MSAwIDAuNzg4MjYzIDBDLT" +
|
||||
"AuMDkwODY4NyAwIC0wLjUxODcyNyAwLjk0MDE1OCAxLjA0OTAyIDEuNTY5NjVDMi40MzU4NCAyLjEyNDU1IDYuMjIzNzYgMS44ODY0NSA2LjIyMzc2IDcuMzY3OTdWNDIuODUxOFpNMTEuNDg0NCAyNi4wNzg2QzExLjQ4NDQgMjUuNDI4NiAxMS4xMTgzIDI0LjQxOCAxNi43NTQ5IDI0LjQxOEMzMy41ODkzIDI0LjQxOCAzMy44ODIgMzMuOTc5IDMzLjg4MiAzOC41OTI5QzMzLjg4MiA0Mi45MTg2IDMyLjQxODYgNDkuNTcxMyAyMS4xNzgzIDQ5LjY5NTlDMTAuNzg2MiA0OS44MTE0IDExLjQ4NDQgNDcuNzUxMiAxMS40ODQ0IDQyLjU1NzlWMjYuMDc4NlpNMTEuNDg3NyA3Ljk0NUMxMS40ODc3IDMuNzYxMyAxMS4xMjM3IDIuNjI1OTUgMTUuNzMzMyAyLjUzMzk4QzI0LjU0OTEgMi4zNTkyMyAzMC43NzU0IDcuMDg0NTUgMzAuNTk1NSAxNC44NzA1QzMwLjQ5NTUgMTkuMTAxMiAyNy41ODg4IDIyLjc3NiAxNy45NDcxIDIyLjYyODhDMTEuMzYyMSAyMi41MjY2IDExLjQ4NzcgMjIuMTUyNiAxMS40ODc3IDE5Ljg0MjFWNy45NDVaIgogICAgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCA1LjQxNzk3KSIKICAvPgo8L3N2Zz4K\n" +
|
||||
"Goldman Sachs analysts predict that the S&P 500 will have a 4.2% price gain and end at 4,700 by the end of 2024, representing a modest increase from recent levels. They believe that the U.S. economy will continue to expand at a modest pace and that earnings will rise by 5%. The forecast includes an equity market valuation at an 18 times multiple. Goldman Sachs also predicts that the 'Magnificent 7' bundle of mega-caps, including Nvidia, Microsoft, Alphabet, Apple, Amazon.com, Meta Platforms, and Tesla, will outperform the rest of the S&P 500 in 2024. They suggest that there are more attractive investment opportunities in quality stocks, growth stocks with high ROIC, and beaten-down cyclicals. According to Lombard Odier, the US economy is expected to experience a mild slowdown in 2024, leading to two interest rate cuts from the Federal Reserve. Despite slower but still positive nominal growth, corporate earnings growth is expected to remain positive into 2024, allowing room for US stocks to advance. The S&P 500's stock index has seen a rebound, supported by strong US consumer spending and cost cuts in the corporate sector. However, corporate profits in the US have already experienced a recession, recording three quarters of year-on-year declines. The article discusses the factors contributing to the apparent 'divergence' between corporate profits and the broader economy. The author expects the S&P 500 to finish 2024 around 4,800 points. The article also mentions the potential risks to the outlook for equities, including a classic economic recession in the US and the possibility of a more bullish environment with a faster recovery in US manufacturing. The article briefly touches on the relative performance of European and emerging markets. \n" +
|
||||
"\n" +
|
||||
"#GoldmanSachs #S&P500 #stockmarket #economy #earnings \n" +
|
||||
"\n" +
|
||||
"References:\n" +
|
||||
"- Barrons: https://www.barrons.com/livecoverage/stock-market-today-111523/card/goldman-sees-4-2-rise-for-s-p-500-through-2024-as-u-s-avoids-recession-OmkU52x3hbSKJGcFBkyj\n" +
|
||||
"- 8world: https://www.8world.com/finance/gang-says-icbc-paid-ransom-over-hack-2292621\n" +
|
||||
"\n"
|
||||
|
||||
@Test
|
||||
fun testShortPost() {
|
||||
assertEquals(76, ExpandableTextCutOffCalculator.indexToCutOff(shortPost))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testLostPost() {
|
||||
assertEquals(77, ExpandableTextCutOffCalculator.indexToCutOff(longPost))
|
||||
}
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* 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.richtext
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GalleryParserTest {
|
||||
@Test
|
||||
fun testMixedImageAndVideoRenderedIndividually() {
|
||||
// Test that when image + video are in consecutive paragraphs, both should be rendered individually (not as gallery)
|
||||
val text =
|
||||
"Renfield (2023)\n" +
|
||||
"https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" +
|
||||
"https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should have 3 paragraphs (text, image, video)
|
||||
Assert.assertEquals(3, state.paragraphs.size)
|
||||
|
||||
// First paragraph is text
|
||||
Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[0].words.size)
|
||||
|
||||
// Second paragraph is image (rendered individually, not as gallery)
|
||||
Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[1].words.size)
|
||||
Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment)
|
||||
Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
|
||||
// Third paragraph is video (rendered individually)
|
||||
Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[2].words.size)
|
||||
Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment)
|
||||
Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[2].words[0].segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMixedImageAndVideoRenderedIndividuallyDoubled() {
|
||||
// Test that when image + video are repeated, both should be rendered individually (not as gallery)
|
||||
val text =
|
||||
"Renfield (2023)\n" +
|
||||
"https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" +
|
||||
"https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4\n" +
|
||||
"Renfield (2023)\n" +
|
||||
"https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg\n" +
|
||||
"https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should have 6 paragraphs (text, image, video, text, image, video)
|
||||
Assert.assertEquals(6, state.paragraphs.size)
|
||||
|
||||
// First set: text, image, video
|
||||
Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[0].words.size)
|
||||
Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment)
|
||||
Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment)
|
||||
Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[2].words[0].segmentText)
|
||||
|
||||
// Second set: text, image, video
|
||||
Assert.assertTrue(state.paragraphs[3] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[3].words.size)
|
||||
Assert.assertTrue(state.paragraphs[4] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue(state.paragraphs[4].words[0] is ImageSegment)
|
||||
Assert.assertEquals("https://image.tmdb.org/t/p/original/ekfIcBvqfqKbI6m227NFipBNh7O.jpg", state.paragraphs[4].words[0].segmentText)
|
||||
Assert.assertTrue(state.paragraphs[5] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue(state.paragraphs[5].words[0] is VideoSegment)
|
||||
Assert.assertEquals("https://archive.org/download/cinema-horror-sci-fi/Renfield.2023.ia.mp4", state.paragraphs[5].words[0].segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleImagesRenderedAsGallery() {
|
||||
// Test that multiple images (no videos) are correctly grouped as gallery
|
||||
val text =
|
||||
"Gallery:\n" +
|
||||
"https://example.com/image1.jpg\n" +
|
||||
"https://example.com/image2.png"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should have 2 image segments (image + video URLs)
|
||||
Assert.assertEquals(2, state.paragraphs.size)
|
||||
|
||||
// Should render as gallery (1 gallery with 2 images)
|
||||
Assert.assertTrue("Should render 1 gallery", state.paragraphs[1] is ImageGalleryParagraph)
|
||||
Assert.assertEquals("Gallery should contain 2 images", 2, state.paragraphs[1].words.size)
|
||||
Assert.assertEquals("https://example.com/image1.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
Assert.assertEquals("https://example.com/image2.png", state.paragraphs[1].words[1].segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleImagesInAParagraphRenderedAsGallery() {
|
||||
// Test that multiple images (no videos) are correctly grouped as gallery
|
||||
val text =
|
||||
"Gallery:\n" +
|
||||
"https://example.com/image1.jpg https://example.com/image2.png"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should have 2 image segments (image + video URLs)
|
||||
Assert.assertEquals(2, state.paragraphs.size)
|
||||
|
||||
// Should render as gallery (1 gallery with 2 images)
|
||||
Assert.assertTrue("Should render 1 gallery", state.paragraphs[1] is ImageGalleryParagraph)
|
||||
Assert.assertEquals("Gallery should contain 2 images", 2, state.paragraphs[1].words.size)
|
||||
Assert.assertEquals("https://example.com/image1.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
Assert.assertEquals("https://example.com/image2.png", state.paragraphs[1].words[1].segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSingleImageRenderedIndividually() {
|
||||
// Test that a single image is rendered individually, not as gallery
|
||||
val text = "Single image:\nhttps://example.com/image.jpg"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should render individually (not as gallery)
|
||||
Assert.assertTrue("Should render 1 individual image", state.paragraphs[1] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue("Should not render as gallery", state.paragraphs[0] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals("https://example.com/image.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGigiMessage() {
|
||||
val text = "I played The Typing of the Dead a lot when I was younger, and now I just found out that there's a new take on it: The Last Sentence. Tempted! https://relay.dergigi.com/d6a3e33b101fe219ef251ac6261c10392c2af9918c3c252d4c202016b0b4ec83.jpg https://relay.dergigi.com/d60c9c562912573f214c2b1958cc20bf8913cd718d2ed9e020621e3e3120634b.jpg"
|
||||
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should render individually (not as gallery)
|
||||
Assert.assertEquals("I played The Typing of the Dead a lot when I was younger, and now I just found out that there's a new take on it: The Last Sentence. Tempted!", state.paragraphs[0].words[0].segmentText)
|
||||
Assert.assertTrue("Should render as gallery", state.paragraphs[1] is ImageGalleryParagraph)
|
||||
Assert.assertEquals("https://relay.dergigi.com/d6a3e33b101fe219ef251ac6261c10392c2af9918c3c252d4c202016b0b4ec83.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
Assert.assertEquals("https://relay.dergigi.com/d60c9c562912573f214c2b1958cc20bf8913cd718d2ed9e020621e3e3120634b.jpg", state.paragraphs[1].words[1].segmentText)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMixedImageAndVideoInSameParagraph() {
|
||||
// Test the user's specific example: mixed image and video in same paragraph should render individually
|
||||
val text =
|
||||
"The Crow (1994)\n" +
|
||||
"https://image.tmdb.org/t/p/original/1r7iOhXSbbuUTORNEUOvCUkQ86K.jpg\n" +
|
||||
"https://archive.org/download/the-crow-1994_20231024/The%20Crow%201994.rar/The%20Crow%201994.1080p.BluRay.x264%20.%20NVEE%2FThe%20Crow%201994.1080p.BluRay.x264%20.%20NVEE.mp4\n" +
|
||||
"#fantasy #action #thriller #film #movies #kinostr"
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should have 4 paragraphs (text, image, video, hashtags)
|
||||
Assert.assertEquals(4, state.paragraphs.size)
|
||||
|
||||
// First paragraph is text
|
||||
Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph)
|
||||
|
||||
// Second paragraph is image (rendered individually, not as gallery)
|
||||
Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment)
|
||||
Assert.assertEquals("https://image.tmdb.org/t/p/original/1r7iOhXSbbuUTORNEUOvCUkQ86K.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
|
||||
// Third paragraph is video (rendered individually)
|
||||
Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph)
|
||||
Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment)
|
||||
Assert.assertEquals("https://archive.org/download/the-crow-1994_20231024/The%20Crow%201994.rar/The%20Crow%201994.1080p.BluRay.x264%20.%20NVEE%2FThe%20Crow%201994.1080p.BluRay.x264%20.%20NVEE.mp4", state.paragraphs[2].words[0].segmentText)
|
||||
|
||||
// Fourth paragraph is hashtags
|
||||
Assert.assertTrue(state.paragraphs[3] !is ImageGalleryParagraph)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDesperadoExample() {
|
||||
// Test the exact example provided by the user
|
||||
val text =
|
||||
"Desperado (1995)\n" +
|
||||
"https://media.themoviedb.org/t/p/w440_and_h660_face/e3gwpBeXpvGZsxUya9zNym5QXrw.jpg\n" +
|
||||
"https://archive.org/download/desperado.-1995.1080p.-blu-ray.x-264.-yify/Desperado.1995.1080p.BluRay.x264.YIFY.mp4\n" +
|
||||
"#action #crime #film #movies #kinostr"
|
||||
val state = RichTextParser().parseText(text, EmptyTagList, null)
|
||||
|
||||
// Should have 4 paragraphs (text, image, video, hashtags)
|
||||
Assert.assertEquals(4, state.paragraphs.size)
|
||||
|
||||
// First paragraph is text
|
||||
Assert.assertTrue(state.paragraphs[0] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals("Desperado (1995)", state.paragraphs[0].words[0].segmentText)
|
||||
|
||||
// Second paragraph is image (rendered individually, not as gallery)
|
||||
Assert.assertTrue(state.paragraphs[1] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[1].words.size)
|
||||
Assert.assertTrue(state.paragraphs[1].words[0] is ImageSegment)
|
||||
Assert.assertEquals("https://media.themoviedb.org/t/p/w440_and_h660_face/e3gwpBeXpvGZsxUya9zNym5QXrw.jpg", state.paragraphs[1].words[0].segmentText)
|
||||
|
||||
// Third paragraph is video (rendered individually)
|
||||
Assert.assertTrue(state.paragraphs[2] !is ImageGalleryParagraph)
|
||||
Assert.assertEquals(1, state.paragraphs[2].words.size)
|
||||
Assert.assertTrue(state.paragraphs[2].words[0] is VideoSegment)
|
||||
Assert.assertEquals("https://archive.org/download/desperado.-1995.1080p.-blu-ray.x-264.-yify/Desperado.1995.1080p.BluRay.x264.YIFY.mp4", state.paragraphs[2].words[0].segmentText)
|
||||
|
||||
// Fourth paragraph is hashtags
|
||||
Assert.assertTrue(state.paragraphs[3] !is ImageGalleryParagraph)
|
||||
}
|
||||
}
|
||||
+4332
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 90 B |
Binary file not shown.
|
After Width: | Height: | Size: 233 B |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 391 B |
Reference in New Issue
Block a user