1
1
import 'dart:convert' ;
2
2
3
- import 'package:flutter/material.dart' ;
4
3
import 'package:flutter/gestures.dart' ;
5
- import 'package:html/parser .dart' as parser ;
4
+ import 'package:flutter/material .dart' ;
6
5
import 'package:html/dom.dart' as dom;
6
+ import 'package:html/parser.dart' as parser;
7
7
8
8
typedef CustomRender = Widget Function (dom.Node node, List <Widget > children);
9
9
typedef CustomTextStyle = TextStyle Function (dom.Node node, TextStyle baseStyle);
@@ -148,6 +148,12 @@ class HtmlRichTextParser extends StatelessWidget {
148
148
this .html,
149
149
this .customTextStyle,
150
150
this .customEdgeInsets,
151
+ this .onImageError,
152
+ this .linkStyle = const TextStyle (
153
+ decoration: TextDecoration .underline,
154
+ color: Colors .blueAccent,
155
+ decorationColor: Colors .blueAccent,
156
+ ),
151
157
});
152
158
153
159
final double indentSize = 10.0 ;
@@ -158,6 +164,8 @@ class HtmlRichTextParser extends StatelessWidget {
158
164
final String html;
159
165
final CustomTextStyle customTextStyle;
160
166
final CustomEdgeInsets customEdgeInsets;
167
+ final ImageErrorListener onImageError;
168
+ final TextStyle linkStyle;
161
169
162
170
// style elements set a default style
163
171
// for all child nodes
@@ -174,7 +182,8 @@ class HtmlRichTextParser extends StatelessWidget {
174
182
"acronym" ,
175
183
"ol" ,
176
184
"ul" ,
177
- "blockquote"
185
+ "blockquote" ,
186
+ "span" ,
178
187
];
179
188
180
189
// specialty elements require unique handling
@@ -266,7 +275,8 @@ class HtmlRichTextParser extends StatelessWidget {
266
275
);
267
276
268
277
// ignore the top level "body"
269
- body.nodes.forEach ((dom.Node node) => _parseNode (node, parseContext));
278
+ body.nodes
279
+ .forEach ((dom.Node node) => _parseNode (node, parseContext, context));
270
280
// _parseNode(body, parseContext);
271
281
272
282
// filter out empty widgets
@@ -303,7 +313,8 @@ class HtmlRichTextParser extends StatelessWidget {
303
313
// function can add child nodes to the parent if it should
304
314
//
305
315
// each iteration creates a new parseContext as a copy of the previous one if it needs to
306
- void _parseNode (dom.Node node, ParseContext parseContext) {
316
+ void _parseNode (
317
+ dom.Node node, ParseContext parseContext, BuildContext buildContext) {
307
318
// TEXT ONLY NODES
308
319
// a text only node is a child of a tag with no inner html
309
320
if (node is dom.Text ) {
@@ -329,14 +340,14 @@ class HtmlRichTextParser extends StatelessWidget {
329
340
if (! parseContext.parentElement.children.isEmpty) {
330
341
lastString = parseContext.parentElement.children.last.text ?? '' ;
331
342
}
332
- if (lastString == '' ||
333
- lastString. endsWith ( ' ' ) ||
334
- lastString. endsWith ( ' \n ' )) finalText = finalText. trimLeft ();
343
+ if (lastString. endsWith ( ' ' ) || lastString. endsWith ( ' \n ' )) {
344
+ finalText = finalText. trimLeft ();
345
+ }
335
346
}
336
347
}
337
348
338
- // if the finalText is actually empty, just return
339
- if (finalText.trim ().isEmpty) return ;
349
+ // if the finalText is actually empty, just return (unless it's just a space)
350
+ if (finalText.trim ().isEmpty && finalText != " " ) return ;
340
351
341
352
// NOW WE HAVE OUR TRULY FINAL TEXT
342
353
// debugPrint("Plain Text Node: '$finalText'");
@@ -477,6 +488,9 @@ class HtmlRichTextParser extends StatelessWidget {
477
488
nextContext.indentLevel += 1 ;
478
489
nextContext.blockType = 'blockquote' ;
479
490
break ;
491
+ case "span" :
492
+ //No additional styles
493
+ break ;
480
494
}
481
495
482
496
if (customTextStyle != null ) {
@@ -511,13 +525,9 @@ class HtmlRichTextParser extends StatelessWidget {
511
525
nextContext.parentElement = linkContainer;
512
526
nextContext.rootWidgetList.add (linkContainer);
513
527
} else {
514
- TextStyle linkStyle = parseContext.childStyle.merge (TextStyle (
515
- decoration: TextDecoration .underline,
516
- color: Colors .blueAccent,
517
- decorationColor: Colors .blueAccent,
518
- ));
528
+ TextStyle _linkStyle = parseContext.childStyle.merge (linkStyle);
519
529
LinkTextSpan span = LinkTextSpan (
520
- style: linkStyle ,
530
+ style: _linkStyle ,
521
531
url: url,
522
532
onLinkTap: onLinkTap,
523
533
children: < TextSpan > [],
@@ -638,9 +648,23 @@ class HtmlRichTextParser extends StatelessWidget {
638
648
if (node.attributes['src' ] != null ) {
639
649
if (node.attributes['src' ].startsWith ("data:image" ) &&
640
650
node.attributes['src' ].contains ("base64," )) {
651
+ precacheImage (
652
+ MemoryImage (
653
+ base64.decode (
654
+ node.attributes['src' ].split ("base64," )[1 ].trim (),
655
+ ),
656
+ ),
657
+ buildContext,
658
+ onError: onImageError,
659
+ );
641
660
parseContext.rootWidgetList.add (Image .memory (base64.decode (
642
661
node.attributes['src' ].split ("base64," )[1 ].trim ())));
643
662
} else {
663
+ precacheImage (
664
+ NetworkImage (node.attributes['src' ]),
665
+ buildContext,
666
+ onError: onImageError,
667
+ );
644
668
parseContext.rootWidgetList
645
669
.add (Image .network (node.attributes['src' ]));
646
670
}
@@ -755,7 +779,7 @@ class HtmlRichTextParser extends StatelessWidget {
755
779
}
756
780
757
781
node.nodes.forEach ((dom.Node childNode) {
758
- _parseNode (childNode, nextContext);
782
+ _parseNode (childNode, nextContext, buildContext );
759
783
});
760
784
}
761
785
}
@@ -829,6 +853,11 @@ class HtmlOldParser extends StatelessWidget {
829
853
this .customRender,
830
854
this .blockSpacing,
831
855
this .html,
856
+ this .onImageError,
857
+ this .linkStyle = const TextStyle (
858
+ decoration: TextDecoration .underline,
859
+ color: Colors .blueAccent,
860
+ decorationColor: Colors .blueAccent),
832
861
});
833
862
834
863
final double width;
@@ -837,6 +866,8 @@ class HtmlOldParser extends StatelessWidget {
837
866
final CustomRender customRender;
838
867
final double blockSpacing;
839
868
final String html;
869
+ final ImageErrorListener onImageError;
870
+ final TextStyle linkStyle;
840
871
841
872
static const _supportedElements = [
842
873
"a" ,
@@ -957,10 +988,7 @@ class HtmlOldParser extends StatelessWidget {
957
988
child: Wrap (
958
989
children: _parseNodeList (node.nodes),
959
990
),
960
- style: const TextStyle (
961
- decoration: TextDecoration .underline,
962
- color: Colors .blueAccent,
963
- decorationColor: Colors .blueAccent),
991
+ style: linkStyle,
964
992
),
965
993
onTap: () {
966
994
if (node.attributes.containsKey ('href' ) && onLinkTap != null ) {
@@ -1289,10 +1317,7 @@ class HtmlOldParser extends StatelessWidget {
1289
1317
case "hr" :
1290
1318
return Padding (
1291
1319
padding: EdgeInsets .only (top: 7.0 , bottom: 7.0 ),
1292
- child: Container (
1293
- height: 0.0 ,
1294
- decoration: BoxDecoration (border: Border .all ()),
1295
- ),
1320
+ child: Divider (height: 1.0 , color: Colors .black38),
1296
1321
);
1297
1322
case "i" :
1298
1323
return DefaultTextStyle .merge (
@@ -1304,24 +1329,39 @@ class HtmlOldParser extends StatelessWidget {
1304
1329
),
1305
1330
);
1306
1331
case "img" :
1307
- if (node.attributes['src' ] != null ) {
1308
- if (node.attributes['src' ].startsWith ("data:image" ) &&
1309
- node.attributes['src' ].contains ("base64," )) {
1310
- return Image .memory (base64
1311
- .decode (node.attributes['src' ].split ("base64," )[1 ].trim ()));
1312
- }
1313
- return Image .network (node.attributes['src' ]);
1314
- } else if (node.attributes['alt' ] != null ) {
1315
- //Temp fix for https://github.com/flutter/flutter/issues/736
1316
- if (node.attributes['alt' ].endsWith (" " )) {
1317
- return Container (
1318
- padding: EdgeInsets .only (right: 2.0 ),
1319
- child: Text (node.attributes['alt' ]));
1320
- } else {
1321
- return Text (node.attributes['alt' ]);
1322
- }
1323
- }
1324
- return Container ();
1332
+ return Builder (
1333
+ builder: (BuildContext context) {
1334
+ if (node.attributes['src' ] != null ) {
1335
+ if (node.attributes['src' ].startsWith ("data:image" ) &&
1336
+ node.attributes['src' ].contains ("base64," )) {
1337
+ precacheImage (
1338
+ MemoryImage (base64.decode (
1339
+ node.attributes['src' ].split ("base64," )[1 ].trim ())),
1340
+ context,
1341
+ onError: onImageError,
1342
+ );
1343
+ return Image .memory (base64.decode (
1344
+ node.attributes['src' ].split ("base64," )[1 ].trim ()));
1345
+ }
1346
+ precacheImage (
1347
+ NetworkImage (node.attributes['src' ]),
1348
+ context,
1349
+ onError: onImageError,
1350
+ );
1351
+ return Image .network (node.attributes['src' ]);
1352
+ } else if (node.attributes['alt' ] != null ) {
1353
+ //Temp fix for https://github.com/flutter/flutter/issues/736
1354
+ if (node.attributes['alt' ].endsWith (" " )) {
1355
+ return Container (
1356
+ padding: EdgeInsets .only (right: 2.0 ),
1357
+ child: Text (node.attributes['alt' ]));
1358
+ } else {
1359
+ return Text (node.attributes['alt' ]);
1360
+ }
1361
+ }
1362
+ return Container ();
1363
+ },
1364
+ );
1325
1365
case "ins" :
1326
1366
return DefaultTextStyle .merge (
1327
1367
child: Wrap (
0 commit comments