Skip to content

Commit

Permalink
it works!
Browse files Browse the repository at this point in the history
  • Loading branch information
rajveermalviya committed Oct 28, 2024
1 parent f86498e commit a83ac47
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 4 deletions.
123 changes: 123 additions & 0 deletions lib/model/content.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:ui';

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:html/dom.dart' as dom;
Expand Down Expand Up @@ -729,6 +731,56 @@ class GlobalTimeNode extends InlineContentNode {
}
}

class TableNode extends BlockContentNode {
const TableNode({super.debugHtmlNode, required this.rows});

final List<TableRowNode> rows;

@override
List<DiagnosticsNode> debugDescribeChildren() {
return rows
.mapIndexed((i, row) => row.toDiagnosticsNode(name: 'row $i'))
.toList();
}
}

class TableRowNode extends BlockContentNode {
const TableRowNode({super.debugHtmlNode, required this.cells, required this.isHeading});

final List<TableCellNode> cells;
final bool isHeading;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<bool>('isHeading', isHeading));
}

@override
List<DiagnosticsNode> debugDescribeChildren() {
return cells
.mapIndexed((i, cell) => cell.toDiagnosticsNode(name: 'cell $i'))
.toList();
}
}

class TableCellNode extends BlockInlineContainerNode {
const TableCellNode({
super.debugHtmlNode,
required super.nodes,
required super.links,
required this.textAlign,
});

final TextAlign textAlign;

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextAlign>('textAlign', textAlign));
}
}

////////////////////////////////////////////////////////////////
/// What sort of nodes a [_ZulipContentParser] is currently expecting to find.
Expand Down Expand Up @@ -1220,6 +1272,73 @@ class _ZulipContentParser {
return EmbedVideoNode(hrefUrl: href, previewImageSrcUrl: imgSrc, debugHtmlNode: debugHtmlNode);
}

BlockContentNode parseTableContent(dom.Element tableElement) {
assert(_debugParserContext == _ParserContext.block);
assert(tableElement.children.length == 2);
final rows = <TableRowNode>[];

TableCellNode? parseTableCell(dom.Element cell) {
final cellStyle = cell.attributes['style'];
final TextAlign textAlign;
switch (cellStyle) {
case null:
textAlign = TextAlign.start;
case 'text-align: left;':
textAlign = TextAlign.left;
case 'text-align: center;':
textAlign = TextAlign.center;
case 'text-align: right;':
textAlign = TextAlign.right;
default:
return null;
}
final parsed = parseBlockInline(cell.nodes);
return TableCellNode(
nodes: parsed.nodes,
links: parsed.links,
textAlign: textAlign);
}

final theadElement = tableElement.children.first;
assert(theadElement.localName == 'thead');
assert(theadElement.children.length == 1);

final headerRow = theadElement.children.single;
assert(headerRow.localName == 'tr');
assert(headerRow.children.isNotEmpty);

final headerCells = <TableCellNode>[];
for (final cell in headerRow.children) {
assert(cell.localName == 'th');

final parsed = parseTableCell(cell);
assert(parsed != null);
headerCells.add(parsed!);
}
rows.add(TableRowNode(cells: headerCells, isHeading: true));

final tbodyElement = tableElement.children.last;
assert(tbodyElement.localName == 'tbody');
assert(tbodyElement.children.isNotEmpty);

for (final row in tbodyElement.children) {
assert(row.localName == 'tr');
assert(row.children.isNotEmpty);

final cells = <TableCellNode>[];
for (final cell in row.children) {
assert(cell.localName == 'td');

final parsed = parseTableCell(cell);
assert(parsed != null);
cells.add(parsed!);
}
rows.add(TableRowNode(cells: cells, isHeading: false));
}

return TableNode(rows: rows);
}

BlockContentNode parseBlockContent(dom.Node node) {
assert(_debugParserContext == _ParserContext.block);
final debugHtmlNode = kDebugMode ? node : null;
Expand Down Expand Up @@ -1288,6 +1407,10 @@ class _ZulipContentParser {
parseBlockContentList(element.nodes));
}

