mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
New timeline event renderer: add new modules (#7721)
* Added HTML parser to abstract tree nodes * Add HTML renderer using `HtmlMessageContent` * Add custom rendering for inline code nodes besides the monospace font * Support `<s>` and `<strike>` too * Add `ContentAvoidingLayoutData` support * Improve the last block node behaviour and its integration with `ContentAvoidingLayout` * Move `ContentAvoidingLayout` to `:libraries:ui-common` so it can be shared * Add missing `:libraries:htmlrenderer:impl` entry in `DependencyHandlerScope.allLibrariesImpl` * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
97d9707d6a
commit
c0bb68c872
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.libraries.htmlrenderer.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.libraries.matrix.api)
|
||||
api(libs.jsoup)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.htmlrenderer.api
|
||||
|
||||
import org.jsoup.nodes.Document
|
||||
|
||||
/**
|
||||
* Parses the HTML [Document] of a formatted message body into a tree of [MessageNode]s
|
||||
* that a Compose renderer can turn into composables.
|
||||
*
|
||||
* The [Document] is expected to have already been sanitized (e.g. via
|
||||
* `FormattedBody.toHtmlDocument`) so that only supported tags remain.
|
||||
*/
|
||||
interface HtmlMessageParser {
|
||||
/**
|
||||
* Converts [document] into a [DocumentNode]. Never throws: unknown or empty content
|
||||
* yields a [DocumentNode] with no children.
|
||||
*/
|
||||
fun parse(document: Document): DocumentNode
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Tag used for the string annotation attached to link (`<a href>`) text ranges in a
|
||||
* [ParagraphNode.text]. The annotation value is the link target URL.
|
||||
*/
|
||||
const val LINK_ANNOTATION_TAG = "url"
|
||||
|
||||
/**
|
||||
* Tag used for the string annotation attached to inline code (`<code>`) text ranges in a
|
||||
* [ParagraphNode.text]. The annotation value is empty; only the range matters. A `<code>`
|
||||
* nested inside a `<pre>` is part of the [CodeBlockNode] and is never annotated this way.
|
||||
*/
|
||||
const val INLINE_CODE_ANNOTATION_TAG = "inline_code"
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.htmlrenderer.api
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
/**
|
||||
* Describes a mention "pill" referenced from a [ParagraphNode].
|
||||
*
|
||||
* Each mention occupies an inline-content placeholder inside the paragraph's
|
||||
* [ParagraphNode.text]; the renderer looks the id up in [ParagraphNode.inlineContent]
|
||||
* and draws the corresponding pill.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface MentionNodeContent {
|
||||
/** The text shown inside the pill (e.g. a display name, prefixed with `@` for users). */
|
||||
val displayText: String
|
||||
|
||||
/** A mention of a specific user (`<a href="matrix.to/#/@user:server">`). */
|
||||
data class User(
|
||||
override val displayText: String,
|
||||
val userId: UserId,
|
||||
) : MentionNodeContent
|
||||
|
||||
/** A mention of a room (`<a href="matrix.to/#/#room:server">`). */
|
||||
data class Room(
|
||||
override val displayText: String,
|
||||
val roomIdOrAlias: RoomIdOrAlias,
|
||||
) : MentionNodeContent
|
||||
|
||||
/** An `@room` mention notifying everyone in the room. */
|
||||
data class Everyone(
|
||||
override val displayText: String,
|
||||
) : MentionNodeContent
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.htmlrenderer.api
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
|
||||
/**
|
||||
* A node in the parsed representation of a formatted (HTML) message body.
|
||||
*
|
||||
* The tree is produced by [HtmlMessageParser] and consumed by a Compose renderer.
|
||||
* Block-level nodes ([BlockNode]) either contain other block nodes (nested lists, a
|
||||
* quote containing paragraphs, ...) or terminate in a [ParagraphNode] / [CodeBlockNode]
|
||||
* that carries the actual text.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface MessageNode
|
||||
|
||||
/** A block-level node, laid out vertically by the renderer. */
|
||||
@Immutable
|
||||
sealed interface BlockNode : MessageNode
|
||||
|
||||
/**
|
||||
* The root of a parsed message: an ordered list of block-level children.
|
||||
*/
|
||||
@Immutable
|
||||
data class DocumentNode(
|
||||
val children: ImmutableList<BlockNode>,
|
||||
) : BlockNode
|
||||
|
||||
/**
|
||||
* A run of inline content (text with inline formatting) already flattened to an
|
||||
* [AnnotatedString]. This is the only leaf that carries styled text.
|
||||
*
|
||||
* Inline formatting (`<b>`, `<i>`, `<u>`, `<del>`, inline `<code>`) is encoded as
|
||||
* [androidx.compose.ui.text.SpanStyle] ranges, and links (`<a>`) as string annotations
|
||||
* tagged [HtmlMessageParser.LINK_ANNOTATION_TAG].
|
||||
*
|
||||
* Mentions (pills) are encoded as inline-content placeholders inside [text] (see
|
||||
* [androidx.compose.foundation.text.appendInlineContent]); [inlineContent] maps each
|
||||
* placeholder id to the [MentionNodeContent] the renderer needs to draw the pill.
|
||||
*/
|
||||
@Immutable
|
||||
data class ParagraphNode(
|
||||
val text: AnnotatedString,
|
||||
val inlineContent: ImmutableMap<String, MentionNodeContent> = persistentMapOf(),
|
||||
) : BlockNode
|
||||
|
||||
/**
|
||||
* A preformatted code block (`<pre>`). Rendered in a monospaced, horizontally
|
||||
* scrollable container. The text is kept raw: no inline styling is applied inside it.
|
||||
*/
|
||||
@Immutable
|
||||
data class CodeBlockNode(
|
||||
val code: String,
|
||||
) : BlockNode
|
||||
|
||||
/**
|
||||
* A block quote (`<blockquote>`). Quotes can nest and can contain any block content.
|
||||
*/
|
||||
@Immutable
|
||||
data class QuoteNode(
|
||||
val children: ImmutableList<BlockNode>,
|
||||
) : BlockNode
|
||||
|
||||
/**
|
||||
* An ordered (`<ol>`) or unordered (`<ul>`) list.
|
||||
*
|
||||
* @param ordered whether the list is numbered (`<ol>`) or bulleted (`<ul>`).
|
||||
* @param startIndex the number the first item of an ordered list starts at (honors `ol[start]`, default 1).
|
||||
* @param items the list items, in document order.
|
||||
*/
|
||||
@Immutable
|
||||
data class ListNode(
|
||||
val ordered: Boolean,
|
||||
val startIndex: Int,
|
||||
val items: ImmutableList<ListItemNode>,
|
||||
) : BlockNode
|
||||
|
||||
/**
|
||||
* A single list item (`<li>`). Being a block container, an item can itself hold
|
||||
* paragraphs, nested lists or quotes.
|
||||
*/
|
||||
@Immutable
|
||||
data class ListItemNode(
|
||||
val children: ImmutableList<BlockNode>,
|
||||
) : MessageNode
|
||||
Reference in New Issue
Block a user