Skip to content

Commit 93d5abf

Browse files
committed
content [nfc]: Add _InlineContentBuilder around building an inline span
This will give us a convenient way to manage "context" information down through the recursion.
1 parent 491a249 commit 93d5abf

File tree

1 file changed

+115
-85
lines changed

1 file changed

+115
-85
lines changed

lib/widgets/content.dart

Lines changed: 115 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class Paragraph extends StatelessWidget {
9797
// The paragraph has vertical CSS margins, but those have no effect.
9898
if (node.nodes.isEmpty) return const SizedBox();
9999

100-
final text = Text.rich(_buildInlineSpan(node.nodes, style: null));
100+
final text = InlineContent(nodes: node.nodes, style: null);
101101

102102
// If the paragraph didn't actually have a `p` element in the HTML,
103103
// then apply no margins. (For example, these are seen in list items.)
@@ -122,9 +122,9 @@ class Heading extends StatelessWidget {
122122
assert(node.level == HeadingLevel.h6);
123123
return Padding(
124124
padding: const EdgeInsets.only(top: 15, bottom: 5),
125-
child: Text.rich(_buildInlineSpan(
125+
child: InlineContent(
126126
style: const TextStyle(fontWeight: FontWeight.w600, height: 1.4),
127-
node.nodes)));
127+
nodes: node.nodes));
128128
}
129129
}
130130

@@ -297,91 +297,121 @@ class _SingleChildScrollViewWithScrollbarState
297297
// Inline layout.
298298
//
299299

