fix: clamp ratex blocks size

This commit is contained in:
infi 2026-06-25 01:49:53 +02:00
parent 54a2b2750c
commit c445ba209e
1 changed files with 87 additions and 59 deletions

View File

@ -76,7 +76,6 @@ import com.mikepenz.markdown.model.State
import com.mikepenz.markdown.model.markdownAnnotator import com.mikepenz.markdown.model.markdownAnnotator
import com.mikepenz.markdown.model.markdownInlineContent import com.mikepenz.markdown.model.markdownInlineContent
import com.mikepenz.markdown.model.rememberMarkdownState import com.mikepenz.markdown.model.rememberMarkdownState
import java.util.concurrent.ConcurrentHashMap
import dev.snipme.highlights.Highlights import dev.snipme.highlights.Highlights
import dev.snipme.highlights.model.SyntaxThemes import dev.snipme.highlights.model.SyntaxThemes
import io.ratex.RaTeXEngine import io.ratex.RaTeXEngine
@ -90,11 +89,19 @@ import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.flavours.gfm.GFMElementTypes import org.intellij.markdown.flavours.gfm.GFMElementTypes
import org.intellij.markdown.parser.MarkdownParser import org.intellij.markdown.parser.MarkdownParser
import java.util.concurrent.ConcurrentHashMap
private data class MathEntry(val key: String, val latex: String, val displayMode: Boolean) private data class MathEntry(val key: String, val latex: String, val displayMode: Boolean)
private val mathSizeCache = ConcurrentHashMap<Triple<String, Boolean, Float>, Size>() private val mathSizeCache = ConcurrentHashMap<Triple<String, Boolean, Float>, Size>()
// Hard-cap ratex blocks size so adversarial expressions can't crash the layout engine
private const val MAX_MATH_DIMENSION_PX = 4096f
private fun Size.clampForLayout(): Size = Size(
width.coerceIn(0f, MAX_MATH_DIMENSION_PX),
height.coerceIn(0f, MAX_MATH_DIMENSION_PX),
)
private fun collectMathEntries(node: ASTNode, content: String): List<MathEntry> { private fun collectMathEntries(node: ASTNode, content: String): List<MathEntry> {
val entries = mutableListOf<MathEntry>() val entries = mutableListOf<MathEntry>()
node.children.forEach { child -> node.children.forEach { child ->
@ -103,10 +110,12 @@ private fun collectMathEntries(node: ASTNode, content: String): List<MathEntry>
val latex = child.getTextInNode(content).toString().removeSurrounding("$") val latex = child.getTextInNode(content).toString().removeSurrounding("$")
entries += MathEntry("math:i:$latex", latex, false) entries += MathEntry("math:i:$latex", latex, false)
} }
GFMElementTypes.BLOCK_MATH -> { GFMElementTypes.BLOCK_MATH -> {
val latex = child.getTextInNode(content).toString().removeSurrounding("$$").trim() val latex = child.getTextInNode(content).toString().removeSurrounding("$$").trim()
entries += MathEntry("math:b:$latex", latex, true) entries += MathEntry("math:b:$latex", latex, true)
} }
else -> entries += collectMathEntries(child, content) else -> entries += collectMathEntries(child, content)
} }
} }
@ -117,7 +126,9 @@ private fun collectEmoteUlids(node: ASTNode, content: String): List<String> {
val ulids = mutableListOf<String>() val ulids = mutableListOf<String>()
node.children.forEach { child -> node.children.forEach { child ->
when (child.type) { when (child.type) {
CUSTOM_EMOTE_ELEMENT_TYPE -> ulids += child.getTextInNode(content).toString().removeSurrounding(":") CUSTOM_EMOTE_ELEMENT_TYPE -> ulids += child.getTextInNode(content).toString()
.removeSurrounding(":")
else -> ulids += collectEmoteUlids(child, content) else -> ulids += collectEmoteUlids(child, content)
} }
} }
@ -212,11 +223,16 @@ fun ChatMarkdown(
var mathSizes by remember(mathEntries, fontSizePx) { var mathSizes by remember(mathEntries, fontSizePx) {
mutableStateOf(buildMap { mutableStateOf(buildMap {
mathEntries.forEach { entry -> mathEntries.forEach { entry ->
mathSizeCache[Triple(entry.latex, entry.displayMode, fontSizePx)]?.let { put(entry.key, it) } mathSizeCache[Triple(
entry.latex,
entry.displayMode,
fontSizePx
)]?.let { put(entry.key, it) }
} }
}) })
} }
val uncachedEntries = mathEntries.filter { mathSizeCache[Triple(it.latex, it.displayMode, fontSizePx)] == null } val uncachedEntries =
mathEntries.filter { mathSizeCache[Triple(it.latex, it.displayMode, fontSizePx)] == null }
LaunchedEffect(mathEntries, fontSizePx) { LaunchedEffect(mathEntries, fontSizePx) {
if (uncachedEntries.isEmpty()) return@LaunchedEffect if (uncachedEntries.isEmpty()) return@LaunchedEffect
withContext(Dispatchers.IO) { RaTeXFontLoader.ensureLoaded(context) } withContext(Dispatchers.IO) { RaTeXFontLoader.ensureLoaded(context) }
@ -224,12 +240,14 @@ fun ChatMarkdown(
buildMap { buildMap {
uncachedEntries.forEach { entry -> uncachedEntries.forEach { entry ->
try { try {
val dl = RaTeXEngine.parseBlocking(entry.latex, entry.displayMode, onSurfaceArgb) val dl =
RaTeXEngine.parseBlocking(entry.latex, entry.displayMode, onSurfaceArgb)
val r = RaTeXRenderer(dl, fontSizePx) { RaTeXFontLoader.getTypeface(it) } val r = RaTeXRenderer(dl, fontSizePx) { RaTeXFontLoader.getTypeface(it) }
val size = Size(r.widthPx, r.totalHeightPx) val size = Size(r.widthPx, r.totalHeightPx).clampForLayout()
mathSizeCache[Triple(entry.latex, entry.displayMode, fontSizePx)] = size mathSizeCache[Triple(entry.latex, entry.displayMode, fontSizePx)] = size
put(entry.key, size) put(entry.key, size)
} catch (_: Exception) { } } catch (_: Exception) {
}
} }
} }
} }
@ -260,7 +278,8 @@ fun ChatMarkdown(
} }
GFMElementTypes.BLOCK_MATH -> { GFMElementTypes.BLOCK_MATH -> {
val latex = child.getTextInNode(content).toString().removeSurrounding("$$").trim() val latex =
child.getTextInNode(content).toString().removeSurrounding("$$").trim()
if (latex.isNotEmpty()) appendInlineContent("math:b:$latex", latex) if (latex.isNotEmpty()) appendInlineContent("math:b:$latex", latex)
true true
} }
@ -457,61 +476,70 @@ fun ChatMarkdown(
} else { } else {
fontSize * 4f to fontSize * 1.5f fontSize * 4f to fontSize * 1.5f
} }
put(entry.key, InlineTextContent( put(
Placeholder( entry.key, InlineTextContent(
width = widthSp, Placeholder(
height = heightSp, width = widthSp,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center, height = heightSp,
) placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
) { latex -> )
val foreground = LocalContentColor.current ) { latex ->
AndroidView( val foreground = LocalContentColor.current
factory = { ctx -> RaTeXView(ctx) }, AndroidView(
update = { view -> factory = { ctx -> RaTeXView(ctx) },
view.displayMode = entry.displayMode update = { view ->
view.latex = latex view.displayMode = entry.displayMode
view.fontSize = with(density) { fontSize.toPx().toDp().value } view.latex = latex
view.color = foreground.toArgb() view.fontSize =
}, with(density) { fontSize.toPx().toDp().value }
modifier = Modifier.fillMaxSize(), view.color = foreground.toArgb()
) },
}) modifier = Modifier.fillMaxSize(),
)
})
} }
emoteUlids.forEach { ulid -> emoteUlids.forEach { ulid ->
put("emote:$ulid", InlineTextContent( put(
Placeholder( "emote:$ulid", InlineTextContent(
width = fontSize * 1.5f, Placeholder(
height = fontSize * 1.5f, width = fontSize * 1.5f,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center, height = fontSize * 1.5f,
) placeholderVerticalAlign = PlaceholderVerticalAlign.Center,
) { _ -> )
val emote = StoatAPI.emojiCache[ulid] ) { _ ->
if (emote == null) { val emote = StoatAPI.emojiCache[ulid]
LaunchedEffect(ulid) { if (emote == null) {
try { LaunchedEffect(ulid) {
StoatAPI.emojiCache[ulid] = fetchEmoji(ulid) try {
} catch (_: Exception) { StoatAPI.emojiCache[ulid] = fetchEmoji(ulid)
} catch (_: Exception) {
}
}
} else {
with(LocalDensity.current) {
RemoteImage(
url = "$STOAT_FILES/emojis/$ulid",
description = emote.name,
contentScale = ContentScale.Fit,
modifier = Modifier
.width((fontSize * 1.5f).toDp())
.height((fontSize * 1.5f).toDp())
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
scope.launch {
ActionChannel.send(
Action.EmoteInfo(
ulid
)
)
}
},
)
} }
} }
} else { })
with(LocalDensity.current) {
RemoteImage(
url = "$STOAT_FILES/emojis/$ulid",
description = emote.name,
contentScale = ContentScale.Fit,
modifier = Modifier
.width((fontSize * 1.5f).toDp())
.height((fontSize * 1.5f).toDp())
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
scope.launch { ActionChannel.send(Action.EmoteInfo(ulid)) }
},
)
}
}
})
} }
} }
), ),