Skip to content

Commit 0b6fb83

Browse files
content: Handle column text-alignment in <table>
1 parent 801c983 commit 0b6fb83

File tree

4 files changed

+166
-45
lines changed

4 files changed

+166
-45
lines changed

lib/model/content.dart

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,12 +543,43 @@ class TableRowNode extends BlockContentNode {
543543
}
544544
}
545545

546+
// The text-alignment setting that applies to a cell's column, from the delimiter row.
547+
//
548+
// See GitHub-flavored Markdown:
549+
// https://github.github.com/gfm/#tables-extension-
550+
enum TableColumnTextAlignment {
551+
/// All cells' text left-aligned, represented in Markdown as `|: --- |`.
552+
left, // TODO(i18n) RTL issues? https://github.com/zulip/zulip/issues/32265
553+
/// All cells' text center-aligned, represented in Markdown as `|: --- :|`.
554+
center,
555+
/// All cells' text right-aligned, represented in Markdown as `| --- :|`.
556+
right, // TODO(i18n) RTL issues? https://github.com/zulip/zulip/issues/32265
557+
/// Cells' text aligned the default way, represented in Markdown as `| --- |`.
558+
defaults
559+
}
560+
546561
class TableCellNode extends BlockInlineContainerNode {
547562
const TableCellNode({
548563
super.debugHtmlNode,
549564
required super.nodes,
550565
required super.links,
566+
required this.textAlignment,
551567
});
568+
569+
/// The table column text-alignment to be used for this cell.
570+
// In Markdown, alignment is defined per column using the delimiter row.
571+
// However, the generated HTML specifies alignment for each cell in a row
572+
// individually, that matches the UI widget implementation which is also
573+
// row based and needs alignment information to be per cell.
574+
final TableColumnTextAlignment textAlignment;
575+
576+
@override
577+
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
578+
super.debugFillProperties(properties);
579+
properties.add(EnumProperty(
580+
'textAlignment', textAlignment,
581+
defaultValue: TableColumnTextAlignment.defaults));
582+
}
552583
}
553584

554585
/// A content node that expects an inline layout context from its parent.
@@ -1278,10 +1309,25 @@ class _ZulipContentParser {
12781309
assert(node.localName == (isHeader ? 'th' : 'td'));
12791310
assert(node.className.isEmpty);
12801311

1312+
final cellStyle = node.attributes['style'];
1313+
final TableColumnTextAlignment? textAlignment;
1314+
switch (cellStyle) {
1315+
case null:
1316+
textAlignment = TableColumnTextAlignment.defaults;
1317+
case 'text-align: left;':
1318+
textAlignment = TableColumnTextAlignment.left;
1319+
case 'text-align: center;':
1320+
textAlignment = TableColumnTextAlignment.center;
1321+
case 'text-align: right;':
1322+
textAlignment = TableColumnTextAlignment.right;
1323+
default:
1324+
return null;
1325+
}
12811326
final parsed = parseBlockInline(node.nodes);
12821327
return TableCellNode(
12831328
nodes: parsed.nodes,
1284-
links: parsed.links);
1329+
links: parsed.links,
1330+
textAlignment: textAlignment);
12851331
}
12861332

12871333
List<TableCellNode>? parseTableCells(dom.NodeList cellNodes, bool isHeader) {

lib/widgets/content.dart

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -835,22 +835,28 @@ class MathBlock extends StatelessWidget {
835835
Widget _buildBlockInlineContainer({
836836
required TextStyle style,
837837
required BlockInlineContainerNode node,
838+
TextAlign? textAlign,
838839
}) {
839840
if (node.links == null) {
840841
return InlineContent(recognizer: null, linkRecognizers: null,
841-
style: style, nodes: node.nodes);
842+
style: style, nodes: node.nodes, textAlign: textAlign);
842843
}
843844
return _BlockInlineContainer(links: node.links!,
844-
style: style, nodes: node.nodes);
845+
style: style, nodes: node.nodes, textAlign: textAlign);
845846
}
846847

