diff --git a/src/domain/session/room/timeline/MessageBody.js b/src/domain/session/room/timeline/MessageBody.js index 5f346017..b739692c 100644 --- a/src/domain/session/room/timeline/MessageBody.js +++ b/src/domain/session/room/timeline/MessageBody.js @@ -64,6 +64,15 @@ export class ListBlock { get type() { return "list"; } } +export class TableBlock { + constructor(head, body) { + this.head = head; + this.body = body; + } + + get type() { return "table"; } +} + export class RulePart { get type( ) { return "rule"; } } diff --git a/src/domain/session/room/timeline/deserialize.js b/src/domain/session/room/timeline/deserialize.js index 4dbb1109..53c1291c 100644 --- a/src/domain/session/room/timeline/deserialize.js +++ b/src/domain/session/room/timeline/deserialize.js @@ -1,4 +1,4 @@ -import { MessageBody, HeaderBlock, ListBlock, CodeBlock, PillPart, FormatPart, NewLinePart, RulePart, TextPart, LinkPart, ImagePart } from "./MessageBody.js" +import { MessageBody, HeaderBlock, TableBlock, ListBlock, CodeBlock, PillPart, FormatPart, NewLinePart, RulePart, TextPart, LinkPart, ImagePart } from "./MessageBody.js" import { linkify } from "./linkify/linkify.js"; import { parsePillLink } from "./pills.js" @@ -53,6 +53,12 @@ class Deserializer { return new ListBlock(start, nodes); } + _ensureElement(node, tag) { + return node && + this.result.isElementNode(node) && + this.result.getNodeElementName(node) === tag; + } + parseCodeBlock(node) { const result = this.result; let codeNode; @@ -60,7 +66,7 @@ class Deserializer { codeNode = child; break; } - if (!(codeNode && result.getNodeElementName(codeNode) === "CODE")) { + if (!this._ensureElement(codeNode, "CODE")) { return null; } let language = ""; @@ -89,6 +95,56 @@ class Deserializer { return new ImagePart(url, width, height, alt, title); } + parseTableRow(row, tag) { + const cells = []; + for (const node of this.result.getChildNodes(row)) { + if(!this._ensureElement(node, tag)) { + continue; + } + const children = this.result.getChildNodes(node); + const inlines = this.parseInlineNodes(children); + cells.push(inlines); + } + return cells; + } + + parseTableHead(head) { + let headRow = null; + for (const node of this.result.getChildNodes(head)) { + headRow = node; + break; + } + if (this._ensureElement(headRow, "TR")) { + return this.parseTableRow(headRow, "TH"); + } + return null; + } + + parseTableBody(body) { + const rows = []; + for (const node of this.result.getChildNodes(body)) { + if(!this._ensureElement(node, "TR")) { + continue; + } + rows.push(this.parseTableRow(node, "TD")); + } + return rows; + } + + parseTable(node) { + // We are only assuming iterable, so convert to arrary for indexing. + const children = Array.from(this.result.getChildNodes(node)); + let head, body; + if (this._ensureElement(children[0], "THEAD") && this._ensureElement(children[1], "TBODY")) { + head = this.parseTableHead(children[0]); + body = this.parseTableBody(children[1]); + } else if (this._ensureElement(children[0], "TBODY")) { + head = null; + body = this.parseTableBody(children[0]); + } + return new TableBlock(head, body); + } + /** Once a node is known to be an element, * attempt to interpret it as an inline element. * @@ -161,6 +217,8 @@ class Deserializer { const inlines = this.parseInlineNodes(children); return new FormatPart(tag, inlines); } + case "TABLE": + return this.parseTable(node); default: { if (!basicBlock.includes(tag)) { return null; diff --git a/src/platform/web/ui/general/html.js b/src/platform/web/ui/general/html.js index 78a1e83d..4be81e3b 100644 --- a/src/platform/web/ui/general/html.js +++ b/src/platform/web/ui/general/html.js @@ -94,6 +94,7 @@ export const TAG_NAMES = { [HTML_NS]: [ "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote", + "table", "thead", "tbody", "tr", "th", "td", "pre", "code", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"], [SVG_NS]: ["svg", "circle"] }; diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index 091947f0..95d7db75 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -64,12 +64,29 @@ function renderPill(pillPart) { return tag.a({ class: "pill", href: pillPart.href }, children); } +function renderTable(tablePart) { + const children = []; + if (tablePart.head) { + const headers = tablePart.head + .map(cell => tag.th({}, renderParts(cell))); + children.push(tag.thead({}, tag.tr({}, headers))) + } + const rows = []; + for (const row of tablePart.body) { + const data = row.map(cell => tag.td({}, renderParts(cell))); + rows.push(tag.tr({}, data)); + } + children.push(tag.tbody({}, rows)); + return tag.table({}, children); +} + /** * Map from part to function that outputs DOM for the part */ const formatFunction = { header: headerBlock => tag["h" + Math.min(6,headerBlock.level)]({}, renderParts(headerBlock.inlines)), codeblock: codeBlock => tag.pre({}, tag.code({}, text(codeBlock.text))), + table: tableBlock => renderTable(tableBlock), emph: emphPart => tag.em({}, renderParts(emphPart.inlines)), code: codePart => tag.code({}, text(codePart.text)), text: textPart => text(textPart.text),