if (localName == 'table') {
return parseTableContent(element);
}

if (localName == 'div' && className == 'spoiler-block') {
return parseSpoilerNode(element);
}
Expand Down
66 changes: 62 additions & 4 deletions lib/widgets/content.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,9 @@ class BlockContentList extends StatelessWidget {
}(),
InlineVideoNode() => MessageInlineVideo(node: node),
EmbedVideoNode() => MessageEmbedVideo(node: node),
TableNode() => MessageTable(node: node),
TableRowNode() => throw UnimplementedError(),
TableCellNode() => throw UnimplementedError(),
UnimplementedBlockContentNode() =>
Text.rich(_errorUnimplemented(node, context: context)),
};
Expand Down Expand Up @@ -806,22 +809,24 @@ class MathBlock extends StatelessWidget {
Widget _buildBlockInlineContainer({
required TextStyle style,
required BlockInlineContainerNode node,
TextAlign? textAlign,
}) {
if (node.links == null) {
return InlineContent(recognizer: null, linkRecognizers: null,
style: style, nodes: node.nodes);
style: style, nodes: node.nodes, textAlign: textAlign);
}
return _BlockInlineContainer(links: node.links!,
style: style, nodes: node.nodes);
style: style, nodes: node.nodes, textAlign: textAlign);
}

class _BlockInlineContainer extends StatefulWidget {
const _BlockInlineContainer(
{required this.links, required this.style, required this.nodes});
{required this.links, required this.style, required this.nodes, this.textAlign});

final List<LinkNode> links;
final TextStyle style;
final List<InlineContentNode> nodes;
final TextAlign? textAlign;

@override
State<_BlockInlineContainer> createState() => _BlockInlineContainerState();
Expand Down Expand Up @@ -877,6 +882,7 @@ class InlineContent extends StatelessWidget {
required this.linkRecognizers,
required this.style,
required this.nodes,
this.textAlign,
}) {
assert(style.fontSize != null);
assert(
Expand All @@ -897,14 +903,15 @@ class InlineContent extends StatelessWidget {
/// e.g., to make their content slightly smaller than surrounding text.
/// Similarly must set a font weight using [weightVariableTextStyle].
final TextStyle style;
final TextAlign? textAlign;

final List<InlineContentNode> nodes;

late final _InlineContentBuilder _builder;

@override
Widget build(BuildContext context) {
return Text.rich(_builder.build(context));
return Text.rich(_builder.build(context), textAlign: textAlign);
}
}

Expand Down Expand Up @@ -1196,6 +1203,57 @@ class GlobalTime extends StatelessWidget {
}
}

class MessageTable extends StatelessWidget {
const MessageTable({super.key, required this.node});

final TableNode node;

@override
Widget build(BuildContext context) {
return SingleChildScrollViewWithScrollbar(
scrollDirection: Axis.horizontal,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: Table(
border: TableBorder.all(
color: const HSLColor.fromAHSL(1, 0, 0, 0.33).toColor()),
defaultColumnWidth: const IntrinsicColumnWidth(),
children: node.rows.map((row) {
return TableRow(
decoration: row.isHeading
? const BoxDecoration(color: Colors.black)
: null,
children: row.cells.map((cell) {
return MessageTableCell(node: cell);
}).toList(growable: false),
);
}).toList(growable: false),
),
),
);
}
}

class MessageTableCell extends StatelessWidget {
const MessageTableCell({super.key, required this.node});

final TableCellNode node;

@override
Widget build(BuildContext context) {
return TableCell(
verticalAlignment: TableCellVerticalAlignment.top,
child: node.nodes.isNotEmpty
? Padding(
padding: const EdgeInsets.all(5),
child: _buildBlockInlineContainer(
node: node,
style: DefaultTextStyle.of(context).style,
textAlign: node.textAlign))
: const SizedBox.shrink());
}
}

