diff --git a/README.md b/README.md index c701156d97..f6163db172 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ 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) @@ -52,8 +52,6 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets. - [customRender](#customrender) - [onImageError](#onimageerror) - - - [onMathError](#onmatherror) - [onImageTap](#onimagetap) @@ -61,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) @@ -171,22 +161,19 @@ 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: @@ -281,7 +268,7 @@ 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! @@ -293,9 +280,9 @@ The `CustomRender` class has two constructors: `CustomRender.widget()` and `Cust 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 @@ -305,7 +292,7 @@ Widget html = Html( """, - customRender: { + customRenders: { birdMatcher(): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")), flutterMatcher(): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo( style: (context.tree.element!.attributes['horizontal'] != null) @@ -325,6 +312,8 @@ CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.local 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 @@ -337,17 +326,18 @@ Widget html = Html( \90 \$60 \$80 \$80 \$100 \$160 \$150 \$110 \$100 \$60 \$30 \$80 """, - customRender: { + 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" ?? false; +CustomRenderMatcher tableMatcher() => (context) => context.tree.element?.localName == "table"; ```
@@ -364,7 +354,7 @@ Widget html = Html(

YouTube iframe:

""", - customRender: { + customRenders: { iframeYT(): CustomRender.widget(widget: (context, buildChildren) { double? width = double.tryParse(context.tree.attributes['width'] ?? ""); double? height = double.tryParse(context.tree.attributes['height'] ?? ""); @@ -431,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. @@ -560,272 +532,127 @@ Widget html = Html( More examples and in-depth details available [here](https://github.com/Sub6Resources/flutter_html/wiki/Style). -### navigationDelegateForIframe: - -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`. - -#### Example Usage - navigationDelegateForIframe: - -```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; - } - }, -); -``` - -### customImageRender: - -A powerful API that allows you to customize what the `Html` widget does when rendering an image, down to the most minute detail. - -`customImageRender` accepts a `Map`. `ImageSourceMatcher` provides the matching function, while `ImageRender` provides the widget to be rendered. - -The default image renders are: - -```dart -final Map defaultImageRenders = { - base64UriMatcher(): base64ImageRender(), - assetUriMatcher(): assetImageRender(), - networkSourceMatcher(extension: "svg"): svgNetworkImageRender(), - networkSourceMatcher(): networkImageRender(), -}; -``` +## Rendering Reference -See [the source code](https://github.com/Sub6Resources/flutter_html/blob/master/lib/image_render.dart) for details on how these are implemented. +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. -When setting `customImageRenders`, the package will prioritize the custom renders first, while the default ones are used as a fallback. +### Image -Note: Order is very important when you set `customImageRenders`. The more specific your `ImageSourceMatcher`, the higher up in the `customImageRender` list it should be. +This package currently has support for base64 images, asset images, and network images. -#### typedef ImageSourceMatcher +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`. -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. +If the rendering of any of the above fails, the package will fall back to rendering the alt text of the image, if any. -A typical usage would look something like this: +Currently the package only considers the width, height, src, and alt text while rendering an image. -```dart -ImageSourceMatcher base64UriMatcher() => (attributes, element) => - attributes["src"] != null && - attributes["src"]!.startsWith("data:image") && - attributes["src"]!.contains("base64,"); -``` +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. -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. +## External Packages -You can also declare your own variables in the function itself, which would look like this: +### `flutter_html_all` -```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"); - }; -``` +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. -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. +### `flutter_html_audio` -#### typedef ImageRender +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 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`. +The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `width`, and `muted` when rendering the audio widget. -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: { + audioMatcher(): audioRender(), + } +); ``` -The above example should be used with the `base64UriMatcher()` in the examples for `ImageSourceMatcher`. - -Just like functions for `ImageSourceMatcher`, you can declare your own variables in the function itself: +### `flutter_html_iframe` -```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; - }, - ); - }; -``` +This package renders iframes using the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin. -Implementing these variables allows you to customize every last detail of how the widget is rendered. +When rendering iframes, the package considers the width, height, and sandbox attributes. -#### Example Usages - customImageRender: +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`. -`customImageRender` can be used in two different ways: +#### Registering the `CustomRender`: -1. Overriding a default render: ```dart Widget html = Html( - data: """ - Flutter
- Google
- """, - 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: { + iframeMatcher(): iframeRender(), + } ); ``` -Above, there are three custom `networkSourceMatcher`s, which will be applied - in order - before the default implementations. +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. -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. +#### `NavigationDelegate` example: -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; - }, - ); - } -}; - Widget html = Html( - data: """ - alt text
- alt text 2
- """, - customImageRenders: { - classAndIdMatcher(classToMatch: "class1", idToMatch: "imageId"): classAndIdRender(classToMatch: "class1", idToMatch: "imageId") - }, + customRenders: { + iframeMatcher(): iframeRender(navigationDelegate: (NavigationRequest request) { + if (request.url.contains("google.com/images")) { + return NavigationDecision.prevent; + } else { + return NavigationDecision.navigate; + } + }), + } ); ``` -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. +### `flutter_html_math` -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! +This package renders MathML elements using the [`flutter_math_fork`](https://pub.dev/packages/flutter_math_fork) plugin. -## Rendering Reference - -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. - -### Image - -This package currently has support for base64 images, asset images, network SVGs inside an ``, and network images. - -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`. - -If the rendering of any of the above fails, the package will fall back to rendering the alt text of the image, if any. - -Currently the package only considers the width, height, src, and alt text while rendering an image. +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`. -Note that there currently is no support for SVGs either in base64 format or asset format. - -### Iframe - -This package renders iframes using the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin. - -When rendering iframes, the package considers the width, height, and sandbox attributes. - -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`. - -You can set the `navigationDelegate` of the webview with the `navigationDelegateForIframe` property - see [here](#navigationdelegateforiframe) for more details. - -### Audio - -This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) plugin. - -The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `width`, and `muted` when rendering the audio widget. - -### Video - -This package renders video elements using the [`chewie`](https://pub.dev/packages/chewie) plugin. - -The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `poster`, `width`, `height`, and `muted` when rendering the video widget. +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. -### SVG +#### Registering the `CustomRender`: -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. +```dart +Widget html = Html( + customRenders: { + mathMatcher(): mathRender(), + } +); +``` -### MathML +If the parsing errors, you can use the `onMathError` property of `mathRender` to catch the error and potentially fix it on your end. -This package renders MathML elements using the [`flutter_math`](https://pub.dev/packages/flutter_math) plugin. +The function exposes the parsed Tex `String`, as well as the error and error with type from `flutter_math_fork` as a `String`. -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`. +You can analyze the error and the parsed string, and finally return a new instance of `Math.tex()` with the corrected Tex string. -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. +#### `onMathError` example: -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. +```dart +Widget html = Html( + customRenders: { + mathMatcher(): mathRender(onMathError: (tex, exception, exceptionWithType) { + print(exception); + //optionally try and correct the Tex string here + return Text(exception); + }), + } +); +``` If you'd like to see more MathML features, feel free to create a PR or file a feature request! -### Tex +#### 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. +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. Use a custom tag inside your HTML (an example could be ``), and place your **raw** Tex string inside. @@ -834,17 +661,14 @@ Then, use the `customRender` parameter to add the widget to render Tex. It could ```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: { + customRenders: { texMatcher(): CustomRender.widget(widget: (context, buildChildren) => Math.tex( context.tree.element?.innerHtml ?? '', mathStyle: MathStyle.display, textStyle: context.style.generateTextStyle(), onErrorFallback: (FlutterMathException e) { - if (context.parser.onMathError != null) { - return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType); - } else { - return Text(e.message); - } + //optionally try and correct the Tex string here + return Text(e.message); }, )), }, @@ -854,12 +678,59 @@ Widget htmlWidget = Html( CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex'; ``` -### Table +### `flutter_html_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. + +The package also exposes a few ways to render SVGs within an `` tag, specifically base64 SVGs, asset SVGs, and network SVGs. + +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRenders: { + svgTagMatcher(): svgTagRender(), + svgDataUriMatcher(): svgDataImageRender(), + svgAssetUriMatcher(): svgAssetImageRender(), + svgNetworkSourceMatcher(): svgNetworkImageRender(), + } +); +``` + +### `flutter_html_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. +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRenders: { + tableMatcher(): tableRender(), + } +); +``` + +### `flutter_html_video` + +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. + +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRenders: { + videoMatcher(): videoRender(), + } +); +``` + ## 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: @@ -867,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/lib/main.dart b/example/lib/main.dart index 74f48fef6e..550ad15bd1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html_all/flutter_html_all.dart'; import 'package:flutter_math_fork/flutter_math.dart'; void main() => runApp(new MyApp()); @@ -268,18 +269,14 @@ class _MyHomePageState extends State { ), 'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis), }, - tagsList: Html.tags..addAll(["tex", "bird", "flutter"]), + 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) { - if (context.parser.onMathError != null) { - return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType); - } else { - return Text(e.message); - } + return Text(e.message); }, )), tagMatcher("bird"): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")), @@ -292,28 +289,34 @@ class _MyHomePageState extends State { )), tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView( scrollDirection: Axis.horizontal, - child: (context.tree as TableLayoutElement).toWidget(context), + child: tableRender.call().widget!.call(context, buildChildren), )), - }, - customImageRenders: { - networkSourceMatcher(domains: ["flutter.dev"]): - (context, attributes, element) { - return FlutterLogo(size: 36); - }, - networkSourceMatcher(domains: ["mydomain.com"]): - networkImageRender( + 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..."); @@ -336,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/pubspec.yaml b/example/pubspec.yaml index 286a7a0b99..79e23feab9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -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 index 1b06bbf82c..9b4abd8890 100644 --- a/lib/custom_render.dart +++ b/lib/custom_render.dart @@ -1,4 +1,8 @@ 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'; @@ -11,9 +15,9 @@ CustomRenderMatcher tagMatcher(String tag) => (context) { }; CustomRenderMatcher blockElementMatcher() => (context) { - return context.tree.style.display == Display.BLOCK && - (context.tree.children.isNotEmpty || context.tree.element?.localName == "hr"); - }; + 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; @@ -23,6 +27,39 @@ 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; }; @@ -67,8 +104,7 @@ class SelectableCustomRender extends CustomRender { CustomRender blockElementRender({ Style? style, - Widget? child, - List? children,}) => + List? children}) => CustomRender.inlineSpan(inlineSpan: (context, buildChildren) { if (context.parser.selectable) { return TextSpan( @@ -173,6 +209,185 @@ 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 @@ -227,6 +442,9 @@ final Map defaultRenders = { blockElementMatcher(): blockElementRender(), listElementMatcher(): listElementRender(), textContentElementMatcher(): textContentElementRender(), + dataUriMatcher(): base64ImageRender(), + assetUriMatcher(): assetImageRender(), + networkSourceMatcher(): networkImageRender(), replacedElementMatcher(): replacedElementRender(), interactableElementMatcher(): interactableElementRender(), layoutElementMatcher(): layoutElementRender(), @@ -281,6 +499,8 @@ InlineSpan _getInteractableChildren(RenderContext context, InteractableElement t } } +final _dataUriFormat = RegExp("^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)"); + double _getVerticalOffset(StyledElement tree) { switch (tree.style.verticalAlign) { case VerticalAlign.SUB: @@ -290,4 +510,40 @@ double _getVerticalOffset(StyledElement tree) { default: return 0; } -} \ No newline at end of file +} + +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; +} + +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 82c41c8b29..1405471220 100644 --- a/lib/flutter_html.dart +++ b/lib/flutter_html.dart @@ -1,35 +1,23 @@ library flutter_html; -import 'package:chewie/chewie.dart'; -import 'package:chewie_audio/chewie_audio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_html/custom_render.dart'; -import 'package:flutter/rendering.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/src/utils.dart'; import 'package:flutter_html/style.dart'; import 'package:html/dom.dart' as dom; -import 'package:flutter_html/src/navigation_delegate.dart'; -import 'package:video_player/video_player.dart'; //export render context api export 'package:flutter_html/html_parser.dart'; //export render context api export 'package:flutter_html/html_parser.dart'; -//export image render api -export 'package:flutter_html/image_render.dart'; export 'package:flutter_html/custom_render.dart'; -//export image render api -export 'package:flutter_html/image_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'; export 'package:flutter_html/src/layout_element.dart'; export 'package:flutter_html/src/replaced_element.dart'; export 'package:flutter_html/src/styled_element.dart'; -export 'package:flutter_html/src/navigation_delegate.dart'; //export style api export 'package:flutter_html/style.dart'; @@ -66,17 +54,14 @@ class Html extends StatefulWidget { this.onLinkTap, this.onAnchorTap, this.customRenders = const {}, - this.customImageRenders = 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); @@ -87,16 +72,13 @@ class Html extends StatefulWidget { this.onLinkTap, this.onAnchorTap, this.customRenders = const {}, - this.customImageRenders = 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); @@ -117,20 +99,12 @@ class Html extends StatefulWidget { /// 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; @@ -148,74 +122,13 @@ class Html extends StatefulWidget { /// 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; - - /// Get the list of supported tags for the [Html] widget - 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); - - /// Protected member to track controllers used in all [Html] widgets. Please - /// refrain from using this member, and rather use the [chewieAudioControllers], - /// [chewieControllers], [videoPlayerControllers], and [audioPlayerControllers] - /// getters to access the controllers in your own code. - @protected - static final InternalControllers controllers = InternalControllers(); - - /// Internal member to track controllers used in the specific [Html] widget. - /// This is only used so controllers can be automatically disposed when the - /// widget disposes. - final InternalControllers _controllers = InternalControllers(); - - /// Getter for all [ChewieAudioController]s initialized by [Html] widgets. - static List get chewieAudioControllers => controllers.chewieAudioControllers.values.toList(); - /// Getter for all [ChewieController]s initialized by [Html] widgets. - static List get chewieControllers => controllers.chewieControllers.values.toList(); - /// Getter for all [VideoPlayerController]s for video widgets initialized by [Html] widgets. - static List get videoPlayerControllers => controllers.videoPlayerControllers.values.toList(); - /// Getter for all [VideoPlayerController]s for audio widgets initialized by [Html] widgets. - static List get audioPlayerControllers => controllers.audioPlayerControllers.values.toList(); - - /// Convenience method to dispose all controllers used by all [Html] widgets - /// at this time. This is not necessary to be called, as each [Html] widget - /// will automatically handle disposing. - static void disposeAll() { - controllers.chewieAudioControllers.values.forEach((element) { - element.dispose(); - }); - controllers.chewieControllers.values.forEach((ChewieController element) { - element.dispose(); - }); - controllers.videoPlayerControllers.values.forEach((element) { - element.dispose(); - }); - controllers.audioPlayerControllers.values.forEach((element) { - element.dispose(); - }); - } - - /// Internal method to add controllers to the global list and widget-specific - /// list. This should not be used in your app code. - void addController(int hashCode, dynamic controller, {bool isAudioController = false}) { - if (controller is ChewieAudioController) { - controllers.chewieAudioControllers[hashCode] = controller; - _controllers.chewieAudioControllers[hashCode] = controller; - } else if (controller is ChewieController) { - controllers.chewieControllers[hashCode] = controller; - _controllers.chewieControllers[hashCode] = controller; - } else if (controller is VideoPlayerController && !isAudioController) { - controllers.videoPlayerControllers[hashCode] = controller; - _controllers.videoPlayerControllers[hashCode] = controller; - } else if (controller is VideoPlayerController) { - controllers.audioPlayerControllers[hashCode] = controller; - _controllers.audioPlayerControllers[hashCode] = controller; - } - } + 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(EXTERNAL_ELEMENTS); @override State createState() => _HtmlState(); @@ -228,24 +141,7 @@ class _HtmlState extends State { void initState() { super.initState(); doc = - widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.document!; - } - - @override - void dispose() { - widget._controllers.chewieAudioControllers.values.forEach((element) { - element.dispose(); - }); - widget._controllers.chewieControllers.values.forEach((ChewieController element) { - element.dispose(); - }); - widget._controllers.videoPlayerControllers.values.forEach((element) { - element.dispose(); - }); - widget._controllers.audioPlayerControllers.values.forEach((element) { - element.dispose(); - }); - super.dispose(); + widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.document!; } @override @@ -260,25 +156,19 @@ class _HtmlState extends State { onImageTap: widget.onImageTap, onCssParseError: widget.onCssParseError, onImageError: widget.onImageError, - onMathError: widget.onMathError, shrinkWrap: widget.shrinkWrap, selectable: false, style: widget.style, customRenders: {} ..addAll(widget.customRenders) ..addAll(defaultRenders), - imageRenders: {} - ..addAll(widget.customImageRenders) - ..addAll(defaultImageRenders), tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList, - navigationDelegateForIframe: widget.navigationDelegateForIframe, - root: widget, ), ); } } -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) /// @@ -386,36 +276,43 @@ class SelectableHtml extends StatelessWidget { /// fallback to the default rendering. final Map customRenders; - /// Get the list of supported tags for the [SelectableHtml] widget 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, + style: widget.style, customRenders: {} - ..addAll(customRenders) + ..addAll(widget.customRenders) ..addAll(defaultRenders), - imageRenders: defaultImageRenders, - tagsList: tagsList.isEmpty ? SelectableHtml.tags : tagsList, - navigationDelegateForIframe: null, - selectionControls: selectionControls, - scrollPhysics: scrollPhysics, + 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 44af0cb306..fdfcf4cbd1 100644 --- a/lib/html_parser.dart +++ b/lib/html_parser.dart @@ -18,11 +18,6 @@ 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, @@ -36,15 +31,12 @@ 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 customRenders; - final Map imageRenders; final List tagsList; - final NavigationDelegate? navigationDelegateForIframe; final OnTap? internalOnAnchorTap; final Html? root; final TextSelectionControls? selectionControls; @@ -60,14 +52,11 @@ 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.customRenders, - required this.imageRenders, required this.tagsList, - required this.navigationDelegateForIframe, this.root, this.selectionControls, this.scrollPhysics, @@ -85,7 +74,6 @@ class HtmlParser extends StatelessWidget { htmlData, customRenders.keys.toList(), tagsList, - navigationDelegateForIframe, context, this, ); @@ -154,7 +142,6 @@ class HtmlParser extends StatelessWidget { dom.Document html, List customRenderMatchers, List tagsList, - NavigationDelegate? navigationDelegateForIframe, BuildContext context, HtmlParser parser, ) { @@ -170,7 +157,6 @@ class HtmlParser extends StatelessWidget { node, customRenderMatchers, tagsList, - navigationDelegateForIframe, context, parser, )); @@ -187,7 +173,6 @@ class HtmlParser extends StatelessWidget { dom.Node node, List customRenderMatchers, List tagsList, - NavigationDelegate? navigationDelegateForIframe, BuildContext context, HtmlParser parser, ) { @@ -198,7 +183,6 @@ class HtmlParser extends StatelessWidget { childNode, customRenderMatchers, tagsList, - navigationDelegateForIframe, context, parser, )); @@ -214,7 +198,7 @@ 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, children, navigationDelegateForIframe); + return parseReplacedElement(node, children); } else if (LAYOUT_ELEMENTS.contains(node.localName)) { return parseLayoutElement(node, children); } else if (TABLE_CELL_ELEMENTS.contains(node.localName)) { diff --git a/lib/image_render.dart b/lib/image_render.dart deleted file mode 100644 index d9c812935f..0000000000 --- a/lib/image_render.dart +++ /dev/null @@ -1,250 +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: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)!; - 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); - } - - return 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(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/css_parser.dart b/lib/src/css_parser.dart index fa2c6eeda1..66c622a8cf 100644 --- a/lib/src/css_parser.dart +++ b/lib/src/css_parser.dart @@ -742,6 +742,8 @@ class ExpressionMapping { return ListStyleType.UPPER_LATIN; case 'upper-roman': return ListStyleType.UPPER_ROMAN; + case 'none': + return ListStyleType.NONE; } return null; } diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart index 5d9c17cf5b..4b096d8f5d 100644 --- a/lib/src/html_elements.dart +++ b/lib/src/html_elements.dart @@ -110,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", @@ -136,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/layout_element.dart b/lib/src/layout_element.dart index 75ae45296e..33093e7493 100644 --- a/lib/src/layout_element.dart +++ b/lib/src/layout_element.dart @@ -1,13 +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/src/utils.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 @@ -23,162 +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), - padding: style.padding?.nonNegative, - margin: style.margin?.nonNegative, - alignment: style.alignment, - 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 = 0; - List rowSpanOffsets = []; - for (final row in rows) { - final cols = row.children.whereType().fold(0, (int value, child) => value + child.colspan) + - rowSpanOffsets.fold(0, (int offset, child) => child); - columnMax = max(cols, columnMax); - rowSpanOffsets = [ - ...rowSpanOffsets.map((value) => value - 1).where((value) => value > 0), - ...row.children.whereType().map((cell) => cell.rowspan - 1), - ]; - } - - // Place the cells in the rows/columns - final cells = []; - final columnRowOffset = List.generate(columnMax, (_) => 0); - final columnColspanOffset = 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; - } - if (child is TableCellElement) { - while (columnRowOffset[columni] > 0) { - columnRowOffset[columni] = columnRowOffset[columni] - 1; - columni += columnColspanOffset[columni].clamp(1, columnMax - columni - 1); - } - cells.add(GridPlacement( - child: Container( - width: child.style.width ?? double.infinity, - height: child.style.height, - padding: child.style.padding?.nonNegative ?? row.style.padding?.nonNegative, - 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; - columnColspanOffset[columni] = child.colspan; - 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()); - - if (finalColumnSizes.isEmpty || rowSizes.isEmpty) { - // No actual cells to show - return SizedBox(); - } - - return LayoutGrid( - gridFit: GridFit.loose, - columnSizes: finalColumnSizes, - rowSizes: rowSizes, - children: cells, - ); - } -} - class TableSectionLayoutElement extends LayoutElement { TableSectionLayoutElement({ required String name, @@ -355,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": @@ -375,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/navigation_delegate.dart b/lib/src/navigation_delegate.dart deleted file mode 100644 index d45bb9ed32..0000000000 --- a/lib/src/navigation_delegate.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:async'; - -/// Information about a navigation action that is about to be executed. -class NavigationRequest { - NavigationRequest({required this.url, required this.isForMainFrame}); - - /// The URL that will be loaded if the navigation is executed. - final String url; - - /// Whether the navigation request is to be loaded as the main frame. - final bool isForMainFrame; - - @override - String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; - } -} - -/// A decision on how to handle a navigation request. -enum NavigationDecision { - /// Prevent the navigation from taking place. - prevent, - - /// Allow the navigation to take place. - navigate, -} - -/// Decides how to handle a specific navigation request. -/// -/// The returned [NavigationDecision] determines how the navigation described by -/// `navigation` should be handled. -/// -/// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( - NavigationRequest navigation); diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart index efaef1c85a..33e844ee7d 100644 --- a/lib/src/replaced_element.dart +++ b/lib/src/replaced_element.dart @@ -1,17 +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_html/flutter_html.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_math_fork/flutter_math.dart'; -import 'package:flutter_svg/flutter_svg.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/style.dart'; import 'package:html/dom.dart' as dom; -import 'package:video_player/video_player.dart'; /// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered. /// @@ -61,169 +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) { - final VideoPlayerController audioController = VideoPlayerController.network( - src.first ?? "", - ); - final ChewieAudioController chewieAudioController = ChewieAudioController( - videoPlayerController: audioController, - autoPlay: autoplay, - looping: loop, - showControls: showControls, - autoInitialize: true, - ); - context.parser.root?.addController(element.hashCode, audioController, isAudioController: true); - context.parser.root?.addController(element.hashCode, chewieAudioController); - 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, - ), - ); - } -} - -/// [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; - final VideoPlayerController videoController = VideoPlayerController.network( - src.first ?? "", - ); - final ChewieController chewieController = ChewieController( - videoPlayerController: videoController, - placeholder: poster != null && poster!.isNotEmpty - ? Image.network(poster!) - : Container(color: Colors.black), - autoPlay: autoplay, - looping: loop, - showControls: showControls, - autoInitialize: true, - aspectRatio: _width / _height, - ); - context.parser.root?.addController(element.hashCode, videoController); - context.parser.root?.addController(element.hashCode, chewieController); - return AspectRatio( - aspectRatio: _width / _height, - child: Container( - key: AnchorKey.of(context.parser.key, this), - child: Chewie( - controller: chewieController, - ), - ), - ); - } -} - -/// [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, alignment: PlaceholderAlignment.middle); - - @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]]"); @@ -295,110 +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, List children, - NavigationDelegate? navigationDelegateForIframe, ) { 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", @@ -406,59 +139,11 @@ 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, children: children, ); - case "math": - return MathElement( - element: element, - ); default: return EmptyContentElement(name: element.localName == null ? "[[No Name]]" : element.localName!); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 6a9ec3432c..389cb4fc6b 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -1,10 +1,4 @@ -import 'dart:convert'; -import 'dart:math'; - -import 'package:chewie/chewie.dart'; -import 'package:chewie_audio/chewie_audio.dart'; import 'package:flutter/material.dart'; -import 'package:video_player/video_player.dart'; import 'package:flutter_html/style.dart'; Map namedColors = { @@ -26,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; @@ -80,21 +57,6 @@ class CustomBorderSide { BorderStyle style; } -/// Helps keep track of controllers used by [Html] widgets. -/// A map is used so that controllers are not duplicated on widget rebuild -class InternalControllers { - Map chewieAudioControllers = {}; - Map chewieControllers = {}; - Map audioPlayerControllers = {}; - Map videoPlayerControllers = {}; -} - -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; @@ -123,8 +85,4 @@ extension TextTransformUtil on String? { return this; } } -} - -extension ClampedEdgeInsets on EdgeInsetsGeometry { - EdgeInsetsGeometry get nonNegative => this.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity)); } \ 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 b1e9fbe71c..0000000000 --- a/lib/src/widgets/iframe_mobile.dart +++ /dev/null @@ -1,61 +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/navigation_delegate.dart'; -import 'package:flutter_html/src/replaced_element.dart'; -import 'package:flutter_html/style.dart'; -import 'package:webview_flutter/webview_flutter.dart' as webview; -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: ContainerSpan( - style: context.style, - newContext: context, - child: webview.WebView( - initialUrl: src, - key: key, - javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts" - ? webview.JavascriptMode.unrestricted - : webview.JavascriptMode.disabled, - navigationDelegate: (request) async { - final result = await navigationDelegate!(NavigationRequest( - url: request.url, - isForMainFrame: request.isForMainFrame, - )); - if (result == NavigationDecision.prevent) { - return webview.NavigationDecision.prevent; - } else { - return webview.NavigationDecision.navigate; - } - }, - 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 38c96eb0ab..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/navigation_delegate.dart'; -import 'package:flutter_html/src/replaced_element.dart'; -import 'package:flutter_html/style.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"), - ); - } -} diff --git a/lib/src/widgets/iframe_web.dart b/lib/src/widgets/iframe_web.dart deleted file mode 100644 index cf68c54449..0000000000 --- a/lib/src/widgets/iframe_web.dart +++ /dev/null @@ -1,54 +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/navigation_delegate.dart'; -import 'package:flutter_html/src/replaced_element.dart'; -import 'package:flutter_html/src/utils.dart'; -import 'package:flutter_html/style.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: ContainerSpan( - style: context.style, - newContext: context, - child: Directionality( - textDirection: TextDirection.ltr, - child: HtmlElementView( - viewType: createdViewId, - ) - ), - ) - ); - } -} \ No newline at end of file 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..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_all/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_all/LICENSE b/packages/flutter_html_all/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_all/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. 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..23ff405edd --- /dev/null +++ b/packages/flutter_html_all/pubspec.yaml @@ -0,0 +1,66 @@ +name: flutter_html_all +description: All optional flutter_html widgets, bundled into a single package. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + # todo change all these + 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 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages 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..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_audio/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_audio/LICENSE b/packages/flutter_html_audio/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_audio/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. 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..5823caffaf --- /dev/null +++ b/packages/flutter_html_audio/pubspec.yaml @@ -0,0 +1,58 @@ +name: flutter_html_audio +description: Audio widget for flutter_html. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + chewie_audio: '>=1.2.0 <2.0.0' + video_player: '>=2.1.1 <3.0.0' + #todo change this + flutter_html: + path: ../.. + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages 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..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_iframe/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_iframe/LICENSE b/packages/flutter_html_iframe/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_iframe/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_html_iframe/README.md b/packages/flutter_html_iframe/README.md new file mode 100644 index 0000000000..c58427a27c --- /dev/null +++ b/packages/flutter_html_iframe/README.md @@ -0,0 +1,36 @@ +# flutter_html_iframe + +Iframe widget for flutter_html. + +This package renders iframes using the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin. + +When rendering iframes, the package considers the width, height, and sandbox attributes. + +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`. + +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRender: { + iframeMatcher(): iframeRender(), + } +); +``` +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. + +#### `NavigationDelegate` example: + +```dart +Widget html = Html( + customRender: { + iframeMatcher(): iframeRender(navigationDelegate: (NavigationRequest request) { + if (request.url.contains("google.com/images")) { + return NavigationDecision.prevent; + } else { + return NavigationDecision.navigate; + } + }), + } +); +``` diff --git a/packages/flutter_html_iframe/lib/flutter_html_iframe.dart b/packages/flutter_html_iframe/lib/flutter_html_iframe.dart new file mode 100644 index 0000000000..ea61f8bb1e --- /dev/null +++ b/packages/flutter_html_iframe/lib/flutter_html_iframe.dart @@ -0,0 +1,11 @@ +library flutter_html_iframe; + +import 'package:flutter_html/custom_render.dart'; + +export 'iframe_unsupported.dart' + if (dart.library.io) 'iframe_mobile.dart' + if (dart.library.html) 'iframe_web.dart'; + +CustomRenderMatcher iframeMatcher() => (context) { + return context.tree.element?.localName == "iframe"; +}; \ No newline at end of file diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart new file mode 100644 index 0000000000..2bd5b4608c --- /dev/null +++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart @@ -0,0 +1,31 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => CustomRender.widget(widget: (context, buildChildren) { + final sandboxMode = context.tree.element?.attributes["sandbox"]; + final UniqueKey key = UniqueKey(); + return Container( + width: double.tryParse(context.tree.element?.attributes['width'] ?? "") + ?? (double.tryParse(context.tree.element?.attributes['height'] ?? "") ?? 150) * 2, + height: double.tryParse(context.tree.element?.attributes['height'] ?? "") + ?? (double.tryParse(context.tree.element?.attributes['width'] ?? "") ?? 300) / 2, + child: ContainerSpan( + style: context.style, + newContext: context, + child: WebView( + initialUrl: context.tree.element?.attributes['src'], + key: key, + javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts" + ? JavascriptMode.unrestricted + : JavascriptMode.disabled, + navigationDelegate: navigationDelegate, + gestureRecognizers: { + Factory(() => VerticalDragGestureRecognizer()) + }, + ), + ), + ); +}); diff --git a/packages/flutter_html_iframe/lib/iframe_unsupported.dart b/packages/flutter_html_iframe/lib/iframe_unsupported.dart new file mode 100644 index 0000000000..91b17575e4 --- /dev/null +++ b/packages/flutter_html_iframe/lib/iframe_unsupported.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => CustomRender.widget(widget: (context, buildChildren) { + return Container( + child: Text("Iframes are currently not supported in this environment"), + ); +}); \ No newline at end of file diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart new file mode 100644 index 0000000000..1e9c50598f --- /dev/null +++ b/packages/flutter_html_iframe/lib/iframe_web.dart @@ -0,0 +1,44 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_html_iframe/shims/dart_ui.dart' as ui; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +import 'package:webview_flutter/webview_flutter.dart'; + +CustomRender iframeRender({NavigationDelegate? navigationDelegate}) => CustomRender.widget(widget: (context, buildChildren) { + final html.IFrameElement iframe = html.IFrameElement() + ..width = (double.tryParse(context.tree.element?.attributes['width'] ?? "") + ?? (double.tryParse(context.tree.element?.attributes['height'] ?? "") ?? 150) * 2).toString() + ..height = (double.tryParse(context.tree.element?.attributes['height'] ?? "") + ?? (double.tryParse(context.tree.element?.attributes['width'] ?? "") ?? 300) / 2).toString() + ..src = context.tree.element?.attributes['src'] + ..style.border = 'none'; + final String createdViewId = getRandString(10); + ui.platformViewRegistry.registerViewFactory(createdViewId, (int viewId) => iframe); + return Container( + width: double.tryParse(context.tree.element?.attributes['width'] ?? "") + ?? (double.tryParse(context.tree.element?.attributes['height'] ?? "") ?? 150) * 2, + height: double.tryParse(context.tree.element?.attributes['height'] ?? "") + ?? (double.tryParse(context.tree.element?.attributes['width'] ?? "") ?? 300) / 2, + child: ContainerSpan( + style: context.style, + newContext: context, + child: Directionality( + textDirection: TextDirection.ltr, + child: HtmlElementView( + viewType: createdViewId, + ) + ), + ) + ); +}); + +String getRandString(int len) { + var random = Random.secure(); + var values = List.generate(len, (i) => random.nextInt(255)); + return base64UrlEncode(values); +} \ No newline at end of file diff --git a/lib/shims/dart_ui.dart b/packages/flutter_html_iframe/lib/shims/dart_ui.dart similarity index 100% rename from lib/shims/dart_ui.dart rename to packages/flutter_html_iframe/lib/shims/dart_ui.dart diff --git a/lib/shims/dart_ui_fake.dart b/packages/flutter_html_iframe/lib/shims/dart_ui_fake.dart similarity index 100% rename from lib/shims/dart_ui_fake.dart rename to packages/flutter_html_iframe/lib/shims/dart_ui_fake.dart diff --git a/lib/shims/dart_ui_real.dart b/packages/flutter_html_iframe/lib/shims/dart_ui_real.dart similarity index 100% rename from lib/shims/dart_ui_real.dart rename to packages/flutter_html_iframe/lib/shims/dart_ui_real.dart diff --git a/packages/flutter_html_iframe/pubspec.yaml b/packages/flutter_html_iframe/pubspec.yaml new file mode 100644 index 0000000000..42a4497864 --- /dev/null +++ b/packages/flutter_html_iframe/pubspec.yaml @@ -0,0 +1,57 @@ +name: flutter_html_iframe +description: Iframe widget for flutter_html. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter: '>=2.0.4 <4.0.0' + # todo change this + flutter_html: + path: ../.. + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_html_math/.gitignore b/packages/flutter_html_math/.gitignore new file mode 100644 index 0000000000..a247422ef7 --- /dev/null +++ b/packages/flutter_html_math/.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_math/.metadata b/packages/flutter_html_math/.metadata new file mode 100644 index 0000000000..a1f847eadf --- /dev/null +++ b/packages/flutter_html_math/.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_math/CHANGELOG.md b/packages/flutter_html_math/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_math/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_math/LICENSE b/packages/flutter_html_math/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_math/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_html_math/README.md b/packages/flutter_html_math/README.md new file mode 100644 index 0000000000..320ec123ce --- /dev/null +++ b/packages/flutter_html_math/README.md @@ -0,0 +1,39 @@ +# flutter_html_math + +Math widget for flutter_html. + +his package renders MathML elements using the [`flutter_math_fork`](https://pub.dev/packages/flutter_math_fork) 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_fork`. + +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`: + +```dart +Widget html = Html( + customRender: { + mathMatcher(): mathRender(), + } +); +``` + +If the parsing errors, you can use the `onMathError` property of `mathRender` to catch the error and potentially fix it on your end. + +The function exposes the parsed Tex `String`, as well as the error and error with type from `flutter_math_fork` as a `String`. + +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( + customRender: { + mathMatcher(): mathRender(onMathError: (tex, exception, exceptionWithType) { + print(exception); + //optionally try and correct the Tex string here + return Text(exception); + }), + } +); +``` \ No newline at end of file diff --git a/packages/flutter_html_math/lib/flutter_html_math.dart b/packages/flutter_html_math/lib/flutter_html_math.dart new file mode 100644 index 0000000000..cd1ca088cf --- /dev/null +++ b/packages/flutter_html_math/lib/flutter_html_math.dart @@ -0,0 +1,103 @@ +library flutter_html_math; + +import 'package:html/dom.dart' as dom; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter_math_fork/flutter_math.dart'; + +CustomRender mathRender({OnMathError? onMathError}) => CustomRender.widget(widget: (context, buildChildren) { + String texStr = context.tree.element == null ? '' : parseMathRecursive(context.tree.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 (onMathError != null) { + return onMathError.call(texStr, e.message, e.messageWithType); + } else { + return Text(e.message); + } + }, + ) + ); +}); + +CustomRenderMatcher mathMatcher() => (context) { + return context.tree.element?.localName == "math"; +}; + +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; +} + +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", +}; + +typedef OnMathError = Widget Function( + String parsedTex, + String exception, + String exceptionWithType, + ); + diff --git a/packages/flutter_html_math/pubspec.yaml b/packages/flutter_html_math/pubspec.yaml new file mode 100644 index 0000000000..a86762223b --- /dev/null +++ b/packages/flutter_html_math/pubspec.yaml @@ -0,0 +1,57 @@ +name: flutter_html_math +description: Math widget for flutter_html. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + flutter_math_fork: '>=0.6.0 <1.0.0' + #todo change this + flutter_html: + path: ../.. + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_html_svg/.gitignore b/packages/flutter_html_svg/.gitignore new file mode 100644 index 0000000000..a247422ef7 --- /dev/null +++ b/packages/flutter_html_svg/.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_svg/.metadata b/packages/flutter_html_svg/.metadata new file mode 100644 index 0000000000..a1f847eadf --- /dev/null +++ b/packages/flutter_html_svg/.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_svg/CHANGELOG.md b/packages/flutter_html_svg/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_svg/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_svg/LICENSE b/packages/flutter_html_svg/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_svg/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_html_svg/README.md b/packages/flutter_html_svg/README.md new file mode 100644 index 0000000000..104c31db5e --- /dev/null +++ b/packages/flutter_html_svg/README.md @@ -0,0 +1,22 @@ +# flutter_html_svg + +SVG widget for flutter_html + +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. + +The package also exposes a few ways to render SVGs within an `` tag, specifically base64 SVGs, asset SVGs, and network SVGs. + +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRender: { + svgTagMatcher(): svgTagRender(), + svgDataUriMatcher(): svgDataImageRender(), + svgAssetUriMatcher(): svgAssetImageRender(), + svgNetworkSourceMatcher(): svgNetworkImageRender(), + } +); +``` diff --git a/packages/flutter_html_svg/lib/flutter_html_svg.dart b/packages/flutter_html_svg/lib/flutter_html_svg.dart new file mode 100644 index 0000000000..e3da0d0f56 --- /dev/null +++ b/packages/flutter_html_svg/lib/flutter_html_svg.dart @@ -0,0 +1,171 @@ +library flutter_html_svg; + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; +// ignore: implementation_imports +import 'package:flutter_html/src/utils.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +CustomRender svgTagRender() => CustomRender.widget(widget: (context, buildChildren) { + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: SvgPicture.string( + context.tree.element?.outerHtml ?? "", + width: double.tryParse(context.tree.element?.attributes['width'] ?? ""), + height: double.tryParse(context.tree.element?.attributes['width'] ?? ""), + ), + onTap: () { + if (MultipleTapGestureDetector.of(buildContext) != null) { + MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); + } + context.parser.onImageTap?.call( + context.tree.element?.outerHtml ?? "", + context, + context.tree.element!.attributes.cast(), + context.tree.element + ); + }, + ); + } + ); +}); + +CustomRender svgDataImageRender() => CustomRender.widget(widget: (context, buildChildren) { + final dataUri = _dataUriFormat.firstMatch(_src(context.tree.element?.attributes.cast() ?? {})!); + final data = dataUri?.namedGroup('data'); + if (data == null) return Container(height: 0, width: 0); + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: dataUri?.namedGroup('encoding') == ';base64' ? SvgPicture.memory( + base64.decode(data.trim()), + width: _width(context.tree.element?.attributes.cast() ?? {}), + height: _height(context.tree.element?.attributes.cast() ?? {}), + ) : SvgPicture.string(Uri.decodeFull(data)), + onTap: () { + if (MultipleTapGestureDetector.of(buildContext) != null) { + MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); + } + context.parser.onImageTap?.call( + Uri.decodeFull(data), + context, + context.tree.element!.attributes.cast(), + context.tree.element + ); + }, + ); + } + ); +}); + +CustomRender svgNetworkImageRender() => CustomRender.widget(widget: (context, buildChildren) { + if (context.tree.element?.attributes["src"] == null) { + return Container(height: 0, width: 0); + } + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: SvgPicture.network( + context.tree.element!.attributes["src"]!, + width: _width(context.tree.element!.attributes.cast()), + height: _height(context.tree.element!.attributes.cast()), + ), + onTap: () { + if (MultipleTapGestureDetector.of(buildContext) != null) { + MultipleTapGestureDetector.of(buildContext)!.onTap?.call(); + } + context.parser.onImageTap?.call( + context.tree.element!.attributes["src"]!, + context, + context.tree.element!.attributes.cast(), + context.tree.element + ); + }, + ); + } + ); +}); + +CustomRender svgAssetImageRender() => CustomRender.widget(widget: (context, buildChildren) { + if ( _src(context.tree.element?.attributes.cast() ?? {}) == null) { + return Container(height: 0, width: 0); + } + final assetPath = _src(context.tree.element!.attributes.cast())!.replaceFirst('asset:', ''); + return Builder( + key: context.key, + builder: (buildContext) { + return GestureDetector( + child: SvgPicture.asset(assetPath), + 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 + ); + }, + ); + } + ); +}); + +CustomRenderMatcher svgTagMatcher() => (context) { + return context.tree.element?.localName == "svg"; +}; + +CustomRenderMatcher svgDataUriMatcher({String? encoding = 'base64', String? mime = 'image/svg+xml'}) => (context) { + if (_src(context.tree.element?.attributes.cast() ?? {}) == null) return false; + final dataUri = _dataUriFormat.firstMatch(_src(context.tree.element?.attributes.cast() ?? {})!); + return context.tree.element?.localName == "img" && + dataUri != null && + (mime == null || dataUri.namedGroup('mime') == mime) && + (encoding == null || dataUri.namedGroup('encoding') == ';$encoding'); +}; + +CustomRenderMatcher svgNetworkSourceMatcher({ + List schemas: const ["https", "http"], + List? domains, + String? extension = "svg", +}) => (context) { + if (_src(context.tree.element?.attributes.cast() ?? {}) == null) return false; + try { + final src = Uri.parse(_src(context.tree.element?.attributes.cast() ?? {})!); + return context.tree.element?.localName == "img" && + schemas.contains(src.scheme) && + (domains == null || domains.contains(src.host)) && + (extension == null || src.path.endsWith(".$extension")); + } catch (e) { + return false; + } + }; + +CustomRenderMatcher svgAssetUriMatcher() => (context) => + context.tree.element?.localName == "img" && + _src(context.tree.element?.attributes.cast() ?? {}) != null + && _src(context.tree.element?.attributes.cast() ?? {})!.startsWith("asset:") + && _src(context.tree.element?.attributes.cast() ?? {})!.endsWith(".svg"); + +final _dataUriFormat = RegExp("^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)"); + +String? _src(Map attributes) { + return attributes["src"]; +} + +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); +} \ No newline at end of file diff --git a/packages/flutter_html_svg/pubspec.yaml b/packages/flutter_html_svg/pubspec.yaml new file mode 100644 index 0000000000..37362f7484 --- /dev/null +++ b/packages/flutter_html_svg/pubspec.yaml @@ -0,0 +1,57 @@ +name: flutter_html_svg +description: SVG widget for flutter_html +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + flutter_svg: '>=1.0.0 <2.0.0' + #todo change this + flutter_html: + path: ../.. + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_html_table/.gitignore b/packages/flutter_html_table/.gitignore new file mode 100644 index 0000000000..a247422ef7 --- /dev/null +++ b/packages/flutter_html_table/.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_table/.metadata b/packages/flutter_html_table/.metadata new file mode 100644 index 0000000000..a1f847eadf --- /dev/null +++ b/packages/flutter_html_table/.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_table/CHANGELOG.md b/packages/flutter_html_table/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_table/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_table/LICENSE b/packages/flutter_html_table/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_table/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_html_table/README.md b/packages/flutter_html_table/README.md new file mode 100644 index 0000000000..7c9220749c --- /dev/null +++ b/packages/flutter_html_table/README.md @@ -0,0 +1,17 @@ +# flutter_html_table + +Table widget for flutter_html. + +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. + +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRender: { + tableMatcher(): tableRender(), + } +); +``` diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart new file mode 100644 index 0000000000..7791f36f2c --- /dev/null +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -0,0 +1,158 @@ +library flutter_html_table; + +import 'dart:math'; + +import 'package:flutter_layout_grid/flutter_layout_grid.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_html/flutter_html.dart'; + +CustomRender tableRender() => CustomRender.widget(widget: (context, buildChildren) { + return Container( + key: context.key, + margin: context.style.margin?.nonNegative, + padding: context.style.padding?.nonNegative, + alignment: context.style.alignment, + decoration: BoxDecoration( + color: context.style.backgroundColor, + border: context.style.border, + ), + width: context.style.width, + height: context.style.height, + child: LayoutBuilder(builder: (_, constraints) => _layoutCells(context, constraints)), + ); +}); + +CustomRenderMatcher tableMatcher() => (context) { + return context.tree.element?.localName == "table"; +}; + +Widget _layoutCells(RenderContext context, BoxConstraints constraints) { + final rows = []; + List columnSizes = []; + for (var child in context.tree.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 = 0; + List rowSpanOffsets = []; + for (final row in rows) { + final cols = row.children.whereType().fold(0, (int value, child) => value + child.colspan) + + rowSpanOffsets.fold(0, (int offset, child) => child); + columnMax = max(cols, columnMax); + rowSpanOffsets = [ + ...rowSpanOffsets.map((value) => value - 1).where((value) => value > 0), + ...row.children.whereType().map((cell) => cell.rowspan - 1), + ]; + } + + // Place the cells in the rows/columns + final cells = []; + final columnRowOffset = List.generate(columnMax, (_) => 0); + final columnColspanOffset = 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; + } + if (child is TableCellElement) { + while (columnRowOffset[columni] > 0) { + columnRowOffset[columni] = columnRowOffset[columni] - 1; + columni += columnColspanOffset[columni].clamp(1, columnMax - columni - 1); + } + cells.add(GridPlacement( + child: Container( + width: child.style.width ?? double.infinity, + height: child.style.height, + padding: child.style.padding?.nonNegative ?? row.style.padding?.nonNegative, + 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 ?? + context.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; + columnColspanOffset[columni] = child.colspan; + 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()); + + if (finalColumnSizes.isEmpty || rowSizes.isEmpty) { + // No actual cells to show + return SizedBox(); + } + + return LayoutGrid( + gridFit: GridFit.loose, + columnSizes: finalColumnSizes, + rowSizes: rowSizes, + children: cells, + ); +} diff --git a/packages/flutter_html_table/pubspec.yaml b/packages/flutter_html_table/pubspec.yaml new file mode 100644 index 0000000000..973f4a4fb7 --- /dev/null +++ b/packages/flutter_html_table/pubspec.yaml @@ -0,0 +1,57 @@ +name: flutter_html_table +description: Table widget for flutter_html. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + flutter_layout_grid: '>=1.0.1 <2.0.0' + #todo change this + flutter_html: + path: ../.. + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_html_video/.gitignore b/packages/flutter_html_video/.gitignore new file mode 100644 index 0000000000..a247422ef7 --- /dev/null +++ b/packages/flutter_html_video/.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_video/.metadata b/packages/flutter_html_video/.metadata new file mode 100644 index 0000000000..a1f847eadf --- /dev/null +++ b/packages/flutter_html_video/.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_video/CHANGELOG.md b/packages/flutter_html_video/CHANGELOG.md new file mode 100644 index 0000000000..41cc7d8192 --- /dev/null +++ b/packages/flutter_html_video/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_html_video/LICENSE b/packages/flutter_html_video/LICENSE new file mode 100644 index 0000000000..ba75c69f7f --- /dev/null +++ b/packages/flutter_html_video/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_html_video/README.md b/packages/flutter_html_video/README.md new file mode 100644 index 0000000000..ebfd64c406 --- /dev/null +++ b/packages/flutter_html_video/README.md @@ -0,0 +1,17 @@ +# flutter_html_video + +Video widget for flutter_html. + +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. + +#### Registering the `CustomRender`: + +```dart +Widget html = Html( + customRender: { + videoMatcher(): videoRender(), + } +); +``` \ No newline at end of file diff --git a/packages/flutter_html_video/lib/flutter_html_video.dart b/packages/flutter_html_video/lib/flutter_html_video.dart new file mode 100644 index 0000000000..90591d5c55 --- /dev/null +++ b/packages/flutter_html_video/lib/flutter_html_video.dart @@ -0,0 +1,93 @@ +library flutter_html_video; + +import 'package:chewie/chewie.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 VideoControllerCallback = void Function(dom.Element?, ChewieController, VideoPlayerController); + +CustomRender videoRender({VideoControllerCallback? onControllerCreated}) + => CustomRender.widget(widget: (context, buildChildren) + => VideoWidget(context: context, callback: onControllerCreated)); + +CustomRenderMatcher videoMatcher() => (context) { + return context.tree.element?.localName == "video"; +}; + +class VideoWidget extends StatefulWidget { + final RenderContext context; + final VideoControllerCallback? callback; + + VideoWidget({ + required this.context, + this.callback, + }); + + @override + State createState() => _VideoWidgetState(); +} + +class _VideoWidgetState extends State { + ChewieController? chewieController; + VideoPlayerController? videoController; + double? _width; + double? _height; + late final List sources; + + @override + void initState() { + final attributes = widget.context.tree.element?.attributes ?? {}; + final sources = [ + if (attributes['src'] != null) + attributes['src'], + ...ReplacedElement.parseMediaSources(widget.context.tree.element!.children), + ]; + if (sources.isNotEmpty && sources.first != null) { + _width = double.tryParse(attributes['width'] ?? (attributes['height'] ?? 150) * 2); + _height = double.tryParse(attributes['height'] ?? (attributes['width'] ?? 300) / 2); + videoController = VideoPlayerController.network(sources.first!); + chewieController = ChewieController( + videoPlayerController: videoController!, + placeholder: attributes['poster'] != null && attributes['poster']!.isNotEmpty + ? Image.network(attributes['poster']!) + : Container(color: Colors.black), + autoPlay: attributes['autoplay'] != null, + looping: attributes['loop'] != null, + showControls: attributes['controls'] != null, + autoInitialize: true, + aspectRatio: _width == null || _height == null ? null : _width! / _height!, + ); + widget.callback?.call(widget.context.tree.element, chewieController!, videoController!); + } + super.initState(); + } + + @override + void dispose() { + chewieController?.dispose(); + videoController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext bContext) { + if (sources.isEmpty || sources.first == null) { + return Container(height: 0, width: 0); + } + final child = Container( + key: widget.context.key, + child: Chewie( + controller: chewieController!, + ), + ); + if (_width == null || _height == null) { + return child; + } + return AspectRatio( + aspectRatio: _width! / _height!, + child: child, + ); + } +} \ No newline at end of file diff --git a/packages/flutter_html_video/pubspec.yaml b/packages/flutter_html_video/pubspec.yaml new file mode 100644 index 0000000000..1741c1192f --- /dev/null +++ b/packages/flutter_html_video/pubspec.yaml @@ -0,0 +1,58 @@ +name: flutter_html_video +description: Video widget for flutter_html. +version: 0.0.1 +homepage: + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + video_player: '>=2.1.1 <3.0.0' + chewie: '>=1.1.0 <2.0.0' + # todo change this + flutter_html: + path: ../.. + +dev_dependencies: + flutter_test: + sdk: flutter + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/pubspec.yaml b/pubspec.yaml index 5fbea00c7d..a56843c86c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,29 +11,10 @@ dependencies: # Plugin for parsing html html: '>=0.15.0 <1.0.0' - # Plugins for parsing css + # Plugin for parsing css csslib: '>=0.17.0 <1.0.0' - # Plugins for rendering the tag. - flutter_layout_grid: '>=1.0.1 <2.0.0' - - # Plugins for rendering the