diff --git a/CHANGELOG.md b/CHANGELOG.md
index e99219d88f..5879aafab7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,47 @@
+## [3.0.0-alpha.2] - January 5, 2022:
+* **BREAKING** Full modularization using split packages; see our upgrade guide or use flutter_html_all
+
+## [3.0.0-alpha.1] - December 21, 2021:
+* **BREAKING** Reworked custom renders pending full modularation in 3.0.0
+* Extended support custom render when using SelectableHtml
+* Updated flutter_svg to 1.0.0
+* Support flutter_webview 3.x
+* Automatic disposal of video and audio controllers
+* Fix block elements bottom spacing in table cells
+
+## [2.2.1] - December 8, 2021:
+* Allow styling on ruby tags
+* Allow width/height/alignment styling on table/tr/td tags
+* Prevent images causing rebuilding and leaking memory
+* Fixes display of list items on iOS with font weights below 400
+* Prevent crash on negative margins or paddings
+
+## [2.2.0] - November 29, 2021:
+* Explicitly declare multiplatform support
+* Extended and fixed list-style (marker) support
+* Basic support for height/width css properties
+* Support changing scroll physics of SelectableText.rich
+* Support text transform css property
+* Bumped minimum flutter_math_fork version for Flutter 2.5 compatibility
+* Fix styling of iframes
+* Fix nested font tag application
+* Fix whitespace rendering between list items
+* Prevent crash on empty
tag and tables with both colspan/rowspan
+* Prevent crash on use of negative margins in css
+
+## [2.1.5] - October 7, 2021:
+* Ignore unsupported custom style selectors when using fromCss
+* Fix SVG tag usage inside tables
+* Properly fix regression in usage of line breaks
+
+## [2.1.4] - October 3, 2021:
+* Fix regression in usage of line breaks in body being stripped
+
+## [2.1.3] - October 1, 2021:
+* Update minimum versions of dependencies for Flutter 2.5 compatibility
+* Extended and fixed support for css shadow
+* Fix block tags with explicit whitespace from being stripped
+
## [2.1.2] - September 2, 2021:
* Allow setting selectionControls with SelectableHtml
* Fix onLinkTap not working with SelectableHtml
diff --git a/LICENSE b/LICENSE
index b0f300d959..89971b33a6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2019 Matthew Whitaker
+Copyright (c) 2019-2022 The flutter_html developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 4b1f52dc11..326ac6ccb7 100644
--- a/README.md
+++ b/README.md
@@ -34,11 +34,13 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
- [API Reference](#api-reference)
- [Constructors](#constructors)
-
- - [Selectable Text](#selectable-text)
+
+ - [Selectable Text](#selectable-text)
- [Parameters Table](#parameters)
+ - [Methods](#methods)
+
- [Getters](#getters)
- [Data](#data)
@@ -50,8 +52,6 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
- [customRender](#customrender)
- [onImageError](#onimageerror)
-
- - [onMathError](#onmatherror)
- [onImageTap](#onimagetap)
@@ -59,33 +59,25 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
- [style](#style)
- - [navigationDelegateForIframe](#navigationdelegateforiframe)
-
- - [customImageRender](#customimagerender)
-
- - [typedef ImageSourceMatcher (with examples)](#typedef-imagesourcematcher)
-
- - [typedef ImageRender (with examples)](#typedef-imagerender)
-
- - [Extended examples](#example-usages---customimagerender)
-
- [Rendering Reference](#rendering-reference)
- [Image](#image)
- - [Iframe](#iframe)
+- [External Packages](#external-packages)
- - [Audio](#audio)
+ - [`flutter_html_all`](#flutter_html_all)
- - [Video](#video)
+ - [`flutter_html_audio`](#flutter_html_audio)
- - [SVG](#svg)
+ - [`flutter_html_iframe`](#flutter_html_iframe)
- - [MathML](#mathml)
+ - [`flutter_html_math`](#flutter_html_math)
- - [Tex](#tex)
+ - [`flutter_html_svg`](#flutter_html_svg)
- - [Table](#table)
+ - [`flutter_html_table`](#flutter_html_table)
+
+ - [`flutter_html_video`](#flutter_html_video)
- [Notes](#notes)
@@ -98,7 +90,7 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
Add the following to your `pubspec.yaml` file:
dependencies:
- flutter_html: ^2.1.2
+ flutter_html: ^3.0.0-alpha.2
## Currently Supported HTML Tags:
| | | | | | | | | | | |
@@ -169,30 +161,41 @@ Please note: Due to Flutter [#38474](https://github.com/flutter/flutter/issues/3
Once the above issue is resolved, the aforementioned compromises will go away. Currently the `SelectableText.rich()` constructor does not support `WidgetSpan`s, resulting in the feature losses above.
-### Parameters:
+### Parameters:
| Parameters | Description |
|--------------|-----------------|
| `data` | The HTML data passed to the `Html` widget. This is required and cannot be null when using `Html()`. |
| `document` | The DOM document passed to the `Html` widget. This is required and cannot be null when using `Html.fromDom()`. |
| `onLinkTap` | A function that defines what the widget should do when a link is tapped. The function exposes the `src` of the link as a `String` to use in your implementation. |
-| `customRender` | A powerful API that allows you to customize everything when rendering a specific HTML tag. |
+| `customRenders` | A powerful API that allows you to customize everything when rendering a specific HTML tag. |
| `onImageError` | A function that defines what the widget should do when an image fails to load. The function exposes the exception `Object` and `StackTrace` to use in your implementation. |
-| `onMathError` | A function that defines what the widget should do when a math fails to render. The function exposes the parsed Tex `String`, as well as the error and error with type from `flutter_math` as a `String`. |
| `shrinkWrap` | A `bool` used while rendering different widgets to specify whether they should be shrink-wrapped or not, like `ContainerSpan` |
| `onImageTap` | A function that defines what the widget should do when an image is tapped. The function exposes the `src` of the image as a `String` to use in your implementation. |
| `tagsList` | A list of elements the `Html` widget should render. The list should contain the tags of the HTML elements you wish to include. |
| `style` | A powerful API that allows you to customize the style that should be used when rendering a specific HTMl tag. |
-| `navigationDelegateForIframe` | Allows you to set the `NavigationDelegate` for the `WebView`s of all the iframes rendered by the `Html` widget. |
-| `customImageRender` | A powerful API that allows you to fully customize how images are loaded. |
| `selectionControls` | A custom text selection controls that allow you to override default toolbar and build toolbar with custom text selection options. See an [example](https://github.com/justinmc/flutter-text-selection-menu-examples/blob/master/lib/custom_menu_page.dart). |
+### Methods:
+
+| Methods | Description |
+|--------------|-----------------|
+| `disposeAll()` | Disposes all `ChewieController`s, `ChewieAudioController`s, and `VideoPlayerController`s being used by every `Html` widget. (Note: `Html` widgets automatically dispose their controllers, this method is only provided in case you need other behavior) |
+
### Getters:
1. `Html.tags`. This provides a list of all the tags the package renders. The main use case is to assist in excluding elements using `tagsList`. See an [example](#example-usage---tagslist---excluding-tags) below.
2. `SelectableHtml.tags`. This provides a list of all the tags that can be rendered in selectable mode.
+3. `Html.chewieAudioControllers`. This provides a list of all `ChewieAudioController`s being used by `Html` widgets.
+
+4. `Html.chewieControllers`. This provides a list of all `ChewieController`s being used by `Html` widgets.
+
+5. `Html.videoPlayerControllers`. This provides a list of all `VideoPlayerController`s being used for video widgets by `Html` widgets.
+
+6. `Html.audioPlayerControllers`. This provides a list of all `VideoPlayerController`s being used for audio widgets by `Html` widgets.
+
### Data:
The HTML data passed to the `Html` widget as a `String`. This is required and cannot be null when using `Html`.
@@ -265,17 +268,21 @@ Widget html = Html(
Inner links (such as `Back to the top ` will work out of the box by scrolling the viewport, as long as your `Html` widget is wrapped in a scroll container such as a `SingleChildScrollView`.
-### customRender:
+### customRenders:
A powerful API that allows you to customize everything when rendering a specific HTML tag. This means you can change the default behaviour or add support for HTML elements that aren't supported natively. You can also make up your own custom tags in your HTML!
-`customRender` accepts a `Map`. The `CustomRender` type is a function that requires a `Widget` or `InlineSpan` to be returned. It exposes `RenderContext` and the `Widget` that would have been rendered by `Html` without a `customRender` defined. The `RenderContext` contains the build context, styling and the HTML element, with attrributes and its subtree,.
+`customRender` accepts a `Map`.
-To use this API, set the key as the tag of the HTML element you wish to provide a custom implementation for, and create a function with the above parameters that returns a `Widget` or `InlineSpan`.
+`CustomRenderMatcher` is a function that requires a `bool` to be returned. It exposes the `RenderContext` which provides `BuildContext` and access to the HTML tree.
+The `CustomRender` class has two constructors: `CustomRender.widget()` and `CustomRender.inlineSpan()`. Both require a ` Function(RenderContext, Function())`. The `Function()` argument is a function that will provide you with the element's children when needed.
+
+To use this API, create a matching function and an instance of `CustomRender`.
+
+#### Example Usages - customRenders:
Note: If you add any custom tags, you must add these tags to the [`tagsList`](#tagslist) parameter, otherwise they will not be rendered. See below for an example.
-#### Example Usages - customRender:
1. Simple example - rendering custom HTML tags
```dart
@@ -285,26 +292,28 @@ Widget html = Html(
""",
- customRender: {
- "bird": (RenderContext context, Widget child) {
- return TextSpan(text: "🐦");
- },
- "flutter": (RenderContext context, Widget child) {
- return FlutterLogo(
- style: (context.tree.element!.attributes['horizontal'] != null)
- ? FlutterLogoStyle.horizontal
- : FlutterLogoStyle.markOnly,
- textColor: context.style.color!,
- size: context.style.fontSize!.size! * 5,
- );
- },
+ customRenders: {
+ birdMatcher(): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
+ flutterMatcher(): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
+ style: (context.tree.element!.attributes['horizontal'] != null)
+ ? FlutterLogoStyle.horizontal
+ : FlutterLogoStyle.markOnly,
+ textColor: context.style.color!,
+ size: context.style.fontSize!.size! * 5,
+ )),
},
tagsList: Html.tags..addAll(["bird", "flutter"]),
);
+
+CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';
+
+CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
```
2. Complex example - wrapping the default widget with your own, in this case placing a horizontal scroll around a (potentially too wide) table.
+Note: Requires the [`flutter_html_table`](#flutter_html_table) package.
+
View code
```dart
@@ -317,15 +326,18 @@ Widget html = Html(
\90 \$60 \$80 \$80 \$100 \$160 \$150 \$110 \$100 \$60 \$30 \$80
""",
- customRender: {
- "table": (context, child) {
+ customRenders: {
+ tableMatcher(): CustomRender.widget(widget: (context, child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
- child: (context.tree as TableLayoutElement).toWidget(context),
+ // this calls the table CustomRender to render a table as normal (it uses a widget so we know widget is not null)
+ child: tableRender.call().widget!.call(context, buildChildren),
);
- }
+ }),
},
);
+
+CustomRenderMatcher tableMatcher() => (context) => context.tree.element?.localName == "table";
```
@@ -342,44 +354,53 @@ Widget html = Html(
YouTube iframe:
""",
- customRender: {
- "iframe": (RenderContext context, Widget child) {
- final attrs = context.tree.element?.attributes;
- if (attrs != null) {
- double? width = double.tryParse(attrs['width'] ?? "");
- double? height = double.tryParse(attrs['height'] ?? "");
- return Container(
- width: width ?? (height ?? 150) * 2,
- height: height ?? (width ?? 300) / 2,
- child: WebView(
- initialUrl: attrs['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: attrs['src'] != null && attrs['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 (attrs['src'] != null && attrs['src']!.contains("youtube.com/embed")) {
- if (!request.url.contains("youtube.com/embed")) {
- return NavigationDecision.prevent;
- } else {
- return NavigationDecision.navigate;
- }
- } else {
- return NavigationDecision.navigate;
- }
- },
- ),
- );
- } else {
- return Container(height: 0);
- }
- }
- }
+ customRenders: {
+ iframeYT(): CustomRender.widget(widget: (context, buildChildren) {
+ double? width = double.tryParse(context.tree.attributes['width'] ?? "");
+ double? height = double.tryParse(context.tree.attributes['height'] ?? "");
+ return Container(
+ width: width ?? (height ?? 150) * 2,
+ height: height ?? (width ?? 300) / 2,
+ child: WebView(
+ initialUrl: context.tree.attributes['src']!,
+ javascriptMode: JavascriptMode.unrestricted,
+ navigationDelegate: (NavigationRequest request) async {
+ //no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
+ if (!request.url.contains("youtube.com/embed")) {
+ return NavigationDecision.prevent;
+ } else {
+ return NavigationDecision.navigate;
+ }
+ },
+ ),
+ );
+ }),
+ iframeOther(): CustomRender.widget(widget: (context, buildChildren) {
+ double? width = double.tryParse(context.tree.attributes['width'] ?? "");
+ double? height = double.tryParse(context.tree.attributes['height'] ?? "");
+ return Container(
+ width: width ?? (height ?? 150) * 2,
+ height: height ?? (width ?? 300) / 2,
+ child: WebView(
+ initialUrl: context.tree.attributes['src'],
+ javascriptMode: JavascriptMode.unrestricted,
+ //on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
+ gestureRecognizers: [
+ Factory(() => VerticalDragGestureRecognizer())
+ ].toSet(),
+ ),
+ );
+ }),
+ iframeNull(): CustomRender.widget(widget: (context, buildChildren) => Container(height: 0, width: 0)),
+ }
);
+
+CustomRenderMatcher iframeYT() => (context) => context.tree.element?.attributes['src']?.contains("youtube.com/embed") ?? false;
+
+CustomRenderMatcher iframeOther() => (context) => !(context.tree.element?.attributes['src']?.contains("youtube.com/embed")
+ ?? context.tree.element?.attributes['src'] == null);
+
+CustomRenderMatcher iframeNull() => (context) => context.tree.element?.attributes['src'] == null;
```
@@ -400,24 +421,6 @@ Widget html = Html(
);
```
-### onMathError:
-
-A function that defines what the widget should do when a math fails to render. The function exposes the parsed Tex `String`, as well as the error and error with type from `flutter_math` as a `String`.
-
-#### Example Usage - onMathError:
-
-```dart
-Widget html = Html(
- data: """""",
- onMathError: (String parsedTex, String error, String errorWithType) {
- //your logic here. A Widget must be returned from this function:
- return Text(error);
- //you can also try and fix the parsing yourself:
- return Math.tex(correctedParsedTex);
- },
-);
-```
-
### onImageTap:
A function that defines what the widget should do when an image is tapped.
@@ -529,299 +532,205 @@ Widget html = Html(
More examples and in-depth details available [here](https://github.com/Sub6Resources/flutter_html/wiki/Style).
-### navigationDelegateForIframe:
+## Rendering Reference
-Allows you to set the `NavigationDelegate` for the `WebView`s of all the iframes rendered by the `Html` widget. You can block or allow the loading of certain URLs with the `NavigationDelegate`.
+This section will describe how certain HTML elements are rendered by this package, so you can evaluate how your HTML will be rendered and structure it accordingly.
-#### Example Usage - navigationDelegateForIframe:
+### Image
-```dart
-Widget html = Html(
- data: """
- YouTube iframe:
-
- Google iframe:
-
- """,
- navigationDelegateForIframe: (NavigationRequest request) {
- if (request.url.contains("google.com/images")) {
- return NavigationDecision.prevent;
- } else {
- return NavigationDecision.navigate;
- }
- },
-);
-```
+This package currently has support for base64 images, asset images, and network images.
-### customImageRender:
+The package uses the `src` of the image to determine which of the above types to render. The order is as follows:
+1. If the `src` is null, render the alt text of the image, if any.
+2. If the `src` starts with "data:image" and contains "base64," (this indicates the image data is indeed base64), render an `Image.memory` from the base64 data.
+3. If the `src` starts with "asset:", render an `Image.asset` from the path in the `src`.
+4. Otherwise, just render an `Image.network`.
-A powerful API that allows you to customize what the `Html` widget does when rendering an image, down to the most minute detail.
+If the rendering of any of the above fails, the package will fall back to rendering the alt text of the image, if any.
-`customImageRender` accepts a `Map`. `ImageSourceMatcher` provides the matching function, while `ImageRender` provides the widget to be rendered.
+Currently the package only considers the width, height, src, and alt text while rendering an image.
-The default image renders are:
+If you would like to support SVGs in an ` `, you should use the [`flutter_html_svg`](#flutter_html_svg) package which provides support for base64, asset, and network SVGs.
-```dart
-final Map defaultImageRenders = {
- base64UriMatcher(): base64ImageRender(),
- assetUriMatcher(): assetImageRender(),
- networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
- networkSourceMatcher(): networkImageRender(),
-};
-```
+## External Packages
-See [the source code](https://github.com/Sub6Resources/flutter_html/blob/master/lib/image_render.dart) for details on how these are implemented.
+### `flutter_html_all`
-When setting `customImageRenders`, the package will prioritize the custom renders first, while the default ones are used as a fallback.
+This package is simply a convenience package that exports all the other external packages below. You should use this if you plan to activate all the renders that require external dependencies.
-Note: Order is very important when you set `customImageRenders`. The more specific your `ImageSourceMatcher`, the higher up in the `customImageRender` list it should be.
+### `flutter_html_audio`
-#### typedef ImageSourceMatcher
+This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) and the [`video_player`](https://pub.dev/packages/video_player) plugin.
-This is type defined as a function that passes the attributes as a `Map` and the DOM element as `dom.Element`. This type is used to define how an image should be matched i.e. whether the package should override the default rendering method and instead use your custom implementation.
+The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `width`, and `muted` when rendering the audio widget.
-A typical usage would look something like this:
+#### Registering the `CustomRender`:
```dart
-ImageSourceMatcher base64UriMatcher() => (attributes, element) =>
- attributes["src"] != null &&
- attributes["src"]!.startsWith("data:image") &&
- attributes["src"]!.contains("base64,");
+Widget html = Html(
+ customRenders: {
+ audioMatcher(): audioRender(),
+ }
+);
```
-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.
+### `flutter_html_iframe`
-You can also declare your own variables in the function itself, which would look like this:
-
-```dart
-ImageSourceMatcher networkSourceMatcher({
-/// all three are optional, you don't need to have these in the function
- List schemas: const ["https", "http"],
- List domains: const ["your domain 1", "your domain 2"],
- String extension: "your extension",
-}) =>
- (attributes, element) {
- final src = Uri.parse(attributes["src"] ?? "about:blank");
- return schemas.contains(src.scheme) &&
- domains.contains(src.host) &&
- src.path.endsWith(".$extension");
- };
-```
-
-In the above example, the possible schemas are checked against the scheme of the `src`, and optionally the domains and extensions are also checked. This implementation allows for extremely granular control over what images are matched, and could even be changed on the fly with a variable.
+This package renders iframes using the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin.
-#### typedef ImageRender
+When rendering iframes, the package considers the width, height, and sandbox attributes.
-This is a type defined as a function that passes the attributes of the image as a `Map`, the current [`RenderContext`](https://github.com/Sub6Resources/flutter_html/wiki/All-About-customRender#rendercontext-context), and the DOM element as `dom.Element`. This type is used to define the widget that should be rendered when used in conjunction with an `ImageSourceMatcher`.
+Sandbox controls the JavaScript mode of the webview - a value of `null` or `allow-scripts` will set `javascriptMode: JavascriptMode.unrestricted`, otherwise it will set `javascriptMode: JavascriptMode.disabled`.
-A typical usage might look like this:
+#### Registering the `CustomRender`:
```dart
-ImageRender base64ImageRender() => (context, attributes, element) {
- final decodedImage = base64.decode(attributes["src"] != null ?
- attributes["src"].split("base64,")[1].trim() : "about:blank");
- return Image.memory(
- decodedImage,
- );
- };
+Widget html = Html(
+ customRenders: {
+ iframeMatcher(): iframeRender(),
+ }
+);
```
-The above example should be used with the `base64UriMatcher()` in the examples for `ImageSourceMatcher`.
+You can set the `navigationDelegate` of the webview with the `navigationDelegate` property on `iframeRender`. This allows you to block or allow the loading of certain URLs.
-Just like functions for `ImageSourceMatcher`, you can declare your own variables in the function itself:
+#### `NavigationDelegate` example:
```dart
-ImageRender networkImageRender({
- Map headers,
- double width,
- double height,
- Widget Function(String) altWidget,
-}) =>
- (context, attributes, element) {
- return Image.network(
- attributes["src"] ?? "about:blank",
- headers: headers,
- width: width,
- height: height,
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return altWidget.call(attributes["alt"]) ??
- Text(attributes["alt"] ?? "",
- style: context.style.generateTextStyle());
- }
- return child;
- },
- );
- };
+Widget html = Html(
+ customRenders: {
+ iframeMatcher(): iframeRender(navigationDelegate: (NavigationRequest request) {
+ if (request.url.contains("google.com/images")) {
+ return NavigationDecision.prevent;
+ } else {
+ return NavigationDecision.navigate;
+ }
+ }),
+ }
+);
```
-Implementing these variables allows you to customize every last detail of how the widget is rendered.
+### `flutter_html_math`
+
+This package renders MathML elements using the [`flutter_math_fork`](https://pub.dev/packages/flutter_math_fork) plugin.
-#### Example Usages - customImageRender:
+When rendering MathML, the package takes the MathML data within the `` tag and tries to parse it to Tex. Then, it will pass the parsed string to `flutter_math_fork`.
-`customImageRender` can be used in two different ways:
+Because this package is parsing MathML to Tex, it may not support some functionalities. The current list of supported tags can be found [above](#currently-supported-html-tags), but some of these only have partial support at the moment.
+
+#### Registering the `CustomRender`:
-1. Overriding a default render:
```dart
Widget html = Html(
- data: """
-
-
- """,
- customImageRenders: {
- networkSourceMatcher(domains: ["flutter.dev"]):
- (context, attributes, element) {
- return FlutterLogo(size: 36);
- },
- networkSourceMatcher(): networkImageRender(
- headers: {"Custom-Header": "some-value"},
- altWidget: (alt) => Text(alt ?? ""),
- loadingWidget: () => Text("Loading..."),
- ),
- (attr, _) => attr["src"] != null && attr["src"]!.startsWith("/wiki"):
- networkImageRender(
- mapUrl: (url) => "https://upload.wikimedia.org" + url),
- },
+ customRenders: {
+ mathMatcher(): mathRender(),
+ }
);
```
-Above, there are three custom `networkSourceMatcher`s, which will be applied - in order - before the default implementations.
+If the parsing errors, you can use the `onMathError` property of `mathRender` to catch the error and potentially fix it on your end.
-When an image with URL `flutter.dev` is detected, rather than displaying the image, the render will display the flutter logo. If the image is any other image, it keeps the default widget, but just sets the headers and the alt text in case that image happens to be broken. The final render handles relative paths by rewriting them, specifically prefixing them with a base url. Note that the customizations of the previous custom renders do not apply. For example, the headers that the second render would apply are not applied in this third render.
+The function exposes the parsed Tex `String`, as well as the error and error with type from `flutter_math_fork` as a `String`.
-2. Creating your own renders:
-```dart
-ImageSourceMatcher classAndIdMatcher({String classToMatch, String idToMatch}) => (attributes, element) =>
- 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"] != null && attributes["class"]!.contains(classToMatch)) {
- return Image.asset(attributes["src"] ?? "about:blank");
- } else {
- return Image.network(
- attributes["src"] ?? "about:blank",
- semanticLabel: attributes["longdesc"] ?? "",
- width: attributes["width"],
- height: attributes["height"],
- color: context.style.color,
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return Text(attributes["alt"] ?? "", style: context.style.generateTextStyle());
- }
- return child;
- },
- );
- }
-};
+You can analyze the error and the parsed string, and finally return a new instance of `Math.tex()` with the corrected Tex string.
+
+#### `onMathError` example:
+```dart
Widget html = Html(
- data: """
-
-
- """,
- customImageRenders: {
- classAndIdMatcher(classToMatch: "class1", idToMatch: "imageId"): classAndIdRender(classToMatch: "class1", idToMatch: "imageId")
- },
+ customRenders: {
+ mathMatcher(): mathRender(onMathError: (tex, exception, exceptionWithType) {
+ print(exception);
+ //optionally try and correct the Tex string here
+ return Text(exception);
+ }),
+ }
);
```
-The above example has a matcher that checks for either a class or an id, and then returns two different widgets based on whether a class was matched or an id was matched.
-
-The sky is the limit when using the custom image renders. You can make it as granular as you want, or as all-encompassing as you want, and you have full control of everything. Plus you get the package's style parsing to use in your custom widgets, so your code looks neat and readable!
+If you'd like to see more MathML features, feel free to create a PR or file a feature request!
-## Rendering Reference
+#### Tex
-This section will describe how certain HTML elements are rendered by this package, so you can evaluate how your HTML will be rendered and structure it accordingly.
+If you have a Tex string you'd like to render inside your HTML you can do that using the same [`flutter_math_fork`](https://pub.dev/packages/flutter_math_fork) plugin.
-### Image
+Use a custom tag inside your HTML (an example could be ``), and place your **raw** Tex string inside.
+
+Then, use the `customRender` parameter to add the widget to render Tex. It could look like this:
-This package currently has support for base64 images, asset images, network SVGs inside an ` `, and network images.
+```dart
+Widget htmlWidget = Html(
+ data: r"""i\hbar\frac{\partial}{\partial t}\Psi(\vec x,t) = -\frac{\hbar}{2m}\nabla^2\Psi(\vec x,t)+ V(\vec x)\Psi(\vec x,t) """,
+ customRenders: {
+ texMatcher(): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
+ context.tree.element?.innerHtml ?? '',
+ mathStyle: MathStyle.display,
+ textStyle: context.style.generateTextStyle(),
+ onErrorFallback: (FlutterMathException e) {
+ //optionally try and correct the Tex string here
+ return Text(e.message);
+ },
+ )),
+ },
+ tagsList: Html.tags..add('tex'),
+);
-The package uses the `src` of the image to determine which of the above types to render. The order is as follows:
-1. If the `src` is null, render the alt text of the image, if any.
-2. If the `src` starts with "data:image" and contains "base64," (this indicates the image data is indeed base64), render an `Image.memory` from the base64 data.
-3. If the `src` starts with "asset:", render an `Image.asset` from the path in the `src`.
-4. If the `src` ends with ".svg", render a `SvgPicture.network` (from the [`flutter_svg`](https://pub.dev/packages/flutter_svg) package)
-5. Otherwise, just render an `Image.network`.
+CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';
+```
-If the rendering of any of the above fails, the package will fall back to rendering the alt text of the image, if any.
+### `flutter_html_svg`
-Currently the package only considers the width, height, src, and alt text while rendering an image.
+This package renders svg elements using the [`flutter_svg`](https://pub.dev/packages/flutter_svg) plugin.
-Note that there currently is no support for SVGs either in base64 format or asset format.
+When rendering SVGs, the package takes the SVG data within the `` tag and passes it to `flutter_svg`. The `width` and `height` attributes are considered while rendering, if given.
-### Iframe
+The package also exposes a few ways to render SVGs within an ` ` tag, specifically base64 SVGs, asset SVGs, and network SVGs.
-This package renders iframes using the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin.
+#### Registering the `CustomRender`:
-When rendering iframes, the package considers the width, height, and sandbox attributes.
+```dart
+Widget html = Html(
+ customRenders: {
+ svgTagMatcher(): svgTagRender(),
+ svgDataUriMatcher(): svgDataImageRender(),
+ svgAssetUriMatcher(): svgAssetImageRender(),
+ svgNetworkSourceMatcher(): svgNetworkImageRender(),
+ }
+);
+```
-Sandbox controls the JavaScript mode of the webview - a value of `null` or `allow-scripts` will set `javascriptMode: JavascriptMode.unrestricted`, otherwise it will set `javascriptMode: JavascriptMode.disabled`.
+### `flutter_html_table`
-You can set the `navigationDelegate` of the webview with the `navigationDelegateForIframe` property - see [here](#navigationdelegateforiframe) for more details.
+This package renders table elements using the [`flutter_layout_grid`](https://pub.dev/packages/flutter_layout_grid) plugin.
-### Audio
+When rendering table elements, the package tries to calculate the best fit for each element and size its cell accordingly. `Rowspan`s and `colspan`s are considered in this process, so cells that span across multiple rows and columns are rendered as expected. Heights are determined intrinsically to maintain an optimal aspect ratio for the cell.
-This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) plugin.
+#### Registering the `CustomRender`:
-The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `width`, and `muted` when rendering the audio widget.
+```dart
+Widget html = Html(
+ customRenders: {
+ tableMatcher(): tableRender(),
+ }
+);
+```
-### Video
+### `flutter_html_video`
-This package renders video elements using the [`chewie`](https://pub.dev/packages/chewie) plugin.
+This package renders video elements using the [`chewie`](https://pub.dev/packages/chewie) and the [`video_player`](https://pub.dev/packages/video_player) plugin.
The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `poster`, `width`, `height`, and `muted` when rendering the video widget.
-### SVG
-
-This package renders svg elements using the [`flutter_svg`](https://pub.dev/packages/flutter_svg) plugin.
-
-When rendering SVGs, the package takes the SVG data within the `` tag and passes it to `flutter_svg`. The `width` and `height` attributes are considered while rendering, if given.
-
-### MathML
-
-This package renders MathML elements using the [`flutter_math`](https://pub.dev/packages/flutter_math) plugin.
-
-When rendering MathML, the package takes the MathML data within the `` tag and tries to parse it to Tex. Then, it will pass the parsed string to `flutter_math`.
-
-Because this package is parsing MathML to Tex, it may not support some functionalities. The current list of supported tags can be found [above](#currently-supported-html-tags), but some of these only have partial support at the moment.
-
-If the parsing errors, you can use the [onMathError](#onmatherror) API to catch the error and potentially fix it on your end - you can analyze the error and the parsed string, and finally return a new instance of `Math.tex()` with the corrected Tex string.
-
-If you'd like to see more MathML features, feel free to create a PR or file a feature request!
-
-### Tex
-
-If you have a Tex string you'd like to render inside your HTML you can do that using the same [`flutter_math`](https://pub.dev/packages/flutter_math) plugin.
-
-Use a custom tag inside your HTML (an example could be ``), and place your **raw** Tex string inside.
-
-Then, use the `customRender` parameter to add the widget to render Tex. It could look like this:
+#### Registering the `CustomRender`:
```dart
-Widget htmlWidget = Html(
- data: r"""i\hbar\frac{\partial}{\partial t}\Psi(\vec x,t) = -\frac{\hbar}{2m}\nabla^2\Psi(\vec x,t)+ V(\vec x)\Psi(\vec x,t) """,
- customRender: {
- "tex": (RenderContext context, _) => Math.tex(
- context.tree.element!.text,
- onErrorFallback: (FlutterMathException e) {
- //return your error widget here e.g.
- return Text(e.message);
- },
- ),
- },
- tagsList: Html.tags..add('tex'),
+Widget html = Html(
+ customRenders: {
+ videoMatcher(): videoRender(),
+ }
);
```
-### Table
-
-This package renders table elements using the [`flutter_layout_grid`](https://pub.dev/packages/flutter_layout_grid) plugin.
-
-When rendering table elements, the package tries to calculate the best fit for each element and size its cell accordingly. `Rowspan`s and `colspan`s are considered in this process, so cells that span across multiple rows and columns are rendered as expected. Heights are determined intrinsically to maintain an optimal aspect ratio for the cell.
-
## Notes
1. If you'd like to use this widget inside of a `Row()`, make sure to set `shrinkWrap: true` and place your widget inside expanded:
@@ -829,13 +738,13 @@ When rendering table elements, the package tries to calculate the best fit for e
```dart
Widget row = Row(
children: [
- Expanded(
- child: Html(
- shrinkWrap: true,
- //other params
- )
- ),
- //whatever other widgets
+ Expanded(
+ child: Html(
+ shrinkWrap: true,
+ //other params
+ )
+ ),
+ //whatever other widgets
]
);
```
diff --git a/example/README.md b/example/README.md
index a8bc76b67d..b37f367d87 100644
--- a/example/README.md
+++ b/example/README.md
@@ -158,6 +158,7 @@ const htmlData = """
body: SingleChildScrollView(
child: Html(
data: htmlData,
+ tagsList: Html.tags..addAll(["flutter"]),
//Optional parameters:
style: {
"html": Style(
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
new file mode 100644
index 0000000000..61b6c4de17
--- /dev/null
+++ b/example/analysis_options.yaml
@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 7007e64bc1..3747830b03 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 28
+ compileSdkVersion 31
lintOptions {
disable 'InvalidPackage'
@@ -34,7 +34,7 @@ android {
defaultConfig {
applicationId "com.example.example"
minSdkVersion 19
- targetSdkVersion 28
+ targetSdkVersion 31
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index b1ad9037da..53ef7e50f7 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -12,7 +12,8 @@
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
- android:windowSoftInputMode="adjustResize">
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
index 6b4c0f78a7..f2872cf474 100644
--- a/example/ios/Flutter/AppFrameworkInfo.plist
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 8.0
+ 9.0
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index e3543a279d..ba2cda3073 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -24,11 +24,11 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/webview_flutter/ios"
SPEC CHECKSUMS:
- Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
+ Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
- wakelock: b0843b2479edbf6504d8d262c2959446f35373aa
+ wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
webview_flutter: 9f491a9b5a66f2573946a389b2677987b0ff8c0b
PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d
-COCOAPODS: 1.10.1
+COCOAPODS: 1.11.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
index aa1606fb05..8baf84a2b7 100644
--- a/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 46;
+ objectVersion = 50;
objects = {
/* Begin PBXBuildFile section */
@@ -167,7 +167,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
- LastUpgradeCheck = 1020;
+ LastUpgradeCheck = 1300;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -335,7 +335,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -414,7 +414,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -463,7 +463,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index a28140cfdb..3db53b6e1f 100644
--- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
runApp(new MyApp());
@@ -267,46 +269,54 @@ class _MyHomePageState extends State {
),
'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis),
},
- customRender: {
- "table": (context, child) {
- return SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child:
- (context.tree as TableLayoutElement).toWidget(context),
- );
- },
- "bird": (RenderContext context, Widget child) {
- return TextSpan(text: "🐦");
- },
- "flutter": (RenderContext context, Widget child) {
- return FlutterLogo(
- style: (context.tree.element!.attributes['horizontal'] != null)
- ? FlutterLogoStyle.horizontal
- : FlutterLogoStyle.markOnly,
- textColor: context.style.color!,
- size: context.style.fontSize!.size! * 5,
- );
- },
- },
- customImageRenders: {
- networkSourceMatcher(domains: ["flutter.dev"]):
- (context, attributes, element) {
- return FlutterLogo(size: 36);
- },
- networkSourceMatcher(domains: ["mydomain.com"]):
- networkImageRender(
+ tagsList: Html.tags..addAll(['tex', 'bird', 'flutter']),
+ customRenders: {
+ tagMatcher("tex"): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
+ context.tree.element?.innerHtml ?? '',
+ mathStyle: MathStyle.display,
+ textStyle: context.style.generateTextStyle(),
+ onErrorFallback: (FlutterMathException e) {
+ return Text(e.message);
+ },
+ )),
+ tagMatcher("bird"): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
+ tagMatcher("flutter"): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
+ style: (context.tree.element!.attributes['horizontal'] != null)
+ ? FlutterLogoStyle.horizontal
+ : FlutterLogoStyle.markOnly,
+ textColor: context.style.color!,
+ size: context.style.fontSize!.size! * 5,
+ )),
+ tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: tableRender.call().widget!.call(context, buildChildren),
+ )),
+ audioMatcher(): audioRender(),
+ iframeMatcher(): iframeRender(),
+ mathMatcher(): mathRender(onMathError: (error, exception, exceptionWithType) {
+ print(exception);
+ return Text(exception);
+ }),
+ svgTagMatcher(): svgTagRender(),
+ svgDataUriMatcher(): svgDataImageRender(),
+ svgAssetUriMatcher(): svgAssetImageRender(),
+ svgNetworkSourceMatcher(): svgNetworkImageRender(),
+ networkSourceMatcher(domains: ["flutter.dev"]): CustomRender.widget(
+ widget: (context, buildChildren) {
+ return FlutterLogo(size: 36);
+ }),
+ networkSourceMatcher(domains: ["mydomain.com"]): networkImageRender(
headers: {"Custom-Header": "some-value"},
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"):
- networkImageRender(
- mapUrl: (url) => "https://upload.wikimedia.org" + url!),
+ (context) => context.tree.element?.attributes["src"] != null
+ && context.tree.element!.attributes["src"]!.startsWith("/wiki"):
+ networkImageRender(mapUrl: (url) => "https://upload.wikimedia.org" + url!),
// Custom placeholder image for broken links
- networkSourceMatcher():
- networkImageRender(altWidget: (_) => FlutterLogo()),
+ networkSourceMatcher(): networkImageRender(altWidget: (_) => FlutterLogo()),
+ videoMatcher(): videoRender(),
},
onLinkTap: (url, _, __, ___) {
print("Opening $url...");
@@ -329,3 +339,9 @@ class _MyHomePageState extends State {
);
}
}
+
+CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';
+
+CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';
+
+CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
diff --git a/example/macos/.gitignore b/example/macos/.gitignore
new file mode 100644
index 0000000000..746adbb6b9
--- /dev/null
+++ b/example/macos/.gitignore
@@ -0,0 +1,7 @@
+# Flutter-related
+**/Flutter/ephemeral/
+**/Pods/
+
+# Xcode-related
+**/dgph
+**/xcuserdata/
diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig
new file mode 100644
index 0000000000..4b81f9b2d2
--- /dev/null
+++ b/example/macos/Flutter/Flutter-Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig
new file mode 100644
index 0000000000..5caa9d1579
--- /dev/null
+++ b/example/macos/Flutter/Flutter-Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "ephemeral/Flutter-Generated.xcconfig"
diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift
new file mode 100644
index 0000000000..1c2c9b0787
--- /dev/null
+++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -0,0 +1,12 @@
+//
+// Generated file. Do not edit.
+//
+
+import FlutterMacOS
+import Foundation
+
+import wakelock_macos
+
+func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
+}
diff --git a/example/macos/Podfile b/example/macos/Podfile
new file mode 100644
index 0000000000..dade8dfad0
--- /dev/null
+++ b/example/macos/Podfile
@@ -0,0 +1,40 @@
+platform :osx, '10.11'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_macos_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_macos_build_settings(target)
+ end
+end
diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock
new file mode 100644
index 0000000000..181bf1def1
--- /dev/null
+++ b/example/macos/Podfile.lock
@@ -0,0 +1,22 @@
+PODS:
+ - FlutterMacOS (1.0.0)
+ - wakelock_macos (0.0.1):
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - FlutterMacOS (from `Flutter/ephemeral`)
+ - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`)
+
+EXTERNAL SOURCES:
+ FlutterMacOS:
+ :path: Flutter/ephemeral
+ wakelock_macos:
+ :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos
+
+SPEC CHECKSUMS:
+ FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+ wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9
+
+PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c
+
+COCOAPODS: 1.11.2
diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..6f0fb6010c
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,632 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 51;
+ objects = {
+
+/* Begin PBXAggregateTarget section */
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = {
+ isa = PBXAggregateTarget;
+ buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */;
+ buildPhases = (
+ 33CC111E2044C6BF0003C045 /* ShellScript */,
+ );
+ dependencies = (
+ );
+ name = "Flutter Assemble";
+ productName = FLX;
+ };
+/* End PBXAggregateTarget section */
+
+/* Begin PBXBuildFile section */
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
+ 5F522694C62AF8897C0CC60C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 504E42C3FE1FD01681D066A3 /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 33CC10E52044A3C60003C045 /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 33CC111A2044C6BA0003C045;
+ remoteInfo = FLX;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 33CC110E2044A8840003C045 /* Bundle Framework */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Bundle Framework";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0E783C689F2D5D66EF50AF12 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; };
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; };
+ 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; };
+ 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
+ 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; };
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; };
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; };
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; };
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; };
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; };
+ 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; };
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; };
+ 504E42C3FE1FD01681D066A3 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
+ 8AEF4381A8969E9CAAA8A18B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 91516C27EDFFADFC91D62744 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 33CC10EA2044A3C60003C045 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5F522694C62AF8897C0CC60C /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 277AC78050EEFCF0171FA2DF /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 8AEF4381A8969E9CAAA8A18B /* Pods-Runner.debug.xcconfig */,
+ 91516C27EDFFADFC91D62744 /* Pods-Runner.release.xcconfig */,
+ 0E783C689F2D5D66EF50AF12 /* Pods-Runner.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ 33BA886A226E78AF003329D5 /* Configs */ = {
+ isa = PBXGroup;
+ children = (
+ 33E5194F232828860026EE4D /* AppInfo.xcconfig */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */,
+ );
+ path = Configs;
+ sourceTree = "";
+ };
+ 33CC10E42044A3C60003C045 = {
+ isa = PBXGroup;
+ children = (
+ 33FAB671232836740065AC1E /* Runner */,
+ 33CEB47122A05771004F2AC0 /* Flutter */,
+ 33CC10EE2044A3C60003C045 /* Products */,
+ D73912EC22F37F3D000D13A0 /* Frameworks */,
+ 277AC78050EEFCF0171FA2DF /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 33CC10EE2044A3C60003C045 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10ED2044A3C60003C045 /* example.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 33CC11242044D66E0003C045 /* Resources */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F22044A3C60003C045 /* Assets.xcassets */,
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */,
+ 33CC10F72044A3C60003C045 /* Info.plist */,
+ );
+ name = Resources;
+ path = ..;
+ sourceTree = "";
+ };
+ 33CEB47122A05771004F2AC0 /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */,
+ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */,
+ 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */,
+ 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */,
+ );
+ path = Flutter;
+ sourceTree = "";
+ };
+ 33FAB671232836740065AC1E /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 33CC10F02044A3C60003C045 /* AppDelegate.swift */,
+ 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */,
+ 33E51913231747F40026EE4D /* DebugProfile.entitlements */,
+ 33E51914231749380026EE4D /* Release.entitlements */,
+ 33CC11242044D66E0003C045 /* Resources */,
+ 33BA886A226E78AF003329D5 /* Configs */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ D73912EC22F37F3D000D13A0 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 504E42C3FE1FD01681D066A3 /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 33CC10EC2044A3C60003C045 /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ C6BCD96E5D82542B4FB48D8B /* [CP] Check Pods Manifest.lock */,
+ 33CC10E92044A3C60003C045 /* Sources */,
+ 33CC10EA2044A3C60003C045 /* Frameworks */,
+ 33CC10EB2044A3C60003C045 /* Resources */,
+ 33CC110E2044A8840003C045 /* Bundle Framework */,
+ 3399D490228B24CF009A79C7 /* ShellScript */,
+ 58533780D417F2F6049B6E1D /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */,
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 33CC10ED2044A3C60003C045 /* example.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 33CC10E52044A3C60003C045 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0920;
+ LastUpgradeCheck = 0930;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 33CC10EC2044A3C60003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
+ SystemCapabilities = {
+ com.apple.Sandbox = {
+ enabled = 1;
+ };
+ };
+ };
+ 33CC111A2044C6BA0003C045 = {
+ CreatedOnToolsVersion = 9.2;
+ ProvisioningStyle = Manual;
+ };
+ };
+ };
+ buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 33CC10E42044A3C60003C045;
+ productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 33CC10EC2044A3C60003C045 /* Runner */,
+ 33CC111A2044C6BA0003C045 /* Flutter Assemble */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 33CC10EB2044A3C60003C045 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
+ 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3399D490228B24CF009A79C7 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ );
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n";
+ };
+ 33CC111E2044C6BF0003C045 /* ShellScript */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ Flutter/ephemeral/FlutterInputs.xcfilelist,
+ );
+ inputPaths = (
+ Flutter/ephemeral/tripwire,
+ );
+ outputFileListPaths = (
+ Flutter/ephemeral/FlutterOutputs.xcfilelist,
+ );
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
+ };
+ 58533780D417F2F6049B6E1D /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ C6BCD96E5D82542B4FB48D8B /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 33CC10E92044A3C60003C045 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */,
+ 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */,
+ 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 33CC11202044C79F0003C045 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */;
+ targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 33CC10F42044A3C60003C045 /* MainMenu.xib */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 33CC10F52044A3C60003C045 /* Base */,
+ );
+ name = MainMenu.xib;
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 338D0CE9231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Profile;
+ };
+ 338D0CEA231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Profile;
+ };
+ 338D0CEB231458BD00FA5F75 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Profile;
+ };
+ 33CC10F92044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 33CC10FA2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CODE_SIGN_IDENTITY = "-";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.11;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = macosx;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ };
+ name = Release;
+ };
+ 33CC10FC2044A3C60003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ 33CC10FD2044A3C60003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
+ 33CC111C2044C6BA0003C045 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Manual;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Debug;
+ };
+ 33CC111D2044C6BA0003C045 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10F92044A3C60003C045 /* Debug */,
+ 33CC10FA2044A3C60003C045 /* Release */,
+ 338D0CE9231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC10FC2044A3C60003C045 /* Debug */,
+ 33CC10FD2044A3C60003C045 /* Release */,
+ 338D0CEA231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 33CC111C2044C6BA0003C045 /* Debug */,
+ 33CC111D2044C6BA0003C045 /* Release */,
+ 338D0CEB231458BD00FA5F75 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 33CC10E52044A3C60003C045 /* Project object */;
+}
diff --git a/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000000..ae8ff59d97
--- /dev/null
+++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000000..21a3cc14c7
--- /dev/null
+++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000000..18d981003d
--- /dev/null
+++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift
new file mode 100644
index 0000000000..d53ef64377
--- /dev/null
+++ b/example/macos/Runner/AppDelegate.swift
@@ -0,0 +1,9 @@
+import Cocoa
+import FlutterMacOS
+
+@NSApplicationMain
+class AppDelegate: FlutterAppDelegate {
+ override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ return true
+ }
+}
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000000..a2ec33f19f
--- /dev/null
+++ b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_32.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "app_icon_64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_256.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_512.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "app_icon_1024.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
new file mode 100644
index 0000000000..3c4935a7ca
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
new file mode 100644
index 0000000000..ed4cc16421
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
new file mode 100644
index 0000000000..483be61389
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
new file mode 100644
index 0000000000..bcbf36df2f
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
new file mode 100644
index 0000000000..9c0a652864
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
new file mode 100644
index 0000000000..e71a726136
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
new file mode 100644
index 0000000000..8a31fe2dd3
Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib
new file mode 100644
index 0000000000..537341abf9
--- /dev/null
+++ b/example/macos/Runner/Base.lproj/MainMenu.xib
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig
new file mode 100644
index 0000000000..cf9be60ca4
--- /dev/null
+++ b/example/macos/Runner/Configs/AppInfo.xcconfig
@@ -0,0 +1,14 @@
+// Application-level settings for the Runner target.
+//
+// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the
+// future. If not, the values below would default to using the project name when this becomes a
+// 'flutter create' template.
+
+// The application's name. By default this is also the title of the Flutter window.
+PRODUCT_NAME = example
+
+// The application's bundle identifier
+PRODUCT_BUNDLE_IDENTIFIER = com.example.example
+
+// The copyright displayed in application information
+PRODUCT_COPYRIGHT = Copyright © 2021 com.example. All rights reserved.
diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig
new file mode 100644
index 0000000000..36b0fd9464
--- /dev/null
+++ b/example/macos/Runner/Configs/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Debug.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig
new file mode 100644
index 0000000000..dff4f49561
--- /dev/null
+++ b/example/macos/Runner/Configs/Release.xcconfig
@@ -0,0 +1,2 @@
+#include "../../Flutter/Flutter-Release.xcconfig"
+#include "Warnings.xcconfig"
diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig
new file mode 100644
index 0000000000..42bcbf4780
--- /dev/null
+++ b/example/macos/Runner/Configs/Warnings.xcconfig
@@ -0,0 +1,13 @@
+WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings
+GCC_WARN_UNDECLARED_SELECTOR = YES
+CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES
+CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE
+CLANG_WARN__DUPLICATE_METHOD_MATCH = YES
+CLANG_WARN_PRAGMA_PACK = YES
+CLANG_WARN_STRICT_PROTOTYPES = YES
+CLANG_WARN_COMMA = YES
+GCC_WARN_STRICT_SELECTOR_MATCH = YES
+CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES
+CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES
+GCC_WARN_SHADOW = YES
+CLANG_WARN_UNREACHABLE_CODE = YES
diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements
new file mode 100644
index 0000000000..dddb8a30c8
--- /dev/null
+++ b/example/macos/Runner/DebugProfile.entitlements
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.network.server
+
+
+
diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist
new file mode 100644
index 0000000000..4789daa6a4
--- /dev/null
+++ b/example/macos/Runner/Info.plist
@@ -0,0 +1,32 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIconFile
+
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSMinimumSystemVersion
+ $(MACOSX_DEPLOYMENT_TARGET)
+ NSHumanReadableCopyright
+ $(PRODUCT_COPYRIGHT)
+ NSMainNibFile
+ MainMenu
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift
new file mode 100644
index 0000000000..2722837ec9
--- /dev/null
+++ b/example/macos/Runner/MainFlutterWindow.swift
@@ -0,0 +1,15 @@
+import Cocoa
+import FlutterMacOS
+
+class MainFlutterWindow: NSWindow {
+ override func awakeFromNib() {
+ let flutterViewController = FlutterViewController.init()
+ let windowFrame = self.frame
+ self.contentViewController = flutterViewController
+ self.setFrame(windowFrame, display: true)
+
+ RegisterGeneratedPlugins(registry: flutterViewController)
+
+ super.awakeFromNib()
+ }
+}
diff --git a/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements
new file mode 100644
index 0000000000..852fa1a472
--- /dev/null
+++ b/example/macos/Runner/Release.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 73ff399401..79e23feab9 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,6 +1,6 @@
name: example
description: flutter_html example app.
-
+publish_to: none
version: 1.0.0+1
environment:
@@ -9,6 +9,8 @@ environment:
dependencies:
flutter_html:
path: ..
+ flutter_html_all:
+ path: ../packages/flutter_html_all
flutter:
sdk: flutter
diff --git a/lib/custom_render.dart b/lib/custom_render.dart
new file mode 100644
index 0000000000..196ee6fafa
--- /dev/null
+++ b/lib/custom_render.dart
@@ -0,0 +1,556 @@
+import 'package:collection/collection.dart';
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_html/src/utils.dart';
+
+typedef CustomRenderMatcher = bool Function(RenderContext context);
+
+CustomRenderMatcher tagMatcher(String tag) => (context) {
+ return context.tree.element?.localName == tag;
+};
+
+CustomRenderMatcher blockElementMatcher() => (context) {
+ return context.tree.style.display == Display.BLOCK &&
+ (context.tree.children.isNotEmpty || context.tree.element?.localName == "hr");
+};
+
+CustomRenderMatcher listElementMatcher() => (context) {
+ return context.tree.style.display == Display.LIST_ITEM;
+};
+
+CustomRenderMatcher replacedElementMatcher() => (context) {
+ return context.tree is ReplacedElement;
+};
+
+CustomRenderMatcher dataUriMatcher({String? encoding = 'base64', String? mime}) => (context) {
+ if (context.tree.element?.attributes == null
+ || _src(context.tree.element!.attributes.cast()) == null) return false;
+ final dataUri = _dataUriFormat.firstMatch(_src(context.tree.element!.attributes.cast())!);
+ return dataUri != null && dataUri.namedGroup('mime') != "image/svg+xml" &&
+ (mime == null || dataUri.namedGroup('mime') == mime) &&
+ (encoding == null || dataUri.namedGroup('encoding') == ';$encoding');
+};
+
+CustomRenderMatcher networkSourceMatcher({
+ List schemas: const ["https", "http"],
+ List? domains,
+ String? extension,
+}) =>
+ (context) {
+ if (context.tree.element?.attributes.cast() == null
+ || _src(context.tree.element!.attributes.cast()) == null) return false;
+ try {
+ final src = Uri.parse(_src(context.tree.element!.attributes.cast())!);
+ return schemas.contains(src.scheme) &&
+ (domains == null || domains.contains(src.host)) &&
+ (extension == null || src.path.endsWith(".$extension"));
+ } catch (e) {
+ return false;
+ }
+ };
+
+CustomRenderMatcher assetUriMatcher() => (context) =>
+ context.tree.element?.attributes.cast() != null
+ && _src(context.tree.element!.attributes.cast()) != null
+ && _src(context.tree.element!.attributes.cast())!.startsWith("asset:")
+ && !_src(context.tree.element!.attributes.cast())!.endsWith(".svg");
+
+CustomRenderMatcher textContentElementMatcher() => (context) {
+ return context.tree is TextContentElement;
+};
+
+CustomRenderMatcher interactableElementMatcher() => (context) {
+ return context.tree is InteractableElement;
+};
+
+CustomRenderMatcher layoutElementMatcher() => (context) {
+ return context.tree is LayoutElement;
+};
+
+CustomRenderMatcher verticalAlignMatcher() => (context) {
+ return context.tree.style.verticalAlign != null
+ && context.tree.style.verticalAlign != VerticalAlign.BASELINE;
+};
+
+CustomRenderMatcher fallbackMatcher() => (context) {
+ return true;
+};
+
+class CustomRender {
+ final InlineSpan Function(RenderContext, List Function())? inlineSpan;
+ final Widget Function(RenderContext, List Function())? widget;
+
+ CustomRender.inlineSpan({
+ required this.inlineSpan,
+ }) : widget = null;
+
+ CustomRender.widget({
+ required this.widget,
+ }) : inlineSpan = null;
+}
+
+class SelectableCustomRender extends CustomRender {
+ final TextSpan Function(RenderContext, List Function()) textSpan;
+
+ SelectableCustomRender.fromTextSpan({
+ required this.textSpan,
+ }) : super.inlineSpan(inlineSpan: null);
+}
+
+CustomRender blockElementRender({
+ Style? style,
+ List? children}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) {
+ if (context.parser.selectable) {
+ return TextSpan(
+ style: context.style.generateTextStyle(),
+ children: (children as List?) ?? context.tree.children
+ .expandIndexed((i, childTree) => [
+ if (childTree.style.display == Display.BLOCK &&
+ i > 0 &&
+ context.tree.children[i - 1] is ReplacedElement)
+ TextSpan(text: "\n"),
+ context.parser.parseTree(context, childTree),
+ if (i != context.tree.children.length - 1 &&
+ childTree.style.display == Display.BLOCK &&
+ childTree.element?.localName != "html" &&
+ childTree.element?.localName != "body")
+ TextSpan(text: "\n"),
+ ])
+ .toList(),
+ );
+ }
+ return WidgetSpan(
+ child: ContainerSpan(
+ key: context.key,
+ newContext: context,
+ style: style ?? context.tree.style,
+ shrinkWrap: context.parser.shrinkWrap,
+ children: children ?? context.tree.children
+ .expandIndexed((i, childTree) => [
+ if (context.parser.shrinkWrap &&
+ childTree.style.display == Display.BLOCK &&
+ i > 0 &&
+ context.tree.children[i - 1] is ReplacedElement)
+ TextSpan(text: "\n"),
+ context.parser.parseTree(context, childTree),
+ if (context.parser.shrinkWrap &&
+ i != context.tree.children.length - 1 &&
+ childTree.style.display == Display.BLOCK &&
+ childTree.element?.localName != "html" &&
+ childTree.element?.localName != "body")
+ TextSpan(text: "\n"),
+ ])
+ .toList(),
+ ));
+ });
+
+CustomRender listElementRender({
+ Style? style,
+ Widget? child,
+ List? children}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) =>
+ WidgetSpan(
+ child: ContainerSpan(
+ key: context.key,
+ newContext: context,
+ style: style ?? context.tree.style,
+ shrinkWrap: context.parser.shrinkWrap,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ textDirection: style?.direction ?? context.tree.style.direction,
+ children: [
+ (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.OUTSIDE ?
+ Padding(
+ padding: style?.padding?.nonNegative ?? context.tree.style.padding?.nonNegative
+ ?? EdgeInsets.only(left: (style?.direction ?? context.tree.style.direction) != TextDirection.rtl ? 10.0 : 0.0,
+ right: (style?.direction ?? context.tree.style.direction) == TextDirection.rtl ? 10.0 : 0.0),
+ child: style?.markerContent ?? context.style.markerContent
+ ) : Container(height: 0, width: 0),
+ Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
+ Expanded(
+ child: Padding(
+ padding: (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.INSIDE ?
+ EdgeInsets.only(left: (style?.direction ?? context.tree.style.direction) != TextDirection.rtl ? 10.0 : 0.0,
+ right: (style?.direction ?? context.tree.style.direction) == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero,
+ child: StyledText(
+ textSpan: TextSpan(
+ children: _getListElementChildren(style?.listStylePosition ?? context.tree.style.listStylePosition, buildChildren)
+ ..insertAll(0, context.tree.style.listStylePosition == ListStylePosition.INSIDE ?
+ [
+ WidgetSpan(alignment: PlaceholderAlignment.middle, child: style?.markerContent ?? context.style.markerContent ?? Container(height: 0, width: 0))
+ ] : []),
+ style: style?.generateTextStyle() ?? context.style.generateTextStyle(),
+ ),
+ style: style ?? context.style,
+ renderContext: context,
+ )
+ )
+ )
+ ],
+ ),
+ ),
+));
+
+CustomRender replacedElementRender({PlaceholderAlignment? alignment, TextBaseline? baseline, Widget? child}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
+ alignment: alignment ?? (context.tree as ReplacedElement).alignment,
+ baseline: baseline ?? TextBaseline.alphabetic,
+ child: child ?? (context.tree as ReplacedElement).toWidget(context)!,
+));
+
+CustomRender textContentElementRender({String? text}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) =>
+ TextSpan(text: (text ?? (context.tree as TextContentElement).text).transformed(context.tree.style.textTransform)));
+
+CustomRender base64ImageRender() => CustomRender.widget(widget: (context, buildChildren) {
+ final decodedImage = base64.decode(_src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim());
+ precacheImage(
+ MemoryImage(decodedImage),
+ context.buildContext,
+ onError: (exception, StackTrace? stackTrace) {
+ context.parser.onImageError?.call(exception, stackTrace);
+ },
+ );
+ final widget = Image.memory(
+ decodedImage,
+ frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle());
+ }
+ return child;
+ },
+ );
+ return Builder(
+ key: context.key,
+ builder: (buildContext) {
+ return GestureDetector(
+ child: widget,
+ onTap: () {
+ if (MultipleTapGestureDetector.of(buildContext) != null) {
+ MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
+ }
+ context.parser.onImageTap?.call(
+ _src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim(),
+ context,
+ context.tree.element!.attributes.cast(),
+ context.tree.element
+ );
+ },
+ );
+ }
+ );
+});
+
+CustomRender assetImageRender({
+ double? width,
+ double? height,
+}) => CustomRender.widget(widget: (context, buildChildren) {
+ final assetPath = _src(context.tree.element!.attributes.cast())!.replaceFirst('asset:', '');
+ final widget = Image.asset(
+ assetPath,
+ width: width ?? _width(context.tree.element!.attributes.cast()),
+ height: height ?? _height(context.tree.element!.attributes.cast()),
+ frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle());
+ }
+ return child;
+ },
+ );
+ return Builder(
+ key: context.key,
+ builder: (buildContext) {
+ return GestureDetector(
+ child: widget,
+ onTap: () {
+ if (MultipleTapGestureDetector.of(buildContext) != null) {
+ MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
+ }
+ context.parser.onImageTap?.call(
+ assetPath,
+ context,
+ context.tree.element!.attributes.cast(),
+ context.tree.element
+ );
+ },
+ );
+ }
+ );
+});
+
+CustomRender networkImageRender({
+ Map? headers,
+ String Function(String?)? mapUrl,
+ double? width,
+ double? height,
+ Widget Function(String?)? altWidget,
+ Widget Function()? loadingWidget,
+}) => CustomRender.widget(widget: (context, buildChildren) {
+ final src = mapUrl?.call(_src(context.tree.element!.attributes.cast()))
+ ?? _src(context.tree.element!.attributes.cast())!;
+ Completer completer = Completer();
+ if (context.parser.cachedImageSizes[src] != null) {
+ completer.complete(context.parser.cachedImageSizes[src]);
+ } else {
+ Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ if (!completer.isCompleted) {
+ completer.completeError("error");
+ }
+ return child;
+ } else {
+ return child;
+ }
+ });
+
+ ImageStreamListener? listener;
+ listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
+ var myImage = imageInfo.image;
+ Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
+ if (!completer.isCompleted) {
+ context.parser.cachedImageSizes[src] = size;
+ completer.complete(size);
+ image.image.resolve(ImageConfiguration()).removeListener(listener!);
+ }
+ }, onError: (object, stacktrace) {
+ if (!completer.isCompleted) {
+ completer.completeError(object);
+ image.image.resolve(ImageConfiguration()).removeListener(listener!);
+ }
+ });
+
+ image.image.resolve(ImageConfiguration()).addListener(listener);
+ }
+ final attributes = context.tree.element!.attributes.cast();
+ final widget = FutureBuilder(
+ future: completer.future,
+ initialData: context.parser.cachedImageSizes[src],
+ builder: (BuildContext buildContext, AsyncSnapshot snapshot) {
+ if (snapshot.hasData) {
+ return Container(
+ constraints: BoxConstraints(
+ maxWidth: width ?? _width(attributes) ?? snapshot.data!.width,
+ maxHeight:
+ (width ?? _width(attributes) ?? snapshot.data!.width) /
+ _aspectRatio(attributes, snapshot)),
+ child: AspectRatio(
+ aspectRatio: _aspectRatio(attributes, snapshot),
+ child: Image.network(
+ src,
+ headers: headers,
+ width: width ?? _width(attributes) ?? snapshot.data!.width,
+ height: height ?? _height(attributes),
+ frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ return altWidget?.call(_alt(attributes)) ??
+ Text(_alt(attributes) ?? "",
+ style: context.style.generateTextStyle());
+ }
+ return child;
+ },
+ ),
+ ),
+ );
+ } else if (snapshot.hasError) {
+ return altWidget?.call(_alt(context.tree.element!.attributes.cast())) ??
+ Text(_alt(context.tree.element!.attributes.cast())
+ ?? "", style: context.style.generateTextStyle());
+ } else {
+ return loadingWidget?.call() ?? const CircularProgressIndicator();
+ }
+ },
+ );
+ return Builder(
+ key: context.key,
+ builder: (buildContext) {
+ return GestureDetector(
+ child: widget,
+ onTap: () {
+ if (MultipleTapGestureDetector.of(buildContext) != null) {
+ MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
+ }
+ context.parser.onImageTap?.call(
+ src,
+ context,
+ context.tree.element!.attributes.cast(),
+ context.tree.element
+ );
+ },
+ );
+ }
+ );
+});
+
+CustomRender interactableElementRender({List? children}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(
+ children: children ?? (context.tree as InteractableElement).children
+ .map((tree) => context.parser.parseTree(context, tree))
+ .map((childSpan) {
+ return _getInteractableChildren(context, context.tree as InteractableElement, childSpan,
+ context.style.generateTextStyle().merge(childSpan.style));
+ }).toList(),
+));
+
+CustomRender layoutElementRender({Widget? child}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
+ child: child ?? (context.tree as LayoutElement).toWidget(context)!,
+));
+
+CustomRender verticalAlignRender({
+ double? verticalOffset,
+ Style? style,
+ List? children}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
+ child: Transform.translate(
+ key: context.key,
+ offset: Offset(0, verticalOffset ?? _getVerticalOffset(context.tree)),
+ child: StyledText(
+ textSpan: TextSpan(
+ style: style?.generateTextStyle() ?? context.style.generateTextStyle(),
+ children: children ?? buildChildren.call(),
+ ),
+ style: context.style,
+ renderContext: context,
+ ),
+ ),
+));
+
+CustomRender fallbackRender({Style? style, List? children}) =>
+ CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(
+ style: style?.generateTextStyle() ?? context.style.generateTextStyle(),
+ children: context.tree.children
+ .expand((tree) => [
+ context.parser.parseTree(context, tree),
+ if (tree.style.display == Display.BLOCK &&
+ tree.element?.parent?.localName != "th" &&
+ tree.element?.parent?.localName != "td" &&
+ tree.element?.localName != "html" &&
+ tree.element?.localName != "body")
+ TextSpan(text: "\n"),
+ ])
+ .toList(),
+));
+
+final Map defaultRenders = {
+ blockElementMatcher(): blockElementRender(),
+ listElementMatcher(): listElementRender(),
+ textContentElementMatcher(): textContentElementRender(),
+ dataUriMatcher(): base64ImageRender(),
+ assetUriMatcher(): assetImageRender(),
+ networkSourceMatcher(): networkImageRender(),
+ replacedElementMatcher(): replacedElementRender(),
+ interactableElementMatcher(): interactableElementRender(),
+ layoutElementMatcher(): layoutElementRender(),
+ verticalAlignMatcher(): verticalAlignRender(),
+ fallbackMatcher(): fallbackRender(),
+};
+
+List _getListElementChildren(ListStylePosition? position, Function() buildChildren) {
+ List children = buildChildren.call();
+ if (position == ListStylePosition.INSIDE) {
+ final tabSpan = WidgetSpan(
+ child: Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
+ );
+ children.insert(0, tabSpan);
+ }
+ return children;
+}
+
+InlineSpan _getInteractableChildren(RenderContext context, InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) {
+ if (childSpan is TextSpan) {
+ return TextSpan(
+ text: childSpan.text,
+ children: childSpan.children
+ ?.map((e) => _getInteractableChildren(context, tree, e, childStyle.merge(childSpan.style)))
+ .toList(),
+ style: context.style.generateTextStyle().merge(
+ childSpan.style == null
+ ? childStyle
+ : childStyle.merge(childSpan.style)),
+ semanticsLabel: childSpan.semanticsLabel,
+ recognizer: TapGestureRecognizer()
+ ..onTap =
+ context.parser.internalOnAnchorTap != null ?
+ () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element)
+ : null,
+ );
+ } else {
+ return WidgetSpan(
+ child: MultipleTapGestureDetector(
+ onTap: context.parser.internalOnAnchorTap != null
+ ? () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element)
+ : null,
+ child: GestureDetector(
+ key: context.key,
+ onTap: context.parser.internalOnAnchorTap != null
+ ? () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element)
+ : null,
+ child: (childSpan as WidgetSpan).child,
+ ),
+ ),
+ );
+ }
+}
+
+final _dataUriFormat = RegExp("^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)");
+
+double _getVerticalOffset(StyledElement tree) {
+ switch (tree.style.verticalAlign) {
+ case VerticalAlign.SUB:
+ return tree.style.fontSize!.size! / 2.5;
+ case VerticalAlign.SUPER:
+ return tree.style.fontSize!.size! / -2.5;
+ default:
+ return 0;
+ }
+}
+
+String? _src(Map attributes) {
+ return attributes["src"];
+}
+
+String? _alt(Map attributes) {
+ return attributes["alt"];
+}
+
+double? _height(Map attributes) {
+ final heightString = attributes["height"];
+ return heightString == null ? heightString as double? : double.tryParse(heightString);
+}
+
+double? _width(Map attributes) {
+ final widthString = attributes["width"];
+ return widthString == null ? widthString as double? : double.tryParse(widthString);
+}
+
+double _aspectRatio(
+ Map attributes, AsyncSnapshot calculated) {
+ double aspectRatio;
+ final heightString = attributes["height"];
+ final widthString = attributes["width"];
+ if (heightString != null && widthString != null) {
+ final height = double.tryParse(heightString);
+ final width = double.tryParse(widthString);
+ aspectRatio = height == null || width == null
+ ? calculated.data!.aspectRatio
+ : width / height;
+ } else {
+ aspectRatio = calculated.data!.aspectRatio;
+ }
+ if (!aspectRatio.isNaN) {
+ return aspectRatio;
+ } else {
+ return 1;
+ }
+}
+
+extension ClampedEdgeInsets on EdgeInsetsGeometry {
+ EdgeInsetsGeometry get nonNegative => this.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity));
+}
diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart
index d0a68217ef..1405471220 100644
--- a/lib/flutter_html.dart
+++ b/lib/flutter_html.dart
@@ -1,17 +1,17 @@
library flutter_html;
import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
+import 'package:flutter_html/custom_render.dart';
import 'package:flutter_html/html_parser.dart';
-import 'package:flutter_html/image_render.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
-import 'package:webview_flutter/webview_flutter.dart';
//export render context api
export 'package:flutter_html/html_parser.dart';
-export 'package:flutter_html/image_render.dart';
+//export render context api
+export 'package:flutter_html/html_parser.dart';
+export 'package:flutter_html/custom_render.dart';
//export src for advanced custom render uses (e.g. casting context.tree)
export 'package:flutter_html/src/anchor.dart';
export 'package:flutter_html/src/interactable_element.dart';
@@ -21,7 +21,7 @@ export 'package:flutter_html/src/styled_element.dart';
//export style api
export 'package:flutter_html/style.dart';
-class Html extends StatelessWidget {
+class Html extends StatefulWidget {
/// The `Html` widget takes HTML as input and displays a RichText
/// tree of the parsed HTML content.
///
@@ -43,7 +43,7 @@ class Html extends StatelessWidget {
///
/// **onImageTap** This is called whenever an image is tapped.
///
- /// **tagsList** Tag names in this array will be the only tags rendered. By default all tags are rendered.
+ /// **tagsList** Tag names in this array will be the only tags rendered. By default all supported HTML tags are rendered.
///
/// **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.
@@ -53,18 +53,15 @@ class Html extends StatelessWidget {
required this.data,
this.onLinkTap,
this.onAnchorTap,
- this.customRender = const {},
- this.customImageRenders = const {},
+ this.customRenders = const {},
this.onCssParseError,
this.onImageError,
- this.onMathError,
this.shrinkWrap = false,
this.onImageTap,
this.tagsList = const [],
this.style = const {},
- this.navigationDelegateForIframe,
- }) : document = null,
- assert(data != null),
+ }) : document = null,
+ assert (data != null),
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -74,17 +71,14 @@ class Html extends StatelessWidget {
@required this.document,
this.onLinkTap,
this.onAnchorTap,
- this.customRender = const {},
- this.customImageRenders = const {},
+ this.customRenders = const {},
this.onCssParseError,
this.onImageError,
- this.onMathError,
this.shrinkWrap = false,
this.onImageTap,
this.tagsList = const [],
this.style = const {},
- this.navigationDelegateForIframe,
- }) : data = null,
+ }) : data = null,
assert(document != null),
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -105,20 +99,12 @@ class Html extends StatelessWidget {
/// the default anchor behaviour is overwritten.
final OnTap? onAnchorTap;
- /// An API that allows you to customize the entire process of image rendering.
- /// See the README for more details.
- final Map customImageRenders;
-
/// A function that defines what to do when CSS fails to parse
final OnCssParseError? onCssParseError;
/// A function that defines what to do when an image errors
final ImageErrorListener? onImageError;
- /// A function that defines what to do when either or fails to render
- /// You can return a widget here to override the default error widget.
- final OnMathError? onMathError;
-
/// A parameter that should be set when the HTML widget is expected to be
/// flexible
final bool shrinkWrap;
@@ -126,60 +112,63 @@ class Html extends StatelessWidget {
/// A function that defines what to do when an image is tapped
final OnTap? onImageTap;
- /// A list of HTML tags that defines what elements are not rendered
+ /// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered.
final List tagsList;
/// Either return a custom widget for specific node types or return null to
/// fallback to the default rendering.
- final Map customRender;
+ final Map customRenders;
/// An API that allows you to override the default style for any HTML element
final Map style;
- /// 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;
-
static List get tags => new List.from(STYLED_ELEMENTS)
..addAll(INTERACTABLE_ELEMENTS)
..addAll(REPLACED_ELEMENTS)
..addAll(LAYOUT_ELEMENTS)
..addAll(TABLE_CELL_ELEMENTS)
- ..addAll(TABLE_DEFINITION_ELEMENTS);
+ ..addAll(TABLE_DEFINITION_ELEMENTS)
+ ..addAll(EXTERNAL_ELEMENTS);
@override
- Widget build(BuildContext context) {
- final dom.Document doc =
- data != null ? HtmlParser.parseHTML(data!) : document!;
- final double? width = shrinkWrap ? null : MediaQuery.of(context).size.width;
+ State createState() => _HtmlState();
+}
+
+class _HtmlState extends State {
+ late final dom.Document doc;
+
+ @override
+ void initState() {
+ super.initState();
+ doc =
+ widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.document!;
+ }
+ @override
+ Widget build(BuildContext context) {
return Container(
- width: width,
+ width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
child: HtmlParser(
- key: _anchorKey,
+ key: widget._anchorKey,
htmlData: doc,
- onLinkTap: onLinkTap,
- onAnchorTap: onAnchorTap,
- onImageTap: onImageTap,
- onCssParseError: onCssParseError,
- onImageError: onImageError,
- onMathError: onMathError,
- shrinkWrap: shrinkWrap,
+ onLinkTap: widget.onLinkTap,
+ onAnchorTap: widget.onAnchorTap,
+ onImageTap: widget.onImageTap,
+ onCssParseError: widget.onCssParseError,
+ onImageError: widget.onImageError,
+ shrinkWrap: widget.shrinkWrap,
selectable: false,
- style: style,
- customRender: customRender,
- imageRenders: {}
- ..addAll(customImageRenders)
- ..addAll(defaultImageRenders),
- tagsList: tagsList.isEmpty ? Html.tags : tagsList,
- navigationDelegateForIframe: navigationDelegateForIframe,
+ style: widget.style,
+ customRenders: {}
+ ..addAll(widget.customRenders)
+ ..addAll(defaultRenders),
+ tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList,
),
);
}
}
-class SelectableHtml extends StatelessWidget {
+class SelectableHtml extends StatefulWidget {
/// The `SelectableHtml` widget takes HTML as input and displays a RichText
/// tree of the parsed HTML content (which is selectable)
///
@@ -193,7 +182,7 @@ class SelectableHtml extends StatelessWidget {
/// **onAnchorTap** This function is called whenever an anchor (#anchor-id)
/// is tapped.
///
- /// **tagsList** Tag names in this array will be the only tags rendered. By default all tags that support selectable content are rendered.
+ /// **tagsList** Tag names in this array will be the only tags rendered. By default, all tags that support selectable content are rendered.
///
/// **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.
@@ -220,8 +209,10 @@ class SelectableHtml extends StatelessWidget {
this.onCssParseError,
this.shrinkWrap = false,
this.style = const {},
+ this.customRenders = const {},
this.tagsList = const [],
- this.selectionControls
+ this.selectionControls,
+ this.scrollPhysics,
}) : document = null,
assert(data != null),
_anchorKey = anchorKey ?? GlobalKey(),
@@ -236,8 +227,10 @@ class SelectableHtml extends StatelessWidget {
this.onCssParseError,
this.shrinkWrap = false,
this.style = const {},
+ this.customRenders = const {},
this.tagsList = const [],
- this.selectionControls
+ this.selectionControls,
+ this.scrollPhysics,
}) : data = null,
assert(document != null),
_anchorKey = anchorKey ?? GlobalKey(),
@@ -266,7 +259,7 @@ class SelectableHtml extends StatelessWidget {
/// flexible
final bool shrinkWrap;
- /// A list of HTML tags that defines what elements are not rendered
+ /// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered.
final List tagsList;
/// An API that allows you to override the default style for any HTML element
@@ -276,32 +269,50 @@ class SelectableHtml extends StatelessWidget {
/// options
final TextSelectionControls? selectionControls;
+ /// Allows you to override the default scrollPhysics for [SelectableText.rich]
+ final ScrollPhysics? scrollPhysics;
+
+ /// Either return a custom widget for specific node types or return null to
+ /// fallback to the default rendering.
+ final Map customRenders;
+
static List get tags => new List.from(SELECTABLE_ELEMENTS);
@override
- Widget build(BuildContext context) {
- final dom.Document doc = data != null ? HtmlParser.parseHTML(data!) : document!;
- final double? width = shrinkWrap ? null : MediaQuery.of(context).size.width;
+ State createState() => _SelectableHtmlState();
+}
+
+class _SelectableHtmlState extends State {
+ late final dom.Document doc;
+ @override
+ void initState() {
+ super.initState();
+ doc =
+ widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.document!;
+ }
+
+ @override
+ Widget build(BuildContext context) {
return Container(
- width: width,
+ width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
child: HtmlParser(
- key: _anchorKey,
+ key: widget._anchorKey,
htmlData: doc,
- onLinkTap: onLinkTap,
- onAnchorTap: onAnchorTap,
+ onLinkTap: widget.onLinkTap,
+ onAnchorTap: widget.onAnchorTap,
onImageTap: null,
- onCssParseError: onCssParseError,
+ onCssParseError: widget.onCssParseError,
onImageError: null,
- onMathError: null,
- shrinkWrap: shrinkWrap,
+ shrinkWrap: widget.shrinkWrap,
selectable: true,
- style: style,
- customRender: {},
- imageRenders: defaultImageRenders,
- tagsList: tagsList.isEmpty ? SelectableHtml.tags : tagsList,
- navigationDelegateForIframe: null,
- selectionControls: selectionControls,
+ style: widget.style,
+ customRenders: {}
+ ..addAll(widget.customRenders)
+ ..addAll(defaultRenders),
+ tagsList: widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList,
+ selectionControls: widget.selectionControls,
+ scrollPhysics: widget.scrollPhysics,
),
);
}
diff --git a/lib/html_parser.dart b/lib/html_parser.dart
index 9818157fd2..fdfcf4cbd1 100644
--- a/lib/html_parser.dart
+++ b/lib/html_parser.dart
@@ -1,24 +1,16 @@
import 'dart:collection';
import 'dart:math';
-import 'package:collection/collection.dart';
import 'package:csslib/parser.dart' as cssparser;
import 'package:csslib/visitor.dart' as css;
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
import 'package:flutter_html/flutter_html.dart';
-import 'package:flutter_html/image_render.dart';
-import 'package:flutter_html/src/anchor.dart';
import 'package:flutter_html/src/css_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
-import 'package:flutter_html/src/layout_element.dart';
import 'package:flutter_html/src/utils.dart';
-import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as htmlparser;
import 'package:numerus/numerus.dart';
-import 'package:webview_flutter/webview_flutter.dart';
typedef OnTap = void Function(
String? url,
@@ -26,19 +18,10 @@ typedef OnTap = void Function(
Map attributes,
dom.Element? element,
);
-typedef OnMathError = Widget Function(
- String parsedTex,
- String exception,
- String exceptionWithType,
-);
typedef OnCssParseError = String? Function(
String css,
List errors,
);
-typedef CustomRender = dynamic Function(
- RenderContext context,
- Widget parsedChild,
-);
class HtmlParser extends StatelessWidget {
final Key? key;
@@ -48,17 +31,18 @@ class HtmlParser extends StatelessWidget {
final OnTap? onImageTap;
final OnCssParseError? onCssParseError;
final ImageErrorListener? onImageError;
- final OnMathError? onMathError;
final bool shrinkWrap;
final bool selectable;
final Map style;
- final Map customRender;
- final Map imageRenders;
+ final Map customRenders;
final List tagsList;
- final NavigationDelegate? navigationDelegateForIframe;
- final OnTap? _onAnchorTap;
+ final OnTap? internalOnAnchorTap;
+ final Html? root;
final TextSelectionControls? selectionControls;
+ final ScrollPhysics? scrollPhysics;
+
+ final Map cachedImageSizes = {};
HtmlParser({
required this.key,
@@ -68,16 +52,15 @@ class HtmlParser extends StatelessWidget {
required this.onImageTap,
required this.onCssParseError,
required this.onImageError,
- required this.onMathError,
required this.shrinkWrap,
required this.selectable,
required this.style,
- required this.customRender,
- required this.imageRenders,
+ required this.customRenders,
required this.tagsList,
- required this.navigationDelegateForIframe,
- this.selectionControls
- }) : this._onAnchorTap = onAnchorTap != null
+ this.root,
+ this.selectionControls,
+ this.scrollPhysics,
+ }) : this.internalOnAnchorTap = onAnchorTap != null
? onAnchorTap
: key != null
? _handleAnchorTap(key, onLinkTap)
@@ -89,10 +72,10 @@ class HtmlParser extends StatelessWidget {
Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError);
StyledElement lexedTree = lexDomTree(
htmlData,
- customRender.keys.toList(),
+ customRenders.keys.toList(),
tagsList,
- navigationDelegateForIframe,
context,
+ this,
);
StyledElement? externalCssStyledTree;
if (declarations.isNotEmpty) {
@@ -128,6 +111,7 @@ class HtmlParser extends StatelessWidget {
style: cleanedTree.style,
),
selectionControls: selectionControls,
+ scrollPhysics: scrollPhysics,
);
}
return StyledText(
@@ -156,10 +140,10 @@ class HtmlParser extends StatelessWidget {
/// [lexDomTree] converts a DOM document to a simplified tree of [StyledElement]s.
static StyledElement lexDomTree(
dom.Document html,
- List customRenderTags,
+ List customRenderMatchers,
List tagsList,
- NavigationDelegate? navigationDelegateForIframe,
BuildContext context,
+ HtmlParser parser,
) {
StyledElement tree = StyledElement(
name: "[Tree Root]",
@@ -171,9 +155,10 @@ class HtmlParser extends StatelessWidget {
html.nodes.forEach((node) {
tree.children.add(_recursiveLexer(
node,
- customRenderTags,
+ customRenderMatchers,
tagsList,
- navigationDelegateForIframe,
+ context,
+ parser,
));
});
@@ -186,18 +171,20 @@ class HtmlParser extends StatelessWidget {
/// element and returns a [StyledElement] tree representing the element.
static StyledElement _recursiveLexer(
dom.Node node,
- List customRenderTags,
+ List customRenderMatchers,
List tagsList,
- NavigationDelegate? navigationDelegateForIframe,
+ BuildContext context,
+ HtmlParser parser,
) {
List children = [];
node.nodes.forEach((childNode) {
children.add(_recursiveLexer(
childNode,
- customRenderTags,
+ customRenderMatchers,
tagsList,
- navigationDelegateForIframe,
+ context,
+ parser,
));
});
@@ -211,16 +198,27 @@ class HtmlParser extends StatelessWidget {
} else if (INTERACTABLE_ELEMENTS.contains(node.localName)) {
return parseInteractableElement(node, children);
} else if (REPLACED_ELEMENTS.contains(node.localName)) {
- return parseReplacedElement(node, navigationDelegateForIframe);
+ return parseReplacedElement(node, children);
} else if (LAYOUT_ELEMENTS.contains(node.localName)) {
return parseLayoutElement(node, children);
} else if (TABLE_CELL_ELEMENTS.contains(node.localName)) {
return parseTableCellElement(node, children);
} else if (TABLE_DEFINITION_ELEMENTS.contains(node.localName)) {
return parseTableDefinitionElement(node, children);
- } else if (customRenderTags.contains(node.localName)) {
- return parseStyledElement(node, children);
} else {
+ final StyledElement tree = parseStyledElement(node, children);
+ for (final entry in customRenderMatchers) {
+ if (entry.call(
+ RenderContext(
+ buildContext: context,
+ parser: parser,
+ tree: tree,
+ style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
+ ),
+ )) {
+ return tree;
+ }
+ }
return EmptyContentElement();
}
} else if (node is dom.Text) {
@@ -273,9 +271,11 @@ class HtmlParser extends StatelessWidget {
/// widget onto the [StyledElement] tree, no cascading of styles is done at this point.
static StyledElement _applyCustomStyles(Map style, StyledElement tree) {
style.forEach((key, style) {
- if (tree.matchesSelector(key)) {
- tree.style = tree.style.merge(style);
- }
+ try {
+ if (tree.matchesSelector(key)) {
+ tree.style = tree.style.merge(style);
+ }
+ } catch (_) {}
});
tree.children.forEach((e) => _applyCustomStyles(style, e));
@@ -309,7 +309,7 @@ class HtmlParser extends StatelessWidget {
/// [parseTree] converts a tree of [StyledElement]s to an [InlineSpan] tree.
///
- /// [parseTree] is responsible for handling the [customRender] parameter and
+ /// [parseTree] is responsible for handling the [customRenders] parameter and
/// deciding what different `Style.display` options look like as Widgets.
InlineSpan parseTree(RenderContext context, StyledElement tree) {
// Merge this element's style into the context so that children
@@ -319,240 +319,33 @@ class HtmlParser extends StatelessWidget {
parser: this,
tree: tree,
style: context.style.copyOnlyInherited(tree.style),
+ key: AnchorKey.of(key, tree),
);
- if (customRender.containsKey(tree.name)) {
- final render = customRender[tree.name]!.call(
- newContext,
- ContainerSpan(
- key: AnchorKey.of(key, tree),
- newContext: newContext,
- style: tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- children: tree.children.map((tree) => parseTree(newContext, tree)).toList(),
- ),
- );
- if (render != null) {
- assert(render is InlineSpan || render is Widget);
- return render is InlineSpan
- ? render
- : WidgetSpan(
- child: ContainerSpan(
- key: AnchorKey.of(key, tree),
- newContext: newContext,
- style: tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- child: render,
- ),
- );
- }
- }
-
- //Return the correct InlineSpan based on the element type.
- if (tree.style.display == Display.BLOCK &&
- (tree.children.isNotEmpty || tree.element?.localName == "hr")) {
- if (newContext.parser.selectable) {
- return TextSpan(
- style: newContext.style.generateTextStyle(),
- children: tree.children
- .expandIndexed((i, childTree) => [
- if (childTree.style.display == Display.BLOCK &&
- i > 0 &&
- tree.children[i - 1] is ReplacedElement)
- TextSpan(text: "\n"),
- parseTree(newContext, childTree),
- if (i != tree.children.length - 1 &&
- childTree.style.display == Display.BLOCK &&
- childTree.element?.localName != "html" &&
- childTree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
- );
- }
- return WidgetSpan(
- child: ContainerSpan(
- key: AnchorKey.of(key, tree),
- newContext: newContext,
- style: tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- children: tree.children
- .expandIndexed((i, childTree) => [
- if (shrinkWrap &&
- childTree.style.display == Display.BLOCK &&
- i > 0 &&
- tree.children[i - 1] is ReplacedElement)
- TextSpan(text: "\n"),
- parseTree(newContext, childTree),
- if (shrinkWrap &&
- i != tree.children.length - 1 &&
- childTree.style.display == Display.BLOCK &&
- childTree.element?.localName != "html" &&
- childTree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
- ),
- );
- } else if (tree.style.display == Display.LIST_ITEM) {
- List getChildren(StyledElement tree) {
- InlineSpan tabSpan = WidgetSpan(child: Text("\t", textAlign: TextAlign.right));
- List children = tree.children.map((tree) => parseTree(newContext, tree)).toList();
- if (tree.style.listStylePosition == ListStylePosition.INSIDE) {
- children.insert(0, tabSpan);
+ for (final entry in customRenders.keys) {
+ if (entry.call(newContext)) {
+ final buildChildren = () => tree.children.map((tree) => parseTree(newContext, tree)).toList();
+ if (newContext.parser.selectable && customRenders[entry] is SelectableCustomRender) {
+ final selectableBuildChildren = () => tree.children.map((tree) => parseTree(newContext, tree) as TextSpan).toList();
+ return (customRenders[entry] as SelectableCustomRender).textSpan.call(newContext, selectableBuildChildren);
+ }
+ if (newContext.parser.selectable) {
+ return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren) as TextSpan;
+ }
+ if (customRenders[entry]?.inlineSpan != null) {
+ return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren);
}
- return children;
- }
-
- return WidgetSpan(
- child: ContainerSpan(
- key: AnchorKey.of(key, tree),
- newContext: newContext,
- style: tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- textDirection: tree.style.direction,
- children: [
- tree.style.listStylePosition == ListStylePosition.OUTSIDE ?
- Padding(
- padding: tree.style.padding ?? EdgeInsets.only(left: tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style.direction == TextDirection.rtl ? 10.0 : 0.0),
- child: Text(
- "${newContext.style.markerContent}",
- textAlign: TextAlign.right,
- style: newContext.style.generateTextStyle()
- ),
- ) : Container(height: 0, width: 0),
- Text("\t", textAlign: TextAlign.right),
- Expanded(
- child: Padding(
- padding: tree.style.listStylePosition == ListStylePosition.INSIDE ?
- EdgeInsets.only(left: tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style.direction == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero,
- child: StyledText(
- textSpan: TextSpan(
- text: (tree.style.listStylePosition ==
- ListStylePosition.INSIDE)
- ? '${newContext.style.markerContent}'
- : null,
- children: getChildren(tree),
- style: newContext.style.generateTextStyle(),
- ),
- style: newContext.style,
- renderContext: context,
- )
- )
- )
- ],
- ),
- ),
- );
- } else if (tree is ReplacedElement) {
- if (tree is TextContentElement) {
- return TextSpan(text: tree.text);
- } else {
return WidgetSpan(
- alignment: tree.alignment,
- baseline: TextBaseline.alphabetic,
- child: tree.toWidget(context)!,
+ child: ContainerSpan(
+ newContext: newContext,
+ style: tree.style,
+ shrinkWrap: newContext.parser.shrinkWrap,
+ child: customRenders[entry]!.widget!.call(newContext, buildChildren),
+ ),
);
}
- } else if (tree is InteractableElement) {
- InlineSpan addTaps(InlineSpan childSpan, TextStyle childStyle) {
- if (childSpan is TextSpan) {
- return TextSpan(
- mouseCursor: SystemMouseCursors.click,
- text: childSpan.text,
- children: childSpan.children
- ?.map((e) => addTaps(e, childStyle.merge(childSpan.style)))
- .toList(),
- style: newContext.style.generateTextStyle().merge(
- childSpan.style == null
- ? childStyle
- : childStyle.merge(childSpan.style)),
- semanticsLabel: childSpan.semanticsLabel,
- recognizer: TapGestureRecognizer()
- ..onTap =
- _onAnchorTap != null ? () => _onAnchorTap!(tree.href, context, tree.attributes, tree.element) : null,
- );
- } else {
- return WidgetSpan(
- child: MouseRegion(
- key: AnchorKey.of(key, tree),
- cursor: SystemMouseCursors.click,
- child: MultipleTapGestureDetector(
- onTap: _onAnchorTap != null
- ? () => _onAnchorTap!(tree.href, context, tree.attributes, tree.element)
- : null,
- child: GestureDetector(
- key: AnchorKey.of(key, tree),
- onTap: _onAnchorTap != null
- ? () => _onAnchorTap!(tree.href, context, tree.attributes, tree.element)
- : null,
- child: (childSpan as WidgetSpan).child,
- ),
- ),
- ),
- );
- }
- }
-
- return TextSpan(
- mouseCursor: SystemMouseCursors.click,
- children: tree.children
- .map((tree) => parseTree(newContext, tree))
- .map((childSpan) {
- return addTaps(childSpan,
- newContext.style.generateTextStyle().merge(childSpan.style));
- }).toList(),
- );
- } else if (tree is LayoutElement) {
- return WidgetSpan(
- child: tree.toWidget(context)!,
- );
- } else if (tree.style.verticalAlign != null &&
- tree.style.verticalAlign != VerticalAlign.BASELINE) {
- late double verticalOffset;
- switch (tree.style.verticalAlign) {
- case VerticalAlign.SUB:
- verticalOffset = tree.style.fontSize!.size! / 2.5;
- break;
- case VerticalAlign.SUPER:
- verticalOffset = tree.style.fontSize!.size! / -2.5;
- break;
- default:
- break;
- }
- //Requires special layout features not available in the TextStyle API.
- return WidgetSpan(
- child: Transform.translate(
- key: AnchorKey.of(key, tree),
- offset: Offset(0, verticalOffset),
- child: StyledText(
- textSpan: TextSpan(
- style: newContext.style.generateTextStyle(),
- children: tree.children.map((tree) => parseTree(newContext, tree)).toList(),
- ),
- style: newContext.style,
- renderContext: newContext,
- ),
- ),
- );
- } else {
- ///[tree] is an inline element.
- return TextSpan(
- style: newContext.style.generateTextStyle(),
- children: tree.children
- .expand((tree) => [
- parseTree(newContext, tree),
- if (tree.style.display == Display.BLOCK &&
- tree.element?.localName != "html" &&
- tree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
- );
}
+ return WidgetSpan(child: Container(height: 0, width: 0));
}
static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) =>
@@ -706,7 +499,10 @@ class HtmlParser extends StatelessWidget {
/// bullet all list items according to the [ListStyleType] they have been given.
static StyledElement _processListCharactersRecursive(
StyledElement tree, ListQueue olStack) {
- if (tree.name == 'ol' && tree.style.listStyleType != null) {
+ if (tree.style.listStylePosition == null) {
+ tree.style.listStylePosition = ListStylePosition.OUTSIDE;
+ }
+ if (tree.name == 'ol' && tree.style.listStyleType != null && tree.style.listStyleType!.type == "marker") {
switch (tree.style.listStyleType!) {
case ListStyleType.LOWER_LATIN:
case ListStyleType.LOWER_ALPHA:
@@ -726,23 +522,30 @@ class HtmlParser extends StatelessWidget {
olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
break;
}
+ } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "widget") {
+ tree.style.markerContent = tree.style.listStyleType!.widget!;
+ } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "image") {
+ tree.style.markerContent = Image.network(tree.style.listStyleType!.text);
} else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) {
+ String marker = "";
switch (tree.style.listStyleType!) {
+ case ListStyleType.NONE:
+ break;
case ListStyleType.CIRCLE:
- tree.style.markerContent = '○';
+ marker = '○';
break;
case ListStyleType.SQUARE:
- tree.style.markerContent = '■';
+ marker = '■';
break;
case ListStyleType.DISC:
- tree.style.markerContent = '•';
+ marker = '•';
break;
case ListStyleType.DECIMAL:
if (olStack.isEmpty) {
olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
}
olStack.last.data += 1;
- tree.style.markerContent = '${olStack.last.data}.';
+ marker = '${olStack.last.data}.';
break;
case ListStyleType.LOWER_LATIN:
case ListStyleType.LOWER_ALPHA:
@@ -757,7 +560,7 @@ class HtmlParser extends StatelessWidget {
}
}
}
- tree.style.markerContent = olStack.last.data.toString() + ".";
+ marker = olStack.last.data.toString() + ".";
olStack.last.data = olStack.last.data.toString().nextLetter();
break;
case ListStyleType.UPPER_LATIN:
@@ -773,7 +576,7 @@ class HtmlParser extends StatelessWidget {
}
}
}
- tree.style.markerContent = olStack.last.data.toString().toUpperCase() + ".";
+ marker = olStack.last.data.toString().toUpperCase() + ".";
olStack.last.data = olStack.last.data.toString().nextLetter();
break;
case ListStyleType.LOWER_ROMAN:
@@ -782,9 +585,9 @@ class HtmlParser extends StatelessWidget {
}
olStack.last.data += 1;
if (olStack.last.data <= 0) {
- tree.style.markerContent = '${olStack.last.data}.';
+ marker = '${olStack.last.data}.';
} else {
- tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
+ marker = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
}
break;
case ListStyleType.UPPER_ROMAN:
@@ -793,12 +596,16 @@ class HtmlParser extends StatelessWidget {
}
olStack.last.data += 1;
if (olStack.last.data <= 0) {
- tree.style.markerContent = '${olStack.last.data}.';
+ marker = '${olStack.last.data}.';
} else {
- tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()! + ".";
+ marker = (olStack.last.data as int).toRomanNumeralString()! + ".";
}
break;
}
+ tree.style.markerContent = Text(
+ marker,
+ textAlign: TextAlign.right,
+ );
}
tree.children.forEach((e) => _processListCharactersRecursive(e, olStack));
@@ -944,13 +751,17 @@ class HtmlParser extends StatelessWidget {
if (child is EmptyContentElement || child is EmptyLayoutElement) {
toRemove.add(child);
} else if (child is TextContentElement
- && child.text!.trim().isEmpty
+ && (tree.name == "body" || tree.name == "ul")
+ && child.text!.replaceAll(' ', '').isEmpty) {
+ toRemove.add(child);
+ } else if (child is TextContentElement
+ && child.text!.isEmpty
&& child.style.whiteSpace != WhiteSpace.PRE) {
toRemove.add(child);
} else if (child is TextContentElement &&
child.style.whiteSpace != WhiteSpace.PRE &&
tree.style.display == Display.BLOCK &&
- child.text!.trim().isEmpty &&
+ child.text!.isEmpty &&
lastChildBlock) {
toRemove.add(child);
} else if (child.style.display == Display.NONE) {
@@ -995,12 +806,14 @@ class RenderContext {
final HtmlParser parser;
final StyledElement tree;
final Style style;
+ final AnchorKey? key;
RenderContext({
required this.buildContext,
required this.parser,
required this.tree,
required this.style,
+ this.key,
});
}
@@ -1034,8 +847,8 @@ class ContainerSpan extends StatelessWidget {
),
height: style.height,
width: style.width,
- padding: style.padding,
- margin: style.margin,
+ padding: style.padding?.nonNegative,
+ margin: style.margin?.nonNegative,
alignment: shrinkWrap ? null : style.alignment,
child: child ??
StyledText(
@@ -1058,6 +871,7 @@ class StyledText extends StatelessWidget {
final AnchorKey? key;
final bool _selectable;
final TextSelectionControls? selectionControls;
+ final ScrollPhysics? scrollPhysics;
const StyledText({
required this.textSpan,
@@ -1066,6 +880,7 @@ class StyledText extends StatelessWidget {
required this.renderContext,
this.key,
this.selectionControls,
+ this.scrollPhysics,
}) : _selectable = false,
super(key: key);
@@ -1075,7 +890,8 @@ class StyledText extends StatelessWidget {
this.textScaleFactor = 1.0,
required this.renderContext,
this.key,
- this.selectionControls
+ this.selectionControls,
+ this.scrollPhysics,
}) : textSpan = textSpan,
_selectable = true,
super(key: key);
@@ -1091,6 +907,7 @@ class StyledText extends StatelessWidget {
textScaleFactor: textScaleFactor,
maxLines: style.maxLines,
selectionControls: selectionControls,
+ scrollPhysics: scrollPhysics,
);
}
return SizedBox(
diff --git a/lib/image_render.dart b/lib/image_render.dart
deleted file mode 100644
index 1840764b37..0000000000
--- a/lib/image_render.dart
+++ /dev/null
@@ -1,255 +0,0 @@
-import 'dart:async';
-import 'dart:convert';
-
-import 'package:flutter/material.dart';
-import 'package:flutter_html/html_parser.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:flutter_svg/parser.dart';
-import 'package:html/dom.dart' as dom;
-
-typedef ImageSourceMatcher = bool Function(
- Map attributes,
- dom.Element? element,
-);
-
-final _dataUriFormat = RegExp(
- "^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)");
-
-ImageSourceMatcher dataUriMatcher(
- {String? encoding = 'base64', String? mime}) =>
- (attributes, element) {
- if (_src(attributes) == null) return false;
- final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
- return dataUri != null &&
- (mime == null || dataUri.namedGroup('mime') == mime) &&
- (encoding == null || dataUri.namedGroup('encoding') == ';$encoding');
- };
-
-ImageSourceMatcher networkSourceMatcher({
- List schemas: const ["https", "http"],
- List? domains,
- String? extension,
-}) =>
- (attributes, element) {
- if (_src(attributes) == null) return false;
- try {
- final src = Uri.parse(_src(attributes)!);
- return schemas.contains(src.scheme) &&
- (domains == null || domains.contains(src.host)) &&
- (extension == null || src.path.endsWith(".$extension"));
- } catch (e) {
- return false;
- }
- };
-
-ImageSourceMatcher assetUriMatcher() => (attributes, element) =>
- _src(attributes) != null && _src(attributes)!.startsWith("asset:");
-
-typedef ImageRender = Widget? Function(
- RenderContext context,
- Map attributes,
- dom.Element? element,
-);
-
-ImageRender base64ImageRender() => (context, attributes, element) {
- final decodedImage =
- base64.decode(_src(attributes)!.split("base64,")[1].trim());
- precacheImage(
- MemoryImage(decodedImage),
- context.buildContext,
- onError: (exception, StackTrace? stackTrace) {
- context.parser.onImageError?.call(exception, stackTrace);
- },
- );
- return Image.memory(
- decodedImage,
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return Text(_alt(attributes) ?? "",
- style: context.style.generateTextStyle());
- }
- return child;
- },
- );
- };
-
-ImageRender assetImageRender({
- double? width,
- double? height,
-}) =>
- (context, attributes, element) {
- final assetPath = _src(attributes)!.replaceFirst('asset:', '');
- if (_src(attributes)!.endsWith(".svg")) {
- return SvgPicture.asset(assetPath,
- width: width ?? _width(attributes),
- height: height ?? _height(attributes));
- } else {
- return Image.asset(
- assetPath,
- width: width ?? _width(attributes),
- height: height ?? _height(attributes),
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return Text(_alt(attributes) ?? "",
- style: context.style.generateTextStyle());
- }
- return child;
- },
- );
- }
- };
-
-ImageRender networkImageRender({
- Map? headers,
- String Function(String?)? mapUrl,
- double? width,
- double? height,
- Widget Function(String?)? altWidget,
- Widget Function()? loadingWidget,
-}) =>
- (context, attributes, element) {
- final src = mapUrl?.call(_src(attributes)) ?? _src(attributes)!;
- precacheImage(
- NetworkImage(
- src,
- headers: headers,
- ),
- context.buildContext,
- onError: (exception, StackTrace? stackTrace) {
- context.parser.onImageError?.call(exception, stackTrace);
- },
- );
- Completer completer = Completer();
- Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- if (!completer.isCompleted) {
- completer.completeError("error");
- }
- return child;
- } else {
- return child;
- }
- });
-
- var listener =
- ImageStreamListener((ImageInfo image, bool synchronousCall) {
- var myImage = image.image;
- Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
- if (!completer.isCompleted) {
- completer.complete(size);
- }
- }, onError: (object, stacktrace) {
- if (!completer.isCompleted) {
- completer.completeError(object);
- }
- });
-
- image.image.resolve(ImageConfiguration()).addListener(listener);
- return FutureBuilder(
- future: completer.future,
- builder: (BuildContext buildContext, AsyncSnapshot snapshot) {
- if (completer.isCompleted) {
- image.image.resolve(ImageConfiguration()).removeListener(listener);
- }
- if (snapshot.hasData) {
- return Container(
- constraints: BoxConstraints(
- maxWidth: width ?? _width(attributes) ?? snapshot.data!.width,
- maxHeight:
- (width ?? _width(attributes) ?? snapshot.data!.width) /
- _aspectRatio(attributes, snapshot)),
- child: AspectRatio(
- aspectRatio: _aspectRatio(attributes, snapshot),
- child: Image.network(
- src,
- headers: headers,
- width: width ?? _width(attributes) ?? snapshot.data!.width,
- height: height ?? _height(attributes),
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return altWidget?.call(_alt(attributes)) ??
- Text(_alt(attributes) ?? "",
- style: context.style.generateTextStyle());
- }
- return child;
- },
- ),
- ),
- );
- } else if (snapshot.hasError) {
- return altWidget?.call(_alt(attributes)) ??
- Text(_alt(attributes) ?? "",
- style: context.style.generateTextStyle());
- } else {
- return loadingWidget?.call() ?? const CircularProgressIndicator();
- }
- },
- );
- };
-
-ImageRender svgDataImageRender() => (context, attributes, element) {
- final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
- final data = dataUri?.namedGroup('data');
- if (data == null) return null;
- if (dataUri?.namedGroup('encoding') == ';base64') {
- final decodedImage = base64.decode(data.trim());
- return SvgPicture.memory(
- decodedImage,
- width: _width(attributes),
- height: _height(attributes),
- );
- }
- return SvgPicture.string(Uri.decodeFull(data));
- };
-
-ImageRender svgNetworkImageRender() => (context, attributes, element) {
- return SvgPicture.network(
- attributes["src"]!,
- width: _width(attributes),
- height: _height(attributes),
- );
- };
-
-final Map defaultImageRenders = {
- dataUriMatcher(mime: 'image/svg+xml', encoding: null): svgDataImageRender(),
- dataUriMatcher(): base64ImageRender(),
- assetUriMatcher(): assetImageRender(),
- networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
- networkSourceMatcher(): networkImageRender(),
-};
-
-String? _src(Map attributes) {
- return attributes["src"];
-}
-
-String? _alt(Map attributes) {
- return attributes["alt"];
-}
-
-double? _height(Map attributes) {
- final heightString = attributes["height"];
- return heightString == null
- ? heightString as double?
- : double.tryParse(heightString);
-}
-
-double? _width(Map attributes) {
- final widthString = attributes["width"];
- return widthString == null
- ? widthString as double?
- : double.tryParse(widthString);
-}
-
-double _aspectRatio(
- Map attributes, AsyncSnapshot calculated) {
- final heightString = attributes["height"];
- final widthString = attributes["width"];
- if (heightString != null && widthString != null) {
- final height = double.tryParse(heightString);
- final width = double.tryParse(widthString);
- return height == null || width == null
- ? calculated.data!.aspectRatio
- : width / height;
- }
- return calculated.data!.aspectRatio;
-}
diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart
index 8592a5a998..bdba172c97 100644
--- a/lib/src/anchor.dart
+++ b/lib/src/anchor.dart
@@ -1,4 +1,3 @@
-import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_html/src/styled_element.dart';
diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart
index 261a09ba27..66c622a8cf 100644
--- a/lib/src/css_parser.dart
+++ b/lib/src/css_parser.dart
@@ -3,11 +3,9 @@ import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:csslib/visitor.dart' as css;
import 'package:csslib/parser.dart' as cssparser;
-import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/src/utils.dart';
-import 'package:flutter_html/style.dart';
Style declarationsToStyle(Map> declarations) {
Style style = new Style();
@@ -150,7 +148,7 @@ Style declarationsToStyle(Map> declarations) {
List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
- css.LiteralTerm borderStyle = potentialStyles.first!;
+ css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right ?? BorderSide.none,
@@ -194,6 +192,46 @@ Style declarationsToStyle(Map> declarations) {
case 'font-weight':
style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
break;
+ case 'list-style':
+ css.LiteralTerm? position = value.firstWhereOrNull((e) => e is css.LiteralTerm && (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?;
+ css.UriTerm? image = value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?;
+ css.LiteralTerm? type = value.firstWhereOrNull((e) => e is css.LiteralTerm && e.text != "outside" && e.text != "inside") as css.LiteralTerm?;
+ if (position != null) {
+ switch (position.text) {
+ case 'outside':
+ style.listStylePosition = ListStylePosition.OUTSIDE;
+ break;
+ case 'inside':
+ style.listStylePosition = ListStylePosition.INSIDE;
+ break;
+ }
+ }
+ if (image != null) {
+ style.listStyleType = ExpressionMapping.expressionToListStyleType(image) ?? style.listStyleType;
+ } else if (type != null) {
+ style.listStyleType = ExpressionMapping.expressionToListStyleType(type) ?? style.listStyleType;
+ }
+ break;
+ case 'list-style-image':
+ if (value.first is css.UriTerm) {
+ style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.UriTerm) ?? style.listStyleType;
+ }
+ break;
+ case 'list-style-position':
+ if (value.first is css.LiteralTerm) {
+ switch ((value.first as css.LiteralTerm).text) {
+ case 'outside':
+ style.listStylePosition = ListStylePosition.OUTSIDE;
+ break;
+ case 'inside':
+ style.listStylePosition = ListStylePosition.INSIDE;
+ break;
+ }
+ }
+ break;
+ case 'height':
+ style.height = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.height;
+ break;
case 'list-style-type':
if (value.first is css.LiteralTerm) {
style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType;
@@ -298,6 +336,21 @@ Style declarationsToStyle(Map> declarations) {
case 'text-shadow':
style.textShadow = ExpressionMapping.expressionToTextShadow(value);
break;
+ case 'text-transform':
+ final val = (value.first as css.LiteralTerm).text;
+ if (val == 'uppercase') {
+ style.textTransform = TextTransform.uppercase;
+ } else if (val == 'lowercase') {
+ style.textTransform = TextTransform.lowercase;
+ } else if (val == 'capitalize') {
+ style.textTransform = TextTransform.capitalize;
+ } else {
+ style.textTransform = TextTransform.none;
+ }
+ break;
+ case 'width':
+ style.width = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.width;
+ break;
}
}
});
@@ -665,6 +718,9 @@ class ExpressionMapping {
}
static ListStyleType? expressionToListStyleType(css.LiteralTerm value) {
+ if (value is css.UriTerm) {
+ return ListStyleType.fromImage(value.text);
+ }
switch (value.text) {
case 'disc':
return ListStyleType.DISC;
@@ -686,6 +742,8 @@ class ExpressionMapping {
return ListStyleType.UPPER_LATIN;
case 'upper-roman':
return ListStyleType.UPPER_ROMAN;
+ case 'none':
+ return ListStyleType.NONE;
}
return null;
}
@@ -809,33 +867,40 @@ class ExpressionMapping {
previousIndex = i + 1;
}
for (List list in valueList) {
- css.Expression exp = list[0];
- css.Expression exp2 = list[1];
- css.LiteralTerm? exp3 = list.length > 2 ? list[2] as css.LiteralTerm? : null;
- css.LiteralTerm? exp4 = list.length > 3 ? list[3] as css.LiteralTerm? : null;
+ css.Expression? offsetX;
+ css.Expression? offsetY;
+ css.Expression? blurRadius;
+ css.HexColorTerm? color;
+ int expressionIndex = 0;
+ list.forEach((element) {
+ if (element is css.HexColorTerm) {
+ color = element;
+ } else if (expressionIndex == 0) {
+ offsetX = element;
+ expressionIndex++;
+ } else if (expressionIndex++ == 1) {
+ offsetY = element;
+ expressionIndex++;
+ } else {
+ blurRadius = element;
+ }
+ });
RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+');
- if (exp is css.LiteralTerm && exp2 is css.LiteralTerm) {
- if (exp3 != null && ExpressionMapping.expressionToColor(exp3) != null) {
+ if (offsetX is css.LiteralTerm && offsetY is css.LiteralTerm) {
+ if (color != null && ExpressionMapping.expressionToColor(color) != null) {
shadow.add(Shadow(
- color: expressionToColor(exp3)!,
- offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!)
+ color: expressionToColor(color)!,
+ offset: Offset(
+ double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!,
+ double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!),
+ blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0,
));
- } else if (exp3 != null && exp3 is css.LiteralTerm) {
- if (exp4 != null && ExpressionMapping.expressionToColor(exp4) != null) {
- shadow.add(Shadow(
- color: expressionToColor(exp4)!,
- offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!),
- blurRadius: double.tryParse(exp3.text.replaceAll(nonNumberRegex, ''))!
- ));
- } else {
- shadow.add(Shadow(
- offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!),
- blurRadius: double.tryParse(exp3.text.replaceAll(nonNumberRegex, ''))!
- ));
- }
} else {
shadow.add(Shadow(
- offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, ''))!, double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))!)
+ offset: Offset(
+ double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!,
+ double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!),
+ blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0,
));
}
}
diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart
index 414a52ee4c..4b096d8f5d 100644
--- a/lib/src/html_elements.dart
+++ b/lib/src/html_elements.dart
@@ -22,6 +22,7 @@ const STYLED_ELEMENTS = [
"kbd",
"mark",
"q",
+ "rt",
"s",
"samp",
"small",
@@ -109,22 +110,15 @@ const INTERACTABLE_ELEMENTS = [
];
const REPLACED_ELEMENTS = [
- "audio",
"br",
- "iframe",
- "img",
- "svg",
"template",
- "video",
"rp",
"rt",
"ruby",
- "math",
];
const LAYOUT_ELEMENTS = [
"details",
- "table",
"tr",
"tbody",
"tfoot",
@@ -135,6 +129,8 @@ const TABLE_CELL_ELEMENTS = ["th", "td"];
const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"];
+const EXTERNAL_ELEMENTS = ["audio", "iframe", "img", "math", "svg", "table", "video"];
+
const SELECTABLE_ELEMENTS = [
"br",
"a",
diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart
index 504f185406..2aab878a64 100644
--- a/lib/src/interactable_element.dart
+++ b/lib/src/interactable_element.dart
@@ -22,20 +22,30 @@ enum Gesture {
TAP,
}
-InteractableElement parseInteractableElement(
+StyledElement parseInteractableElement(
dom.Element element, List children) {
switch (element.localName) {
case "a":
- return InteractableElement(
+ if (element.attributes.containsKey('href')) {
+ return InteractableElement(
+ name: element.localName!,
+ children: children,
+ href: element.attributes['href'],
+ style: Style(
+ color: Colors.blue,
+ textDecoration: TextDecoration.underline,
+ ),
+ node: element,
+ elementId: element.id
+ );
+ }
+ // When tag have no href, it must be non clickable and without decoration.
+ return StyledElement(
name: element.localName!,
children: children,
- href: element.attributes['href'],
- style: Style(
- color: Colors.blue,
- textDecoration: TextDecoration.underline,
- ),
+ style: Style(),
node: element,
- elementId: element.id
+ elementId: element.id,
);
/// will never be called, just to suppress missing return warning
default:
@@ -48,4 +58,4 @@ InteractableElement parseInteractableElement(
elementId: "[[No ID]]"
);
}
-}
\ No newline at end of file
+}
diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart
index b677e9d872..33093e7493 100644
--- a/lib/src/layout_element.dart
+++ b/lib/src/layout_element.dart
@@ -1,12 +1,9 @@
-import 'dart:math';
-
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/anchor.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/styled_element.dart';
import 'package:flutter_html/style.dart';
-import 'package:flutter_layout_grid/flutter_layout_grid.dart';
import 'package:html/dom.dart' as dom;
/// A [LayoutElement] is an element that breaks the normal Inline flow of
@@ -22,148 +19,6 @@ abstract class LayoutElement extends StyledElement {
Widget? toWidget(RenderContext context);
}
-class TableLayoutElement extends LayoutElement {
- TableLayoutElement({
- required String name,
- required List children,
- required dom.Element node,
- }) : super(name: name, children: children, node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- return Container(
- key: AnchorKey.of(context.parser.key, this),
- margin: style.margin,
- padding: style.padding,
- decoration: BoxDecoration(
- color: style.backgroundColor,
- border: style.border,
- ),
- width: style.width,
- height: style.height,
- child: LayoutBuilder(builder: (_, constraints) => _layoutCells(context, constraints)),
- );
- }
-
- Widget _layoutCells(RenderContext context, BoxConstraints constraints) {
- final rows = [];
- List columnSizes = [];
- for (var child in children) {
- if (child is TableStyleElement) {
- // Map tags to predetermined column track sizes
- columnSizes = child.children
- .where((c) => c.name == "col")
- .map((c) {
- final span = int.tryParse(c.attributes["span"] ?? "1") ?? 1;
- final colWidth = c.attributes["width"];
- return List.generate(span, (index) {
- if (colWidth != null && colWidth.endsWith("%")) {
- if (!constraints.hasBoundedWidth) {
- // In a horizontally unbounded container; always wrap content instead of applying flex
- return IntrinsicContentTrackSize();
- }
- final percentageSize = double.tryParse(
- colWidth.substring(0, colWidth.length - 1));
- return percentageSize != null && !percentageSize.isNaN
- ? FlexibleTrackSize(percentageSize * 0.01)
- : IntrinsicContentTrackSize();
- } else if (colWidth != null) {
- final fixedPxSize = double.tryParse(colWidth);
- return fixedPxSize != null
- ? FixedTrackSize(fixedPxSize)
- : IntrinsicContentTrackSize();
- } else {
- return IntrinsicContentTrackSize();
- }
- });
- })
- .expand((element) => element)
- .toList(growable: false);
- } else if (child is TableSectionLayoutElement) {
- rows.addAll(child.children.whereType());
- } else if (child is TableRowLayoutElement) {
- rows.add(child);
- }
- }
-
- // All table rows have a height intrinsic to their (spanned) contents
- final rowSizes =
- List.generate(rows.length, (_) => IntrinsicContentTrackSize());
-
- // Calculate column bounds
- int columnMax = rows
- .map((row) => row.children
- .whereType()
- .fold(0, (int value, child) => value + child.colspan))
- .fold(0, max);
-
- // Place the cells in the rows/columns
- final cells = [];
- final columnRowOffset = List.generate(columnMax, (_) => 0);
- int rowi = 0;
- for (var row in rows) {
- int columni = 0;
- for (var child in row.children) {
- if (columni > columnMax - 1 ) {
- break;
- }
- while (columnRowOffset[columni] > 0) {
- columnRowOffset[columni] = columnRowOffset[columni] - 1;
- columni++;
- }
- if (child is TableCellElement) {
- cells.add(GridPlacement(
- child: Container(
- width: double.infinity,
- padding: child.style.padding ?? row.style.padding,
- decoration: BoxDecoration(
- color: child.style.backgroundColor ?? row.style.backgroundColor,
- border: child.style.border ?? row.style.border,
- ),
- child: SizedBox.expand(
- child: Container(
- alignment: child.style.alignment ??
- style.alignment ??
- Alignment.centerLeft,
- child: StyledText(
- textSpan: context.parser.parseTree(context, child),
- style: child.style,
- renderContext: context,
- ),
- ),
- ),
- ),
- columnStart: columni,
- columnSpan: min(child.colspan, columnMax - columni),
- rowStart: rowi,
- rowSpan: min(child.rowspan, rows.length - rowi),
- ));
- columnRowOffset[columni] = child.rowspan - 1;
- columni += child.colspan;
- }
- }
- while (columni < columnRowOffset.length) {
- columnRowOffset[columni] = columnRowOffset[columni] - 1;
- columni++;
- }
- rowi++;
- }
-
- // Create column tracks (insofar there were no colgroups that already defined them)
- List finalColumnSizes = columnSizes.take(columnMax).toList();
- finalColumnSizes += List.generate(
- max(0, columnMax - finalColumnSizes.length),
- (_) => IntrinsicContentTrackSize());
-
- return LayoutGrid(
- gridFit: GridFit.loose,
- columnSizes: finalColumnSizes,
- rowSizes: rowSizes,
- children: cells,
- );
- }
-}
-
class TableSectionLayoutElement extends LayoutElement {
TableSectionLayoutElement({
required String name,
@@ -340,12 +195,6 @@ LayoutElement parseLayoutElement(
children: children,
elementList: element.children
);
- case "table":
- return TableLayoutElement(
- name: element.localName!,
- children: children,
- node: element,
- );
case "thead":
case "tbody":
case "tfoot":
@@ -360,10 +209,6 @@ LayoutElement parseLayoutElement(
node: element,
);
default:
- return TableLayoutElement(
- children: children,
- name: "[[No Name]]",
- node: element
- );
+ return EmptyLayoutElement(name: "[[No Name]]");
}
}
diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart
index 141b242c0d..33e844ee7d 100644
--- a/lib/src/replaced_element.dart
+++ b/lib/src/replaced_element.dart
@@ -1,22 +1,12 @@
import 'dart:math';
-import 'package:chewie/chewie.dart';
-import 'package:chewie_audio/chewie_audio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/anchor.dart';
import 'package:flutter_html/src/html_elements.dart';
-import 'package:flutter_html/src/utils.dart';
-import 'package:flutter_html/src/widgets/iframe_unsupported.dart'
- if (dart.library.io) 'package:flutter_html/src/widgets/iframe_mobile.dart'
- if (dart.library.html) 'package:flutter_html/src/widgets/iframe_web.dart';
import 'package:flutter_html/style.dart';
-import 'package:flutter_math_fork/flutter_math.dart';
-import 'package:flutter_svg/flutter_svg.dart';
import 'package:html/dom.dart' as dom;
-import 'package:video_player/video_player.dart';
-import 'package:webview_flutter/webview_flutter.dart';
/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered.
///
@@ -29,9 +19,10 @@ abstract class ReplacedElement extends StyledElement {
required String name,
required Style style,
required String elementId,
+ List? children,
dom.Element? node,
this.alignment = PlaceholderAlignment.aboveBaseline,
- }) : super(name: name, children: [], style: style, node: node, elementId: elementId);
+ }) : super(name: name, children: children ?? [], style: style, node: node, elementId: elementId);
static List parseMediaSources(List elements) {
return elements
@@ -65,161 +56,6 @@ class TextContentElement extends ReplacedElement {
Widget? toWidget(_) => null;
}
-/// [ImageContentElement] is a [ReplacedElement] with an image as its content.
-/// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img
-class ImageContentElement extends ReplacedElement {
- final String? src;
- final String? alt;
-
- ImageContentElement({
- required String name,
- required this.src,
- required this.alt,
- required dom.Element node,
- }) : super(name: name, style: Style(), node: node, alignment: PlaceholderAlignment.middle, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- for (final entry in context.parser.imageRenders.entries) {
- if (entry.key.call(attributes, element)) {
- final widget = entry.value.call(context, attributes, element);
- return Builder(
- builder: (buildContext) {
- return GestureDetector(
- key: AnchorKey.of(context.parser.key, this),
- child: widget,
- onTap: () {
- if (MultipleTapGestureDetector.of(buildContext) != null) {
- MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
- }
- context.parser.onImageTap?.call(src, context, attributes, element);
- },
- );
- }
- );
- }
- }
- return SizedBox(width: 0, height: 0);
- }
-}
-
-/// [AudioContentElement] is a [ContentElement] with an audio file as its content.
-class AudioContentElement extends ReplacedElement {
- final List src;
- final bool showControls;
- final bool autoplay;
- final bool loop;
- final bool muted;
-
- AudioContentElement({
- required String name,
- required this.src,
- required this.showControls,
- required this.autoplay,
- required this.loop,
- required this.muted,
- required dom.Element node,
- }) : super(name: name, style: Style(), node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- return Container(
- key: AnchorKey.of(context.parser.key, this),
- width: context.style.width ?? 300,
- height: Theme.of(context.buildContext).platform == TargetPlatform.android
- ? 48 : 75,
- child: ChewieAudio(
- controller: ChewieAudioController(
- videoPlayerController: VideoPlayerController.network(
- src.first ?? "",
- ),
- autoPlay: autoplay,
- looping: loop,
- showControls: showControls,
- autoInitialize: true,
- ),
- ),
- );
- }
-}
-
-/// [VideoContentElement] is a [ContentElement] with a video file as its content.
-class VideoContentElement extends ReplacedElement {
- final List src;
- final String? poster;
- final bool showControls;
- final bool autoplay;
- final bool loop;
- final bool muted;
- final double? width;
- final double? height;
-
- VideoContentElement({
- required String name,
- required this.src,
- required this.poster,
- required this.showControls,
- required this.autoplay,
- required this.loop,
- required this.muted,
- required this.width,
- required this.height,
- required dom.Element node,
- }) : super(name: name, style: Style(), node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- final double _width = width ?? (height ?? 150) * 2;
- final double _height = height ?? (width ?? 300) / 2;
- return AspectRatio(
- aspectRatio: _width / _height,
- child: Container(
- key: AnchorKey.of(context.parser.key, this),
- child: Chewie(
- controller: ChewieController(
- videoPlayerController: VideoPlayerController.network(
- src.first ?? "",
- ),
- placeholder: poster != null
- ? Image.network(poster!)
- : Container(color: Colors.black),
- autoPlay: autoplay,
- looping: loop,
- showControls: showControls,
- autoInitialize: true,
- aspectRatio: _width / _height,
- ),
- ),
- ),
- );
- }
-}
-
-/// [SvgContentElement] is a [ReplacedElement] with an SVG as its contents.
-class SvgContentElement extends ReplacedElement {
- final String data;
- final double? width;
- final double? height;
-
- SvgContentElement({
- required String name,
- required this.data,
- required this.width,
- required this.height,
- required dom.Element node,
- }) : super(name: name, style: Style(), node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- return SvgPicture.string(
- data,
- key: AnchorKey.of(context.parser.key, this),
- width: width,
- height: height,
- );
- }
-}
-
class EmptyContentElement extends ReplacedElement {
EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]");
@@ -230,22 +66,24 @@ class EmptyContentElement extends ReplacedElement {
class RubyElement extends ReplacedElement {
dom.Element element;
- RubyElement({required this.element, String name = "ruby"})
- : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(), elementId: element.id);
+ RubyElement({
+ required this.element,
+ required List children,
+ String name = "ruby"
+ }) : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(), elementId: element.id, children: children);
@override
Widget toWidget(RenderContext context) {
- dom.Node? textNode;
+ String? textNode;
List widgets = [];
- //TODO calculate based off of parent font size.
final rubySize = max(9.0, context.style.fontSize!.size! / 2);
final rubyYPos = rubySize + rubySize / 2;
- element.nodes.forEach((c) {
- if (c.nodeType == dom.Node.TEXT_NODE) {
- textNode = c;
+ context.tree.children.forEach((c) {
+ if (c is TextContentElement) {
+ textNode = c.text;
}
- if (c is dom.Element) {
- if (c.localName == "rt" && textNode != null) {
+ if (!(c is TextContentElement)) {
+ if (c.name == "rt" && textNode != null) {
final widget = Stack(
alignment: Alignment.center,
children: [
@@ -255,12 +93,23 @@ class RubyElement extends ReplacedElement {
child: Transform(
transform:
Matrix4.translationValues(0, -(rubyYPos), 0),
- child: Text(c.innerHtml,
- style: context.style
- .generateTextStyle()
- .copyWith(fontSize: rubySize))))),
- Container(
- child: Text(textNode!.text!.trim(),
+ child: ContainerSpan(
+ newContext: RenderContext(
+ buildContext: context.buildContext,
+ parser: context.parser,
+ style: c.style,
+ tree: c,
+ ),
+ style: c.style,
+ child: Text(c.element!.innerHtml,
+ style: c.style
+ .generateTextStyle()
+ .copyWith(fontSize: rubySize)),
+ )))),
+ ContainerSpan(
+ newContext: context,
+ style: context.style,
+ child: Text(textNode!.trim(),
style: context.style.generateTextStyle())),
],
);
@@ -278,109 +127,11 @@ class RubyElement extends ReplacedElement {
}
}
-class MathElement extends ReplacedElement {
- dom.Element element;
- String? texStr;
-
- MathElement({
- required this.element,
- this.texStr,
- String name = "math",
- }) : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(display: Display.BLOCK), elementId: element.id);
-
- @override
- Widget toWidget(RenderContext context) {
- texStr = parseMathRecursive(element, r'');
- return Container(
- width: context.parser.shrinkWrap ? null : MediaQuery.of(context.buildContext).size.width,
- child: Math.tex(
- texStr ?? '',
- mathStyle: MathStyle.display,
- textStyle: context.style.generateTextStyle(),
- onErrorFallback: (FlutterMathException e) {
- if (context.parser.onMathError != null) {
- return context.parser.onMathError!.call(texStr ?? '', e.message, e.messageWithType);
- } else {
- return Text(e.message);
- }
- },
- )
- );
- }
-
- String parseMathRecursive(dom.Node node, String parsed) {
- if (node is dom.Element) {
- List nodeList = node.nodes.whereType().toList();
- if (node.localName == "math" || node.localName == "mrow") {
- nodeList.forEach((element) {
- parsed = parseMathRecursive(element, parsed);
- });
- }
- // note: munder, mover, and munderover do not support placing braces and other
- // markings above/below elements, instead they are treated as super/subscripts for now.
- if ((node.localName == "msup" || node.localName == "msub"
- || node.localName == "munder" || node.localName == "mover") && nodeList.length == 2) {
- parsed = parseMathRecursive(nodeList[0], parsed);
- parsed = parseMathRecursive(nodeList[1],
- parsed + "${node.localName == "msup" || node.localName == "mover" ? "^" : "_"}{") + "}";
- }
- if ((node.localName == "msubsup" || node.localName == "munderover") && nodeList.length == 3) {
- parsed = parseMathRecursive(nodeList[0], parsed);
- parsed = parseMathRecursive(nodeList[1], parsed + "_{") + "}";
- parsed = parseMathRecursive(nodeList[2], parsed + "^{") + "}";
- }
- if (node.localName == "mfrac" && nodeList.length == 2) {
- parsed = parseMathRecursive(nodeList[0], parsed + r"\frac{") + "}";
- parsed = parseMathRecursive(nodeList[1], parsed + "{") + "}";
- }
- // note: doesn't support answer & intermediate steps
- if (node.localName == "mlongdiv" && nodeList.length == 4) {
- parsed = parseMathRecursive(nodeList[0], parsed);
- parsed = parseMathRecursive(nodeList[2], parsed + r"\overline{)") + "}";
- }
- if (node.localName == "msqrt" && nodeList.length == 1) {
- parsed = parseMathRecursive(nodeList[0], parsed + r"\sqrt{") + "}";
- }
- if (node.localName == "mroot" && nodeList.length == 2) {
- parsed = parseMathRecursive(nodeList[1], parsed + r"\sqrt[") + "]";
- parsed = parseMathRecursive(nodeList[0], parsed + "{") + "}";
- }
- if (node.localName == "mi" || node.localName == "mn" || node.localName == "mo") {
- if (mathML2Tex.keys.contains(node.text.trim())) {
- parsed = parsed + mathML2Tex[mathML2Tex.keys.firstWhere((e) => e == node.text.trim())]!;
- } else if (node.text.startsWith("&") && node.text.endsWith(";")) {
- parsed = parsed + node.text.trim().replaceFirst("&", r"\").substring(0, node.text.trim().length - 1);
- } else {
- parsed = parsed + node.text.trim();
- }
- }
- }
- return parsed;
- }
-}
-
ReplacedElement parseReplacedElement(
dom.Element element,
- NavigationDelegate? navigationDelegateForIframe,
+ List children,
) {
switch (element.localName) {
- case "audio":
- final sources = [
- if (element.attributes['src'] != null) element.attributes['src'],
- ...ReplacedElement.parseMediaSources(element.children),
- ];
- if (sources.isEmpty || sources.first == null) {
- return EmptyContentElement();
- }
- return AudioContentElement(
- name: "audio",
- src: sources,
- showControls: element.attributes['controls'] != null,
- loop: element.attributes['loop'] != null,
- autoplay: element.attributes['autoplay'] != null,
- muted: element.attributes['muted'] != null,
- node: element,
- );
case "br":
return TextContentElement(
text: "\n",
@@ -388,57 +139,10 @@ ReplacedElement parseReplacedElement(
element: element,
node: element
);
- case "iframe":
- return IframeContentElement(
- name: "iframe",
- src: element.attributes['src'],
- width: double.tryParse(element.attributes['width'] ?? ""),
- height: double.tryParse(element.attributes['height'] ?? ""),
- navigationDelegate: navigationDelegateForIframe,
- node: element,
- );
- case "img":
- return ImageContentElement(
- name: "img",
- src: element.attributes['src'],
- alt: element.attributes['alt'],
- node: element,
- );
- case "video":
- final sources = [
- if (element.attributes['src'] != null) element.attributes['src'],
- ...ReplacedElement.parseMediaSources(element.children),
- ];
- if (sources.isEmpty || sources.first == null) {
- return EmptyContentElement();
- }
- return VideoContentElement(
- name: "video",
- src: sources,
- poster: element.attributes['poster'],
- showControls: element.attributes['controls'] != null,
- loop: element.attributes['loop'] != null,
- autoplay: element.attributes['autoplay'] != null,
- muted: element.attributes['muted'] != null,
- width: double.tryParse(element.attributes['width'] ?? ""),
- height: double.tryParse(element.attributes['height'] ?? ""),
- node: element,
- );
- case "svg":
- return SvgContentElement(
- name: "svg",
- data: element.outerHtml,
- width: double.tryParse(element.attributes['width'] ?? ""),
- height: double.tryParse(element.attributes['height'] ?? ""),
- node: element,
- );
case "ruby":
return RubyElement(
element: element,
- );
- case "math":
- return MathElement(
- element: element,
+ children: children,
);
default:
return EmptyContentElement(name: element.localName == null ? "[[No Name]]" : element.localName!);
diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart
index f3331fc045..9561d8f228 100644
--- a/lib/src/styled_element.dart
+++ b/lib/src/styled_element.dart
@@ -3,6 +3,7 @@ import 'package:flutter_html/src/css_parser.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
//TODO(Sub6Resources): don't use the internal code of the html package as it may change unexpectedly.
+//ignore: implementation_imports
import 'package:html/src/query_selector.dart';
/// A [StyledElement] applies a style to all of its children.
@@ -12,7 +13,7 @@ class StyledElement {
final List elementClasses;
List children;
Style style;
- final dom.Node? _node;
+ final dom.Element? _node;
StyledElement({
this.name = "[[No name]]",
@@ -24,7 +25,7 @@ class StyledElement {
}) : this._node = node;
bool matchesSelector(String selector) =>
- _node != null && matches(_node as dom.Element, selector);
+ (_node != null && matches(_node!, selector)) || name == selector;
Map get attributes =>
_node?.attributes.map((key, value) {
@@ -32,7 +33,7 @@ class StyledElement {
}) ??
Map();
- dom.Element? get element => _node as dom.Element?;
+ dom.Element? get element => _node;
@override
String toString() {
@@ -188,7 +189,7 @@ StyledElement parseStyledElement(
ExpressionMapping.namedColorToColor(element.attributes['color']!) :
null,
fontFamily: element.attributes['face']?.split(",").first,
- fontSize: numberToFontSize(element.attributes['size'] ?? ''),
+ fontSize: element.attributes['size'] != null ? numberToFontSize(element.attributes['size']!) : null,
);
break;
case "h1":
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index dc8d2f4b1c..389cb4fc6b 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -1,8 +1,5 @@
-import 'dart:convert';
-import 'dart:math';
-
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_html/style.dart';
Map namedColors = {
"White": "#FFFFFF",
@@ -23,23 +20,6 @@ Map namedColors = {
"Purple": "#800080",
};
-Map mathML2Tex = {
- "sin": r"\sin",
- "sinh": r"\sinh",
- "csc": r"\csc",
- "csch": r"csch",
- "cos": r"\cos",
- "cosh": r"\cosh",
- "sec": r"\sec",
- "sech": r"\sech",
- "tan": r"\tan",
- "tanh": r"\tanh",
- "cot": r"\cot",
- "coth": r"\coth",
- "log": r"\log",
- "ln": r"\ln",
-};
-
class Context {
T data;
@@ -77,8 +57,32 @@ class CustomBorderSide {
BorderStyle style;
}
-String getRandString(int len) {
- var random = Random.secure();
- var values = List.generate(len, (i) => random.nextInt(255));
- return base64UrlEncode(values);
+extension TextTransformUtil on String? {
+ String? transformed(TextTransform? transform) {
+ if (this == null) return null;
+ if (transform == TextTransform.uppercase) {
+ return this!.toUpperCase();
+ } else if (transform == TextTransform.lowercase) {
+ return this!.toLowerCase();
+ } else if (transform == TextTransform.capitalize) {
+ final stringBuffer = StringBuffer();
+
+ var capitalizeNext = true;
+ for (final letter in this!.toLowerCase().codeUnits) {
+ // UTF-16: A-Z => 65-90, a-z => 97-122.
+ if (capitalizeNext && letter >= 97 && letter <= 122) {
+ stringBuffer.writeCharCode(letter - 32);
+ capitalizeNext = false;
+ } else {
+ // UTF-16: 32 == space, 46 == period
+ if (letter == 32 || letter == 46) capitalizeNext = true;
+ stringBuffer.writeCharCode(letter);
+ }
+ }
+
+ return stringBuffer.toString();
+ } else {
+ return this;
+ }
+ }
}
\ No newline at end of file
diff --git a/lib/src/widgets/iframe_mobile.dart b/lib/src/widgets/iframe_mobile.dart
deleted file mode 100644
index 55223b3478..0000000000
--- a/lib/src/widgets/iframe_mobile.dart
+++ /dev/null
@@ -1,46 +0,0 @@
-import 'package:flutter/foundation.dart';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_html/html_parser.dart';
-import 'package:flutter_html/src/replaced_element.dart';
-import 'package:flutter_html/style.dart';
-import 'package:webview_flutter/webview_flutter.dart';
-import 'package:html/dom.dart' as dom;
-
-/// [IframeContentElement is a [ReplacedElement] with web content.
-class IframeContentElement extends ReplacedElement {
- final String? src;
- final double? width;
- final double? height;
- final NavigationDelegate? navigationDelegate;
- final UniqueKey key = UniqueKey();
-
- IframeContentElement({
- required String name,
- required this.src,
- required this.width,
- required this.height,
- required dom.Element node,
- required this.navigationDelegate,
- }) : super(name: name, style: Style(), node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- final sandboxMode = attributes["sandbox"];
- return Container(
- width: width ?? (height ?? 150) * 2,
- height: height ?? (width ?? 300) / 2,
- child: WebView(
- initialUrl: src,
- key: key,
- javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts"
- ? JavascriptMode.unrestricted
- : JavascriptMode.disabled,
- navigationDelegate: navigationDelegate,
- gestureRecognizers: {
- Factory(() => VerticalDragGestureRecognizer())
- },
- ),
- );
- }
-}
\ No newline at end of file
diff --git a/lib/src/widgets/iframe_unsupported.dart b/lib/src/widgets/iframe_unsupported.dart
deleted file mode 100644
index 4adae1a5d2..0000000000
--- a/lib/src/widgets/iframe_unsupported.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_html/html_parser.dart';
-import 'package:flutter_html/src/replaced_element.dart';
-import 'package:flutter_html/style.dart';
-import 'package:webview_flutter/webview_flutter.dart';
-import 'package:html/dom.dart' as dom;
-
-/// [IframeContentElement is a [ReplacedElement] with web content.
-class IframeContentElement extends ReplacedElement {
- final String? src;
- final double? width;
- final double? height;
- final NavigationDelegate? navigationDelegate;
- final UniqueKey key = UniqueKey();
-
- IframeContentElement({
- required String name,
- required this.src,
- required this.width,
- required this.height,
- required dom.Element node,
- required this.navigationDelegate,
- }) : super(name: name, style: Style(), node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- return Container(
- width: width ?? (height ?? 150) * 2,
- height: height ?? (width ?? 300) / 2,
- child: Text("Iframes are currently not supported in this environment"),
- );
- }
-}
\ No newline at end of file
diff --git a/lib/src/widgets/iframe_web.dart b/lib/src/widgets/iframe_web.dart
deleted file mode 100644
index 2ca3c79148..0000000000
--- a/lib/src/widgets/iframe_web.dart
+++ /dev/null
@@ -1,50 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter_html/html_parser.dart';
-import 'package:flutter_html/shims/dart_ui.dart' as ui;
-import 'package:flutter_html/src/replaced_element.dart';
-import 'package:flutter_html/src/utils.dart';
-import 'package:flutter_html/style.dart';
-import 'package:webview_flutter/webview_flutter.dart';
-import 'package:html/dom.dart' as dom;
-// ignore: avoid_web_libraries_in_flutter
-import 'dart:html' as html;
-
-/// [IframeContentElement is a [ReplacedElement] with web content.
-class IframeContentElement extends ReplacedElement {
- final String? src;
- final double? width;
- final double? height;
- final NavigationDelegate? navigationDelegate;
- final UniqueKey key = UniqueKey();
- final String createdViewId = getRandString(10);
-
- IframeContentElement({
- required String name,
- required this.src,
- required this.width,
- required this.height,
- required dom.Element node,
- required this.navigationDelegate,
- }) : super(name: name, style: Style(), node: node, elementId: node.id);
-
- @override
- Widget toWidget(RenderContext context) {
- final html.IFrameElement iframe = html.IFrameElement()
- ..width = (width ?? (height ?? 150) * 2).toString()
- ..height = (height ?? (width ?? 300) / 2).toString()
- ..src = src
- ..style.border = 'none';
- //not actually an error
- ui.platformViewRegistry.registerViewFactory(createdViewId, (int viewId) => iframe);
- return Container(
- width: width ?? (height ?? 150) * 2,
- height: height ?? (width ?? 300) / 2,
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: HtmlElementView(
- viewType: createdViewId,
- )
- )
- );
- }
-}
\ No newline at end of file
diff --git a/lib/style.dart b/lib/style.dart
index 41a5b6cd14..fc2c3f6d21 100644
--- a/lib/style.dart
+++ b/lib/style.dart
@@ -175,7 +175,7 @@ class Style {
String? after;
Border? border;
Alignment? alignment;
- String? markerContent;
+ Widget? markerContent;
/// MaxLine
///
@@ -191,6 +191,8 @@ class Style {
///
TextOverflow? textOverflow;
+ TextTransform? textTransform;
+
Style({
this.backgroundColor = Colors.transparent,
this.color,
@@ -205,7 +207,7 @@ class Style {
this.lineHeight,
this.letterSpacing,
this.listStyleType,
- this.listStylePosition = ListStylePosition.OUTSIDE,
+ this.listStylePosition,
this.padding,
this.margin,
this.textAlign,
@@ -225,6 +227,7 @@ class Style {
this.markerContent,
this.maxLines,
this.textOverflow,
+ this.textTransform = TextTransform.none,
}) {
if (this.alignment == null &&
(display == Display.BLOCK || display == Display.LIST_ITEM)) {
@@ -317,6 +320,7 @@ class Style {
markerContent: other.markerContent,
maxLines: other.maxLines,
textOverflow: other.textOverflow,
+ textTransform: other.textTransform,
);
}
@@ -354,6 +358,7 @@ class Style {
wordSpacing: child.wordSpacing ?? wordSpacing,
maxLines: child.maxLines ?? maxLines,
textOverflow: child.textOverflow ?? textOverflow,
+ textTransform: child.textTransform ?? textTransform,
);
}
@@ -388,9 +393,10 @@ class Style {
String? after,
Border? border,
Alignment? alignment,
- String? markerContent,
+ Widget? markerContent,
int? maxLines,
TextOverflow? textOverflow,
+ TextTransform? textTransform,
bool? beforeAfterNull,
}) {
return Style(
@@ -428,6 +434,7 @@ class Style {
markerContent: markerContent ?? this.markerContent,
maxLines: maxLines ?? this.maxLines,
textOverflow: textOverflow ?? this.textOverflow,
+ textTransform: textTransform ?? this.textTransform,
);
}
@@ -447,6 +454,7 @@ class Style {
this.textShadow = textStyle.shadows;
this.wordSpacing = textStyle.wordSpacing;
this.lineHeight = LineHeight(textStyle.height ?? 1.2);
+ this.textTransform = TextTransform.none;
}
}
@@ -519,17 +527,28 @@ class LineHeight {
static const normal = LineHeight(1.2);
}
-enum ListStyleType {
- LOWER_ALPHA,
- UPPER_ALPHA,
- LOWER_LATIN,
- UPPER_LATIN,
- CIRCLE,
- DISC,
- DECIMAL,
- LOWER_ROMAN,
- UPPER_ROMAN,
- SQUARE,
+class ListStyleType {
+ final String text;
+ final String type;
+ final Widget? widget;
+
+ const ListStyleType(this.text, {this.type = "marker", this.widget});
+
+ factory ListStyleType.fromImage(String url) => ListStyleType(url, type: "image");
+
+ factory ListStyleType.fromWidget(Widget widget) => ListStyleType("", widget: widget, type: "widget");
+
+ static const LOWER_ALPHA = ListStyleType("LOWER_ALPHA");
+ static const UPPER_ALPHA = ListStyleType("UPPER_ALPHA");
+ static const LOWER_LATIN = ListStyleType("LOWER_LATIN");
+ static const UPPER_LATIN = ListStyleType("UPPER_LATIN");
+ static const CIRCLE = ListStyleType("CIRCLE");
+ static const DISC = ListStyleType("DISC");
+ static const DECIMAL = ListStyleType("DECIMAL");
+ static const LOWER_ROMAN = ListStyleType("LOWER_ROMAN");
+ static const UPPER_ROMAN = ListStyleType("UPPER_ROMAN");
+ static const SQUARE = ListStyleType("SQUARE");
+ static const NONE = ListStyleType("NONE");
}
enum ListStylePosition {
@@ -537,6 +556,13 @@ enum ListStylePosition {
INSIDE,
}
+enum TextTransform {
+ uppercase,
+ lowercase,
+ capitalize,
+ none,
+}
+
enum VerticalAlign {
BASELINE,
SUB,
diff --git a/packages/flutter_html_all/.gitignore b/packages/flutter_html_all/.gitignore
new file mode 100644
index 0000000000..a247422ef7
--- /dev/null
+++ b/packages/flutter_html_all/.gitignore
@@ -0,0 +1,75 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/ephemeral
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/packages/flutter_html_all/.metadata b/packages/flutter_html_all/.metadata
new file mode 100644
index 0000000000..a1f847eadf
--- /dev/null
+++ b/packages/flutter_html_all/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 0941968447ea8058e56e1479f7e53147149b739e
+ channel: beta
+
+project_type: package
diff --git a/packages/flutter_html_all/CHANGELOG.md b/packages/flutter_html_all/CHANGELOG.md
new file mode 100644
index 0000000000..f81d3c94fd
--- /dev/null
+++ b/packages/flutter_html_all/CHANGELOG.md
@@ -0,0 +1,2 @@
+## [3.0.0-alpha.2] - January 5, 2022:
+* Initial modularized flutter_html release; use flutter_html_all for full tag support or flutter_html for just the basics
diff --git a/packages/flutter_html_all/LICENSE b/packages/flutter_html_all/LICENSE
new file mode 100644
index 0000000000..89971b33a6
--- /dev/null
+++ b/packages/flutter_html_all/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-2022 The flutter_html developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/flutter_html_all/README.md b/packages/flutter_html_all/README.md
new file mode 100644
index 0000000000..45275b7b3a
--- /dev/null
+++ b/packages/flutter_html_all/README.md
@@ -0,0 +1,5 @@
+# flutter_html_all
+
+All optional flutter_html widgets, bundled into a single package.
+
+This package is simply a convenience package that exports all the other external packages. You should use this if you plan to activate all the renders that require external dependencies.
diff --git a/packages/flutter_html_all/lib/flutter_html_all.dart b/packages/flutter_html_all/lib/flutter_html_all.dart
new file mode 100644
index 0000000000..9fb8378795
--- /dev/null
+++ b/packages/flutter_html_all/lib/flutter_html_all.dart
@@ -0,0 +1,8 @@
+library flutter_html_all;
+
+export 'package:flutter_html_audio/flutter_html_audio.dart';
+export 'package:flutter_html_iframe/flutter_html_iframe.dart';
+export 'package:flutter_html_math/flutter_html_math.dart';
+export 'package:flutter_html_svg/flutter_html_svg.dart';
+export 'package:flutter_html_table/flutter_html_table.dart';
+export 'package:flutter_html_video/flutter_html_video.dart';
diff --git a/packages/flutter_html_all/pubspec.yaml b/packages/flutter_html_all/pubspec.yaml
new file mode 100644
index 0000000000..9bea10a832
--- /dev/null
+++ b/packages/flutter_html_all/pubspec.yaml
@@ -0,0 +1,38 @@
+name: flutter_html_all
+description: All optional flutter_html widgets, bundled into a single package.
+version: 3.0.0-alpha.2
+homepage: https://github.com/Sub6Resources/flutter_html
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=2.2.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ html: '>=0.15.0 <1.0.0'
+ flutter_html: '>=3.0.0-alpha.2 <4.0.0'
+ flutter_html_audio: '>=3.0.0-alpha.2 <4.0.0'
+ flutter_html_iframe: '>=3.0.0-alpha.2 <4.0.0'
+ flutter_html_math: '>=3.0.0-alpha.2 <4.0.0'
+ flutter_html_svg: '>=3.0.0-alpha.2 <4.0.0'
+ flutter_html_table: '>=3.0.0-alpha.2 <4.0.0'
+ flutter_html_video: '>=3.0.0-alpha.2 <4.0.0'
+# flutter_html_audio:
+# path: ../flutter_html_audio
+# flutter_html_iframe:
+# path: ../flutter_html_iframe
+# flutter_html_math:
+# path: ../flutter_html_math
+# flutter_html_svg:
+# path: ../flutter_html_svg
+# flutter_html_table:
+# path: ../flutter_html_table
+# flutter_html_video:
+# path: ../flutter_html_video
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
diff --git a/packages/flutter_html_audio/.gitignore b/packages/flutter_html_audio/.gitignore
new file mode 100644
index 0000000000..a247422ef7
--- /dev/null
+++ b/packages/flutter_html_audio/.gitignore
@@ -0,0 +1,75 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/ephemeral
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/packages/flutter_html_audio/.metadata b/packages/flutter_html_audio/.metadata
new file mode 100644
index 0000000000..a1f847eadf
--- /dev/null
+++ b/packages/flutter_html_audio/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 0941968447ea8058e56e1479f7e53147149b739e
+ channel: beta
+
+project_type: package
diff --git a/packages/flutter_html_audio/CHANGELOG.md b/packages/flutter_html_audio/CHANGELOG.md
new file mode 100644
index 0000000000..fe3f5ccafe
--- /dev/null
+++ b/packages/flutter_html_audio/CHANGELOG.md
@@ -0,0 +1,2 @@
+## [3.0.0-alpha.2] - January 5, 2022:
+* Initial modularized flutter_html release; use flutter_html_audio if you need support for the `` tag
diff --git a/packages/flutter_html_audio/LICENSE b/packages/flutter_html_audio/LICENSE
new file mode 100644
index 0000000000..89971b33a6
--- /dev/null
+++ b/packages/flutter_html_audio/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019-2022 The flutter_html developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/flutter_html_audio/README.md b/packages/flutter_html_audio/README.md
new file mode 100644
index 0000000000..70680b78d0
--- /dev/null
+++ b/packages/flutter_html_audio/README.md
@@ -0,0 +1,17 @@
+# flutter_html_audio
+
+Audio widget for flutter_html.
+
+This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) and the [`video_player`](https://pub.dev/packages/video_player) plugin.
+
+The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `width`, and `muted` when rendering the audio widget.
+
+#### Registering the `CustomRender`:
+
+```dart
+Widget html = Html(
+ customRender: {
+ audioMatcher(): audioRender(),
+ }
+);
+```
\ No newline at end of file
diff --git a/packages/flutter_html_audio/lib/flutter_html_audio.dart b/packages/flutter_html_audio/lib/flutter_html_audio.dart
new file mode 100644
index 0000000000..8b85545527
--- /dev/null
+++ b/packages/flutter_html_audio/lib/flutter_html_audio.dart
@@ -0,0 +1,82 @@
+library flutter_html_audio;
+
+import 'package:chewie_audio/chewie_audio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
+import 'package:video_player/video_player.dart';
+import 'package:html/dom.dart' as dom;
+
+typedef AudioControllerCallback = void Function(dom.Element?, ChewieAudioController, VideoPlayerController);
+
+CustomRender audioRender({AudioControllerCallback? onControllerCreated})
+ => CustomRender.widget(widget: (context, buildChildren)
+ => AudioWidget(context: context, callback: onControllerCreated));
+
+CustomRenderMatcher audioMatcher() => (context) {
+ return context.tree.element?.localName == "audio";
+};
+
+class AudioWidget extends StatefulWidget {
+ final RenderContext context;
+ final AudioControllerCallback? callback;
+
+ AudioWidget({
+ required this.context,
+ this.callback,
+ });
+
+ @override
+ State createState() => _AudioWidgetState();
+}
+
+class _AudioWidgetState extends State {
+ ChewieAudioController? chewieAudioController;
+ VideoPlayerController? audioController;
+ late final List sources;
+
+ @override
+ void initState() {
+ final sources = [
+ if (widget.context.tree.element?.attributes['src'] != null)
+ widget.context.tree.element!.attributes['src'],
+ ...ReplacedElement.parseMediaSources(widget.context.tree.element!.children),
+ ];
+ if (sources.isNotEmpty && sources.first != null) {
+ audioController = VideoPlayerController.network(
+ sources.first ?? "",
+ );
+ chewieAudioController = ChewieAudioController(
+ videoPlayerController: audioController!,
+ autoPlay: widget.context.tree.element?.attributes['autoplay'] != null,
+ looping: widget.context.tree.element?.attributes['loop'] != null,
+ showControls: widget.context.tree.element?.attributes['controls'] != null,
+ autoInitialize: true,
+ );
+ widget.callback?.call(widget.context.tree.element, chewieAudioController!, audioController!);
+ }
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ chewieAudioController?.dispose();
+ audioController?.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext bContext) {
+ if (sources.isEmpty || sources.first == null) {
+ return Container(height: 0, width: 0);
+ }
+ return Container(
+ key: widget.context.key,
+ width: widget.context.style.width ?? 300,
+ height: Theme.of(bContext).platform == TargetPlatform.android
+ ? 48 : 75,
+ child: ChewieAudio(
+ controller: chewieAudioController!,
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/packages/flutter_html_audio/pubspec.yaml b/packages/flutter_html_audio/pubspec.yaml
new file mode 100644
index 0000000000..16076e952b
--- /dev/null
+++ b/packages/flutter_html_audio/pubspec.yaml
@@ -0,0 +1,25 @@
+name: flutter_html_audio
+description: Audio widget for flutter_html.
+version: 3.0.0-alpha.2
+homepage: https://github.com/Sub6Resources/flutter_html
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=2.2.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ html: '>=0.15.0 <1.0.0'
+ flutter_html: '>=3.0.0-alpha.2 <4.0.0'
+# flutter_html:
+# path: ../..
+
+ video_player: '>=2.2.8 <3.0.0'
+ chewie_audio: '>=1.3.0 <2.0.0'
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+flutter:
diff --git a/packages/flutter_html_iframe/.gitignore b/packages/flutter_html_iframe/.gitignore
new file mode 100644
index 0000000000..a247422ef7
--- /dev/null
+++ b/packages/flutter_html_iframe/.gitignore
@@ -0,0 +1,75 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/ephemeral
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
diff --git a/packages/flutter_html_iframe/.metadata b/packages/flutter_html_iframe/.metadata
new file mode 100644
index 0000000000..a1f847eadf
--- /dev/null
+++ b/packages/flutter_html_iframe/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: 0941968447ea8058e56e1479f7e53147149b739e
+ channel: beta
+
+project_type: package
diff --git a/packages/flutter_html_iframe/CHANGELOG.md b/packages/flutter_html_iframe/CHANGELOG.md
new file mode 100644
index 0000000000..31b4275bb4
--- /dev/null
+++ b/packages/flutter_html_iframe/CHANGELOG.md
@@ -0,0 +1,2 @@
+## [3.0.0-alpha.2] - January 5, 2022:
+* Initial modularized flutter_html release; use flutter_html_iframe if you need support for the `