300-
InlineSpan _buildInlineSpan(List<InlineContentNode> nodes, {required TextStyle? style}) {
301-
return TextSpan(
302-
style: style,
303-
children: nodes.map(_buildInlineNode).toList(growable: false));
304-
}
300+
class InlineContent extends StatelessWidget {
301+
InlineContent({
302+
super.key,
303+
required this.nodes,
304+
required this.style,
305+
}) {
306+
_builder = _InlineContentBuilder(this);
307+
}
305308

306-
InlineSpan _buildInlineNode(InlineContentNode node) {
307-
InlineSpan styled(List<InlineContentNode> nodes, TextStyle style) =>
308-
_buildInlineSpan(nodes, style: style);
309-
310-
if (node is TextNode) {
311-
return TextSpan(text: node.text);
312-
} else if (node is LineBreakInlineNode) {
313-
// Each `<br/>` is followed by a newline, which browsers apparently ignore
314-
// and our parser doesn't. So don't do anything here.
315-
return const TextSpan(text: "");
316-
} else if (node is StrongNode) {
317-
return styled(node.nodes, const TextStyle(fontWeight: FontWeight.w600));
318-
} else if (node is EmphasisNode) {
319-
return styled(node.nodes, const TextStyle(fontStyle: FontStyle.italic));
320-
} else if (node is InlineCodeNode) {
321-
return inlineCode(node);
322-
} else if (node is LinkNode) {
323-
// TODO make link touchable
324-
return styled(node.nodes,
325-
TextStyle(color: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor()));
326-
} else if (node is UserMentionNode) {
327-
return WidgetSpan(alignment: PlaceholderAlignment.middle,
328-
child: UserMention(node: node));
329-
} else if (node is UnicodeEmojiNode) {
330-
return WidgetSpan(alignment: PlaceholderAlignment.middle,
331-
child: MessageUnicodeEmoji(node: node));
332-
} else if (node is ImageEmojiNode) {
333-
return WidgetSpan(alignment: PlaceholderAlignment.middle,
334-
child: MessageImageEmoji(node: node));
335-
} else if (node is UnimplementedInlineContentNode) {
336-
return _errorUnimplemented(node);
337-
} else {
338-
// TODO(dart-3): Use a sealed class / pattern matching to eliminate this case.
339-
throw Exception("impossible InlineContentNode: ${node.debugHtmlText}");
309+
final List<InlineContentNode> nodes;
310+
final TextStyle? style;
311+
312+
late final _InlineContentBuilder _builder;
313+
314+
@override
315+
Widget build(BuildContext context) {
316+
return Text.rich(_builder.build());
340317
}
341318
}
342319

343-
InlineSpan inlineCode(InlineCodeNode node) {
344-
// TODO `code` elements: border, padding -- seems hard
345-
//
346-
// Hard because this is an inline span, which we want to be able to break
347-
// between lines when wrapping paragraphs. That means we can't just make it a
348-
// widget; it needs to be a [TextSpan]. And in that inline setting, Flutter
349-
// does not appear to have an equivalent for CSS's `border` or `padding`:
350-
// https://api.flutter.dev/flutter/painting/TextStyle-class.html
351-
//
352-
// One attempt was to use [TextDecoration] for the top and bottom,
353-
// passing this to the [TextStyle] constructor:
354-
// decoration: TextDecoration.combine([TextDecoration.overline, TextDecoration.underline]),
355-
// (Then we could handle the left and right borders with 1px-wide [WidgetSpan]s.)
356-
// The overline comes out OK, but sadly the underline is, well, where a normal
357-
// text underline should go: it cuts right through descenders.
358-
//
359-
// Another option would be to break the text up on whitespace ourselves, and
360-
// make a [WidgetSpan] for each word and space.
361-
//
362-
// Or we could find a different design for displaying inline code.
363-
// One such alternative is implemented below.
364-
365-
// TODO `code`: find equivalent of web's `unicode-bidi: embed; direction: ltr`
366-
367-
// Use a light gray background, instead of a border.
368-
return _buildInlineSpan(style: _kInlineCodeStyle, node.nodes);
369-
370-
// Another fun solution -- we can in fact have a border! Like so:
371-
// TextStyle(
372-
// background: Paint()..color = Color(0xff000000)
373-
// ..style = PaintingStyle.stroke,
374-
// // … fontSize, fontFamily, …
375-
// The trouble is that this border hugs the text tightly -- no padding.
376-
// That doesn't come out looking good.
377-
378-
// Here's a more different solution: add delimiters.
379-
// return TextSpan(children: [
380-
// // TO.DO(selection): exclude these brackets from text selection
381-
// const TextSpan(text: _kInlineCodeLeftBracket),
382-
// TextSpan(style: _kCodeStyle, children: _buildInlineList(element.nodes)),
383-
// const TextSpan(text: _kInlineCodeRightBracket),
384-
// ]);
320+
class _InlineContentBuilder {
321+
_InlineContentBuilder(this.widget);
322+
323+
final InlineContent widget;
324+
325+
InlineSpan build() {
326+
return _buildNodes(widget.nodes, style: widget.style);
327+
}
328+
329+
InlineSpan _buildNodes(List<InlineContentNode> nodes, {required TextStyle? style}) {
330+
return TextSpan(
331+
style: style,
332+
children: nodes.map(_buildNode).toList(growable: false));
333+
}
334+
335+
InlineSpan _buildNode(InlineContentNode node) {
336+
InlineSpan styled(List<InlineContentNode> nodes, TextStyle style) =>
337+
_buildNodes(nodes, style: style);
338+
339+
if (node is TextNode) {
340+
return TextSpan(text: node.text);
341+
} else if (node is LineBreakInlineNode) {
342+
// Each `<br/>` is followed by a newline, which browsers apparently ignore
343+
// and our parser doesn't. So don't do anything here.
344+
return const TextSpan(text: "");
345+
} else if (node is StrongNode) {
346+
return styled(node.nodes, const TextStyle(fontWeight: FontWeight.w600));
347+
} else if (node is EmphasisNode) {
348+
return styled(node.nodes, const TextStyle(fontStyle: FontStyle.italic));
349+
} else if (node is InlineCodeNode) {
350+
return _buildInlineCode(node);
351+
} else if (node is LinkNode) {
352+
// TODO make link touchable
353+
return styled(node.nodes,
354+
TextStyle(color: const HSLColor.fromAHSL(1, 200, 1, 0.4).toColor()));
355+
} else if (node is UserMentionNode) {
356+
return WidgetSpan(alignment: PlaceholderAlignment.middle,
357+
child: UserMention(node: node));
358+
} else if (node is UnicodeEmojiNode) {
359+
return WidgetSpan(alignment: PlaceholderAlignment.middle,
360+
child: MessageUnicodeEmoji(node: node));
361+
} else if (node is ImageEmojiNode) {
362+
return WidgetSpan(alignment: PlaceholderAlignment.middle,
363+
child: MessageImageEmoji(node: node));
364+
} else if (node is UnimplementedInlineContentNode) {
365+
return _errorUnimplemented(node);
366+
} else {
367+
// TODO(dart-3): Use a sealed class / pattern matching to eliminate this case.
368+
throw Exception("impossible InlineContentNode: ${node.debugHtmlText}");
369+
}
370+
}
371+
372+
InlineSpan _buildInlineCode(InlineCodeNode node) {
373+
// TODO `code` elements: border, padding -- seems hard
374+
//
375+
// Hard because this is an inline span, which we want to be able to break
376+
// between lines when wrapping paragraphs. That means we can't just make it a
377+
// widget; it needs to be a [TextSpan]. And in that inline setting, Flutter
378+
// does not appear to have an equivalent for CSS's `border` or `padding`:
379+
// https://api.flutter.dev/flutter/painting/TextStyle-class.html
380+
//
381+
// One attempt was to use [TextDecoration] for the top and bottom,
382+
// passing this to the [TextStyle] constructor:
383+
// decoration: TextDecoration.combine([TextDecoration.overline, TextDecoration.underline]),
384+
// (Then we could handle the left and right borders with 1px-wide [WidgetSpan]s.)
385+
// The overline comes out OK, but sadly the underline is, well, where a normal
386+
// text underline should go: it cuts right through descenders.
387+
//
388+
// Another option would be to break the text up on whitespace ourselves, and
389+
// make a [WidgetSpan] for each word and space.
390+
//
391+
// Or we could find a different design for displaying inline code.
392+
// One such alternative is implemented below.
393+
394+
// TODO `code`: find equivalent of web's `unicode-bidi: embed; direction: ltr`
395+
396+
// Use a light gray background, instead of a border.
397+
return _buildNodes(style: _kInlineCodeStyle, node.nodes);
398+
399+
// Another fun solution -- we can in fact have a border! Like so:
400+
// TextStyle(
401+
// background: Paint()..color = Color(0xff000000)
402+
// ..style = PaintingStyle.stroke,
403+
// // … fontSize, fontFamily, …
404+
// The trouble is that this border hugs the text tightly -- no padding.
405+
// That doesn't come out looking good.
406+
407+
// Here's a more different solution: add delimiters.
408+
// return TextSpan(children: [
409+
// // TO.DO(selection): exclude these brackets from text selection
410+
// const TextSpan(text: _kInlineCodeLeftBracket),
411+
// TextSpan(style: _kCodeStyle, children: _buildInlineList(element.nodes)),
412+
// const TextSpan(text: _kInlineCodeRightBracket),
413+
// ]);
414+
}
385415
}
386416

387417
final _kInlineCodeStyle = kMonospaceTextStyle
@@ -430,7 +460,7 @@ class UserMention extends StatelessWidget {
430460
return Container(
431461
decoration: _kDecoration,
432462
padding: const EdgeInsets.symmetric(horizontal: 0.2 * kBaseFontSize),
433-
child: Text.rich(_buildInlineSpan(node.nodes, style: null)));
463+
child: InlineContent(nodes: node.nodes, style: null));
434464
}
435465

436466
static get _kDecoration => BoxDecoration(

0 commit comments

Comments
 (0)