Skip to content

Commit f8b1334

Browse files
content: Handle column text-alignment in <table>
1 parent be17750 commit f8b1334

File tree

4 files changed

+193
-45
lines changed

4 files changed

+193
-45
lines changed

lib/model/content.dart

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -543,12 +543,42 @@ 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('textAlignment', textAlignment,
580+
defaultValue: TableColumnTextAlignment.defaults));
581+
}
552582
}
553583

554584
/// A content node that expects an inline layout context from its parent.
@@ -1278,10 +1308,25 @@ class _ZulipContentParser {
12781308
assert(node.localName == (isHeader ? 'th' : 'td'));
12791309
assert(node.className.isEmpty);
12801310

1311+
final cellStyle = node.attributes['style'];
1312+
final TableColumnTextAlignment? textAlignment;
1313+
switch (cellStyle) {
1314+
case null:
1315+
textAlignment = TableColumnTextAlignment.defaults;
1316+
case 'text-align: left;':
1317+
textAlignment = TableColumnTextAlignment.left;
1318+
case 'text-align: center;':
1319+
textAlignment = TableColumnTextAlignment.center;
1320+
case 'text-align: right;':
1321+
textAlignment = TableColumnTextAlignment.right;
1322+
default:
1323+
return null;
1324+
}
12811325
final parsed = parseBlockInline(node.nodes);
12821326
return TableCellNode(
12831327
nodes: parsed.nodes,
1284-
links: parsed.links);
1328+
links: parsed.links,
1329+
textAlignment: textAlignment);
12851330
}
12861331

12871332
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: 86 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([
@@ -1021,10 +1021,10 @@ class ContentExample {
10211021
'<tbody>\n<tr>\n<td></td>\n</tr>\n</tbody>\n</table>', [
10221022
TableNode(rows: [
10231023
TableRowNode(cells: [
1024-
TableCellNode(nodes: [TextNode('table')], links: []),
1024+
TableCellNode(nodes: [TextNode('table')], links: [], textAlignment: TableColumnTextAlignment.defaults),
10251025
], isHeader: true),
10261026
TableRowNode(cells: [
1027-
TableCellNode(nodes: [], links: []),
1027+
TableCellNode(nodes: [], links: [], textAlignment: TableColumnTextAlignment.defaults),
10281028
], isHeader: false),
10291029
]),
10301030
]);
@@ -1037,12 +1037,58 @@ class ContentExample {
10371037
'<tbody>\n<tr>\n<td>text</td>\n<td></td>\n</tr>\n</tbody>\n</table>', [
10381038
TableNode(rows: [
10391039
TableRowNode(cells: [
1040-
TableCellNode(nodes: [TextNode('a')], links: []),
1041-
TableCellNode(nodes: [TextNode('b')], links: []),
1040+
TableCellNode(nodes: [TextNode('a')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1041+
TableCellNode(nodes: [TextNode('b')], links: [], textAlignment: TableColumnTextAlignment.defaults),
10421042
], isHeader: true),
10431043
TableRowNode(cells: [
1044-
TableCellNode(nodes: [TextNode('text')], links: []),
1045-
TableCellNode(nodes: [], links: []),
1044+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1045+
TableCellNode(nodes: [], links: [], textAlignment: TableColumnTextAlignment.defaults),
1046+
], isHeader: false),
1047+
]),
1048+
]);
1049+
1050+
static const tableWithDifferentTextAlignmentInColumns = ContentExample(
1051+
'table with different text alignment in columns',
1052+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/Rajesh/near/1971201
1053+
'| 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 |',
1054+
'<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'
1055+
'<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'
1056+
'<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'
1057+
'</tbody>\n</table>', [
1058+
TableNode(rows: [
1059+
TableRowNode(cells: [
1060+
TableCellNode(nodes: [TextNode('default-aligned')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1061+
TableCellNode(nodes: [TextNode('left-aligned')], links: [], textAlignment: TableColumnTextAlignment.left),
1062+
TableCellNode(nodes: [TextNode('center-aligned')], links: [], textAlignment: TableColumnTextAlignment.center),
1063+
TableCellNode(nodes: [TextNode('right-aligned')], links: [], textAlignment: TableColumnTextAlignment.right),
1064+
], isHeader: true),
1065+
TableRowNode(cells: [
1066+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1067+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.left),
1068+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.center),
1069+
TableCellNode(nodes: [TextNode('text')], links: [], textAlignment: TableColumnTextAlignment.right),
1070+
], isHeader: false),
1071+
TableRowNode(cells: [
1072+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.defaults),
1073+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.left),
1074+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.center),
1075+
TableCellNode(nodes: [TextNode('long text long text long text')], links: [], textAlignment: TableColumnTextAlignment.right),
1076+
], isHeader: false),
1077+
]),
1078+
]);
1079+
1080+
static const tableWithLinkCenterAligned = ContentExample(
1081+
'table with link; center aligned',
1082+
// https://chat.zulip.org/#narrow/channel/7-test-here/topic/.E2.9C.94.20Rajesh/near/1987982
1083+
'| header |\n| :-: |\n| https://zulip.com |',
1084+
'<table>\n<thead>\n<tr>\n<th style="text-align: center;">header</th>\n</tr>\n</thead>\n'
1085+
'<tbody>\n<tr>\n<td style="text-align: center;"><a href="https://zulip.com">https://zulip.com</a></td>\n</tr>\n</tbody>\n</table>', [
1086+
TableNode(rows: [
1087+
TableRowNode(cells: [
1088+
TableCellNode(nodes: [TextNode('header')], links: [], textAlignment: TableColumnTextAlignment.center),
1089+
], isHeader: true),
1090+
TableRowNode(cells: [
1091+
TableCellNode(nodes: [LinkNode(nodes: [TextNode('https://zulip.com')], url: 'https://zulip.com')], links: [], textAlignment: TableColumnTextAlignment.center),
10461092
], isHeader: false),
10471093
]),
10481094
]);
@@ -1382,6 +1428,8 @@ void main() {
13821428
testParseExample(ContentExample.tableWithImage);
13831429
testParseExample(ContentExample.tableWithoutAnyBodyCellsInMarkdown);
13841430
testParseExample(ContentExample.tableMissingOneBodyColumnInMarkdown);
1431+
testParseExample(ContentExample.tableWithDifferentTextAlignmentInColumns);
1432+
testParseExample(ContentExample.tableWithLinkCenterAligned);
13851433

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

0 commit comments

Comments
 (0)