From 3083fe2218c4cd8cb289306a57d334e5cbec0bfe Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 6 Jan 2024 07:27:12 +0530 Subject: [PATCH 1/7] Lock clock when testing date time formatters This is done to fix the failing date formatters test. The reason it fails is, since one of the date format we have doesn't have year attached to it "MM-dd HH:mm:ss", it picks it from the current system time. The expected value I provided was for year 2023, but since the year is now 2024, the tests has started to fail. So locking the clock (which I should've done from the start) to a specific date time, so that the tests are reproducible. --- .../parser/DateTimeFormatters.android.kt | 10 +++-- .../core/network/parser/DateTimeFormatters.kt | 4 +- .../network/parser/DateTimeFormattersTest.kt | 4 +- .../reader/core/network/parser/TestClock.kt | 38 +++++++++++++++++++ .../network/parser/DateTimeFormatters.ios.kt | 7 ++-- 5 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/TestClock.kt diff --git a/core/network/src/androidMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.android.kt b/core/network/src/androidMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.android.kt index 18880c255..8c5f374df 100644 --- a/core/network/src/androidMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.android.kt +++ b/core/network/src/androidMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.android.kt @@ -16,13 +16,15 @@ package dev.sasikanth.rss.reader.core.network.parser -import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField import java.time.temporal.UnsupportedTemporalTypeException +import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone +import kotlinx.datetime.toJavaLocalDateTime import kotlinx.datetime.toJavaZoneId +import kotlinx.datetime.toLocalDateTime private val dateFormatters = listOf( @@ -45,13 +47,15 @@ private val dateFormatters = ) @Throws(DateTimeFormatException::class) -internal actual fun String?.dateStringToEpochMillis(): Long? { +internal actual fun String?.dateStringToEpochMillis(clock: Clock): Long? { if (this.isNullOrBlank()) return null + val currentDate = + clock.now().toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() + for (dateFormatter in dateFormatters) { try { val parsedValue = dateFormatter.parse(this) - val currentDate = LocalDateTime.now() val year = try { diff --git a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.kt b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.kt index dca7945df..77bdfef34 100644 --- a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.kt +++ b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.kt @@ -16,6 +16,8 @@ package dev.sasikanth.rss.reader.core.network.parser -internal expect fun String?.dateStringToEpochMillis(): Long? +import kotlinx.datetime.Clock + +internal expect fun String?.dateStringToEpochMillis(clock: Clock = Clock.System): Long? data class DateTimeFormatException(val exception: Exception) : Exception() diff --git a/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormattersTest.kt b/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormattersTest.kt index 9062c644d..1528e5ede 100644 --- a/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormattersTest.kt +++ b/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormattersTest.kt @@ -72,7 +72,7 @@ class DateTimeFormattersTest { val epochMillis = dates.map { try { - it.dateStringToEpochMillis() + it.dateStringToEpochMillis(clock = TestClock) } catch (e: Exception) { // no-op } @@ -88,7 +88,7 @@ class DateTimeFormattersTest { val invalidDate = "Invalid date" // when - val epochMillis = invalidDate.dateStringToEpochMillis() + val epochMillis = invalidDate.dateStringToEpochMillis(clock = TestClock) // then assertEquals(null, epochMillis) diff --git a/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/TestClock.kt b/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/TestClock.kt new file mode 100644 index 000000000..96e82e495 --- /dev/null +++ b/core/network/src/commonTest/kotlin/dev/sasikanth/rss/reader/core/network/parser/TestClock.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Sasikanth Miriyampalli + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.sasikanth.rss.reader.core.network.parser + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.Month +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant + +object TestClock : Clock { + + override fun now(): Instant { + return LocalDateTime( + year = 2023, + month = Month.JANUARY, + dayOfMonth = 1, + hour = 8, + minute = 0, + ) + .toInstant(TimeZone.UTC) + } +} diff --git a/core/network/src/iosMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.ios.kt b/core/network/src/iosMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.ios.kt index 621882361..2807342d6 100644 --- a/core/network/src/iosMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.ios.kt +++ b/core/network/src/iosMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/DateTimeFormatters.ios.kt @@ -16,7 +16,9 @@ package dev.sasikanth.rss.reader.core.network.parser +import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone +import kotlinx.datetime.toNSDate import kotlinx.datetime.toNSTimeZone import platform.Foundation.NSCalendar import platform.Foundation.NSCalendarIdentifierGregorian @@ -27,7 +29,6 @@ import platform.Foundation.NSCalendarUnitMonth import platform.Foundation.NSCalendarUnitNanosecond import platform.Foundation.NSCalendarUnitSecond import platform.Foundation.NSCalendarUnitYear -import platform.Foundation.NSDate import platform.Foundation.NSDateFormatter import platform.Foundation.NSLocale import platform.Foundation.timeIntervalSince1970 @@ -68,7 +69,7 @@ private val dateFormatters = ) @Throws(DateTimeFormatException::class) -internal actual fun String?.dateStringToEpochMillis(): Long? { +internal actual fun String?.dateStringToEpochMillis(clock: Clock): Long? { if (this.isNullOrBlank()) return null try { @@ -78,7 +79,7 @@ internal actual fun String?.dateStringToEpochMillis(): Long? { } if (date != null) { - val currentDate = NSDate() + val currentDate = clock.now().toNSDate() val calendar = NSCalendar(NSCalendarIdentifierGregorian) val currentYear = calendar.component(NSCalendarUnitYear, currentDate) From 355a8df853d8c237a74f5f5652183c9435549798 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 02:29:13 +0000 Subject: [PATCH 2/7] Update dependency com.mohamedrejeb.ksoup:ksoup-html to v0.3.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 391df4a64..a5eb401ac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,7 +31,7 @@ ktfmt = "0.44" kotlininject = "0.6.3" ksp = "1.9.21-1.0.16" material_color_utilities = "1.0.0-alpha01" -ksoup = "0.3.0" +ksoup = "0.3.1" sentry = "0.3.0" sentry_android = "4.1.1" buildKonfig = "0.15.1" From 34d7e26900407898b79a86a3850248b9520491c9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 04:37:22 +0000 Subject: [PATCH 3/7] Update android.gradle.plugin to v8.2.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5eb401ac..abc23f77d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "1.9.21" -android_gradle_plugin = "8.2.0" +android_gradle_plugin = "8.2.1" compose = "1.5.11" android_sdk_compile = "34" From 48d7101fe70596a6a06d6d33f31582093eba57b3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 05:10:57 +0000 Subject: [PATCH 4/7] Update tramlinehq/deploy-action action to v0.1.6 --- .github/workflows/android_prod_release.yml | 2 +- .github/workflows/ios_prod_release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android_prod_release.yml b/.github/workflows/android_prod_release.yml index 3c2391cb2..139c33d71 100644 --- a/.github/workflows/android_prod_release.yml +++ b/.github/workflows/android_prod_release.yml @@ -21,7 +21,7 @@ jobs: steps: - name: Configure Tramline id: tramline - uses: tramlinehq/deploy-action@v0.1.5 + uses: tramlinehq/deploy-action@v0.1.6 with: input: ${{ github.event.inputs.tramline-input }} diff --git a/.github/workflows/ios_prod_release.yml b/.github/workflows/ios_prod_release.yml index 561c4dc94..4b02009d1 100644 --- a/.github/workflows/ios_prod_release.yml +++ b/.github/workflows/ios_prod_release.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Configure Tramline id: tramline - uses: tramlinehq/deploy-action@v0.1.5 + uses: tramlinehq/deploy-action@v0.1.6 with: input: ${{ github.event.inputs.tramline-input }} From 3d5f3887e57ad6dff67e82ccde3b8a4dec643fa8 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 6 Jan 2024 11:26:41 +0530 Subject: [PATCH 5/7] Refactor `HtmlContentParser` --- .../core/network/parser/HtmlContentParser.kt | 55 ++++++++----------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt index c78c04077..a8bfad6a3 100644 --- a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt +++ b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt @@ -20,48 +20,41 @@ import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlHandler internal class HtmlContentParser(private val onEnd: (HtmlContent) -> Unit) : KsoupHtmlHandler { private val contentStringBuilder = StringBuilder() - private val currentData: MutableMap = mutableMapOf() + private var imageUrl: String? = null private var currentTag: String? = null + private val allowedContentTags = setOf("p", "a", "span", "em", "u", "b", "i", "strong") + override fun onText(text: String) { - when (currentTag) { - "p" -> contentStringBuilder.append("\n" + text.cleanWhitespaces()) - "a", - "span", - "em", - "u", - "b", - "i", - "strong" -> { - contentStringBuilder.append(text.cleanWhitespaces()) - } + if (currentTag in allowedContentTags) { + contentStringBuilder.append(text.cleanWhitespaces()) } - - currentData["content"] = contentStringBuilder.toString() } override fun onOpenTag(name: String, attributes: Map, isImplied: Boolean) { currentTag = name - when { + + if (currentTag == "p" || currentTag == "br") { + contentStringBuilder.appendLine() + } + + val srcAttr = attributes["src"].orEmpty() + if ( currentTag == "img" && - attributes.containsKey("src") && - !currentData.containsKey("imageUrl") -> { - val imageUrl = attributes["src"].toString() - if (!imageUrl.endsWith(".gif")) { - currentData["imageUrl"] = imageUrl - } - } + imageUrl.isNullOrBlank() && + srcAttr.isNotBlank() && + !srcAttr.endsWith(".gif") + ) { + this.imageUrl = srcAttr } } + override fun onCloseTag(name: String, isImplied: Boolean) { + currentTag = null + } + override fun onEnd() { - onEnd( - HtmlContent( - imageUrl = currentData["imageUrl"], - content = currentData["content"].orEmpty().trim() - ) - ) - currentData.clear() + onEnd(HtmlContent(imageUrl = imageUrl, content = contentStringBuilder.toString())) } private fun String.cleanWhitespaces(): String { @@ -76,6 +69,6 @@ internal class HtmlContentParser(private val onEnd: (HtmlContent) -> Unit) : Kso } return formattedText } -} -internal data class HtmlContent(val imageUrl: String?, val content: String) + data class HtmlContent(val imageUrl: String?, val content: String) +} From 6b41d5d3a2e97fc1e34b7ad79c0755d13d20ab49 Mon Sep 17 00:00:00 2001 From: Sasikanth Miriyampalli Date: Sat, 6 Jan 2024 12:03:35 +0530 Subject: [PATCH 6/7] Handle nested tags when parsing html content --- .../core/network/parser/HtmlContentParser.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt index a8bfad6a3..e7c4fb4d5 100644 --- a/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt +++ b/core/network/src/commonMain/kotlin/dev/sasikanth/rss/reader/core/network/parser/HtmlContentParser.kt @@ -21,36 +21,35 @@ internal class HtmlContentParser(private val onEnd: (HtmlContent) -> Unit) : Kso private val contentStringBuilder = StringBuilder() private var imageUrl: String? = null - private var currentTag: String? = null + + private var currentTagsStack: ArrayDeque = ArrayDeque() private val allowedContentTags = setOf("p", "a", "span", "em", "u", "b", "i", "strong") override fun onText(text: String) { - if (currentTag in allowedContentTags) { + val tag = currentTagsStack.firstOrNull() + if (tag in allowedContentTags || tag.isNullOrBlank()) { contentStringBuilder.append(text.cleanWhitespaces()) } } override fun onOpenTag(name: String, attributes: Map, isImplied: Boolean) { - currentTag = name + currentTagsStack.addFirst(name) - if (currentTag == "p" || currentTag == "br") { + if (name == "p" || name == "br") { contentStringBuilder.appendLine() } val srcAttr = attributes["src"].orEmpty() if ( - currentTag == "img" && - imageUrl.isNullOrBlank() && - srcAttr.isNotBlank() && - !srcAttr.endsWith(".gif") + name == "img" && imageUrl.isNullOrBlank() && srcAttr.isNotBlank() && !srcAttr.endsWith(".gif") ) { this.imageUrl = srcAttr } } override fun onCloseTag(name: String, isImplied: Boolean) { - currentTag = null + currentTagsStack.removeFirst() } override fun onEnd() { From f38829160f9983c96be447c142b860853c551d4f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 6 Jan 2024 05:25:07 +0000 Subject: [PATCH 7/7] Update dependency io.github.aakira:napier to v2.7.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abc23f77d..3189e2462 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,7 +9,7 @@ android_sdk_min = "26" sqldelight = "2.0.1" ktor = "2.3.7" -napier = "2.6.1" +napier = "2.7.1" kotlinx_coroutines = "1.7.3" kotlinx_date_time = "0.5.0" kotlinx_immutable_collections = "0.3.7"