Skip to content

Introduce block-level content model for rich message rendering #109

Description

@subpop

The HTML parser (MatrixHTMLParser) currently produces a single NSAttributedString for the entire message body. This works well for inline formatting but limits how we can render block-level elements that don't map cleanly to attributed string attributes -- tables being the most immediate example (currently rendered as tab-delimited text per #93), but also relevant for future support of <img> with mxc:// sources, <details>/<summary>, and potentially math blocks.

Proposed approach:

Introduce an intermediate block model that splits parsed output into typed segments:

enum MessageBlock: Identifiable {
    case richText(NSAttributedString)
    case table(MessageTable)
    // Future: .image, .details, .math, etc.
}

struct MessageTable {
    let caption: NSAttributedString?
    let headers: [NSAttributedString]
    let alignments: [HorizontalAlignment]
    let rows: [[NSAttributedString]]
}

The parser would return [MessageBlock] instead of NSAttributedString?. When it encounters a <table>, it flushes accumulated text as a .richText block, builds a structured MessageTable from the table's cells (preserving inline formatting within each cell as NSAttributedString), then continues accumulating text into a new .richText block.

On the view side, MessageBubbleContent would iterate over the blocks within a single bubble, rendering .richText blocks with the existing MessageTextView and .table blocks with a new SwiftUI Grid-based MessageTableView. This pattern is proven in Quack, which uses the same block-level split to render Markdown tables as SwiftUI grids alongside AttributedString text.

Key changes:

  • MatrixHTMLParser.parse() returns [MessageBlock] instead of NSAttributedString?
  • Parser flushes accumulated NSMutableAttributedString when entering/leaving table context, managing blockquote depth, list state, and style stacks across the split
  • New MessageTable model type and MessageTableView (SwiftUI Grid)
  • MessageBubbleContent.textContent renders a ForEach over blocks instead of a single MessageTextView
  • Parse caches (htmlCache, markdownCache) change value type from NSAttributedString to [MessageBlock]
  • Table cells containing inline formatting (bold, links, code) are preserved as NSAttributedString within the MessageTable model

Benefits over the current tab-delimited approach:

  • Proper column alignment independent of content width
  • Bold headers, column alignment markers (:---:)
  • Alternating row backgrounds for readability
  • Text selection within individual cells
  • Extensible to other block-level elements without further architectural changes

Reference: Quack's implementation lives in MarkdownRenderer.renderBlocks(), MarkdownContentView, and MarkdownTableView.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions