Revert "fix(richtext): don't truncate single-atom content mid-token"

This reverts commit 3dbf247108.
This commit is contained in:
Claude
2026-05-28 20:44:14 +00:00
parent f6b3eb0552
commit a19a025274
3 changed files with 6 additions and 132 deletions
@@ -45,28 +45,12 @@ class ExpandableTextCutOffCalculator {
// if it is still too big, finds the first space or new line BEFORE the cut off.
val newString = content.take(SHORT_TEXT_LENGTH)
val firstSpaceBeforeCut = newString.lastIndexOf(' ')
val firstNewLineBeforeCut = newString.lastIndexOf('\n')
val firstSpaceBeforeCut =
newString.lastIndexOf(' ').let { if (it < 0) newString.length else it }
val firstNewLineBeforeCut =
newString.lastIndexOf('\n').let { if (it < 0) newString.length else it }
if (firstSpaceBeforeCut < 0 && firstNewLineBeforeCut < 0) {
// No whitespace boundary anywhere in the first
// SHORT_TEXT_LENGTH characters either. The
// entire content is one indivisible atom — a
// long cashuA/cashuB token, a base64 data: URI,
// a Lightning invoice, a single huge URL. Cutting
// mid-atom corrupts the segment so the inline
// renderer can't decode it (the cashuB prefix
// survives so it gets matched, but the truncated
// body fails to base64-decode and falls back to
// rendering the raw text + a useless Show More
// button). Render the whole atom.
return content.length
}
maxOf(
firstSpaceBeforeCut.coerceAtLeast(0),
firstNewLineBeforeCut.coerceAtLeast(0),
)
maxOf(firstSpaceBeforeCut, firstNewLineBeforeCut)
} else {
min
}
@@ -1,79 +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.richtext
import com.vitorpamplona.amethyst.commons.model.EmptyTagList
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class CashuTokenParserTest {
private val realCashuBToken =
"cashuBv2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YWRkVEVTVGF0n79haUgAEHk32wzI" +
"ZWFwn79hYQJhc3hAZWM1YWI3Yjc1NjViYjBjZTZhNzg2NzBkMDA0OGExMjVlZGQzMjJhYmVjMTEzYWMwZTBmZGVkZmE3NTQ4Mzg3OWFj" +
"WCED0-ops8Ta6NjKChNJPe_jgIbXLlyxg2KSy2WaSTADo5D_v2FhCGFzeEBkNDZlODU5MDExNjU0NmNjZjAwNTE3ZTQ1NmU0MTY0N2Fm" +
"ZWUxOWNlMzY2N2IzYTcxODZkMzEwZDY1MjM3OTM4YWNYIQMTDGTY943O4ojhKopoYdemsUE2rSLfzwNBODL8WgOX0v______"
@Test
fun detectsCashuBAlone() {
val state = RichTextParser().parseText(realCashuBToken, EmptyTagList, null)
val segment = state.paragraphs[0].words[0]
assertTrue(
segment is CashuSegment,
"Expected CashuSegment for cashuB token alone, got ${segment::class.simpleName} (\"${segment.segmentText.take(20)}\")",
)
assertEquals(realCashuBToken, segment.segmentText)
}
@Test
fun detectsCashuBSurroundedByText() {
val text = "Here is a token: $realCashuBToken — enjoy"
val state = RichTextParser().parseText(text, EmptyTagList, null)
val tokenSegment =
state.paragraphs[0]
.words
.firstOrNull { it is CashuSegment }
assertTrue(
tokenSegment != null,
"Expected a CashuSegment in the parsed words; got " +
state.paragraphs[0]
.words
.joinToString { (it::class.simpleName ?: "?") + "(\"${it.segmentText.take(12)}\")" },
)
assertEquals(realCashuBToken, tokenSegment.segmentText)
}
@Test
fun detectsCashuBWithLeadingNewline() {
val text = "Here is a token:\n$realCashuBToken"
val state = RichTextParser().parseText(text, EmptyTagList, null)
val tokenSegment =
state.paragraphs
.flatMap { it.words }
.firstOrNull { it is CashuSegment }
assertTrue(
tokenSegment != null,
"Expected a CashuSegment after a newline; got " +
state.paragraphs.flatMap { it.words }.joinToString { it::class.simpleName ?: "?" },
)
assertEquals(realCashuBToken, tokenSegment.segmentText)
}
}
@@ -56,37 +56,6 @@ class ExpandableTextCutOffCalculatorTest {
@Test
fun testImage() {
// The data: URI has no whitespace anywhere — cutting it mid-base64 would
// give RichTextViewer a malformed token that fails the BASE64_IMAGE regex
// and falls back to a wall of raw text. Render the whole atom instead.
assertEquals(image.length, ExpandableTextCutOffCalculator.indexToCutOff(image))
}
@Test
fun testStandaloneCashuBTokenIsNotTruncated() {
// Real 480-char cashuB token reported by a user — no whitespace, no
// newlines. The pre-fix calculator cut at 350, leaving the renderer
// with a corrupt token that failed to decode (wall of base64 + a
// useless "Show more" button). With the fix, the whole token is
// rendered so CashuPreview can decode and show the redeem card.
val token =
"cashuBv2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YWRkVEVTVGF0n79haUgAEHk32wzI" +
"ZWFwn79hYQJhc3hAZWM1YWI3Yjc1NjViYjBjZTZhNzg2NzBkMDA0OGExMjVlZGQzMjJhYmVjMTEzYWMwZTBmZGVkZmE3NTQ4Mzg3OWFj" +
"WCED0-ops8Ta6NjKChNJPe_jgIbXLlyxg2KSy2WaSTADo5D_v2FhCGFzeEBkNDZlODU5MDExNjU0NmNjZjAwNTE3ZTQ1NmU0MTY0N2Fm" +
"ZWUxOWNlMzY2N2IzYTcxODZkMzEwZDY1MjM3OTM4YWNYIQMTDGTY943O4ojhKopoYdemsUE2rSLfzwNBODL8WgOX0v______"
assertEquals(token.length, ExpandableTextCutOffCalculator.indexToCutOff(token))
}
@Test
fun testTextBeforeLongTokenCutsAtTheBoundary() {
// Preamble of breathable text followed by a single very long atomic
// token. The cut should land at the boundary BEFORE the token starts,
// not inside it.
val preamble = "Here is a token for you: "
val token = "cashuBv2F" + "x".repeat(800)
val content = preamble + token
val cut = ExpandableTextCutOffCalculator.indexToCutOff(content)
assertEquals(preamble.length - 1, cut) // index of the space right before the token
assertEquals(350, ExpandableTextCutOffCalculator.indexToCutOff(image))
}
}