@@ -97,7 +97,7 @@ class Paragraph extends StatelessWidget {
97
97
// The paragraph has vertical CSS margins, but those have no effect.
98
98
if (node.nodes.isEmpty) return const SizedBox ();
99
99
100
- final text = Text . rich ( _buildInlineSpan ( node.nodes, style: null ) );
100
+ final text = InlineContent (nodes : node.nodes, style: null );
101
101
102
102
// If the paragraph didn't actually have a `p` element in the HTML,
103
103
// then apply no margins. (For example, these are seen in list items.)
@@ -122,9 +122,9 @@ class Heading extends StatelessWidget {
122
122
assert (node.level == HeadingLevel .h6);
123
123
return Padding (
124
124
padding: const EdgeInsets .only (top: 15 , bottom: 5 ),
125
- child: Text . rich ( _buildInlineSpan (
125
+ child: InlineContent (
126
126
style: const TextStyle (fontWeight: FontWeight .w600, height: 1.4 ),
127
- node.nodes) ));
127
+ nodes : node.nodes));
128
128
}
129
129
}
130
130
@@ -297,91 +297,121 @@ class _SingleChildScrollViewWithScrollbarState
297
297
// Inline layout.
298
298
//
299
299
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
+ }
305
308
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 ());
340
317
}
341
318
}
342
319
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
+ }
385
415
}
386
416
387
417
final _kInlineCodeStyle = kMonospaceTextStyle
@@ -430,7 +460,7 @@ class UserMention extends StatelessWidget {
430
460
return Container (
431
461
decoration: _kDecoration,
432
462
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 ));
434
464
}
435
465
436
466
static get _kDecoration => BoxDecoration (
0 commit comments