Colour list markers by depth, highlight fences, draw images as links, and stream a list an item at a time
Items 1-5 of TRANSCRIPT_RENDERING.md's list, plus the AGP 9.4.0 bump from 7. MarkdownRoot provides the renderer's locals itself instead of calling its Markdown() composable; fences and indented blocks go through CodeFence.kt, which shares the tool-input highlighter and a fence-language alias table; an image in a paragraph is a link carrying its alt text, so every paragraph is now platform text; LiveParse freezes the finished items of the tail list so a forty-item list streams as forty paragraphs would. Measured before, on the emulator (report from transcript-bench.sh over the 200-line fence fixture): draw phase 0.72ms per frame, transcript 0.36ms. stream-bench.sh (new) streaming forty linked bullets on the old build: markdown reparsed while streaming 483, 3.9ms mean, 11.6ms worst; record: one block worst 1.6ms. The after runs, the on-screen check of the glyphs and lint are recorded as owed in the doc's "What is next". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
801618ba0e
commit
6892dc7caf
9 files changed
+533
-161
No files matched your search
@@ -21,9 +21,10 @@ import com.mikepenz.markdown.annotator.annotatorSettings
|
||||
import com.mikepenz.markdown.annotator.buildMarkdownAnnotatedString
|
||||
import com.mikepenz.markdown.compose.LocalMarkdownColors
|
||||
import com.mikepenz.markdown.compose.components.MarkdownComponentModel
|
||||
import com.mikepenz.markdown.compose.elements.MarkdownText
|
||||
import com.mikepenz.markdown.model.markdownAnnotator
|
||||
import com.mikepenz.markdown.utils.getUnescapedTextInNode
|
||||
import com.mikepenz.markdown.utils.resolveImageAlt
|
||||
import com.mikepenz.markdown.utils.resolveImageLink
|
||||
import org.intellij.markdown.MarkdownElementTypes
|
||||
import org.intellij.markdown.MarkdownTokenTypes
|
||||
import org.intellij.markdown.ast.ASTNode
|
||||
@@ -51,6 +52,12 @@ import org.intellij.markdown.flavours.gfm.GFMTokenTypes
|
||||
* paragraphs inside lists, quotes and alerts, and so does every table cell through
|
||||
* [LinkedTableRow]. Reference-style links are the one kind still drawn the renderer's way; it
|
||||
* resolves those against its definitions.
|
||||
*
|
||||
* An image is a link too, carrying its alt text. The app has no image loader and the renderer's
|
||||
* transformer was the no-op one, so an image in a reply drew as nothing at all -- a hole where the
|
||||
* model put something, with no sign of what fell out. The link says what was there and where, and
|
||||
* opens it. It also means no paragraph needs the renderer's own text composable, which existed to
|
||||
* place inline images and charged every paragraph for the possibility.
|
||||
*/
|
||||
@Composable
|
||||
fun LinkedText(model: MarkdownComponentModel, style: TextStyle) {
|
||||
@@ -82,41 +89,23 @@ fun LinkedText(content: String, node: ASTNode, style: TextStyle, modifier: Modif
|
||||
}
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val layout = remember { Ref<TextLayoutResult>() }
|
||||
val tapping =
|
||||
modifier.pointerInput(text) {
|
||||
detectTapGestures { position ->
|
||||
val url = text.linkAt(layout.value, position) ?: return@detectTapGestures
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
}
|
||||
// The renderer's text composable exists to place inline images, and it charges every text
|
||||
// for the possibility: a placement callback, a derived map of inline content, a semantics
|
||||
// group and a size animation, per paragraph. Almost no paragraph has an image, so those go
|
||||
// straight to the platform text; the few that do keep the renderer's path.
|
||||
if (remember(node) { node.hasImage() }) {
|
||||
MarkdownText(
|
||||
content = text,
|
||||
node = node,
|
||||
modifier = tapping,
|
||||
style = style,
|
||||
onTextLayout = { result, _ -> layout.value = result },
|
||||
)
|
||||
} else {
|
||||
// The renderer's own rule for a style that names no colour: the theme's text colour.
|
||||
val color = if (style.color.isSpecified) style.color else LocalMarkdownColors.current.text
|
||||
BasicText(
|
||||
text = text,
|
||||
modifier = tapping,
|
||||
style = style,
|
||||
color = { color },
|
||||
onTextLayout = { layout.value = it },
|
||||
)
|
||||
}
|
||||
// The renderer's own rule for a style that names no colour: the theme's text colour.
|
||||
val color = if (style.color.isSpecified) style.color else LocalMarkdownColors.current.text
|
||||
BasicText(
|
||||
text = text,
|
||||
modifier =
|
||||
modifier.pointerInput(text) {
|
||||
detectTapGestures { position ->
|
||||
val url = text.linkAt(layout.value, position) ?: return@detectTapGestures
|
||||
uriHandler.openUri(url)
|
||||
}
|
||||
},
|
||||
style = style,
|
||||
color = { color },
|
||||
onTextLayout = { layout.value = it },
|
||||
)
|
||||
}
|
||||
|
||||
private fun ASTNode.hasImage(): Boolean =
|
||||
type == MarkdownElementTypes.IMAGE || children.any { it.hasImage() }
|
||||
|
||||
/**
|
||||
* The address under [position], if a link's glyph is there rather than merely nearest to it.
|
||||
*
|
||||
@@ -153,7 +142,7 @@ private fun plainLinkSettings(): AnnotatorSettings {
|
||||
|
||||
/**
|
||||
* Appends [node] as a styled, annotated span if it is a link the renderer would otherwise emit a
|
||||
* `LinkAnnotation` for; false leaves anything else to the renderer.
|
||||
* `LinkAnnotation` for, or an image it would place; false leaves anything else to the renderer.
|
||||
*/
|
||||
private fun appendPlainLink(
|
||||
builder: AnnotatedString.Builder,
|
||||
@@ -162,7 +151,10 @@ private fun appendPlainLink(
|
||||
settings: AnnotatorSettings,
|
||||
): Boolean {
|
||||
val destination: String
|
||||
val label: List<ASTNode>?
|
||||
/** The label's own inline nodes, when it has markup of its own to draw. */
|
||||
var label: List<ASTNode>? = null
|
||||
/** Plain words for the label; the address itself when there are none. */
|
||||
var words: String? = null
|
||||
when (node.type) {
|
||||
MarkdownElementTypes.INLINE_LINK -> {
|
||||
val text = node.findChildOfType(MarkdownElementTypes.LINK_TEXT) ?: return false
|
||||
@@ -174,20 +166,20 @@ private fun appendPlainLink(
|
||||
// The brackets are the first and last children of the label.
|
||||
label = text.children.drop(1).dropLast(1)
|
||||
}
|
||||
MarkdownElementTypes.AUTOLINK -> {
|
||||
MarkdownElementTypes.AUTOLINK ->
|
||||
destination = node.getUnescapedTextInNode(content).removeSurrounding("<", ">")
|
||||
label = null
|
||||
}
|
||||
GFMTokenTypes.GFM_AUTOLINK -> {
|
||||
destination = node.getUnescapedTextInNode(content)
|
||||
label = null
|
||||
GFMTokenTypes.GFM_AUTOLINK -> destination = node.getUnescapedTextInNode(content)
|
||||
MarkdownElementTypes.IMAGE -> {
|
||||
destination =
|
||||
node.resolveImageLink(content, settings.referenceLinkHandler) ?: return false
|
||||
words = node.resolveImageAlt(content)
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
builder.pushStringAnnotation(LINK_URL, destination)
|
||||
builder.pushStyle(settings.linkTextSpanStyle.style ?: SpanStyle())
|
||||
if (label == null) builder.append(destination)
|
||||
else builder.buildMarkdownAnnotatedString(content, label, settings)
|
||||
if (label != null) builder.buildMarkdownAnnotatedString(content, label, settings)
|
||||
else builder.append(words ?: destination)
|
||||
builder.pop()
|
||||
builder.pop()
|
||||
return true
|
||||
|
||||
Reference in new issue
Block a user