void _launchUrl(BuildContext context, String urlString) async {
DialogStatus showError(BuildContext context, String? message) {
return showErrorDialog(context: context,
Expand Down
50 changes: 50 additions & 0 deletions test/model/content_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:ui';

import 'package:checks/checks.dart';
import 'package:html/parser.dart';
Expand Down Expand Up @@ -881,6 +882,51 @@ class ContentExample {
]),
InlineVideoNode(srcUrl: '/user_uploads/2/78/_KoRecCHZTFrVtyTKCkIh5Hq/Big-Buck-Bunny.webm'),
]);

static const tableSmoke = ContentExample(
'table smoke',
'| a | b |\n| - | - |\n| c | d |',
'<table>\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>c</td>\n<td>d</td>\n</tr>\n</tbody>\n</table>',[
TableNode(rows: [
TableRowNode(cells: [
TableCellNode(nodes: [TextNode('a')], links: [], textAlign: TextAlign.start),
TableCellNode(nodes: [TextNode('b')], links: [], textAlign: TextAlign.start),
], isHeading: true),
TableRowNode(cells: [
TableCellNode(nodes: [TextNode('c')], links: [], textAlign: TextAlign.start),
TableCellNode(nodes: [TextNode('d')], links: [], textAlign: TextAlign.start),
], isHeading: false),
]),
]);

static const tableSmoke2 = ContentExample(
'table smoke2',
'| default-aligned | left-aligned | center-aligned | right-aligned |\n| - | :- | :-: | -: |\n| text | text | text | text |\n| long text long text long text | long text long text long text | long text long text long text | long text long text long text |',
'<table>\n<thead>\n<tr>\n<th>default-aligned</th>\n<th style="text-align: left;">left-aligned</th>\n<th style="text-align: center;">center-aligned</th>\n<th style="text-align: right;">right-aligned</th>\n</tr>\n'
'</thead>\n<tbody>\n<tr>\n<td>text</td>\n<td style="text-align: left;">text</td>\n<td style="text-align: center;">text</td>\n<td style="text-align: right;">text</td>\n</tr>\n'
'<tr>\n<td>long text long text long text</td>\n<td style="text-align: left;">long text long text long text</td>\n<td style="text-align: center;">long text long text long text</td>\n<td style="text-align: right;">long text long text long text</td>\n</tr>\n'
'</tbody>\n</table>', [
TableNode(rows: [
TableRowNode(cells: [
TableCellNode(nodes: [TextNode('default-aligned')], links: [], textAlign: TextAlign.start),
TableCellNode(nodes: [TextNode('left-aligned')], links: [], textAlign: TextAlign.left),
TableCellNode(nodes: [TextNode('center-aligned')], links: [], textAlign: TextAlign.center),
TableCellNode(nodes: [TextNode('right-aligned')], links: [], textAlign: TextAlign.right),
], isHeading: true),
TableRowNode(cells: [
TableCellNode(nodes: [TextNode('text')], links: [], textAlign: TextAlign.start),
TableCellNode(nodes: [TextNode('text')], links: [], textAlign: TextAlign.left),
TableCellNode(nodes: [TextNode('text')], links: [], textAlign: TextAlign.center),
TableCellNode(nodes: [TextNode('text')], links: [], textAlign: TextAlign.right),
], isHeading: false),
TableRowNode(cells: [
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlign: TextAlign.start),
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlign: TextAlign.left),
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlign: TextAlign.center),
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlign: TextAlign.right),
], isHeading: false),
]),
]);
}

UnimplementedBlockContentNode blockUnimplemented(String html) {
Expand Down Expand Up @@ -1208,6 +1254,10 @@ void main() {
testParseExample(ContentExample.videoInline);
testParseExample(ContentExample.videoInlineClassesFlipped);

testParseExample(ContentExample.tableSmoke);
testParseExample(ContentExample.tableSmoke2);
// testParseExample(ContentExample.tableSmoke3);

testParse('parse nested lists, quotes, headings, code blocks',
// "1. > ###### two\n > * three\n\n four"
'<ol>\n<li>\n<blockquote>\n<h6>two</h6>\n<ul>\n<li>three</li>\n'
Expand Down

0 comments on commit a83ac47

Please sign in to comment.