-
Notifications
You must be signed in to change notification settings - Fork 13.8k
feat(message-parser): add GFM table support #41109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0c87ec3
f04125f
58ad8a3
98e529f
37a736a
e0075e6
509f163
5516f46
7ebe1dd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@rocket.chat/message-parser": minor | ||
| "@rocket.chat/gazzodown": minor | ||
| --- | ||
|
|
||
| Adds GFM-style table support to the message parser and renders it in gazzodown. | ||
|
|
||
| Parser: tables require a leading and trailing pipe on every row, support column alignment via the delimiter row (`:---`, `:--:`, `---:`), and allow inline markup inside cells (a literal pipe must be escaped as `\|`). New `TABLE`, `TABLE_ROW`, and `TABLE_CELL` AST nodes are emitted. The `TABLE` node also carries an optional `fallback` — a `[start, end]` offset span into the original source — so renderers without table support can slice the source to show the raw markup instead of dropping it, without duplicating the text into the AST. | ||
|
|
||
| Rendering: gazzodown renders these tables using Fuselage's `Table` components with per-column alignment, and shows a compact single-row preview of the table header in message previews. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { Table, TableBody, TableCell, TableHead, TableRow } from '@rocket.chat/fuselage'; | ||
| import type * as MessageParser from '@rocket.chat/message-parser'; | ||
|
|
||
| import InlineElements from '../elements/InlineElements'; | ||
|
|
||
| type TableBlockProps = { | ||
| header: MessageParser.TableCell[]; | ||
| rows: MessageParser.TableRow[]; | ||
| }; | ||
|
|
||
| // Explicit mapping (not an object lookup) so a crafted AST align like `__proto__` | ||
| // or `toString` can never resolve to an inherited value. | ||
| const toAlign = (align: MessageParser.TableCell['align']): 'start' | 'center' | 'end' | undefined => { | ||
| switch (align) { | ||
| case 'left': | ||
| return 'start'; | ||
| case 'center': | ||
| return 'center'; | ||
| case 'right': | ||
| return 'end'; | ||
| default: | ||
| return undefined; | ||
| } | ||
| }; | ||
|
|
||
| const TableBlock = ({ header, rows }: TableBlockProps) => ( | ||
| <Table striped fixed={false}> | ||
| <TableHead> | ||
| <TableRow> | ||
| {header.map((cell, index) => ( | ||
| <TableCell key={index} align={toAlign(cell.align)}> | ||
| <InlineElements>{cell.value}</InlineElements> | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| </TableHead> | ||
| <TableBody> | ||
| {rows.map((row, rowIndex) => ( | ||
| <TableRow key={rowIndex}> | ||
| {row.value.map((cell, cellIndex) => ( | ||
| <TableCell key={cellIndex} align={toAlign(cell.align)}> | ||
| <InlineElements>{cell.value}</InlineElements> | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| ))} | ||
| </TableBody> | ||
| </Table> | ||
| ); | ||
|
|
||
| export default TableBlock; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,6 +31,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spoiler, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spoilerBlock, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| strike, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| table, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| task, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tasks, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unorderedList, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -59,6 +60,7 @@ Blocks | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / BlockSpoiler | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / Code | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / HorizontalRule | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / Table | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / Heading | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / Tasks | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / OrderedList | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -88,6 +90,37 @@ BlockquoteLine | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BlockSpoiler = "||" EndOfLine first:(&(! "||") @Paragraph) rest:(&(! "||") @Paragraph)* EndOfLine? "||" { return spoilerBlock([first, ...rest]); } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Table (GFM) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * e.g: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | Header 1 | Header 2 | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | -------- | :------: | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | Cell 1 | Cell 2 | | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * v1 requires a leading and trailing pipe on every row. Alignment comes from | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * the delimiter row: `:---` left, `:--:` center, `---:` right, `---` none. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * A literal pipe inside a cell must be escaped as `\|`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Table = header:TableRowLine aligns:TableDelimiterRow body:TableRowLine* { return table(header, aligns, body, [range().start, range().end]); } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableRowLine = "|" cells:(@TableCell "|")+ EndOfLine? { return cells; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableCell = items:TableCellItem* { return reducePlainTexts(items); } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableCellItem | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| = "\\|" { return plain('|'); } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| / !"|" !EndOfLine @(InlineItemPattern / Any) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableDelimiterRow = "|" aligns:(@TableDelimiterCell "|")+ EndOfLine? { return aligns; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableDelimiterCell = [ \t]* left:":"? "-"+ right:":"? [ \t]* { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+105
to
+117
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Reject header/delimiter width mismatches.
Suggested fix-Table = header:TableRowLine aligns:TableDelimiterRow body:TableRowLine* { return table(header, aligns, body, [range().start, range().end]); }
+Table
+ = header:TableRowLine aligns:TableDelimiterRow &{ return header.length === aligns.length; } body:TableRowLine* {
+ return table(header, aligns, body, [range().start, range().end]);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (left && right) { return 'center'; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (right) { return 'right'; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (left) { return 'left'; } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // <t:1630360800:?{format}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // <t:2025-07-22T10:00:00.000Z?:?{format}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // <t:2025-07-22T10:00:00:?{format}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: Implement GFM column-count semantics in the table rule. Malformed header/delimiter counts should fall back to paragraphs, and ragged body rows should be padded/truncated before rendering.
Prompt for AI agents