Skip to content

Commit f1decee

Browse files
committed
Merge changes
1 parent aa4dde1 commit f1decee

27 files changed

+395
-278
lines changed

example/lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ class _MyHomePageState extends State<MyHomePage> {
301301
svgDataUriMatcher(): svgDataImageRender(),
302302
svgAssetUriMatcher(): svgAssetImageRender(),
303303
svgNetworkSourceMatcher(): svgNetworkImageRender(),
304-
networkSourceMatcher(domains: ["flutter.dev"]): CustomRender.fromWidget(
304+
networkSourceMatcher(domains: ["flutter.dev"]): CustomRender.widget(
305305
widget: (context, buildChildren) {
306306
return FlutterLogo(size: 36);
307307
}),

lib/custom_render.dart

Lines changed: 77 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ CustomRenderMatcher tagMatcher(String tag) => (context) {
1515
};
1616

1717
CustomRenderMatcher blockElementMatcher() => (context) {
18-
return context.tree.style.display == Display.BLOCK &&
19-
(context.tree.children.isNotEmpty || context.tree.element?.localName == "hr");
20-
};
18+
return context.tree.style.display == Display.BLOCK &&
19+
(context.tree.children.isNotEmpty || context.tree.element?.localName == "hr");
20+
};
2121

2222
CustomRenderMatcher listElementMatcher() => (context) {
2323
return context.tree.style.display == Display.LIST_ITEM;
@@ -31,7 +31,7 @@ CustomRenderMatcher dataUriMatcher({String? encoding = 'base64', String? mime})
3131
if (context.tree.element?.attributes == null
3232
|| _src(context.tree.element!.attributes.cast()) == null) return false;
3333
final dataUri = _dataUriFormat.firstMatch(_src(context.tree.element!.attributes.cast())!);
34-
return dataUri != null &&
34+
return dataUri != null && dataUri.namedGroup('mime') != "image/svg+xml" &&
3535
(mime == null || dataUri.namedGroup('mime') == mime) &&
3636
(encoding == null || dataUri.namedGroup('encoding') == ';$encoding');
3737
};
@@ -57,7 +57,8 @@ CustomRenderMatcher networkSourceMatcher({
5757
CustomRenderMatcher assetUriMatcher() => (context) =>
5858
context.tree.element?.attributes.cast() != null
5959
&& _src(context.tree.element!.attributes.cast()) != null
60-
&& _src(context.tree.element!.attributes.cast())!.startsWith("asset:");
60+
&& _src(context.tree.element!.attributes.cast())!.startsWith("asset:")
61+
&& !_src(context.tree.element!.attributes.cast())!.endsWith(".svg");
6162

6263
CustomRenderMatcher textContentElementMatcher() => (context) {
6364
return context.tree is TextContentElement;
@@ -209,7 +210,7 @@ CustomRender textContentElementRender({String? text}) =>
209210
CustomRender.inlineSpan(inlineSpan: (context, buildChildren) =>
210211
TextSpan(text: (text ?? (context.tree as TextContentElement).text).transformed(context.tree.style.textTransform)));
211212

212-
CustomRender base64ImageRender() => CustomRender.fromWidget(widget: (context, buildChildren) {
213+
CustomRender base64ImageRender() => CustomRender.widget(widget: (context, buildChildren) {
213214
final decodedImage = base64.decode(_src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim());
214215
precacheImage(
215216
MemoryImage(decodedImage),
@@ -228,9 +229,9 @@ CustomRender base64ImageRender() => CustomRender.fromWidget(widget: (context, bu
228229
},
229230
);
230231
return Builder(
232+
key: context.key,
231233
builder: (buildContext) {
232234
return GestureDetector(
233-
key: context.key,
234235
child: widget,
235236
onTap: () {
236237
if (MultipleTapGestureDetector.of(buildContext) != null) {
@@ -251,7 +252,7 @@ CustomRender base64ImageRender() => CustomRender.fromWidget(widget: (context, bu
251252
CustomRender assetImageRender({
252253
double? width,
253254
double? height,
254-
}) => CustomRender.fromWidget(widget: (context, buildChildren) {
255+
}) => CustomRender.widget(widget: (context, buildChildren) {
255256
final assetPath = _src(context.tree.element!.attributes.cast())!.replaceFirst('asset:', '');
256257
final widget = Image.asset(
257258
assetPath,
@@ -265,9 +266,9 @@ CustomRender assetImageRender({
265266
},
266267
);
267268
return Builder(
269+
key: context.key,
268270
builder: (buildContext) {
269271
return GestureDetector(
270-
key: context.key,
271272
child: widget,
272273
onTap: () {
273274
if (MultipleTapGestureDetector.of(buildContext) != null) {
@@ -292,62 +293,71 @@ CustomRender networkImageRender({
292293
double? height,
293294
Widget Function(String?)? altWidget,
294295
Widget Function()? loadingWidget,
295-
}) => CustomRender.fromWidget(widget: (context, buildChildren) {
296+
}) => CustomRender.widget(widget: (context, buildChildren) {
296297
final src = mapUrl?.call(_src(context.tree.element!.attributes.cast()))
297298
?? _src(context.tree.element!.attributes.cast())!;
298-
precacheImage(
299-
NetworkImage(
300-
src,
301-
headers: headers,
302-
),
303-
context.buildContext,
304-
onError: (exception, StackTrace? stackTrace) {
305-
context.parser.onImageError?.call(exception, stackTrace);
306-
},
307-
);
308299
Completer<Size> completer = Completer();
309-
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
310-
if (frame == null) {
311-
if (!completer.isCompleted) {
312-
completer.completeError("error");
300+
if (context.parser.cachedImageSizes[src] != null) {
301+
completer.complete(context.parser.cachedImageSizes[src]);
302+
} else {
303+
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
304+
if (frame == null) {
305+
if (!completer.isCompleted) {
306+
completer.completeError("error");
307+
}
308+
return child;
309+
} else {
310+
return child;
313311
}
314-
return child;
315-
} else {
316-
return child;
317-
}
318-
});
312+
});
319313

320-
image.image.resolve(ImageConfiguration()).addListener(
321-
ImageStreamListener((ImageInfo image, bool synchronousCall) {
322-
var myImage = image.image;
314+
ImageStreamListener? listener;
315+
listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
316+
var myImage = imageInfo.image;
323317
Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
324318
if (!completer.isCompleted) {
319+
context.parser.cachedImageSizes[src] = size;
325320
completer.complete(size);
321+
image.image.resolve(ImageConfiguration()).removeListener(listener!);
326322
}
327323
}, onError: (object, stacktrace) {
328324
if (!completer.isCompleted) {
329325
completer.completeError(object);
326+
image.image.resolve(ImageConfiguration()).removeListener(listener!);
330327
}
331-
}),
332-
);
328+
});
329+
330+
image.image.resolve(ImageConfiguration()).addListener(listener);
331+
}
332+
final attributes = context.tree.element!.attributes.cast<String, String>();
333333
final widget = FutureBuilder<Size>(
334334
future: completer.future,
335+
initialData: context.parser.cachedImageSizes[src],
335336
builder: (BuildContext buildContext, AsyncSnapshot<Size> snapshot) {
336337
if (snapshot.hasData) {
337-
return Image.network(
338-
src,
339-
headers: headers,
340-
width: width ?? _width(context.tree.element!.attributes.cast())
341-
?? snapshot.data!.width,
342-
height: height ?? _height(context.tree.element!.attributes.cast()),
343-
frameBuilder: (ctx, child, frame, _) {
344-
if (frame == null) {
345-
return altWidget?.call(_alt(context.tree.element!.attributes.cast())) ??
346-
Text(_alt(context.tree.element!.attributes.cast())
347-
?? "", style: context.style.generateTextStyle());
348-
}
349-
return child;
350-
},
338+
return Container(
339+
constraints: BoxConstraints(
340+
maxWidth: width ?? _width(attributes) ?? snapshot.data!.width,
341+
maxHeight:
342+
(width ?? _width(attributes) ?? snapshot.data!.width) /
343+
_aspectRatio(attributes, snapshot)),
344+
child: AspectRatio(
345+
aspectRatio: _aspectRatio(attributes, snapshot),
346+
child: Image.network(
347+
src,
348+
headers: headers,
349+
width: width ?? _width(attributes) ?? snapshot.data!.width,
350+
height: height ?? _height(attributes),
351+
frameBuilder: (ctx, child, frame, _) {
352+
if (frame == null) {
353+
return altWidget?.call(_alt(attributes)) ??
354+
Text(_alt(attributes) ?? "",
355+
style: context.style.generateTextStyle());
356+
}
357+
return child;
358+
},
359+
),
360+
),
351361
);
352362
} else if (snapshot.hasError) {
353363
return altWidget?.call(_alt(context.tree.element!.attributes.cast())) ??
@@ -359,9 +369,9 @@ CustomRender networkImageRender({
359369
},
360370
);
361371
return Builder(
372+
key: context.key,
362373
builder: (buildContext) {
363374
return GestureDetector(
364-
key: context.key,
365375
child: widget,
366376
onTap: () {
367377
if (MultipleTapGestureDetector.of(buildContext) != null) {
@@ -520,3 +530,21 @@ double? _width(Map<String, String> attributes) {
520530
final widthString = attributes["width"];
521531
return widthString == null ? widthString as double? : double.tryParse(widthString);
522532
}
533+
534+
double _aspectRatio(
535+
Map<String, String> attributes, AsyncSnapshot<Size> calculated) {
536+
final heightString = attributes["height"];
537+
final widthString = attributes["width"];
538+
if (heightString != null && widthString != null) {
539+
final height = double.tryParse(heightString);
540+
final width = double.tryParse(widthString);
541+
return height == null || width == null
542+
? calculated.data!.aspectRatio
543+
: width / height;
544+
}
545+
return calculated.data!.aspectRatio;
546+
}
547+
548+
extension ClampedEdgeInsets on EdgeInsetsGeometry {
549+
EdgeInsetsGeometry get nonNegative => this.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity));
550+
}

lib/flutter_html.dart

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ library flutter_html;
22

33
import 'package:flutter/material.dart';
44
import 'package:flutter_html/custom_render.dart';
5-
import 'package:flutter/rendering.dart';
65
import 'package:flutter_html/html_parser.dart';
76
import 'package:flutter_html/src/html_elements.dart';
87
import 'package:flutter_html/style.dart';
@@ -19,11 +18,10 @@ export 'package:flutter_html/src/interactable_element.dart';
1918
export 'package:flutter_html/src/layout_element.dart';
2019
export 'package:flutter_html/src/replaced_element.dart';
2120
export 'package:flutter_html/src/styled_element.dart';
22-
export 'package:flutter_html/src/navigation_delegate.dart';
2321
//export style api
2422
export 'package:flutter_html/style.dart';
2523

26-
class Html extends StatelessWidget {
24+
class Html extends StatefulWidget {
2725
/// The `Html` widget takes HTML as input and displays a RichText
2826
/// tree of the parsed HTML content.
2927
///
@@ -133,34 +131,44 @@ class Html extends StatelessWidget {
133131
..addAll(EXTERNAL_ELEMENTS);
134132

135133
@override
136-
Widget build(BuildContext context) {
137-
final dom.Document doc =
138-
data != null ? HtmlParser.parseHTML(data!) : document!;
139-
final double? width = shrinkWrap ? null : MediaQuery.of(context).size.width;
134+
State<StatefulWidget> createState() => _HtmlState();
135+
}
136+
137+
class _HtmlState extends State<Html> {
138+
late final dom.Document doc;
139+
140+
@override
141+
void initState() {
142+
super.initState();
143+
doc =
144+
widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.document!;
145+
}
140146

147+
@override
148+
Widget build(BuildContext context) {
141149
return Container(
142-
width: width,
150+
width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
143151
child: HtmlParser(
144-
key: _anchorKey,
152+
key: widget._anchorKey,
145153
htmlData: doc,
146-
onLinkTap: onLinkTap,
147-
onAnchorTap: onAnchorTap,
148-
onImageTap: onImageTap,
149-
onCssParseError: onCssParseError,
150-
onImageError: onImageError,
151-
shrinkWrap: shrinkWrap,
154+
onLinkTap: widget.onLinkTap,
155+
onAnchorTap: widget.onAnchorTap,
156+
onImageTap: widget.onImageTap,
157+
onCssParseError: widget.onCssParseError,
158+
onImageError: widget.onImageError,
159+
shrinkWrap: widget.shrinkWrap,
152160
selectable: false,
153-
style: style,
161+
style: widget.style,
154162
customRenders: {}
155-
..addAll(customRenders)
163+
..addAll(widget.customRenders)
156164
..addAll(defaultRenders),
157-
tagsList: tagsList.isEmpty ? Html.tags : tagsList,
165+
tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList,
158166
),
159167
);
160168
}
161169
}
162170

163-
class SelectableHtml extends StatelessWidget {
171+
class SelectableHtml extends StatefulWidget {
164172
/// The `SelectableHtml` widget takes HTML as input and displays a RichText
165173
/// tree of the parsed HTML content (which is selectable)
166174
///
@@ -271,32 +279,40 @@ class SelectableHtml extends StatelessWidget {
271279
static List<String> get tags => new List<String>.from(SELECTABLE_ELEMENTS);
272280

273281
@override
274-
Widget build(BuildContext context) {
275-
final dom.Document doc = data != null ? HtmlParser.parseHTML(data!) : document!;
276-
final double? width = shrinkWrap ? null : MediaQuery.of(context).size.width;
282+
State<StatefulWidget> createState() => _SelectableHtmlState();
283+
}
284+
285+
class _SelectableHtmlState extends State<SelectableHtml> {
286+
late final dom.Document doc;
287+
288+
@override
289+
void initState() {
290+
super.initState();
291+
doc =
292+
widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.document!;
293+
}
277294

295+
@override
296+
Widget build(BuildContext context) {
278297
return Container(
279-
width: width,
298+
width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
280299
child: HtmlParser(
281-
key: _anchorKey,
300+
key: widget._anchorKey,
282301
htmlData: doc,
283-
onLinkTap: onLinkTap,
284-
onAnchorTap: onAnchorTap,
302+
onLinkTap: widget.onLinkTap,
303+
onAnchorTap: widget.onAnchorTap,
285304
onImageTap: null,
286-
onCssParseError: onCssParseError,
305+
onCssParseError: widget.onCssParseError,
287306
onImageError: null,
288-
onMathError: null,
289-
shrinkWrap: shrinkWrap,
307+
shrinkWrap: widget.shrinkWrap,
290308
selectable: true,
291-
style: style,
309+
style: widget.style,
292310
customRenders: {}
293-
..addAll(customRenders)
311+
..addAll(widget.customRenders)
294312
..addAll(defaultRenders),
295-
imageRenders: defaultImageRenders,
296-
tagsList: tagsList.isEmpty ? SelectableHtml.tags : tagsList,
297-
navigationDelegateForIframe: null,
298-
selectionControls: selectionControls,
299-
scrollPhysics: scrollPhysics,
313+
tagsList: widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList,
314+
selectionControls: widget.selectionControls,
315+
scrollPhysics: widget.scrollPhysics,
300316
),
301317
);
302318
}

0 commit comments

Comments
 (0)