Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AnkiDroid/src/main/java/com/ichi2/anki/CoroutineHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import com.ichi2.anki.pages.DeckOptionsDestination
import com.ichi2.anki.snackbar.showSnackbar
import com.ichi2.anki.utils.openUrl
import com.ichi2.utils.create
import com.ichi2.utils.formatErrorMessage
import com.ichi2.utils.message
import com.ichi2.utils.neutralButton
import com.ichi2.utils.positiveButton
Expand Down Expand Up @@ -287,7 +288,7 @@ fun Context.showError(
.Builder(this)
.create {
title(R.string.vague_error)
message(text = message)
message(text = formatErrorMessage(message))
positiveButton(R.string.dialog_ok)
if (crashReportData?.helpAction != null) {
neutralButton(R.string.help)
Expand Down
37 changes: 37 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/utils/ErrorMessageFormatter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2026 Aryan Jaiswal <aryanjaiswal123123@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.utils

import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.TypefaceSpan

private val PRE_TAG_REGEX = Regex("<pre>(.*?)</pre>", RegexOption.DOT_MATCHES_ALL)

/**
* Renders `<pre>...</pre>` blocks from backend error messages as monospaced
* text instead of leaking the raw markup to the user.
*/
fun formatErrorMessage(message: String): CharSequence {
val matches = PRE_TAG_REGEX.findAll(message).toList()
if (matches.isEmpty()) return message

val builder = SpannableStringBuilder()
var cursor = 0
for (match in matches) {
builder.append(message, cursor, match.range.first)

val codeStart = builder.length
builder.append(match.groupValues[1])
val codeEnd = builder.length

builder.setSpan(TypefaceSpan("monospace"), codeStart, codeEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

cursor = match.range.last + 1
}
if (cursor < message.length) {
builder.append(message, cursor, message.length)
}
return builder
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2026 Aryan Jaiswal <aryanjaiswal123123@gmail.com>
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.utils

import android.text.Spanned
import android.text.style.TypefaceSpan
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.ichi2.anki.EmptyApplicationCategory
import com.ichi2.testutils.EmptyApplication
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsString
import org.hamcrest.Matchers.equalTo
import org.hamcrest.Matchers.not
import org.junit.Test
import org.junit.experimental.categories.Category
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

@RunWith(AndroidJUnit4::class)
@Config(application = EmptyApplication::class)
@Category(EmptyApplicationCategory::class)
class ErrorMessageFormatterTest {
@Test
fun `plain message is returned unchanged`() {
val input = "Invalid search: an `and` was found but it is not connecting two search terms."
assertThat(formatErrorMessage(input).toString(), equalTo(input))
}

@Test
fun `pre tags are replaced with a monospace span`() {
val input = "<pre>regex parse error:\n (?i)i[\n ^\nerror: unclosed character class</pre>"

val output = formatErrorMessage(input) as Spanned

assertThat(output.toString(), not(containsString("<pre>")))
assertThat(output.toString(), not(containsString("</pre>")))
assertThat(output.toString(), equalTo("regex parse error:\n (?i)i[\n ^\nerror: unclosed character class"))

val typefaceSpans = output.getSpans(0, output.length, TypefaceSpan::class.java)
assertThat(typefaceSpans.size, equalTo(1))
assertThat(typefaceSpans[0].family, equalTo("monospace"))
}
}
Loading