847848
class _BlockInlineContainer extends StatefulWidget {
848-
const _BlockInlineContainer(
849-
{required this.links, required this.style, required this.nodes});
849+
const _BlockInlineContainer({
850+
required this.links,
851+
required this.style,
852+
required this.nodes,
853+
this.textAlign,
854+
});
850855

851856
final List<LinkNode> links;
852857
final TextStyle style;
853858
final List<InlineContentNode> nodes;
859+
final TextAlign? textAlign;
854860

855861
@override
856862
State<_BlockInlineContainer> createState() => _BlockInlineContainerState();
@@ -895,7 +901,7 @@ class _BlockInlineContainerState extends State<_BlockInlineContainer> {
895901
@override
896902
Widget build(BuildContext context) {
897903
return InlineContent(recognizer: null, linkRecognizers: _recognizers,
898-
style: widget.style, nodes: widget.nodes);
904+
style: widget.style, nodes: widget.nodes, textAlign: widget.textAlign);
899905
}
900906
}
901907

@@ -906,6 +912,7 @@ class InlineContent extends StatelessWidget {
906912
required this.linkRecognizers,
907913
required this.style,
908914
required this.nodes,
915+
this.textAlign,
909916
}) {
910917
assert(style.fontSize != null);
911918
assert(
@@ -927,13 +934,16 @@ class InlineContent extends StatelessWidget {
927934
/// Similarly must set a font weight using [weightVariableTextStyle].
928935
final TextStyle style;
929936

937+
/// A [TextAlign] applied to this content.
938+
final TextAlign? textAlign;
939+
930940
final List<InlineContentNode> nodes;
931941

932942
late final _InlineContentBuilder _builder;
933943

934944
@override
935945
Widget build(BuildContext context) {
936-
return Text.rich(_builder.build(context));
946+
return Text.rich(_builder.build(context), textAlign: textAlign);
937947
}
938948
}
939949

@@ -1260,6 +1270,18 @@ class MessageTableCell extends StatelessWidget {
12601270

12611271
@override
12621272
Widget build(BuildContext context) {
1273+
final textAlign = switch (node.textAlignment) {
1274+
TableColumnTextAlignment.left => TextAlign.left,
1275+
TableColumnTextAlignment.center => TextAlign.center,
1276+
TableColumnTextAlignment.right => TextAlign.right,
1277+
// The web client sets `text-align: left;` for the header cells,
1278+
// overriding the default browser alignment (which is `center` for header
1279+
// and `start` for body). By default, the [Table] widget uses `start` for
1280+
// text alignment, a saner choice that supports RTL text. So, defer to that.
1281+
// See discussion:
1282+
// https://github.com/zulip/zulip-flutter/pull/1031#discussion_r1831950371
1283+
TableColumnTextAlignment.defaults => null,
1284+
};
12631285
return TableCell(
12641286
verticalAlignment: TableCellVerticalAlignment.middle,
12651287
child: Padding(
@@ -1273,6 +1295,7 @@ class MessageTableCell extends StatelessWidget {
12731295
? const SizedBox.shrink()
12741296
: _buildBlockInlineContainer(
12751297
node: node,
1298+
textAlign: textAlign,
12761299
style: !isHeader
12771300
? DefaultTextStyle.of(context).style
12781301
: DefaultTextStyle.of(context).style

test/model/content_test.dart

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -901,16 +901,16 @@ class ContentExample {
901901
'<tbody>\n<tr>\n<td>1</td>\n<td>2</td>\n<td>3</td>\n<td>4</td>\n</tr>\n</tbody>\n</table>', [
902902
TableNode(rows: [
903903
TableRowNode(cells: [
904-
TableCellNode(nodes: [TextNode('a')], links: []),
905-
TableCellNode(nodes: [TextNode('b')], links: []),
906-
TableCellNode(nodes: [TextNode('c')], links: []),
907-
TableCellNode(nodes: [TextNode('d')], links: []),
904+
TableCellNode(nodes: [TextNode('a')], links: [], textAlignment: TableColumnTextAlignment.defaults),
905+
TableCellNode(nodes: [TextNode('b')], links: [], textAlignment: TableColumnTextAlignment.defaults),
906+
TableCellNode(nodes: [TextNode('c')], links: [], textAlignment: TableColumnTextAlignment.defaults),
907+
TableCellNode(nodes: [TextNode('d')], links: [], textAlignment: TableColumnTextAlignment.defaults),
908908
], isHeader: true),
909909
TableRowNode(cells: [
910-
TableCellNode(nodes: [TextNode('1')], links: []),
911-
TableCellNode(nodes: [TextNode('2')], links: []),
912-
TableCellNode(nodes: [TextNode('3')], links: []),
913-
TableCellNode(nodes: [TextNode('4')], links: []),
910+
TableCellNode(nodes: [TextNode('1')], links: [], textAlignment: TableColumnTextAlignment.defaults),
911+
TableCellNode(nodes: [TextNode('2')], links: [], textAlignment: TableColumnTextAlignment.defaults),
912+
TableCellNode(nodes: [TextNode('3')], links: [], textAlignment: TableColumnTextAlignment.defaults),
913+
TableCellNode(nodes: [TextNode('4')], links: [], textAlignment: TableColumnTextAlignment.defaults),
914914
], isHeader: false),
915915
]),
916916
]);
@@ -925,24 +925,24 @@ class ContentExample {
925925
'<tr>\n<td>body31</td>\n<td>body32</td>\n<td>body33</td>\n</tr>\n</tbody>\n</table>', [
926926
TableNode(rows: [
927927
TableRowNode(cells: [
928-
TableCellNode(nodes: [TextNode('heading 1')], links: []),
929-
TableCellNode(nodes: [TextNode('heading 2')], links: []),
930-
TableCellNode(nodes: [TextNode('heading 3')], links: []),
928+
TableCellNode(nodes: [TextNode('heading 1')], links: [], textAlignment: TableColumnTextAlignment.defaults),
929+
TableCellNode(nodes: [TextNode('heading 2')], links: [], textAlignment: TableColumnTextAlignment.defaults),
930+
TableCellNode(nodes: [TextNode('heading 3')], links: [], textAlignment: TableColumnTextAlignment.defaults),
931931
], isHeader: true),
932932
TableRowNode(cells: [
933-
TableCellNode(nodes: [TextNode('body11')], links: []),
934-
TableCellNode(nodes: [TextNode('body12')], links: []),
935-
TableCellNode(nodes: [TextNode('body13')], links: []),
933+
TableCellNode(nodes: [TextNode('body11')], links: [], textAlignment: TableColumnTextAlignment.defaults),
934+
TableCellNode(nodes: [TextNode('body12')], links: [], textAlignment: TableColumnTextAlignment.defaults),
935+
TableCellNode(nodes: [TextNode('body13')], links: [], textAlignment: TableColumnTextAlignment.defaults),
936936
], isHeader: false),
937937
TableRowNode(cells: [
938-
TableCellNode(nodes: [TextNode('body21')], links: []),
939-
TableCellNode(nodes: [TextNode('body22')], links: []),
940-
TableCellNode(nodes: [TextNode('body23')], links: []),
938+
TableCellNode(nodes: [TextNode('body21')], links: [], textAlignment: TableColumnTextAlignment.defaults),
939+
TableCellNode(nodes: [TextNode('body22')], links: [], textAlignment: TableColumnTextAlignment.defaults),
940+
TableCellNode(nodes: [TextNode('body23')], links: [], textAlignment: TableColumnTextAlignment.defaults),
941941
], isHeader: false),
942942
TableRowNode(cells: [
943-
TableCellNode(nodes: [TextNode('body31')], links: []),
944-
TableCellNode(nodes: [TextNode('body32')], links: []),
945-
TableCellNode(nodes: [TextNode('body33')], links: []),
943+
TableCellNode(nodes: [TextNode('body31')], links: [], textAlignment: TableColumnTextAlignment.defaults),
944+
TableCellNode(nodes: [TextNode('body32')], links: [], textAlignment: TableColumnTextAlignment.defaults),
945+
TableCellNode(nodes: [TextNode('body33')], links: [], textAlignment: TableColumnTextAlignment.defaults),
946946
], isHeader: false),
947947
]),
948948
]);
@@ -955,16 +955,16 @@ class ContentExample {
955955
'<tbody>\n<tr>\n<td>text</td>\n<td>text</td>\n<td>text</td>\n<td>text</td>\n</tr>\n</tbody>\n</table>', [
956956
TableNode(rows: [
957957
TableRowNode(cells: [
958-
TableCellNode(nodes: [TextNode('normal heading')], links: []),
959-
TableCellNode(nodes: [EmphasisNode(nodes: [TextNode('italic heading')])], links: []),
960-
TableCellNode(nodes: [StrongNode(nodes: [TextNode('bold heading')])], links: []),
961-
TableCellNode(nodes: [StrongNode(nodes: [EmphasisNode(nodes: [TextNode('italic bold heading')])])], links: []),
958+
TableCellNode(nodes: [TextNode('normal heading')], links: [], textAlignment: TableColumnTextAlignment.defaults),
959+
TableCellNode(nodes: [EmphasisNode(nodes: [TextNode('italic heading')])], links: [], textAlignment: TableColumnTextAlignment.defaults),
960+
TableCellNode(nodes: [StrongNode(nodes: [TextNode('bold heading')])], links: [], textAlignment: TableColumnTextAlignment.defaults),
961+
TableCellNode(nodes: [StrongNode(nodes: [EmphasisNode(nodes: [TextNode('italic bold heading')])])], links: [], textAlignment: TableColumnTextAlignment.defaults),
962962
], isHeader: true),
963963
TableRowNode(cells: [
964-
TableCellNode(nodes: [TextNode('text')], links: []),
965-
TableCellNode(nodes: [TextNode('text')], links: []),
966-
TableCellNode(nodes: [TextNode('text')], links: []),
967-
TableCellNode(nodes: [TextNode('text')], links: []),
964+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
965+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
966+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
967+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
968968
], isHeader: false),
969969
]),
970970
]);
@@ -977,10 +977,10 @@ class ContentExample {
977977
'<tbody>\n<tr>\n<td><a href="https://zulip.com">https://zulip.com</a></td>\n</tr>\n</tbody>\n</table>', [
978978
TableNode(rows: [
979979
TableRowNode(cells: [
980-
TableCellNode(nodes: [LinkNode(nodes: [TextNode('https://zulip.com')], url: 'https://zulip.com')], links: []),
980+
TableCellNode(nodes: [LinkNode(nodes: [TextNode('https://zulip.com')], url: 'https://zulip.com')], links: [], textAlignment: TableColumnTextAlignment.defaults),
981981
], isHeader: true),
982982
TableRowNode(cells: [
983-
TableCellNode(nodes: [LinkNode(nodes: [TextNode('https://zulip.com')], url: 'https://zulip.com')], links: []),
983+
TableCellNode(nodes: [LinkNode(nodes: [TextNode('https://zulip.com')], url: 'https://zulip.com')], links: [], textAlignment: TableColumnTextAlignment.defaults),
984984
], isHeader: false),
985985
]),
986986
]);
@@ -994,10 +994,10 @@ class ContentExample {
994994
'<div class="message_inline_image"><a href="/user_uploads/2/6f/KS3vNT9c2tbMfMBkSbQF_Jlj/image2.jpg" title="image2.jpg"><img data-original-dimensions="2760x4912" src="/user_uploads/thumbnail/2/6f/KS3vNT9c2tbMfMBkSbQF_Jlj/image2.jpg/840x560.webp"></a></div>', [
995995
TableNode(rows: [
996996
TableRowNode(cells: [
997-
TableCellNode(nodes: [TextNode('a')], links: []),
997+
TableCellNode(nodes: [TextNode('a')], links: [], textAlignment: TableColumnTextAlignment.defaults),
998998
], isHeader: true),
999999
TableRowNode(cells: [
1000-
TableCellNode(nodes: [LinkNode(nodes: [TextNode('image2.jpg')], url: '/user_uploads/2/6f/KS3vNT9c2tbMfMBkSbQF_Jlj/image2.jpg')], links: []),
1000+
TableCellNode(nodes: [LinkNode(nodes: [TextNode('image2.jpg')], url: '/user_uploads/2/6f/KS3vNT9c2tbMfMBkSbQF_Jlj/image2.jpg')], links: [], textAlignment: TableColumnTextAlignment.defaults),
10011001
], isHeader: false),
10021002
]),
10031003
ImageNodeList([
@@ -1017,10 +1017,10 @@ class ContentExample {
10171017
'<tbody>\n<tr>\n<td></td>\n</tr>\n</tbody>\n</table>', [
10181018
TableNode(rows: [
10191019
TableRowNode(cells: [
1020-
TableCellNode(nodes: [TextNode('table')], links: []),
1020+
TableCellNode(nodes: [TextNode('table')], links: [], textAlignment: TableColumnTextAlignment.defaults),
10211021
], isHeader: true),
10221022
TableRowNode(cells: [
1023-
TableCellNode(nodes: [], links: []),
1023+
TableCellNode(nodes: [], links: [], textAlignment: TableColumnTextAlignment.defaults),
10241024
], isHeader: false),
10251025
])
10261026
]);
@@ -1033,15 +1033,45 @@ class ContentExample {
10331033
'<tbody>\n<tr>\n<td>text</td>\n<td></td>\n</tr>\n</tbody>\n</table>', [
10341034
TableNode(rows: [
10351035
TableRowNode(cells: [
1036-
TableCellNode(nodes: [TextNode('a')], links: []),
1037-
TableCellNode(nodes: [TextNode('b')], links: []),
1036+
TableCellNode(nodes: [TextNode('a')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1037+
TableCellNode(nodes: [TextNode('b')], links: [], textAlignment: TableColumnTextAlignment.defaults),
10381038
], isHeader: true),
10391039
TableRowNode(cells: [
1040-
TableCellNode(nodes: [TextNode('text')], links: []),
1041-
TableCellNode(nodes: [], links: []),
1040+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1041+
TableCellNode(nodes: [], links: [], textAlignment: TableColumnTextAlignment.defaults),
10421042
], isHeader: false),
10431043
])
10441044
]);
1045+
1046+
static const tableWithDifferentTextAlignmentInColumns = ContentExample(
1047+
'table with different text aligment in columns',
1048+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/1971201
1049+
'| 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 |',
1050+
'<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'
1051+
'<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'
1052+
'<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'
1053+
'</tbody>\n</table>', [
1054+
TableNode(rows: [
1055+
TableRowNode(cells: [
1056+
TableCellNode(nodes: [TextNode('default-aligned')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1057+
TableCellNode(nodes: [TextNode('left-aligned')], links: [], textAlignment: TableColumnTextAlignment.left),
1058+
TableCellNode(nodes: [TextNode('center-aligned')], links: [], textAlignment: TableColumnTextAlignment.center),
1059+
TableCellNode(nodes: [TextNode('right-aligned')], links: [], textAlignment: TableColumnTextAlignment.right),
1060+
], isHeader: true),
1061+
TableRowNode(cells: [
1062+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1063+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.left),
1064+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.center),
1065+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.right),
1066+
], isHeader: false),
1067+
TableRowNode(cells: [
1068+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1069+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.left),
1070+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.center),
1071+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.right),
1072+
], isHeader: false),
1073+
]),
1074+
]);
10451075
}
10461076

10471077
UnimplementedBlockContentNode blockUnimplemented(String html) {
@@ -1378,6 +1408,7 @@ void main() {
13781408
testParseExample(ContentExample.tableWithImagesInCells);
13791409
testParseExample(ContentExample.tableWithoutAnyBodyCellsInMarkdown);
13801410
testParseExample(ContentExample.tableMissingOneBodyColumnInMarkdown);
1411+
testParseExample(ContentExample.tableWithDifferentTextAlignmentInColumns);
13811412

13821413
testParse('parse nested lists, quotes, headings, code blocks',
13831414
// "1. > ###### two\n > * three\n\n four"

0 commit comments

Comments
 (0)