diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart index 24c9bff7ba..acbd83d868 100644 --- a/lib/widgets/content.dart +++ b/lib/widgets/content.dart @@ -58,61 +58,34 @@ class BlockContentList extends StatelessWidget { @override Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - ...nodes.map((node) => BlockContentNodeWidget(node: node)), - // Text(nodes.map((n) => n.debugHtmlText ?? "").join()) + ...nodes.map((node) { + if (node is LineBreakNode) { + // This goes in a Column. So to get the effect of a newline, + // just use an empty Text. + return const Text(''); + } else if (node is ParagraphNode) { + return Paragraph(node: node); + } else if (node is HeadingNode) { + return Heading(node: node); + } else if (node is QuotationNode) { + return Quotation(node: node); + } else if (node is ListNode) { + return ListNodeWidget(node: node); + } else if (node is CodeBlockNode) { + return CodeBlock(node: node); + } else if (node is ImageNode) { + return MessageImage(node: node); + } else if (node is UnimplementedBlockContentNode) { + return Text.rich(_errorUnimplemented(node)); + } else { + // TODO(dart-3): Use a sealed class / pattern-matching to exclude this. + throw Exception("impossible BlockContentNode: ${node.debugHtmlText}"); + } + }), ]); } } -/// A single DOM node to display in block layout. -class BlockContentNodeWidget extends StatelessWidget { - const BlockContentNodeWidget({super.key, required this.node}); - - final BlockContentNode node; - - @override - Widget build(BuildContext context) { - final node = this.node; - if (node is LineBreakNode) { - // In block context, the widget we return is going into a Column. - // So to get the effect of a newline, just use an empty Text. - return const Text(''); - } else if (node is ParagraphNode) { - return Paragraph(node: node); - } else if (node is HeadingNode) { - // TODO(#192) h1, h2, h3, h4, h5 -- same as h6 except font size - assert(node.level == HeadingLevel.h6); - return Padding( - padding: const EdgeInsets.only(top: 15, bottom: 5), - child: Text.rich(TextSpan( - style: const TextStyle(fontWeight: FontWeight.w600, height: 1.4), - children: _buildInlineList(node.nodes)))); - } else if (node is ListNode) { - return ListNodeWidget(node: node); - } else if (node is QuotationNode) { - return Padding( - padding: const EdgeInsets.only(left: 10), - child: Container( - padding: const EdgeInsets.only(left: 5), - decoration: BoxDecoration( - border: Border( - left: BorderSide( - width: 5, - color: const HSLColor.fromAHSL(1, 0, 0, 0.87).toColor()))), - child: BlockContentList(nodes: node.nodes))); - } else if (node is CodeBlockNode) { - return CodeBlock(node: node); - } else if (node is ImageNode) { - return MessageImage(node: node); - } else if (node is UnimplementedBlockContentNode) { - return Text.rich(_errorUnimplemented(node)); - } else { - // TODO(dart-3): Use a sealed class / pattern-matching to exclude this. - throw Exception("impossible BlockContentNode: ${node.debugHtmlText}"); - } - } -} - class Paragraph extends StatelessWidget { const Paragraph({super.key, required this.node}); @@ -124,7 +97,7 @@ class Paragraph extends StatelessWidget { // The paragraph has vertical CSS margins, but those have no effect. if (node.nodes.isEmpty) return const SizedBox(); - final text = Text.rich(TextSpan(children: _buildInlineList(node.nodes))); + final text = Text.rich(_buildInlineSpan(node.nodes, style: null)); // If the paragraph didn't actually have a `p` element in the HTML, // then apply no margins. (For example, these are seen in list items.) @@ -138,6 +111,43 @@ class Paragraph extends StatelessWidget { } } +class Heading extends StatelessWidget { + const Heading({super.key, required this.node}); + + final HeadingNode node; + + @override + Widget build(BuildContext context) { + // TODO(#192) h1, h2, h3, h4, h5 -- same as h6 except font size + assert(node.level == HeadingLevel.h6); + return Padding( + padding: const EdgeInsets.only(top: 15, bottom: 5), + child: Text.rich(_buildInlineSpan( + style: const TextStyle(fontWeight: FontWeight.w600, height: 1.4), + node.nodes))); + } +} + +class Quotation extends StatelessWidget { + const Quotation({super.key, required this.node}); + + final QuotationNode node; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 10), + child: Container( + padding: const EdgeInsets.only(left: 5), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + width: 5, + color: const HSLColor.fromAHSL(1, 0, 0, 0.87).toColor()))), + child: BlockContentList(nodes: node.nodes))); + } +} + class ListNodeWidget extends StatelessWidget { const ListNodeWidget({super.key, required this.node}); @@ -287,12 +297,15 @@ class _SingleChildScrollViewWithScrollbarState // Inline layout. // -List _buildInlineList(List nodes) => - List.of(nodes.map(_buildInlineNode)); +InlineSpan _buildInlineSpan(List nodes, {required TextStyle? style}) { + return TextSpan( + style: style, + children: nodes.map(_buildInlineNode).toList(growable: false)); +} InlineSpan _buildInlineNode(InlineContentNode node) { InlineSpan styled(List nodes, TextStyle style) => - TextSpan(children: _buildInlineList(nodes), style: style); + _buildInlineSpan(nodes, style: style); if (node is TextNode) { return TextSpan(text: node.text); @@ -352,8 +365,7 @@ InlineSpan inlineCode(InlineCodeNode node) { // TODO `code`: find equivalent of web's `unicode-bidi: embed; direction: ltr` // Use a light gray background, instead of a border. - return TextSpan(style: _kInlineCodeStyle, - children: _buildInlineList(node.nodes)); + return _buildInlineSpan(style: _kInlineCodeStyle, node.nodes); // Another fun solution -- we can in fact have a border! Like so: // TextStyle( @@ -418,7 +430,7 @@ class UserMention extends StatelessWidget { return Container( decoration: _kDecoration, padding: const EdgeInsets.symmetric(horizontal: 0.2 * kBaseFontSize), - child: Text.rich(TextSpan(children: _buildInlineList(node.nodes)))); + child: Text.rich(_buildInlineSpan(node.nodes, style: null))); } static get _kDecoration => BoxDecoration(