Add support for <details> and <summary> tags (#6077)

This commit is contained in:
Stefan Ceriu
2026-09-03 11:47:12 +03:00
committed by GitHub
parent 7052751e38
commit 4355307ba0
15 changed files with 134 additions and 22 deletions
@@ -24,6 +24,7 @@ nonisolated protocol MentionBuilderProtocol: Sendable {
nonisolated extension NSAttributedString.Key {
static let MatrixBlockquote: NSAttributedString.Key = .init(rawValue: BlockquoteAttribute.name)
static let MatrixDetails: NSAttributedString.Key = .init(rawValue: DetailsAttribute.name)
static let MatrixUserID: NSAttributedString.Key = .init(rawValue: UserIDAttribute.name)
static let MatrixUserDisplayName: NSAttributedString.Key = .init(rawValue: UserDisplayNameAttribute.name)
static let MatrixRoomDisplayName: NSAttributedString.Key = .init(rawValue: RoomDisplayNameAttribute.name)
@@ -203,6 +204,22 @@ nonisolated struct AttributedStringBuilder: AttributedStringBuilderProtocol {
content = attributedString(element: childElement, documentBody: documentBody, preserveFormatting: preserveFormatting, listTag: listTag, listIndex: &childIndex, indentLevel: indentLevel)
content.addAttribute(.MatrixBlockquote, value: true, range: NSRange(location: 0, length: content.length))
case "details":
// Pull the summary out of the tree so that it isn't rendered inline with the content,
// it is used as the title of the collapsed component instead.
let summaryElement = childElement.getChildNodes()
.lazy
.compactMap { $0 as? Element }
.first { $0.tagName().lowercased() == "summary" }
let summary = summaryElement.flatMap { try? $0.text() } ?? ""
try? summaryElement?.remove()
content = attributedString(element: childElement, documentBody: documentBody, preserveFormatting: preserveFormatting, listTag: listTag, listIndex: &childIndex, indentLevel: indentLevel)
// Browsers fall back to a default title when the summary is missing, match them.
let title = summary.isEmpty ? L10n.a11yViewDetails : summary
content.addAttribute(.MatrixDetails, value: title, range: NSRange(location: 0, length: content.length))
case "code", "pre":
let isCodeBlock = tag == "pre"
@@ -9,10 +9,11 @@
import Foundation
nonisolated struct AttributedStringBuilderComponent: Hashable, Identifiable {
enum ComponentType {
enum ComponentType: Hashable {
case plainText
case blockquote
case codeBlock
case details(summary: String)
}
/// Identifier for the `Identifiable` conformance, allows edits to the `FormattedBodyText` to animate seamlessly
@@ -8,11 +8,6 @@
import Foundation
enum BlockquoteAttribute: AttributedStringKey {
typealias Value = Bool
static let name = "MXBlockquoteAttribute"
}
enum UserIDAttribute: AttributedStringKey {
typealias Value = String
static let name = "MXUserIDAttribute"
@@ -65,6 +60,17 @@ enum AllUsersMentionAttribute: AttributedStringKey {
static let name = "MXAllUsersMentionAttribute"
}
enum BlockquoteAttribute: AttributedStringKey {
typealias Value = Bool
static let name = "MXBlockquoteAttribute"
}
/// Marks the content of a `<details>` element, carrying its `<summary>` as the value.
enum DetailsAttribute: AttributedStringKey {
typealias Value = String
static let name = "MXDetailsAttribute"
}
enum CodeBlockAttribute: AttributedStringKey {
typealias Value = Bool
static let name = "MXCodeBlockAttribute"
@@ -78,6 +84,7 @@ enum InlineCodeAttribute: AttributedStringKey {
nonisolated extension AttributeScopes {
struct ElementXAttributes: AttributeScope {
let blockquote: BlockquoteAttribute
let details: DetailsAttribute
let userID: UserIDAttribute
// periphery:ignore - required to make NSAttributedString to AttributedString conversion even if not used directly
@@ -19,6 +19,7 @@ nonisolated enum HTMLFixtures: String, CaseIterable {
case wideCodeBlock
case unorderedList
case orderedList
case details
var rawValue: String {
switch self {
@@ -123,6 +124,13 @@ nonisolated enum HTMLFixtures: String, CaseIterable {
<li>Skittles</li>
</ol>
"""
case .details:
"""
Text before the details\
<details><summary>Expand me</summary><p>Hidden <b>content</b> with a <a href="https://www.matrix.org">link</a> in it.</p></details>\
<details><p>These details have no summary.</p></details>\
Text after the details
"""
}
}
}