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.
The HTML parser (
MatrixHTMLParser) currently produces a singleNSAttributedStringfor 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>withmxc://sources,<details>/<summary>, and potentially math blocks.Proposed approach:
Introduce an intermediate block model that splits parsed output into typed segments:
The parser would return
[MessageBlock]instead ofNSAttributedString?. When it encounters a<table>, it flushes accumulated text as a.richTextblock, builds a structuredMessageTablefrom the table's cells (preserving inline formatting within each cell asNSAttributedString), then continues accumulating text into a new.richTextblock.On the view side,
MessageBubbleContentwould iterate over the blocks within a single bubble, rendering.richTextblocks with the existingMessageTextViewand.tableblocks with a new SwiftUIGrid-basedMessageTableView. This pattern is proven in Quack, which uses the same block-level split to render Markdown tables as SwiftUI grids alongsideAttributedStringtext.Key changes:
MatrixHTMLParser.parse()returns[MessageBlock]instead ofNSAttributedString?NSMutableAttributedStringwhen entering/leaving table context, managing blockquote depth, list state, and style stacks across the splitMessageTablemodel type andMessageTableView(SwiftUIGrid)MessageBubbleContent.textContentrenders aForEachover blocks instead of a singleMessageTextViewhtmlCache,markdownCache) change value type fromNSAttributedStringto[MessageBlock]NSAttributedStringwithin theMessageTablemodelBenefits over the current tab-delimited approach:
:---:)Reference: Quack's implementation lives in
MarkdownRenderer.renderBlocks(),MarkdownContentView, andMarkdownTableView.