Skip to content

Nullsafety #548

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Mar 6, 2021
Merged
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
## [2.0.0-nullsafety.0] - March 5, 2021:
* Nullsafety support
* Official Flutter Web support
* New features & fixes for lists:
* Support start attribute (e.g. `start="5";`)
* Support RTL direction
* Support setting padding - you can remove the starting padding if you choose
* Fixed unknown character box on iOS when font-weight is below w400
* Upgraded link functions to provide more granular control
* Fixed errors in text-decoration parsing
* Fixed `<audio>` on iOS ("_duration called on null" exception)
* Updated dependencies

## [1.3.0] - February 16, 2021:
* New image loading API
* Image loading with request headers, from relative paths and custom loading widget
Expand Down
54 changes: 27 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ Widget html = Html(
data: """<p>
Linking to <a href='https://github.com'>websites</a> has never been easier.
</p>""",
onLinkTap: (String url) {
onLinkTap: (String? url, RenderContext context, Map<String, String> attributes, dom.Element? element) {
//open URL in webview, or launch URL in browser, or any other logic here
}
);
Expand All @@ -197,10 +197,10 @@ Widget html = Html(
<flutter horizontal></flutter>
""",
customRender: {
"bird": (RenderContext context, Widget child, Map<String, String> attributes, _) {
"bird": (RenderContext context, Widget child, Map<String, String> attributes, dom.Element? element) {
return TextSpan(text: "🐦");
},
"flutter": (RenderContext context, Widget child, Map<String, String> attributes, _) {
"flutter": (RenderContext context, Widget child, Map<String, String> attributes, dom.Element? element) {
return FlutterLogo(
style: (attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
Expand All @@ -227,26 +227,25 @@ Widget html = Html(
<iframe src="https://www.youtube.com/embed/tgbNymZ7vqY"></iframe>
""",
customRender: {
"iframe": (RenderContext context, Widget child, Map<String, String> attributes, _) {
"iframe": (RenderContext context, Widget child, Map<String, String> attributes, dom.Element? element) {
if (attributes != null) {
double width = double.tryParse(attributes['width'] ?? "");
double height = double.tryParse(attributes['height'] ?? "");
print(attributes['src']);
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: attributes['src'],
initialUrl: attributes['src'] ?? "about:blank",
javascriptMode: JavascriptMode.unrestricted,
//no need for scrolling gesture recognizers on embedded youtube, so set gestureRecognizers null
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
gestureRecognizers: attributes['src'].contains("youtube.com/embed") ? null : [
gestureRecognizers: attributes['src'] != null && attributes['src']!.contains("youtube.com/embed") ? null : [
Factory(() => VerticalDragGestureRecognizer())
].toSet(),
navigationDelegate: (NavigationRequest request) async {
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
//on other iframe content allow all url loading
if (attributes['src'].contains("youtube.com/embed")) {
if (attributes['src'] != null && attributes['src']!.contains("youtube.com/embed")) {
if (!request.url.contains("youtube.com/embed")) {
return NavigationDecision.prevent;
} else {
Expand Down Expand Up @@ -293,7 +292,7 @@ A function that defines what the widget should do when an image is tapped.
```dart
Widget html = Html(
data: """<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />""",
onImageTap: (String url) {
onImageTap: (String? url, RenderContext context, Map<String, String> attributes, dom.Element? element) {
//open image in webview, or launch image in browser, or any other logic here
}
);
Expand Down Expand Up @@ -432,8 +431,9 @@ A typical usage would look something like this:

```dart
ImageSourceMatcher base64UriMatcher() => (attributes, element) =>
attributes["src"].startsWith("data:image") &&
attributes["src"].contains("base64,");
attributes["src"] != null &&
attributes["src"]!.startsWith("data:image") &&
attributes["src"]!.contains("base64,");
```

In the above example, the matcher checks whether the image's `src` either starts with "data:image" or contains "base64,", since these indicate an image in base64 format.
Expand All @@ -448,10 +448,10 @@ ImageSourceMatcher networkSourceMatcher({
String extension: "your extension",
}) =>
(attributes, element) {
final src = Uri.parse(attributes["src"]);
final src = Uri.parse(attributes["src"] ?? "about:blank");
return schemas.contains(src.scheme) &&
(domains == null || domains.contains(src.host)) &&
(extension == null || src.path.endsWith(".$extension"));
domains.contains(src.host) &&
src.path.endsWith(".$extension");
};
```

Expand All @@ -465,7 +465,8 @@ A typical usage might look like this:

```dart
ImageRender base64ImageRender() => (context, attributes, element) {
final decodedImage = base64.decode(attributes["src"].split("base64,")[1].trim());
final decodedImage = base64.decode(attributes["src"] != null ?
attributes["src"].split("base64,")[1].trim() : "about:blank");
return Image.memory(
decodedImage,
);
Expand All @@ -485,7 +486,7 @@ ImageRender networkImageRender({
}) =>
(context, attributes, element) {
return Image.network(
attributes["src"],
attributes["src"] ?? "about:blank",
headers: headers,
width: width,
height: height,
Expand Down Expand Up @@ -521,10 +522,10 @@ Widget html = Html(
},
networkSourceMatcher(): networkImageRender(
headers: {"Custom-Header": "some-value"},
altWidget: (alt) => Text(alt),
altWidget: (alt) => Text(alt ?? ""),
loadingWidget: () => Text("Loading..."),
),
(attr, _) => attr["src"] != null && attr["src"].startsWith("/wiki"):
(attr, _) => attr["src"] != null && attr["src"]!.startsWith("/wiki"):
networkImageRender(
mapUrl: (url) => "https://upload.wikimedia.org" + url),
},
Expand All @@ -538,16 +539,17 @@ When an image with URL `flutter.dev` is detected, rather than displaying the ima
2. Creating your own renders:
```dart
ImageSourceMatcher classAndIdMatcher({String classToMatch, String idToMatch}) => (attributes, element) =>
attributes["class"].contains(classToMatch) ||
attributes["id"].contains(idToMatch);
attributes["class"] != null && attributes["id"] != null &&
(attributes["class"]!.contains(classToMatch) ||
attributes["id"]!.contains(idToMatch));

ImageRender classAndIdRender({String classToMatch, String idToMatch}) => (context, attributes, element) {
if (attributes["class"].contains(classToMatch)) {
return Image.asset(attributes["src"]);
if (attributes["class"] != null && attributes["class"]!.contains(classToMatch)) {
return Image.asset(attributes["src"] ?? "about:blank");
} else {
return Image.network(
attributes["src"],
semanticLabel: attributes["longdesc"],
attributes["src"] ?? "about:blank",
semanticLabel: attributes["longdesc"] ?? "",
width: attributes["width"],
height: attributes["height"],
color: context.style.color,
Expand Down Expand Up @@ -609,9 +611,7 @@ You can set the `navigationDelegate` of the webview with the `navigationDelegate

### Audio

This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) plugin.

Note: Audio elements currently do not work on iOS due to a bug with `chewie_audio`. Once [#509](https://github.com/Sub6Resources/flutter_html/pull/509) is merged, it will work again.
This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) plugin.

The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `width`, and `muted` when rendering the audio widget.

Expand Down
8 changes: 4 additions & 4 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class MyApp extends StatelessWidget {
}

class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
MyHomePage({Key? key, required this.title}) : super(key: key);

final String title;

Expand Down Expand Up @@ -157,13 +157,13 @@ class _MyHomePageState extends State<MyHomePage> {
},
networkSourceMatcher(domains: ["mydomain.com"]): networkImageRender(
headers: {"Custom-Header": "some-value"},
altWidget: (alt) => Text(alt),
altWidget: (alt) => Text(alt ?? ""),
loadingWidget: () => Text("Loading..."),
),
// On relative paths starting with /wiki, prefix with a base url
(attr, _) => attr["src"] != null && attr["src"].startsWith("/wiki"):
(attr, _) => attr["src"] != null && attr["src"]!.startsWith("/wiki"):
networkImageRender(
mapUrl: (url) => "https://upload.wikimedia.org" + url),
mapUrl: (url) => "https://upload.wikimedia.org" + url!),
// Custom placeholder image for broken links
networkSourceMatcher(): networkImageRender(altWidget: (_) => FlutterLogo()),
},
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: flutter_html example app.
version: 1.0.0+1

environment:
sdk: ">=2.1.0 <3.0.0"
sdk: '>=2.12.0 <3.0.0'

dependencies:
flutter_html:
Expand Down
18 changes: 9 additions & 9 deletions lib/flutter_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,27 +33,27 @@ class Html extends StatelessWidget {
/// **style** Pass in the style information for the Html here.
/// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/Style) for more info.
Html({
Key key,
@required this.data,
Key? key,
required this.data,
this.onLinkTap,
this.customRender,
this.customRender = const {},
this.customImageRenders = const {},
this.onImageError,
this.shrinkWrap = false,
this.onImageTap,
this.blacklistedElements = const [],
this.style,
this.style = const {},
this.navigationDelegateForIframe,
}) : super(key: key);

final String data;
final OnTap onLinkTap;
final OnTap? onLinkTap;
final Map<ImageSourceMatcher, ImageRender> customImageRenders;
final ImageErrorListener onImageError;
final ImageErrorListener? onImageError;
final bool shrinkWrap;

/// Properties for the Image widget that gets rendered by the rich text parser
final OnTap onImageTap;
final OnTap? onImageTap;

final List<String> blacklistedElements;

Expand All @@ -67,11 +67,11 @@ class Html extends StatelessWidget {
/// Decides how to handle a specific navigation request in the WebView of an
/// Iframe. It's necessary to use the webview_flutter package inside the app
/// to use NavigationDelegate.
final NavigationDelegate navigationDelegateForIframe;
final NavigationDelegate? navigationDelegateForIframe;

@override
Widget build(BuildContext context) {
final double width = shrinkWrap ? null : MediaQuery.of(context).size.width;
final double? width = shrinkWrap ? null : MediaQuery.of(context).size.width;

return Container(
width: width,
Expand Down
Loading