Commit d13e7637 authored by 汪林玲's avatar 汪林玲

版本1.3.0

parents
version: 2.1
orbs:
codecov: codecov/codecov@1.0.2
jobs:
build:
docker:
- image: cirrusci/flutter
steps:
- checkout
- run: flutter --version
- run: flutter test --coverage
- codecov/upload:
file: coverage/lcov.info
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: flutter_html
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with a single custom sponsorship URL
.DS_Store
# Created by https://www.gitignore.io/api/flutter
# Edit at https://www.gitignore.io/?templates=flutter
### Flutter ###
# Flutter/Dart/Pub related
**/doc/api/
.dart_tool/
.flutter-plugins
.packages
.pub-cache/
.pub/
build/
pubspec.lock
# 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/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# End of https://www.gitignore.io/api/flutter
### JetBrains+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### JetBrains+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
# End of https://www.gitignore.io/api/flutter,jetbrains+all
**/.flutter-plugins-dependencies
**/flutter_export_environment.sh
# 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: 20e59316b8b8474554b38493b8ca888794b0234a
channel: stable
project_type: package
## [1.3.0] - February 16, 2021:
* New image loading API
* Image loading with request headers, from relative paths and custom loading widget
* SVG image support from network or local assets
* Support for `<details>`/`<summary>` tags
* Allow returning spans from custom tag renders
* Inline font styling
* Content-based table column sizing
* Respect iframe sandbox attribute
* Fixed text flow and styling when using tags inside `<a>` links
* Fixed issue where `shrinkWrap` property would not constrain the widget to take up the space it needs
* See the [Notes](https://github.com/Sub6Resources/flutter_html#notes) for an example usage with `shrinkWrap`
* Fixed issue where iframes would not update when their `src`s changed in the HTML data
* Updated dependencies for Flutter 1.26+
## [1.2.0] - January 14, 2021:
* Support irregular table sizes
* Allow for returning `null` from a customRender function to disable the widget
## [1.1.1] - November 22, 2020:
* Update dependencies
## [1.1.0] - November 22, 2020:
* Add support for inline styles
* Update dependencies
## [1.0.2] - August 8, 2020:
* Fix text scaling issues
* Update dependencies
## [1.0.1] - August 8, 2020:
* Fixed flutter_svg: ^0.18.0
# [1.0.0]
* BREAKING CHANGES (see the [Migration Guide](https://github.com/Sub6Resources/flutter_html/wiki/1.0.0-Migration-Guide) for a full overview of breaking changes.):
* The default parser has been completely rewritten and the RichText parser has been removed.
* `useRichText` no longer is necessary (The new parser uses RichText under the hood)
* `customRender` now works for the default parser.
* Adds support for `<audio>`, `<video>`, `<iframe>`, `<svg>`, `<ruby>`, `<rt>`, `<rp>`, `<sub>`, and `<sup>`
* Adds support for over 20 CSS attributes when using the `style` parameter.
* Fixes many many issues (see the list at [#122](https://github.com/Sub6Resources/flutter_html/pull/122))
* The following parameters of `Html` have been removed and should no longer be used (see the migration guide):
* `useRichText`
* `padding`
* `backgroundColor`
* `defaultTextStyle`
* `renderNewlines`
* `customEdgeInsets`
* `customTextStyle`
* `blockSpacing`
* `customTextAlign`
* `linkStyle`
* `imageProperties`
* `showImages`
* The default text style now matches the app's Material `TextTheme.bodyText2` (Fixes [#18](https://github.com/Sub6Resources/flutter_html/issues/18)).
* Requires Flutter v1.17.0 or greater
* Fixed quite a few issues with `img`
* Added a fancy new `style` attribute (this should be used in place of the deprecated styling parameters).
## [1.0.0-pre.1] - December 27, 2019
* For a list of pre-release changes, including several BREAKING CHANGES, see release notes for 1.0.0 above.
## [0.11.1] - December 14, 2019:
* Add support for `AssetImage`s using the `asset:` prefix ([#162](https://github.com/Sub6Resources/flutter_html/pull/162)).
## [0.11.0] - September 10, 2019:
* Make it so `width=100%` doesn't throw error. Fixes [#118](https://github.com/Sub6Resources/flutter_html/issues/118).
* You can now set width and/or height in `ImageProperties` to negative to ignore the `width` and/or `height` values from the html. Fixes [#97](https://github.com/Sub6Resources/flutter_html/issues/97)
* The `img` `alt` property now renders correctly when the image fails to load and with the correct style. Fixes [#96](https://github.com/Sub6Resources/flutter_html/issues/96)
* Add partial support for `sub` tag.
* Add new option: `shrinkToFit` ([#148](https://github.com/Sub6Resources/flutter_html/pull/148)). Fixes [#75](https://github.com/Sub6Resources/flutter_html/issues/75).
## [0.10.4] - June 22, 2019:
* Add support for `customTextStyle` to block and specialty HTML elements.
## [0.10.3] - June 20, 2019:
* Add `src` to the `onImageTap` callback ([#93](https://github.com/Sub6Resources/flutter_html/pull/93))
## [0.10.2] - June 19, 2019:
* Add `customTextAlign` property ([#112](https://github.com/Sub6Resources/flutter_html/pull/112))
* Use `tryParse` instead of `parse` for image width and height attributes so that `%` values are ignored safely. Fixes [#98](https://github.com/Sub6Resources/flutter_html/issues/98)
## [0.10.1] - May 20, 2019:
* Image properties and onImageTap for the richTextParser, plus some fixes ([#90](https://github.com/Sub6Resources/flutter_html/pull/90))
* Hotfix 1 (June 6, 2019): Fixes [#100](https://github.com/Sub6Resources/flutter_html/issues/100)
## [0.10.0] - May 18, 2019:
* **BREAKING:** `useRichText` now defaults to `true`
* Support for `aside`, `bdi`, `big`, `cite`, `data`, `ins`, `kbd`, `mark`, `nav`, `noscript`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `strike`, `template`, `time`, `tt`, and `var` added to `RichText` parser.
## [0.9.9] - May 17, 2019:
* Fixes extra padding issue ([#87](https://github.com/Sub6Resources/flutter_html/issues/87))
## [0.9.8] - May 14, 2019:
* Add support for `address` tag in `RichText` parser.
## [0.9.7] - May 13, 2019:
* Added onImageError callback
* Added custom textstyle and edgeinsets callback ([#72](https://github.com/Sub6Resources/flutter_html/pull/72))
* Update dependency versions ([#84](https://github.com/Sub6Resources/flutter_html/issues/84))
* Fixes [#82](https://github.com/Sub6Resources/flutter_html/issues/82) and [#86](https://github.com/Sub6Resources/flutter_html/issues/86)
## [0.9.6] - March 11, 2019:
* Fix whitespace issue. ([#59](https://github.com/Sub6Resources/flutter_html/issues/59))
## [0.9.5] - March 11, 2019:
* Add support for `span` in `RichText` parser. ([#61](https://github.com/Sub6Resources/flutter_html/issues/61))
* Adds `linkStyle` attribute. ([#70](https://github.com/Sub6Resources/flutter_html/pull/70))
* Adds tests for `header`, `hr`, and `i` ([#62](https://github.com/Sub6Resources/flutter_html/issues/62))
## [0.9.4] - February 5, 2019:
* Fixes `table` error in `RichText` parser. ([#58](https://github.com/Sub6Resources/flutter_html/issues/58))
## [0.9.3] - January 31, 2019:
* Adds support for base64 encoded images
## [0.9.2] - January 31, 2019:
* Adds partial support for deprecated `font` tag.
## [0.9.1] - January 31, 2019:
* Adds full support for `sub` and `sup`. ([#46](https://github.com/Sub6Resources/flutter_html/pull/46))
* Fixes weak warning caught by Pub analysis ([#54](https://github.com/Sub6Resources/flutter_html/issues/54))
## [0.9.0] - January 31, 2019:
* Adds an alternate `RichText` parser and `useRichText` parameter. ([#37](https://github.com/Sub6Resources/flutter_html/pull/37))
## [0.8.2] - November 1, 2018:
* Removes debug prints.
## [0.8.1] - October 19, 2018:
* Adds `typedef` for `onLinkTap` function.
## [0.8.0] - October 18, 2018:
* Adds custom tag callback
* Logging no longer shows up in production.
## [0.7.1] - September 11, 2018:
* Fixes issue with text nodes that contain only a space. ([#24](https://github.com/Sub6Resources/flutter_html/issues/24))
* Fixes typo in README.md from 0.7.0.
## [0.7.0] - September 10, 2018:
* Adds full support for `ul` and `ol`
## [0.6.2] - September 5, 2018:
* Adds check for `img src` before trying to load it.
* Adds support for `img alt` attribute.
## [0.6.1] - September 4, 2018:
* Fixed minor typo
## [0.6.0] - September 4, 2018:
* Update README.md and example
* GitHub version 0.6.0 milestone reached
## [0.5.6] - September 4, 2018:
* Adds partial support for `center` and a `renderNewlines` property on the `Html` widget.
## [0.5.5] - September 4, 2018:
* Adds support for `acronym`, and `big`.
## [0.5.4] - August 31, 2018:
* Adds `onLinkTap` callback.
## [0.5.3] - August 25, 2018:
* Adds support for `strike`, and `tt`.
## [0.5.2] - August 25, 2018:
* Adds support for `bdi` and `bdo`
## [0.5.1] - August 25, 2018:
* Fixed issue with table rows not lining up correctly ([#4](https://github.com/Sub6Resources/flutter_html/issues/4))
## [0.5.0] - August 23, 2018:
* Major refactor that makes entire tree a Widget and eliminates the need to distinguish between inline and block elements.
* Fixed [#7](https://github.com/Sub6Resources/flutter_html/issues/7), [#9](https://github.com/Sub6Resources/flutter_html/issues/9), [#10](https://github.com/Sub6Resources/flutter_html/issues/10), and [#11](https://github.com/Sub6Resources/flutter_html/issues/11).
## [0.4.1] - August 15, 2018:
* Fixed issue with images not loading when inside of `p` tag ([#6](https://github.com/Sub6Resources/flutter_html/issues/6))
## [0.4.0] - August 15, 2018:
* Adds support for `table`, `tbody`, `tfoot`, `thead`, `tr`, `td`, `th`, and `caption`
## [0.3.1] - August 15, 2018:
* Fixed issue where `p` was not rendered with the `defaultTextStyle`.
## [0.3.0] - August 15, 2018:
* Adds support for `abbr`, `address`, `article`, `aside`, `blockquote`, `br`, `cite`, `code`, `data`, `dd`,
`del`, `dfn`, `dl`, `dt`, `figcaption`, `figure`, `footer`, `header`, `hr`, `img`, `ins`, `kbd`, `li`,
`main`, `mark`, `nav`, `noscript`, `pre`, `q`, `rp`, `rt`, `ruby`, `s`, `samp`, `section`, `small`, `span`,
`template`, `time`, and `var`
* Adds partial support for `a`, `ol`, and `ul`
## [0.2.0] - August 14, 2018:
* Adds support for `img`.
## [0.1.1] - August 14, 2018:
* Fixed `b` to be bold, not italic...
* Adds support for `em`, and `strong`
* Adds support for a default `TextStyle`
## [0.1.0] - August 14, 2018:
* Renamed widget from `HtmlWidget` to `Html`
* Adds support for `p`, `h1`, `h2`, `h3`, `h4`, `h5`, and `h6`.
## [0.0.1] - August 14, 2018:
* Adds support for `body`, `div`, `b`, `i`, and `u`.
Thanks for your interest in contributing to `flutter_html`!
I'm pretty busy, so in order to help me best make use of the time I spend working on this project, please adhere to the following guidelines when contributing:
1. In general, don't submit a pull request without discussing the feature with me, in an issue, first. I don't want you to have to do a whole bunch of work for nothing. This also makes it so there won't be a whole bunch of changes I make you do in order to have your pull request merged.
2. Please read the [wiki](https://github.com/Sub6Resources/flutter_html/wiki) before contributing (there are only two pages at the moment). This will give you an idea of what my plans are for this repository, and what you do and don't need to work on.
More specific guidelines will be added soon.
MIT License
Copyright (c) 2019 Matthew Whitaker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# flutter_html
[![pub package](https://img.shields.io/pub/v/flutter_html.svg)](https://pub.dev/packages/flutter_html)
[![codecov](https://codecov.io/gh/Sub6Resources/flutter_html/branch/master/graph/badge.svg)](https://codecov.io/gh/Sub6Resources/flutter_html)
[![CircleCI](https://circleci.com/gh/Sub6Resources/flutter_html.svg?style=svg)](https://circleci.com/gh/Sub6Resources/flutter_html)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/Sub6Resources/flutter_html/blob/master/LICENSE)
A Flutter widget for rendering HTML and CSS as Flutter widgets.
<table>
<tr>
<td align="center">Screenshot 1</td>
<td align="center">Screenshot 2</td>
<td align="center">Screenshot 3</td>
</tr>
<tr>
<td><img alt="A Screenshot of flutter_html" src="https://github.com/Sub6Resources/flutter_html/blob/master/.github/flutter_html_screenshot.png" width="250"/></td>
<td><img alt="Another Screenshot of flutter_html" src="https://github.com/Sub6Resources/flutter_html/blob/master/.github/flutter_html_screenshot2.png" width="250"/></td>
<td><img alt="Yet another Screenshot of flutter_html" src="https://github.com/Sub6Resources/flutter_html/blob/master/.github/flutter_html_screenshot3.png" width="250"/></td>
</tr>
</table>
## Table of Contents:
- [Installing](#installing)
- [Currently Supported HTML Tags](#currently-supported-html-tags)
- [Currently Supported CSS Attributes](#currently-supported-css-attributes)
- [Why flutter_html?](#why-this-package)
- [API Reference](#api-reference)
- [Parameters Table](#parameters)
- [Data](#data)
- [onLinkTap](#onlinktap)
- [customRender](#customrender)
- [onImageError](#onimageerror)
- [onImageTap](#onimagetap)
- [blacklistedElements](#blacklistedelements)
- [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)
- [Audio](#audio)
- [Video](#video)
- [SVG](#svg)
- [Table](#table)
- [Notes](#notes)
- [Migration Guide](#migration-guides)
- [Contribution Guide](#contribution-guide)
## Installing:
Add the following to your `pubspec.yaml` file:
dependencies:
flutter_html: ^1.3.0
## Currently Supported HTML Tags:
| | | | | | | | | | | |
|------------|-----------|-------|-------------|---------|---------|-------|------|--------|--------|--------|
|`a` | `abbr` | `acronym`| `address` | `article`| `aside` | `audio`| `b` | `bdi` | `bdo` | `big` |
|`blockquote`| `body` | `br` | `caption` | `cite` | `code` | `data`| `dd` | `del` | `details` | `dfn` |
| `div` | `dl` | `dt` | `em` | `figcaption`| `figure`| `footer`| `h1` | `h2` | `h3` | `h4` |
| `h5` |`h6` | `header` | `hr` | `i` | `iframe`| `img` | `ins` | `kbd`| `li` | `main` |
| `mark` | `nav` | `noscript`|`ol` | `p` | `pre` | `q` | `rp` | `rt` | `ruby` | `s` |
| `samp` | `section` | `small` | `span`| `strike` | `strong`| `sub` | `sup` | `summary` | `svg`| `table`|
| `tbody` | `td` | `template` | `tfoot` | `th` | `thead` |`time` | `tr` | `tt` | `u` | `ul` |
| `var` | `video` | | | | | | | | | |
## Currently Supported CSS Attributes:
| | | | | | | |
|------------------|--------|------------|----------|--------------|------------------------|------------|
|`background-color`| `color`| `direction`| `display`| `font-family`| `font-feature-settings`| `font-size`|
|`font-style` | `font-weight`| `height` | `letter-spacing`| `line-height`| `list-style-type` | `list-style-position`|
|`padding` | `margin`| `text-align`| `text-decoration`| `text-decoration-color`| `text-decoration-style`| `text-decoration-thickness`|
|`text-shadow` | `vertical-align`| `white-space`| `width` | `word-spacing`| | |
Don't see a tag or attribute you need? File a feature request or contribute to the project!
## Why this package?
This package is designed with simplicity in mind. Originally created to allow basic rendering of HTML content into the Flutter widget tree,
this project has expanded to include support for basic styling as well!
If you need something more robust and customizable, the package also provides a number of optional custom APIs for extremely granular control over widget rendering!
## API Reference:
For the full API reference, see [here](https://pub.dev/documentation/flutter_html/latest/).
For a full example, see [here](https://github.com/Sub6Resources/flutter_html/tree/master/example).
Below, you will find brief descriptions of the parameters the`Html` widget accepts and some code snippets to help you use this package.
### Parameters:
| Parameters | Description |
|--------------|-----------------|
| `data` | The HTML data passed to the `Html` widget. This is required and cannot be null. |
| `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. |
| `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. |
| `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. |
| `blacklistedElements` | A list of elements the `Html` widget should not render. The list should contain the tags of the HTML elements you wish to blacklist. |
| `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. |
### Data:
The HTML data passed to the `Html` widget as a `String`. This is required and cannot be null.
Any HTML tags in the `String` that are not supported by the package will not be rendered.
#### Example Usage - Data:
```dart
Widget html = Html(
data: """<div>
<h1>Demo Page</h1>
<p>This is a fantastic product that you should buy!</p>
<h3>Features</h3>
<ul>
<li>It actually works</li>
<li>It exists</li>
<li>It doesn't cost much!</li>
</ul>
<!--You can pretty much put any html in here!-->
</div>""",
);
```
### onLinkTap:
A function that defines what the widget should do when a link is tapped.
#### Example Usage - onLinkTap:
```dart
Widget html = Html(
data: """<p>
Linking to <a href='https://github.com'>websites</a> has never been easier.
</p>""",
onLinkTap: (String url) {
//open URL in webview, or launch URL in browser, or any other logic here
}
);
```
### customRender:
A powerful API that allows you to customize everything when rendering a specific HTML tag. This means you can add support for HTML elements that aren't supported natively. You can also make up your own custom tags in your HTML!
`customRender` accepts a `Map<String, CustomRender>`. The `CustomRender` type is a function that requires a `Widget` to be returned. It exposes `RenderContext`, the `Widget` that would have been rendered by `Html` without a `customRender` defined, the `attributes` of the HTML element as a `Map<String, String>`, and the HTML element itself as `Element`.
To use this API, set the key as the tag of the HTML element you wish to provide a custom implementation for, and create a function with the above parameters that returns a `Widget`.
#### Example Usages - customRender:
1. Simple example - rendering custom HTML tags
<details><summary>View code</summary>
```dart
Widget html = Html(
data: """
<h3>Display bird element and flutter element <bird></bird></h3>
<flutter></flutter>
<flutter horizontal></flutter>
""",
customRender: {
"bird": (RenderContext context, Widget child, Map<String, String> attributes, _) {
return TextSpan(text: "🐦");
},
"flutter": (RenderContext context, Widget child, Map<String, String> attributes, _) {
return FlutterLogo(
style: (attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color,
size: context.style.fontSize.size * 5,
);
},
},
);
```
</details>
2. Complex example - rendering an `iframe` differently based on whether it is an embedded youtube video or some other embedded content
<details><summary>View code</summary>
```dart
Widget html = Html(
data: """
<h3>Google iframe:</h3>
<iframe src="https://google.com"></iframe>
<h3>YouTube iframe:</h3>
<iframe src="https://www.youtube.com/embed/tgbNymZ7vqY"></iframe>
""",
customRender: {
"iframe": (RenderContext context, Widget child, Map<String, String> attributes, _) {
if (attributes != null) {
double width = double.tryParse(attributes['width'] ?? "");
double height = double.tryParse(attributes['height'] ?? "");
print(attributes['src']);
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: attributes['src'],
javascriptMode: JavascriptMode.unrestricted,
//no need for scrolling gesture recognizers on embedded youtube, so set gestureRecognizers null
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
gestureRecognizers: attributes['src'].contains("youtube.com/embed") ? null : [
Factory(() => VerticalDragGestureRecognizer())
].toSet(),
navigationDelegate: (NavigationRequest request) async {
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
//on other iframe content allow all url loading
if (attributes['src'].contains("youtube.com/embed")) {
if (!request.url.contains("youtube.com/embed")) {
return NavigationDecision.prevent;
} else {
return NavigationDecision.navigate;
}
} else {
return NavigationDecision.navigate;
}
},
),
);
} else {
return Container(height: 0);
}
}
}
);
```
</details>
More example usages and in-depth details available [here](https://github.com/Sub6Resources/flutter_html/wiki/All-About-customRender).
### 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.
#### Example Usage - onImageError:
```dart
Widget html = Html(
data: """<img alt='Alt Text of an intentionally broken image' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30d'/>""",
onImageError: (Exception exception, StackTrace stackTrace) {
FirebaseCrashlytics.instance.recordError(exception, stackTrace);
},
);
```
### onImageTap:
A function that defines what the widget should do when an image is tapped.
#### Example Usage - onImageTap:
```dart
Widget html = Html(
data: """<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />""",
onImageTap: (String url) {
//open image in webview, or launch image in browser, or any other logic here
}
);
```
### blacklistedElements:
A list of elements the `Html` widget should not render. The list should contain the tags of the HTML elements you wish to blacklist.
#### Example Usage - blacklistedElements:
You may have instances where you can choose between two different types of HTML tags to display the same content. In the example below, the `<video>` and `<iframe>` elements are going to display the same content.
The `blacklistedElements` parameter allows you to change which element is rendered. Iframes can be advantageous because they allow parallel loading - Flutter just has to wait for the webview to be initialized before rendering the page, possibly cutting down on load time. Video can be advantageous because it provides a 100% native experience with Flutter widgets, but it may take more time to render the page. You may know that Flutter webview is a little janky in its current state on Android, so using `blacklistedElements` and a simple condition, you can get the best of both worlds - choose the video widget to render on Android and the iframe webview to render on iOS.
```dart
Widget html = Html(
data: """
<video controls>
<source src="https://www.w3schools.com/html/mov_bbb.mp4" />
</video>
<iframe src="https://www.w3schools.com/html/mov_bbb.mp4"></iframe>""",
blacklistedElements: [Platform.isAndroid ? "iframe" : "video"]
);
```
### style:
A powerful API that allows you to customize the style that should be used when rendering a specific HTMl tag.
`style` accepts a `Map<String, Style>`. The `Style` type is a class that allows you to set all the CSS styling the package currently supports. See [here](https://pub.dev/documentation/flutter_html/latest/style/Style-class.html) for the full list.
To use this API, set the key as the tag of the HTML element you wish to provide a custom implementation for, and set the value to be a `Style` with your customizations.
#### Example Usage - style:
```dart
Widget html = Html(
data: """
<h1>Table support:</h1>
<table>
<colgroup>
<col width="50%" />
<col span="2" width="25%" />
</colgroup>
<thead>
<tr><th>One</th><th>Two</th><th>Three</th></tr>
</thead>
<tbody>
<tr>
<td rowspan='2'>Rowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan</td><td>Data</td><td>Data</td>
</tr>
<tr>
<td colspan="2"><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></td>
</tr>
</tbody>
<tfoot>
<tr><td>fData</td><td>fData</td><td>fData</td></tr>
</tfoot>
</table>""",
style: {
// tables will have the below background color
"table": Style(
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
),
// some other granular customizations are also possible
"tr": Style(
border: Border(bottom: BorderSide(color: Colors.grey)),
),
"th": Style(
padding: EdgeInsets.all(6),
backgroundColor: Colors.grey,
),
"td": Style(
padding: EdgeInsets.all(6),
alignment: Alignment.topLeft,
),
// text that renders h1 elements will be red
"h1": Style(color: Colors.red),
}
);
```
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: """
<h3>YouTube iframe:</h3>
<iframe src="https://google.com"></iframe>
<h3>Google iframe:</h3>
<iframe src="https://www.youtube.com/embed/tgbNymZ7vqY"></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, ImageRender>`. `ImageSourceMatcher` provides the matching function, while `ImageRender` provides the widget to be rendered.
The default image renders are:
```dart
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
base64UriMatcher(): base64ImageRender(),
assetUriMatcher(): assetImageRender(),
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
networkSourceMatcher(): networkImageRender(),
};
```
See [the source code](https://github.com/Sub6Resources/flutter_html/blob/master/lib/image_render.dart) for details on how these are implemented.
When setting `customImageRenders`, the package will prioritize the custom renders first, while the default ones are used as a fallback.
Note: Order is very important when you set `customImageRenders`. The more specific your `ImageSourceMatcher`, the higher up in the `customImageRender` list it should be.
#### typedef ImageSourceMatcher
This is type defined as a function that passes the attributes as a `Map<String, String>` 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.
A typical usage would look something like this:
```dart
ImageSourceMatcher base64UriMatcher() => (attributes, element) =>
attributes["src"].startsWith("data:image") &&
attributes["src"].contains("base64,");
```
In the above example, the matcher checks whether the image's `src` either starts with "data:image" or contains "base64,", since these indicate an image in base64 format.
You can also declare your own variables in the function itself, which would look like this:
```dart
ImageSourceMatcher networkSourceMatcher({
/// all three are optional, you don't need to have these in the function
List<String> schemas: const ["https", "http"],
List<String> domains: const ["your domain 1", "your domain 2"],
String extension: "your extension",
}) =>
(attributes, element) {
final src = Uri.parse(attributes["src"]);
return schemas.contains(src.scheme) &&
(domains == null || domains.contains(src.host)) &&
(extension == null || src.path.endsWith(".$extension"));
};
```
In the above example, the possible schemas are checked against the scheme of the `src`, and optionally the domains and extensions are also checked. This implementation allows for extremely granular control over what images are matched, and could even be changed on the fly with a variable.
#### typedef ImageRender
This is a type defined as a function that passes the attributes of the image as a `Map<String, String>`, 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`.
A typical usage might look like this:
```dart
ImageRender base64ImageRender() => (context, attributes, element) {
final decodedImage = base64.decode(attributes["src"].split("base64,")[1].trim());
return Image.memory(
decodedImage,
);
};
```
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:
```dart
ImageRender networkImageRender({
Map<String, String> headers,
double width,
double height,
Widget Function(String) altWidget,
}) =>
(context, attributes, element) {
return Image.network(
attributes["src"],
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;
},
);
};
```
Implementing these variables allows you to customize every last detail of how the widget is rendered.
#### Example Usages - customImageRender:
`customImageRender` can be used in two different ways:
1. Overriding a default render:
```dart
Widget html = Html(
data: """
<img alt='Flutter' src='https://flutter.dev/assets/flutter-lockup-1caf6476beed76adec3c477586da54de6b552b2f42108ec5bc68dc63bae2df75.png' /><br />
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /><br />
""",
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),
},
);
```
Above, there are three custom `networkSourceMatcher`s, which will be applied - in order - before the default implementations.
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.
2. Creating your own renders:
```dart
ImageSourceMatcher classAndIdMatcher({String classToMatch, String idToMatch}) => (attributes, element) =>
attributes["class"].contains(classToMatch) ||
attributes["id"].contains(idToMatch);
ImageRender classAndIdRender({String classToMatch, String idToMatch}) => (context, attributes, element) {
if (attributes["class"].contains(classToMatch)) {
return Image.asset(attributes["src"]);
} else {
return Image.network(
attributes["src"],
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: """
<img alt='alt text' class='class1-class2' src='assets/flutter.png' /><br />
<img alt='alt text 2' id='imageId' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /><br />
""",
customImageRenders: {
classAndIdMatcher(classToMatch: "class1", idToMatch: "imageId"): classAndIdRender(classToMatch: "class1", idToMatch: "imageId")
},
);
```
The above example has a matcher that checks for either a class or an id, and then returns two different widgets based on whether a class was matched or an id was matched.
The sky is the limit when using the custom image renders. You can make it as granular as you want, or as all-encompassing as you want, and you have full control of everything. Plus you get the package's style parsing to use in your custom widgets, so your code looks neat and readable!
## 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 `<img>`, 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.
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.
Note: Audio elements currently do not work on iOS due to a bug with `chewie_audio`. Once [#509](https://github.com/Sub6Resources/flutter_html/pull/509) is merged, it will work again.
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.
### 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 `<svg>` tag and passes it to `flutter_svg`. The `width` and `height` attributes are considered while rendering, if given.
### Table
This package renders table elements using the [`flutter_layout_grid`](https://pub.dev/packages/flutter_layout_grid) plugin.
When rendering table elements, the package tries to calculate the best fit for each element and size its cell accordingly. `Rowspan`s and `colspan`s are considered in this process, so cells that span across multiple rows and columns are rendered as expected. Heights are determined intrinsically to maintain an optimal aspect ratio for the cell.
## Notes
1. If you'd like to use this widget inside of a `Row()`, make sure to set `shrinkWrap: true` and place your widget inside expanded:
```dart
Widget row = Row(
children: [
Expanded(
child: Html(
shrinkWrap: true,
//other params
)
),
//whatever other widgets
]
);
```
## Migration Guides
- For Version 1.0 - [Guide](https://github.com/Sub6Resources/flutter_html/wiki/1.0.0-Migration-Guide)
## Contribution Guide
> Coming soon!
>
> Meanwhile, PRs are always welcome
# 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
.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/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
**/.flutter-plugins-dependencies
**/ios/Flutter/flutter_export_environment.sh
\ No newline at end of file
# 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: 20e59316b8b8474554b38493b8ca888794b0234a
channel: stable
project_type: app
# Example
Detailed Example for flutter_html package
# Basic Example
```dart
Widget html = Html(
data: """
<div>
<h1>Demo Page</h1>
<p>This is a fantastic product that you should buy!</p>
<h3>Features</h3>
<ul>
<li>It actually works</li>
<li>It exists</li>
<li>It doesn't cost much!</li>
</ul>
<!--You can pretty much put any html in here!-->
</div>
""",
//Optional parameters:
backgroundColor: Colors.white70,
onLinkTap: (url) {
// open url in a webview
},
style: {
"div": Style(
block: Block(
margin: EdgeInsets.all(16),
border: Border.all(width: 6),
backgroundColor: Colors.grey,
),
textStyle: TextStyle(
color: Colors.red,
),
),
},
onImageTap: (src) {
// Display the image in large form.
},
);
```
# Detailed Example
## Example HTML data string/body
```html
const htmlData = """
<h1>Header 1</h1>
<h2>Header 2</h2>
<h3>Header 3</h3>
<h4>Header 4</h4>
<h5>Header 5</h5>
<h6>Header 6</h6>
<h3>Ruby Support:</h3>
<p>
<ruby>
<rt>かん</rt>
<rt></rt>
</ruby>
&nbsp;is Japanese Kanji.
</p>
<h3>Support for <code>sub</code>/<code>sup</code></h3>
Solve for <var>x<sub>n</sub></var>: log<sub>2</sub>(<var>x</var><sup>2</sup>+<var>n</var>) = 9<sup>3</sup>
<p>One of the most <span>common</span> equations in all of physics is <br /><var>E</var>=<var>m</var><var>c</var><sup>2</sup>.</p>
<h3>Table support (with custom styling!):</h3>
<p>
<q>Famous quote...</q>
</p>
<table>
<colgroup>
<col width="50%" />
<col width="25%" />
<col width="25%" />
</colgroup>
<thead>
<tr><th>One</th><th>Two</th><th>Three</th></tr>
</thead>
<tbody>
<tr>
<td>Data</td><td>Data</td><td>Data</td>
</tr>
<tr>
<td>Data</td><td>Data</td><td>Data</td>
</tr>
</tbody>
<tfoot>
<tr><td>fData</td><td>fData</td><td>fData</td></tr>
</tfoot>
</table>
<h3>Custom Element Support:</h3>
<flutter></flutter>
<flutter horizontal></flutter>
<h3>SVG support:</h3>
<svg id='svg1' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'>
<circle r="32" cx="35" cy="65" fill="#F00" opacity="0.5"/>
<circle r="32" cx="65" cy="65" fill="#0F0" opacity="0.5"/>
<circle r="32" cx="50" cy="35" fill="#00F" opacity="0.5"/>
</svg>
<h3>List support:</h3>
<ol>
<li>This</li>
<li><p>is</p></li>
<li>an</li>
<li>
ordered
<ul>
<li>With<br /><br />...</li>
<li>a</li>
<li>nested</li>
<li>unordered
<ol>
<li>With a nested</li>
<li>ordered list.</li>
</ol>
</li>
<li>list</li>
</ul>
</li>
<li>list! Lorem ipsum dolor sit amet.</li>
<li><h2>Header 2</h2></li>
<h2><li>Header 2</li></h2>
</ol>
<h3>Link support:</h3>
<p>
Linking to <a href='https://github.com'>websites</a> has never been easier.
</p>
<h3>Image support:</h3>
<p>
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
<a href='https://google.com'><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></a>
<img alt='Alt Text of an intentionally broken image' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30d' />
</p>
<!--
<h3>Video support:</h3>
<video controls>
<source src="https://www.w3schools.com/html/mov_bbb.mp4" />
</video>
<h3>Audio support:</h3>
<audio controls>
<source src="https://www.w3schools.com/html/horse.mp3" />
</audio>
<h3>IFrame support:</h3>
<iframe src="https://google.com"></iframe>
-->
""";
```
## How to use
```dart
return new Scaffold(
appBar: AppBar(
title: Text('flutter_html Example'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Html(
data: htmlData,
//Optional parameters:
style: {
"html": Style(
backgroundColor: Colors.black12,
),
"table": Style(
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
),
"tr": Style(
border: Border(bottom: BorderSide(color: Colors.grey)),
),
"th": Style(
padding: EdgeInsets.all(6),
backgroundColor: Colors.grey,
),
"td": Style(
padding: EdgeInsets.all(6),
),
"var": Style(fontFamily: 'serif'),
},
customRender: {
"flutter": (RenderContext context, Widget child, attributes, _) {
return FlutterLogo(
style: (attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color,
size: context.style.fontSize.size * 5,
);
},
},
onLinkTap: (url) {
print("Opening $url...");
},
onImageTap: (src) {
print(src);
},
onImageError: (exception, stackTrace) {
print(exception);
},
),
),
);
```
\ No newline at end of file
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.example.example"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.example">
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="example"
android:icon="@mipmap/ic_launcher">
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
</resources>
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.1'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
#Thu Nov 19 14:42:53 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}
<svg viewBox='0 0 90 100' xmlns='http://www.w3.org/2000/svg'>
<path d='M62,0c2,10-9,24-20,24c-3-14,9-22,20-24M5,36c5-8,13-12,21-12c7,0,12,4,19,4c6,0,10-4,19-4c6,0,14,3,19,10c-16,4-15,35,3,39c-7,17-18,27-24,27c-7,0-8-5-17-5c-9,0-11,5-17,5c-7-1-13-7-17-13c-9-10-15-40-6-51' fill='#AAA'/>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>8.0</string>
</dict>
</plist>
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
PODS:
- Flutter (1.0.0)
- video_player (0.0.1):
- Flutter
- wakelock (0.0.1):
- Flutter
- webview_flutter (0.0.1):
- Flutter
DEPENDENCIES:
- Flutter (from `Flutter`)
- video_player (from `.symlinks/plugins/video_player/ios`)
- wakelock (from `.symlinks/plugins/wakelock/ios`)
- webview_flutter (from `.symlinks/plugins/webview_flutter/ios`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
video_player:
:path: ".symlinks/plugins/video_player/ios"
wakelock:
:path: ".symlinks/plugins/wakelock/ios"
webview_flutter:
:path: ".symlinks/plugins/webview_flutter/ios"
SPEC CHECKSUMS:
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
video_player: 9cc823b1d9da7e8427ee591e8438bfbcde500e6e
wakelock: bfc7955c418d0db797614075aabbc58a39ab5107
webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96
PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d
COCOAPODS: 1.10.1
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A65EE5648FB89B4FBB7BE7EF /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 49DCEF6F09B6F3E8BBB9D4EA /* libPods-Runner.a */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
49DCEF6F09B6F3E8BBB9D4EA /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
A7CDEE80872A41183CD903E6 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
DD595D932B697B14AB4BA7CB /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
F2E16627F28351910CCEBD53 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
A65EE5648FB89B4FBB7BE7EF /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
092B7351CFBC8F0782F51612 /* Frameworks */ = {
isa = PBXGroup;
children = (
49DCEF6F09B6F3E8BBB9D4EA /* libPods-Runner.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
CEDA619A9460C453C4DE104D /* Pods */,
092B7351CFBC8F0782F51612 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */,
7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
97C146F11CF9000F007C117D /* Supporting Files */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
);
path = Runner;
sourceTree = "<group>";
};
97C146F11CF9000F007C117D /* Supporting Files */ = {
isa = PBXGroup;
children = (
97C146F21CF9000F007C117D /* main.m */,
);
name = "Supporting Files";
sourceTree = "<group>";
};
CEDA619A9460C453C4DE104D /* Pods */ = {
isa = PBXGroup;
children = (
DD595D932B697B14AB4BA7CB /* Pods-Runner.debug.xcconfig */,
A7CDEE80872A41183CD903E6 /* Pods-Runner.release.xcconfig */,
F2E16627F28351910CCEBD53 /* Pods-Runner.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
8446656B443667928C090391 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
8446656B443667928C090391 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */,
97C146F31CF9000F007C117D /* main.m in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = S8QB4VV633;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
PRODUCT_NAME = "$(TARGET_NAME)";
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
@interface AppDelegate : FlutterAppDelegate
@end
#include "AppDelegate.h"
#include "GeneratedPluginRegistrant.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch.
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>example</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>io.flutter.embedded_views_preview</key>
<string>YES</string>
</dict>
</plist>
#import "GeneratedPluginRegistrant.h"
\ No newline at end of file
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char* argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/image_render.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.deepPurple,
),
home: new MyHomePage(title: 'flutter_html Example'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
const htmlData = """
<h1>Header 1</h1>
<h2>Header 2</h2>
<h3>Header 3</h3>
<h4>Header 4</h4>
<h5>Header 5</h5>
<h6>Header 6</h6>
<h3>Ruby Support:</h3>
<p>
<ruby>
漢<rt>かん</rt>
字<rt>じ</rt>
</ruby>
&nbsp;is Japanese Kanji.
</p>
<h3>Support for <code>sub</code>/<code>sup</code></h3>
Solve for <var>x<sub>n</sub></var>: log<sub>2</sub>(<var>x</var><sup>2</sup>+<var>n</var>) = 9<sup>3</sup>
<p>One of the most <span>common</span> equations in all of physics is <br /><var>E</var>=<var>m</var><var>c</var><sup>2</sup>.</p>
<h3>Inline Styles:</h3>
<p>The should be <span style='color: blue;'>BLUE style='color: blue;'</span></p>
<p>The should be <span style='color: red;'>RED style='color: red;'</span></p>
<p>The should be <span style='color: rgba(0, 0, 0, 0.10);'>BLACK with 10% alpha style='color: rgba(0, 0, 0, 0.10);</span></p>
<p>The should be <span style='color: rgb(0, 97, 0);'>GREEN style='color: rgb(0, 97, 0);</span></p>
<p>The should be <span style='background-color: red; color: rgb(0, 97, 0);'>GREEN style='color: rgb(0, 97, 0);</span></p>
<p style="text-align: center;"><span style="color: rgba(0, 0, 0, 0.95);">blasdafjklasdlkjfkl</span></p>
<p style="text-align: right;"><span style="color: rgba(0, 0, 0, 0.95);">blasdafjklasdlkjfkl</span></p>
<p style="text-align: justify;"><span style="color: rgba(0, 0, 0, 0.95);">blasdafjklasdlkjfkl</span></p>
<p style="text-align: center;"><span style="color: rgba(0, 0, 0, 0.95);">blasdafjklasdlkjfkl</span></p>
<h3>Table support (with custom styling!):</h3>
<p>
<q>Famous quote...</q>
</p>
<table>
<colgroup>
<col width="50%" />
<col span="2" width="25%" />
</colgroup>
<thead>
<tr><th>One</th><th>Two</th><th>Three</th></tr>
</thead>
<tbody>
<tr>
<td rowspan='2'>Rowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan</td><td>Data</td><td>Data</td>
</tr>
<tr>
<td colspan="2"><img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' /></td>
</tr>
</tbody>
<tfoot>
<tr><td>fData</td><td>fData</td><td>fData</td></tr>
</tfoot>
</table>
<h3>Custom Element Support (inline: <bird></bird> and as block):</h3>
<flutter></flutter>
<flutter horizontal></flutter>
<h3>SVG support:</h3>
<svg id='svg1' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'>
<circle r="32" cx="35" cy="65" fill="#F00" opacity="0.5"/>
<circle r="32" cx="65" cy="65" fill="#0F0" opacity="0.5"/>
<circle r="32" cx="50" cy="35" fill="#00F" opacity="0.5"/>
</svg>
<h3>List support:</h3>
<ol>
<li>This</li>
<li><p>is</p></li>
<li>an</li>
<li>
ordered
<ul>
<li>With<br /><br />...</li>
<li>a</li>
<li>nested</li>
<li>unordered
<ol>
<li>With a nested</li>
<li>ordered list.</li>
</ol>
</li>
<li>list</li>
</ul>
</li>
<li>list! Lorem ipsum dolor sit amet.</li>
<li><h2>Header 2</h2></li>
<h2><li>Header 2</li></h2>
</ol>
<h3>Link support:</h3>
<p>
Linking to <a href='https://github.com'>websites</a> has never been easier.
</p>
<h3>Image support:</h3>
<h3>Network png</h3>
<img alt='Google' src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
<h3>Network svg</h3>
<img src='https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/android.svg' />
<h3>Local asset png</h3>
<img src='asset:assets/html5.png' width='100' />
<h3>Local asset svg</h3>
<img src='asset:assets/mac.svg' width='100' />
<h3>Base64</h3>
<img alt='Red dot' src='data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' />
<h3>Custom source matcher (relative paths)</h3>
<img src='/wikipedia/commons/thumb/e/ef/Octicons-logo-github.svg/200px-Octicons-logo-github.svg.png' />
<h3>Custom image render (flutter.dev)</h3>
<img src='https://flutter.dev/images/flutter-mono-81x100.png' />
<h3>No image source</h3>
<img alt='No source' />
<img alt='Empty source' src='' />
<h3>Broken network image</h3>
<img alt='Broken image' src='https://www.notgoogle.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png' />
""";
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('flutter_html Example'),
centerTitle: true,
),
body: SingleChildScrollView(
child: Html(
data: htmlData,
//Optional parameters:
customImageRenders: {
networkSourceMatcher(domains: ["flutter.dev"]):
(context, attributes, element) {
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),
// Custom placeholder image for broken links
networkSourceMatcher(): networkImageRender(altWidget: (_) => FlutterLogo()),
},
onLinkTap: (url) {
print("Opening $url...");
},
onImageTap: (src) {
print(src);
},
onImageError: (exception, stackTrace) {
print(exception);
},
),
),
);
}
}
name: example
description: flutter_html example app.
version: 1.0.0+1
environment:
sdk: ">=2.1.0 <3.0.0"
dependencies:
flutter_html:
path: ..
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
uses-material-design: true
assets:
- assets/html5.png
- assets/mac.svg
\ No newline at end of file
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
library flutter_html;
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/image_render.dart';
import 'package:flutter_html/style.dart';
import 'package:webview_flutter/webview_flutter.dart';
class Html extends StatelessWidget {
/// The `Html` widget takes HTML as input and displays a RichText
/// tree of the parsed HTML content.
///
/// **Attributes**
/// **data** *required* takes in a String of HTML data.
///
///
/// **onLinkTap** This function is called whenever a link (`<a href>`)
/// is tapped.
/// **customRender** This function allows you to return your own widgets
/// for existing or custom HTML tags.
/// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/All-About-customRender) for more info.
///
/// **onImageError** This is called whenever an image fails to load or
/// display on the page.
///
/// **shrinkWrap** This makes the Html widget take up only the width it
/// needs and no more.
///
/// **onImageTap** This is called whenever an image is tapped.
///
/// **blacklistedElements** Tag names in this array are ignored during parsing and rendering.
///
/// **style** Pass in the style information for the Html here.
/// See [its wiki page](https://github.com/Sub6Resources/flutter_html/wiki/Style) for more info.
Html({
Key key,
@required this.data,
this.onLinkTap,
this.customRender,
this.customImageRenders = const {},
this.onImageError,
this.shrinkWrap = false,
this.onImageTap,
this.blacklistedElements = const [],
this.style,
this.navigationDelegateForIframe,
}) : super(key: key);
final String data;
final OnTap onLinkTap;
final Map<ImageSourceMatcher, ImageRender> customImageRenders;
final ImageErrorListener onImageError;
final bool shrinkWrap;
/// Properties for the Image widget that gets rendered by the rich text parser
final OnTap onImageTap;
final List<String> blacklistedElements;
/// Either return a custom widget for specific node types or return null to
/// fallback to the default rendering.
final Map<String, CustomRender> customRender;
/// Fancy New Parser parameters
final Map<String, Style> 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;
@override
Widget build(BuildContext context) {
final double width = shrinkWrap ? null : MediaQuery.of(context).size.width;
return Container(
width: width,
child: HtmlParser(
htmlData: data,
onLinkTap: onLinkTap,
onImageTap: onImageTap,
onImageError: onImageError,
shrinkWrap: shrinkWrap,
style: style,
customRender: customRender,
imageRenders: {}
..addAll(customImageRenders)
..addAll(defaultImageRenders),
blacklistedElements: blacklistedElements,
navigationDelegateForIframe: navigationDelegateForIframe,
),
);
}
}
import 'dart:collection';
import 'dart:math';
import 'package:csslib/parser.dart' as cssparser;
import 'package:csslib/visitor.dart' as css;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/image_render.dart';
import 'package:flutter_html/src/css_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/layout_element.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as htmlparser;
import 'package:webview_flutter/webview_flutter.dart';
typedef OnTap = void Function(String url);
typedef CustomRender = dynamic Function(
RenderContext context,
Widget parsedChild,
Map<String, String> attributes,
dom.Element element,
);
class HtmlParser extends StatelessWidget {
final String htmlData;
final OnTap onLinkTap;
final OnTap onImageTap;
final ImageErrorListener onImageError;
final bool shrinkWrap;
final Map<String, Style> style;
final Map<String, CustomRender> customRender;
final Map<ImageSourceMatcher, ImageRender> imageRenders;
final List<String> blacklistedElements;
final NavigationDelegate navigationDelegateForIframe;
HtmlParser({
@required this.htmlData,
this.onLinkTap,
this.onImageTap,
this.onImageError,
this.shrinkWrap,
this.style,
this.customRender,
this.imageRenders,
this.blacklistedElements,
this.navigationDelegateForIframe,
});
@override
Widget build(BuildContext context) {
dom.Document document = parseHTML(htmlData);
StyledElement lexedTree = lexDomTree(
document,
customRender?.keys?.toList() ?? [],
blacklistedElements,
navigationDelegateForIframe,
);
StyledElement styledTree = applyCSS(lexedTree);
StyledElement inlineStyledTree = applyInlineStyles(styledTree);
StyledElement customStyledTree = _applyCustomStyles(inlineStyledTree);
StyledElement cascadedStyledTree = _cascadeStyles(customStyledTree);
StyledElement cleanedTree = cleanTree(cascadedStyledTree);
InlineSpan parsedTree = parseTree(
RenderContext(
buildContext: context,
parser: this,
style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2),
),
cleanedTree,
);
// This is the final scaling that assumes any other StyledText instances are
// using textScaleFactor = 1.0 (which is the default). This ensures the correct
// scaling is used, but relies on https://github.com/flutter/flutter/pull/59711
// to wrap everything when larger accessibility fonts are used.
return StyledText(
textSpan: parsedTree,
style: cleanedTree.style,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
renderContext: RenderContext(
buildContext: context,
parser: this,
style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2),
),
);
}
/// [parseHTML] converts a string of HTML to a DOM document using the dart `html` library.
static dom.Document parseHTML(String data) {
return htmlparser.parse(data);
}
/// [parseCSS] converts a string of CSS to a CSS stylesheet using the dart `csslib` library.
static css.StyleSheet parseCSS(String data) {
return cssparser.parse(data);
}
/// [lexDomTree] converts a DOM document to a simplified tree of [StyledElement]s.
static StyledElement lexDomTree(
dom.Document html,
List<String> customRenderTags,
List<String> blacklistedElements,
NavigationDelegate navigationDelegateForIframe,
) {
StyledElement tree = StyledElement(
name: "[Tree Root]",
children: new List<StyledElement>(),
node: html.documentElement,
);
html.nodes.forEach((node) {
tree.children.add(_recursiveLexer(
node,
customRenderTags,
blacklistedElements,
navigationDelegateForIframe,
));
});
return tree;
}
/// [_recursiveLexer] is the recursive worker function for [lexDomTree].
///
/// It runs the parse functions of every type of
/// element and returns a [StyledElement] tree representing the element.
static StyledElement _recursiveLexer(
dom.Node node,
List<String> customRenderTags,
List<String> blacklistedElements,
NavigationDelegate navigationDelegateForIframe,
) {
List<StyledElement> children = List<StyledElement>();
node.nodes.forEach((childNode) {
children.add(_recursiveLexer(
childNode,
customRenderTags,
blacklistedElements,
navigationDelegateForIframe,
));
});
//TODO(Sub6Resources): There's probably a more efficient way to look this up.
if (node is dom.Element) {
if (blacklistedElements?.contains(node.localName) ?? false) {
return EmptyContentElement();
}
if (STYLED_ELEMENTS.contains(node.localName)) {
return parseStyledElement(node, children);
} else if (INTERACTABLE_ELEMENTS.contains(node.localName)) {
return parseInteractableElement(node, children);
} else if (REPLACED_ELEMENTS.contains(node.localName)) {
return parseReplacedElement(node, navigationDelegateForIframe);
} else if (LAYOUT_ELEMENTS.contains(node.localName)) {
return parseLayoutElement(node, children);
} else if (TABLE_CELL_ELEMENTS.contains(node.localName)) {
return parseTableCellElement(node, children);
} else if (TABLE_DEFINITION_ELEMENTS.contains(node.localName)) {
return parseTableDefinitionElement(node, children);
} else if (customRenderTags.contains(node.localName)) {
return parseStyledElement(node, children);
} else {
return EmptyContentElement();
}
} else if (node is dom.Text) {
return TextContentElement(text: node.text);
} else {
return EmptyContentElement();
}
}
///TODO document
static StyledElement applyCSS(StyledElement tree) {
//Make sure style is never null.
if (tree.style == null) {
tree.style = Style();
}
tree.children?.forEach((e) => applyCSS(e));
return tree;
}
static StyledElement applyInlineStyles(StyledElement tree) {
if (tree.attributes.containsKey("style")) {
tree.style = tree.style.merge(inlineCSSToStyle(tree.attributes['style']));
}
tree.children?.forEach(applyInlineStyles);
return tree;
}
/// [applyCustomStyles] applies the [Style] objects passed into the [Html]
/// widget onto the [StyledElement] tree, no cascading of styles is done at this point.
StyledElement _applyCustomStyles(StyledElement tree) {
if (style == null) return tree;
style.forEach((key, style) {
if (tree.matchesSelector(key)) {
if (tree.style == null) {
tree.style = style;
} else {
tree.style = tree.style.merge(style);
}
}
});
tree.children?.forEach(_applyCustomStyles);
return tree;
}
/// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each
/// child that doesn't specify a different style.
StyledElement _cascadeStyles(StyledElement tree) {
tree.children?.forEach((child) {
child.style = tree.style.copyOnlyInherited(child.style);
_cascadeStyles(child);
});
return tree;
}
/// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are
/// on the first level, redundant levels are collapsed, empty elements are
/// removed, and specialty elements are processed.
static StyledElement cleanTree(StyledElement tree) {
tree = _processInternalWhitespace(tree);
tree = _processInlineWhitespace(tree);
tree = _removeEmptyElements(tree);
tree = _processListCharacters(tree);
tree = _processBeforesAndAfters(tree);
tree = _collapseMargins(tree);
tree = _processFontSize(tree);
return tree;
}
/// [parseTree] converts a tree of [StyledElement]s to an [InlineSpan] tree.
///
/// [parseTree] is responsible for handling the [customRender] parameter and
/// deciding what different `Style.display` options look like as Widgets.
InlineSpan parseTree(RenderContext context, StyledElement tree) {
// Merge this element's style into the context so that children
// inherit the correct style
RenderContext newContext = RenderContext(
buildContext: context.buildContext,
parser: this,
style: context.style?.copyOnlyInherited(tree.style),
);
if (customRender?.containsKey(tree.name) ?? false) {
final render = customRender[tree.name].call(
newContext,
ContainerSpan(
newContext: newContext,
style: tree.style,
shrinkWrap: context.parser.shrinkWrap,
children: tree.children
?.map((tree) => parseTree(newContext, tree))
?.toList() ??
[],
),
tree.attributes,
tree.element,
);
if (render != null) {
assert(render is InlineSpan || render is Widget);
return render is InlineSpan
? render
: WidgetSpan(
child: ContainerSpan(
newContext: newContext,
style: tree.style,
shrinkWrap: context.parser.shrinkWrap,
child: render,
),
);
}
}
//Return the correct InlineSpan based on the element type.
if (tree.style?.display == Display.BLOCK) {
return WidgetSpan(
child: ContainerSpan(
newContext: newContext,
style: tree.style,
shrinkWrap: context.parser.shrinkWrap,
children: tree.children
?.map((tree) => parseTree(newContext, tree))
?.toList() ??
[],
),
);
} else if (tree.style?.display == Display.LIST_ITEM) {
return WidgetSpan(
child: ContainerSpan(
newContext: newContext,
style: tree.style,
shrinkWrap: context.parser.shrinkWrap,
child: Stack(
children: [
if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE ||
tree.style?.listStylePosition == null)
PositionedDirectional(
width: 30, //TODO derive this from list padding.
start: 0,
child: Text('${newContext.style.markerContent}\t',
textAlign: TextAlign.right,
style: newContext.style.generateTextStyle()),
),
Padding(
padding: EdgeInsetsDirectional.only(
start: 30), //TODO derive this from list padding.
child: StyledText(
textSpan: TextSpan(
text: (tree.style?.listStylePosition ==
ListStylePosition.INSIDE)
? '${newContext.style.markerContent}\t'
: null,
children: tree.children
?.map((tree) => parseTree(newContext, tree))
?.toList() ??
[],
style: newContext.style.generateTextStyle(),
),
style: newContext.style,
renderContext: context,
),
)
],
),
),
);
} else if (tree is ReplacedElement) {
if (tree is TextContentElement) {
return TextSpan(text: tree.text);
} else {
return WidgetSpan(
alignment: tree.alignment,
baseline: TextBaseline.alphabetic,
child: tree.toWidget(context),
);
}
} else if (tree is InteractableElement) {
InlineSpan addTaps(InlineSpan childSpan, TextStyle childStyle) {
if (childSpan is TextSpan) {
return TextSpan(
text: childSpan.text,
children: childSpan.children
?.map((e) => addTaps(e, childStyle.merge(childSpan.style)))
?.toList(),
style: newContext.style.generateTextStyle().merge(
childSpan.style == null
? childStyle
: childStyle.merge(childSpan.style)),
semanticsLabel: childSpan.semanticsLabel,
recognizer: TapGestureRecognizer()
..onTap = () => onLinkTap?.call(tree.href),
);
} else {
return WidgetSpan(
child: RawGestureDetector(
gestures: {
MultipleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<
MultipleTapGestureRecognizer>(
() => MultipleTapGestureRecognizer(),
(instance) {
instance..onTap = () => onLinkTap?.call(tree.href);
},
),
},
child: (childSpan as WidgetSpan).child,
),
);
}
}
return TextSpan(
children: tree.children
.map((tree) => parseTree(newContext, tree))
.map((childSpan) {
return addTaps(childSpan,
newContext.style.generateTextStyle().merge(childSpan.style));
}).toList() ??
[],
);
} else if (tree is LayoutElement) {
return WidgetSpan(
child: tree.toWidget(context),
);
} else if (tree.style.verticalAlign != null &&
tree.style.verticalAlign != VerticalAlign.BASELINE) {
double verticalOffset;
switch (tree.style.verticalAlign) {
case VerticalAlign.SUB:
verticalOffset = tree.style.fontSize.size / 2.5;
break;
case VerticalAlign.SUPER:
verticalOffset = tree.style.fontSize.size / -2.5;
break;
default:
break;
}
//Requires special layout features not available in the TextStyle API.
return WidgetSpan(
child: Transform.translate(
offset: Offset(0, verticalOffset),
child: StyledText(
textSpan: TextSpan(
style: newContext.style.generateTextStyle(),
children: tree.children
.map((tree) => parseTree(newContext, tree))
.toList() ??
[],
),
style: newContext.style,
renderContext: context,
),
),
);
} else {
///[tree] is an inline element.
return TextSpan(
style: newContext.style.generateTextStyle(),
children:
tree.children.map((tree) => parseTree(newContext, tree)).toList(),
);
}
}
/// [processWhitespace] removes unnecessary whitespace from the StyledElement tree.
///
/// The criteria for determining which whitespace is replaceable is outlined
/// at https://www.w3.org/TR/css-text-3/
/// and summarized at https://medium.com/@patrickbrosset/when-does-white-space-matter-in-html-b90e8a7cdd33
static StyledElement _processInternalWhitespace(StyledElement tree) {
if ((tree.style?.whiteSpace ?? WhiteSpace.NORMAL) == WhiteSpace.PRE) {
// Preserve this whitespace
} else if (tree is TextContentElement) {
tree.text = _removeUnnecessaryWhitespace(tree.text);
} else {
tree.children?.forEach(_processInternalWhitespace);
}
return tree;
}
/// [_processInlineWhitespace] is responsible for removing redundant whitespace
/// between and among inline elements. It does so by creating a boolean [Context]
/// and passing it to the [_processInlineWhitespaceRecursive] function.
static StyledElement _processInlineWhitespace(StyledElement tree) {
final whitespaceParsingContext = Context(false);
tree = _processInlineWhitespaceRecursive(tree, whitespaceParsingContext);
return tree;
}
/// [_processInlineWhitespaceRecursive] analyzes the whitespace between and among different
/// inline elements, and replaces any instance of two or more spaces with a single space, according
/// to the w3's HTML whitespace processing specification linked to above.
static StyledElement _processInlineWhitespaceRecursive(
StyledElement tree,
Context<bool> wpc,
) {
if (tree.style.display == Display.BLOCK) {
wpc.data = false;
}
if (tree is ImageContentElement || tree is SvgContentElement) {
wpc.data = false;
}
if (tree is TextContentElement) {
if (wpc.data && tree.text.startsWith(' ')) {
tree.text = tree.text.replaceFirst(' ', '');
}
if (tree.text.endsWith(' ') || tree.text.endsWith('\n')) {
wpc.data = true;
} else {
wpc.data = false;
}
}
tree.children?.forEach((e) => _processInlineWhitespaceRecursive(e, wpc));
return tree;
}
/// [removeUnnecessaryWhitespace] removes "unnecessary" white space from the given String.
///
/// The steps for removing this whitespace are as follows:
/// (1) Remove any whitespace immediately preceding or following a newline.
/// (2) Replace all newlines with a space
/// (3) Replace all tabs with a space
/// (4) Replace any instances of two or more spaces with a single space.
static String _removeUnnecessaryWhitespace(String text) {
return text
.replaceAll(RegExp("\ *(?=\n)"), "\n")
.replaceAll(RegExp("(?:\n)\ *"), "\n")
.replaceAll("\n", " ")
.replaceAll("\t", " ")
.replaceAll(RegExp(" {2,}"), " ");
}
/// [processListCharacters] adds list characters to the front of all list items.
///
/// The function uses the [_processListCharactersRecursive] function to do most of its work.
static StyledElement _processListCharacters(StyledElement tree) {
final olStack = ListQueue<Context<int>>();
tree = _processListCharactersRecursive(tree, olStack);
return tree;
}
/// [_processListCharactersRecursive] uses a Stack of integers to properly number and
/// bullet all list items according to the [ListStyleType] they have been given.
static StyledElement _processListCharactersRecursive(
StyledElement tree, ListQueue<Context<int>> olStack) {
if (tree.name == 'ol') {
olStack.add(Context(0));
} else if (tree.style.display == Display.LIST_ITEM) {
switch (tree.style.listStyleType) {
case ListStyleType.DISC:
tree.style.markerContent = '•';
break;
case ListStyleType.DECIMAL:
olStack.last.data += 1;
tree.style.markerContent = '${olStack.last.data}.';
}
}
tree.children?.forEach((e) => _processListCharactersRecursive(e, olStack));
if (tree.name == 'ol') {
olStack.removeLast();
}
return tree;
}
/// [_processBeforesAndAfters] adds text content to the beginning and end of
/// the list of the trees children according to the `before` and `after` Style
/// properties.
static StyledElement _processBeforesAndAfters(StyledElement tree) {
if (tree.style?.before != null) {
tree.children?.insert(
0, TextContentElement(text: tree.style.before, style: tree.style));
}
if (tree.style?.after != null) {
tree.children
?.add(TextContentElement(text: tree.style.after, style: tree.style));
} else {
tree.children?.forEach(_processBeforesAndAfters);
}
return tree;
}
/// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS21/box.html#collapsing-margins
/// for collapsing margins of block-level boxes. This prevents the doubling of margins between
/// boxes, and makes for a more correct rendering of the html content.
///
/// Paraphrased from the CSS specification:
/// Margins are collapsed if both belong to vertically-adjacent box edges, i.e form one of the following pairs:
/// (1) Top margin of a box and top margin of its first in-flow child
/// (2) Bottom margin of a box and top margin of its next in-flow following sibling
/// (3) Bottom margin of a last in-flow child and bottom margin of its parent (if the parent's height is not explicit)
/// (4) Top and Bottom margins of a box with a height of zero or no in-flow children.
static StyledElement _collapseMargins(StyledElement tree) {
//Short circuit if we've reached a leaf of the tree
if (tree.children == null || tree.children.isEmpty) {
// Handle case (4) from above.
if ((tree.style.height ?? 0) == 0) {
tree.style.margin = EdgeInsets.zero;
}
return tree;
}
//Collapsing should be depth-first.
tree.children?.forEach(_collapseMargins);
//The root boxes do not collapse.
if (tree.name == '[Tree Root]' || tree.name == 'html') {
return tree;
}
// Handle case (1) from above.
// Top margins cannot collapse if the element has padding
if ((tree.style.padding?.top ?? 0) == 0) {
final parentTop = tree.style.margin?.top ?? 0;
final firstChildTop = tree.children.first.style.margin?.top ?? 0;
final newOuterMarginTop = max(parentTop, firstChildTop);
// Set the parent's margin
if (tree.style.margin == null) {
tree.style.margin = EdgeInsets.only(top: newOuterMarginTop);
} else {
tree.style.margin = tree.style.margin.copyWith(top: newOuterMarginTop);
}
// And remove the child's margin
if (tree.children.first.style.margin == null) {
tree.children.first.style.margin = EdgeInsets.zero;
} else {
tree.children.first.style.margin =
tree.children.first.style.margin.copyWith(top: 0);
}
}
// Handle case (3) from above.
// Bottom margins cannot collapse if the element has padding
if ((tree.style.padding?.bottom ?? 0) == 0) {
final parentBottom = tree.style.margin?.bottom ?? 0;
final lastChildBottom = tree.children.last.style.margin?.bottom ?? 0;
final newOuterMarginBottom = max(parentBottom, lastChildBottom);
// Set the parent's margin
if (tree.style.margin == null) {
tree.style.margin = EdgeInsets.only(bottom: newOuterMarginBottom);
} else {
tree.style.margin =
tree.style.margin.copyWith(bottom: newOuterMarginBottom);
}
// And remove the child's margin
if (tree.children.last.style.margin == null) {
tree.children.last.style.margin = EdgeInsets.zero;
} else {
tree.children.last.style.margin =
tree.children.last.style.margin.copyWith(bottom: 0);
}
}
// Handle case (2) from above.
if (tree.children.length > 1) {
for (int i = 1; i < tree.children.length; i++) {
final previousSiblingBottom =
tree.children[i - 1].style.margin?.bottom ?? 0;
final thisTop = tree.children[i].style.margin?.top ?? 0;
final newInternalMargin = max(previousSiblingBottom, thisTop) / 2;
if (tree.children[i - 1].style.margin == null) {
tree.children[i - 1].style.margin =
EdgeInsets.only(bottom: newInternalMargin);
} else {
tree.children[i - 1].style.margin = tree.children[i - 1].style.margin
.copyWith(bottom: newInternalMargin);
}
if (tree.children[i].style.margin == null) {
tree.children[i].style.margin =
EdgeInsets.only(top: newInternalMargin);
} else {
tree.children[i].style.margin =
tree.children[i].style.margin.copyWith(top: newInternalMargin);
}
}
}
return tree;
}
/// [removeEmptyElements] recursively removes empty elements.
///
/// An empty element is any [EmptyContentElement], any empty [TextContentElement],
/// or any block-level [TextContentElement] that contains only whitespace and doesn't follow
/// a block element or a line break.
static StyledElement _removeEmptyElements(StyledElement tree) {
List<StyledElement> toRemove = new List<StyledElement>();
bool lastChildBlock = true;
tree.children?.forEach((child) {
if (child is EmptyContentElement || child is EmptyLayoutElement) {
toRemove.add(child);
} else if (child is TextContentElement && (child.text.isEmpty)) {
toRemove.add(child);
} else if (child is TextContentElement &&
child.style.whiteSpace != WhiteSpace.PRE &&
tree.style.display == Display.BLOCK &&
child.text.trim().isEmpty &&
lastChildBlock) {
toRemove.add(child);
} else if (child.style.display == Display.NONE) {
toRemove.add(child);
} else {
_removeEmptyElements(child);
}
// This is used above to check if the previous element is a block element or a line break.
lastChildBlock = (child.style.display == Display.BLOCK ||
child.style.display == Display.LIST_ITEM ||
(child is TextContentElement && child.text == '\n'));
});
tree.children?.removeWhere((element) => toRemove.contains(element));
return tree;
}
/// [_processFontSize] changes percent-based font sizes (negative numbers in this implementation)
/// to pixel-based font sizes.
static StyledElement _processFontSize(StyledElement tree) {
double parentFontSize = tree.style?.fontSize?.size ?? FontSize.medium.size;
tree.children?.forEach((child) {
if ((child.style.fontSize?.size ?? parentFontSize) < 0) {
child.style.fontSize =
FontSize(parentFontSize * -child.style.fontSize.size);
}
_processFontSize(child);
});
return tree;
}
}
/// The [RenderContext] is available when parsing the tree. It contains information
/// about the [BuildContext] of the `Html` widget, contains the configuration available
/// in the [HtmlParser], and contains information about the [Style] of the current
/// tree root.
class RenderContext {
final BuildContext buildContext;
final HtmlParser parser;
final Style style;
RenderContext({
this.buildContext,
this.parser,
this.style,
});
}
/// A [ContainerSpan] is a widget with an [InlineSpan] child or children.
///
/// A [ContainerSpan] can have a border, background color, height, width, padding, and margin
/// and can represent either an INLINE or BLOCK-level element.
class ContainerSpan extends StatelessWidget {
final Widget child;
final List<InlineSpan> children;
final Style style;
final RenderContext newContext;
final bool shrinkWrap;
ContainerSpan({
this.child,
this.children,
this.style,
this.newContext,
this.shrinkWrap = false,
});
@override
Widget build(BuildContext _) {
return Container(
decoration: BoxDecoration(
border: style?.border,
color: style?.backgroundColor,
),
height: style?.height,
width: style?.width,
padding: style?.padding,
margin: style?.margin,
alignment: shrinkWrap ? null : style?.alignment,
child: child ??
StyledText(
textSpan: TextSpan(
style: newContext.style.generateTextStyle(),
children: children,
),
style: newContext.style,
renderContext: newContext,
),
);
}
}
class StyledText extends StatelessWidget {
final InlineSpan textSpan;
final Style style;
final double textScaleFactor;
final RenderContext renderContext;
const StyledText({
this.textSpan,
this.style,
this.textScaleFactor = 1.0,
this.renderContext,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: calculateWidth(style.display, renderContext),
child: Text.rich(
textSpan,
style: style.generateTextStyle(),
textAlign: style.textAlign,
textDirection: style.direction,
textScaleFactor: textScaleFactor,
),
);
}
double calculateWidth(Display display, RenderContext context) {
if ((display == Display.BLOCK || display == Display.LIST_ITEM) && !renderContext.parser.shrinkWrap) {
return double.infinity;
}
if (renderContext.parser.shrinkWrap) {
return MediaQuery.of(context.buildContext).size.width;
}
return null;
}
}
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<String, String> attributes,
dom.Element element,
);
ImageSourceMatcher base64DataUriMatcher() => (attributes, element) =>
_src(attributes) != null &&
_src(attributes).startsWith("data:image") &&
_src(attributes).contains("base64,");
ImageSourceMatcher networkSourceMatcher({
List<String> schemas: const ["https", "http"],
List<String> 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<String, String> 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);
} 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<String, String> headers,
String Function(String) mapUrl,
double width,
double height,
Widget Function(String) altWidget,
Widget Function() loadingWidget,
}) =>
(context, attributes, element) {
final src = mapUrl?.call(_src(attributes)) ?? _src(attributes);
precacheImage(
NetworkImage(
src,
headers: headers,
),
context.buildContext,
onError: (exception, StackTrace stackTrace) {
context.parser.onImageError?.call(exception, stackTrace);
},
);
Completer<Size> completer = Completer();
Image image =
Image.network(src, frameBuilder: (ctx, child, frame, _) {
if (frame == null) {
if (!completer.isCompleted) {
completer.completeError("error");
}
return child;
} else {
return child;
}
});
image.image.resolve(ImageConfiguration()).addListener(
ImageStreamListener((ImageInfo image, bool synchronousCall) {
var myImage = image.image;
Size size =
Size(myImage.width.toDouble(), myImage.height.toDouble());
if (!completer.isCompleted) {
completer.complete(size);
}
}, onError: (object, stacktrace) {
if (!completer.isCompleted) {
completer.completeError(object);
}
}),
);
return FutureBuilder<Size>(
future: completer.future,
builder: (BuildContext buildContext, AsyncSnapshot<Size> snapshot) {
if (snapshot.hasData) {
return 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 svgNetworkImageRender() => (context, attributes, element) {
return SvgPicture.network(attributes["src"]);
};
final Map<ImageSourceMatcher, ImageRender> defaultImageRenders = {
base64DataUriMatcher(): base64ImageRender(),
assetUriMatcher(): assetImageRender(),
networkSourceMatcher(extension: "svg"): svgNetworkImageRender(),
networkSourceMatcher(): networkImageRender(),
};
String _src(Map<String, String> attributes) {
return attributes["src"];
}
String _alt(Map<String, String> attributes) {
return attributes["alt"];
}
double _height(Map<String, String> attributes) {
final heightString = attributes["height"];
return heightString == null ? heightString : double.tryParse(heightString);
}
double _width(Map<String, String> attributes) {
final widthString = attributes["width"];
return widthString == null ? widthString : double.tryParse(widthString);
}
import 'dart:ui';
import 'package:csslib/visitor.dart' as css;
import 'package:csslib/parser.dart' as cssparser;
import 'package:flutter/cupertino.dart';
import 'package:flutter_html/style.dart';
Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
Style style = new Style();
declarations.forEach((property, value) {
switch (property) {
case 'background-color':
style.backgroundColor = ExpressionMapping.expressionToColor(value.first);
break;
case 'color':
style.color = ExpressionMapping.expressionToColor(value.first);
break;
case 'direction':
style.direction = ExpressionMapping.expressionToDirection(value.first);
break;
case 'display':
style.display = ExpressionMapping.expressionToDisplay(value.first);
break;
case 'line-height':
style.lineHeight = ExpressionMapping.expressionToLineHeight(value.first);
break;
case 'font-family':
style.fontFamily = ExpressionMapping.expressionToFontFamily(value.first);
break;
case 'font-feature-settings':
style.fontFeatureSettings = ExpressionMapping.expressionToFontFeatureSettings(value);
break;
case 'font-size':
style.fontSize = ExpressionMapping.expressionToFontSize(value.first);
break;
case 'font-style':
style.fontStyle = ExpressionMapping.expressionToFontStyle(value.first);
break;
case 'font-weight':
style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
break;
case 'text-align':
style.textAlign = ExpressionMapping.expressionToTextAlign(value.first);
break;
case 'text-decoration':
List<css.LiteralTerm> textDecorationList = value.whereType<css.LiteralTerm>().toList();
/// List<css.LiteralTerm> might include other values than the ones we want for [textDecorationList], so make sure to remove those before passing it to [ExpressionMapping]
textDecorationList.removeWhere((element) => element.text != "none" && element.text != "overline" && element.text != "underline" && element.text != "line-through");
css.Expression textDecorationColor = value.firstWhere((element) => element is css.HexColorTerm || element is css.FunctionTerm, orElse: null);
List<css.LiteralTerm> temp = value.whereType<css.LiteralTerm>().toList();
/// List<css.LiteralTerm> might include other values than the ones we want for [textDecorationStyle], so make sure to remove those before passing it to [ExpressionMapping]
temp.removeWhere((element) => element.text != "solid" && element.text != "double" && element.text != "dashed" && element.text != "dotted" && element.text != "wavy");
css.LiteralTerm textDecorationStyle = temp.last ?? null;
style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList);
if (textDecorationColor != null) style.textDecorationColor = ExpressionMapping.expressionToColor(textDecorationColor);
if (textDecorationStyle != null) style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(textDecorationStyle);
break;
case 'text-decoration-color':
style.textDecorationColor = ExpressionMapping.expressionToColor(value.first);
break;
case 'text-decoration-line':
style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(value);
break;
case 'text-decoration-style':
style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(value.first);
break;
case 'text-shadow':
style.textShadow = ExpressionMapping.expressionToTextShadow(value);
break;
}
});
return style;
}
Style inlineCSSToStyle(String inlineStyle) {
final sheet = cssparser.parse("*{$inlineStyle}");
final declarations = DeclarationVisitor().getDeclarations(sheet);
return declarationsToStyle(declarations);
}
class DeclarationVisitor extends css.Visitor {
Map<String, List<css.Expression>> _result;
String _currentProperty;
Map<String, List<css.Expression>> getDeclarations(css.StyleSheet sheet) {
_result = new Map<String, List<css.Expression>>();
sheet.visit(this);
return _result;
}
@override
void visitDeclaration(css.Declaration node) {
_currentProperty = node.property;
_result[_currentProperty] = new List<css.Expression>();
node.expression.visit(this);
}
@override
void visitExpressions(css.Expressions node) {
node.expressions.forEach((expression) {
_result[_currentProperty].add(expression);
});
}
}
//Mapping functions
class ExpressionMapping {
static Color expressionToColor(css.Expression value) {
if (value is css.HexColorTerm) {
return stringToColor(value.text);
} else if (value is css.FunctionTerm) {
if (value.text == 'rgba') {
return rgbOrRgbaToColor(value.span.text);
} else if (value.text == 'rgb') {
return rgbOrRgbaToColor(value.span.text);
}
}
return null;
}
static TextDirection expressionToDirection(css.Expression value) {
if (value is css.LiteralTerm) {
switch(value.text) {
case "ltr":
return TextDirection.ltr;
case "rtl":
return TextDirection.rtl;
}
}
return TextDirection.ltr;
}
static Display expressionToDisplay(css.Expression value) {
if (value is css.LiteralTerm) {
switch(value.text) {
case 'block':
return Display.BLOCK;
case 'inline-block':
return Display.INLINE_BLOCK;
case 'inline':
return Display.INLINE;
case 'list-item':
return Display.LIST_ITEM;
case 'none':
return Display.NONE;
}
}
return Display.INLINE;
}
static List<FontFeature> expressionToFontFeatureSettings(List<css.Expression> value) {
List<FontFeature> fontFeatures = [];
for (int i = 0; i < value.length; i++) {
css.Expression exp = value[i];
if (exp is css.LiteralTerm) {
if (exp.text != "on" && exp.text != "off" && exp.text != "1" && exp.text != "0") {
if (i < value.length - 1) {
css.Expression nextExp = value[i+1];
if (nextExp is css.LiteralTerm && (nextExp.text == "on" || nextExp.text == "off" || nextExp.text == "1" || nextExp.text == "0")) {
fontFeatures.add(FontFeature(exp.text, nextExp.text == "on" || nextExp.text == "1" ? 1 : 0));
} else {
fontFeatures.add(FontFeature.enable(exp.text));
}
} else {
fontFeatures.add(FontFeature.enable(exp.text));
}
}
}
}
List<FontFeature> finalFontFeatures = fontFeatures.toSet().toList();
return finalFontFeatures;
}
static FontSize expressionToFontSize(css.Expression value) {
if (value is css.NumberTerm) {
return FontSize(double.tryParse(value.text));
} else if (value is css.PercentageTerm) {
return FontSize.percent(int.tryParse(value.text));
} else if (value is css.EmTerm) {
return FontSize.em(double.tryParse(value.text));
} else if (value is css.RemTerm) {
return FontSize.rem(double.tryParse(value.text));
} else if (value is css.LengthTerm) {
return FontSize(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')));
} else if (value is css.LiteralTerm) {
switch (value.text) {
case "xx-small":
return FontSize.xxSmall;
case "x-small":
return FontSize.xSmall;
case "small":
return FontSize.small;
case "medium":
return FontSize.medium;
case "large":
return FontSize.large;
case "x-large":
return FontSize.xLarge;
case "xx-large":
return FontSize.xxLarge;
}
}
return null;
}
static FontStyle expressionToFontStyle(css.Expression value) {
if (value is css.LiteralTerm) {
switch(value.text) {
case "italic":
case "oblique":
return FontStyle.italic;
}
return FontStyle.normal;
}
return FontStyle.normal;
}
static FontWeight expressionToFontWeight(css.Expression value) {
if (value is css.NumberTerm) {
switch (value.text) {
case "100":
return FontWeight.w100;
case "200":
return FontWeight.w200;
case "300":
return FontWeight.w300;
case "400":
return FontWeight.w400;
case "500":
return FontWeight.w500;
case "600":
return FontWeight.w600;
case "700":
return FontWeight.w700;
case "800":
return FontWeight.w800;
case "900":
return FontWeight.w900;
}
} else if (value is css.LiteralTerm) {
switch(value.text) {
case "bold":
return FontWeight.bold;
case "bolder":
return FontWeight.w900;
case "lighter":
return FontWeight.w200;
}
return FontWeight.normal;
}
return FontWeight.normal;
}
static String expressionToFontFamily(css.Expression value) {
if (value is css.LiteralTerm) return value.text;
return null;
}
static LineHeight expressionToLineHeight(css.Expression value) {
if (value is css.NumberTerm) {
return LineHeight.number(double.tryParse(value.text));
} else if (value is css.PercentageTerm) {
return LineHeight.percent(double.tryParse(value.text));
} else if (value is css.EmTerm) {
return LineHeight.em(double.tryParse(value.text));
} else if (value is css.RemTerm) {
return LineHeight.rem(double.tryParse(value.text));
} else if (value is css.LengthTerm) {
return LineHeight(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')), units: "length");
}
return LineHeight.normal;
}
static TextAlign expressionToTextAlign(css.Expression value) {
if (value is css.LiteralTerm) {
switch(value.text) {
case "center":
return TextAlign.center;
case "left":
return TextAlign.left;
case "right":
return TextAlign.right;
case "justify":
return TextAlign.justify;
case "end":
return TextAlign.end;
case "start":
return TextAlign.start;
}
}
return TextAlign.start;
}
static TextDecoration expressionToTextDecorationLine(List<css.LiteralTerm> value) {
List<TextDecoration> decorationList = [];
for (css.LiteralTerm term in value) {
switch(term.text) {
case "overline":
decorationList.add(TextDecoration.overline);
break;
case "underline":
decorationList.add(TextDecoration.underline);
break;
case "line-through":
decorationList.add(TextDecoration.lineThrough);
break;
default:
decorationList.add(TextDecoration.none);
break;
}
}
if (decorationList.contains(TextDecoration.none)) decorationList = [TextDecoration.none];
return TextDecoration.combine(decorationList);
}
static TextDecorationStyle expressionToTextDecorationStyle(css.LiteralTerm value) {
switch(value.text) {
case "wavy":
return TextDecorationStyle.wavy;
case "dotted":
return TextDecorationStyle.dotted;
case "dashed":
return TextDecorationStyle.dashed;
case "double":
return TextDecorationStyle.double;
default:
return TextDecorationStyle.solid;
}
}
static List<Shadow> expressionToTextShadow(List<css.Expression> value) {
List<Shadow> shadow = [];
List<int> indices = [];
List<List<css.Expression>> valueList = [];
for (css.Expression e in value) {
if (e is css.OperatorComma) {
indices.add(value.indexOf(e));
}
}
indices.add(value.length);
int previousIndex = 0;
for (int i in indices) {
valueList.add(value.sublist(previousIndex, i));
previousIndex = i + 1;
}
for (List<css.Expression> list in valueList) {
css.Expression exp = list[0];
css.Expression exp2 = list[1];
css.LiteralTerm exp3 = list.length > 2 ? list[2] : null;
css.LiteralTerm exp4 = list.length > 3 ? list[3] : null;
RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+');
if (exp is css.LiteralTerm && exp2 is css.LiteralTerm) {
if (exp3 != null && (exp3 is css.HexColorTerm || exp3 is css.FunctionTerm)) {
shadow.add(Shadow(
color: expressionToColor(exp3),
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, '')), double.tryParse(exp2.text.replaceAll(nonNumberRegex, '')))
));
} else if (exp3 != null && exp3 is css.LiteralTerm) {
if (exp4 != null && (exp4 is css.HexColorTerm || exp4 is css.FunctionTerm)) {
shadow.add(Shadow(
color: expressionToColor(exp4),
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, '')), double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))),
blurRadius: double.tryParse(exp3.text.replaceAll(nonNumberRegex, ''))
));
} else {
shadow.add(Shadow(
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, '')), double.tryParse(exp2.text.replaceAll(nonNumberRegex, ''))),
blurRadius: double.tryParse(exp3.text.replaceAll(nonNumberRegex, ''))
));
}
} else {
shadow.add(Shadow(
offset: Offset(double.tryParse(exp.text.replaceAll(nonNumberRegex, '')), double.tryParse(exp2.text.replaceAll(nonNumberRegex, '')))
));
}
}
}
List<Shadow> finalShadows = shadow.toSet().toList();
return finalShadows;
}
static Color stringToColor(String _text) {
var text = _text.replaceFirst('#', '');
if (text.length == 3)
text = text.replaceAllMapped(
RegExp(r"[a-f]|\d"), (match) => '${match.group(0)}${match.group(0)}');
int color = int.parse(text, radix: 16);
if (color <= 0xffffff) {
return new Color(color).withAlpha(255);
} else {
return new Color(color);
}
}
static Color rgbOrRgbaToColor(String text) {
final rgbaText = text.replaceAll(')', '').replaceAll(' ', '');
try {
final rgbaValues =
rgbaText.split(',').map((value) => double.parse(value)).toList();
if (rgbaValues.length == 4) {
return Color.fromRGBO(
rgbaValues[0].toInt(),
rgbaValues[1].toInt(),
rgbaValues[2].toInt(),
rgbaValues[3],
);
} else if (rgbaValues.length == 3) {
return Color.fromRGBO(
rgbaValues[0].toInt(),
rgbaValues[1].toInt(),
rgbaValues[2].toInt(),
1.0,
);
}
return null;
} catch (e) {
return null;
}
}
}
export 'styled_element.dart';
export 'interactable_element.dart';
export 'replaced_element.dart';
const STYLED_ELEMENTS = [
"abbr",
"acronym",
"address",
"b",
"bdi",
"bdo",
"big",
"cite",
"code",
"data",
"del",
"dfn",
"em",
"font",
"i",
"ins",
"kbd",
"mark",
"q",
"s",
"samp",
"small",
"span",
"strike",
"strong",
"sub",
"sup",
"time",
"tt",
"u",
"var",
"wbr",
//BLOCK ELEMENTS
"article",
"aside",
"blockquote",
"body",
"center",
"dd",
"div",
"dl",
"dt",
"figcaption",
"figure",
"footer",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hr",
"html",
"li",
"main",
"nav",
"noscript",
"ol",
"p",
"pre",
"section",
"summary",
"ul",
];
const INTERACTABLE_ELEMENTS = [
"a",
];
const REPLACED_ELEMENTS = [
"audio",
"br",
"head",
"iframe",
"img",
"svg",
"template",
"video",
"rp",
"rt",
"ruby",
];
const LAYOUT_ELEMENTS = [
"details",
"table",
"tr",
"tbody",
"tfoot",
"thead",
];
const TABLE_CELL_ELEMENTS = ["th", "td"];
const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"];
/**
Here is a list of elements with planned support:
a - i [x]
abbr - s [x]
acronym - s [x]
address - s [x]
audio - c [x]
article - b [x]
aside - b [x]
b - s [x]
bdi - s [x]
bdo - s [x]
big - s [x]
blockquote- b [x]
body - b [x]
br - b [x]
button - i [ ]
caption - b [ ]
center - b [x]
cite - s [x]
code - s [x]
data - s [x]
dd - b [x]
del - s [x]
dfn - s [x]
div - b [x]
dl - b [x]
dt - b [x]
em - s [x]
figcaption- b [x]
figure - b [x]
font - s [x]
footer - b [x]
h1 - b [x]
h2 - b [x]
h3 - b [x]
h4 - b [x]
h5 - b [x]
h6 - b [x]
head - e [x]
header - b [x]
hr - b [x]
html - b [x]
i - s [x]
img - c [x]
ins - s [x]
kbd - s [x]
li - b [x]
main - b [x]
mark - s [x]
nav - b [x]
noscript - b [x]
ol - b [x] post
p - b [x]
pre - b [x]
q - s [x] post
rp - s [x]
rt - s [x]
ruby - s [x]
s - s [x]
samp - s [x]
section - b [x]
small - s [x]
source - [-] child of content
span - s [x]
strike - s [x]
strong - s [x]
sub - s [x]
sup - s [x]
svg - c [x]
table - b [x]
tbody - b [x]
td - s [ ]
template - e [x]
tfoot - b [x]
th - s [ ]
thead - b [x]
time - s [x]
tr - ? [ ]
track - [-] child of content
tt - s [x]
u - s [x]
ul - b [x] post
var - s [x]
video - c [x]
wbr - s [x]
*/
import 'package:flutter/material.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
/// An [InteractableElement] is a [StyledElement] that takes user gestures (e.g. tap).
class InteractableElement extends StyledElement {
String href;
InteractableElement({
String name,
List<StyledElement> children,
Style style,
this.href,
dom.Node node,
}) : super(name: name, children: children, style: style, node: node);
}
/// A [Gesture] indicates the type of interaction by a user.
enum Gesture {
TAP,
}
InteractableElement parseInteractableElement(
dom.Element element, List<StyledElement> children) {
InteractableElement interactableElement = InteractableElement(
name: element.localName,
children: children,
node: element,
);
switch (element.localName) {
case "a":
interactableElement.href = element.attributes['href'];
interactableElement.style = Style(
color: Colors.blue,
textDecoration: TextDecoration.underline,
);
break;
}
return interactableElement;
}
\ No newline at end of file
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/styled_element.dart';
import 'package:flutter_html/style.dart';
import 'package:flutter_layout_grid/flutter_layout_grid.dart';
import 'package:html/dom.dart' as dom;
/// A [LayoutElement] is an element that breaks the normal Inline flow of
/// an html document with a more complex layout. LayoutElements handle
abstract class LayoutElement extends StyledElement {
LayoutElement({
String name,
List<StyledElement> children,
Style style,
dom.Element node,
}) : super(name: name, children: children, style: style, node: node);
Widget toWidget(RenderContext context);
}
class TableLayoutElement extends LayoutElement {
TableLayoutElement({
String name,
Style style,
@required List<StyledElement> children,
dom.Element node,
}) : super(name: name, style: style, children: children, node: node);
@override
Widget toWidget(RenderContext context) {
return Container(
decoration: BoxDecoration(
color: style.backgroundColor,
border: style.border,
),
width: style.width,
height: style.height,
child: _layoutCells(context),
);
}
Widget _layoutCells(RenderContext context) {
final rows = <TableRowLayoutElement>[];
List<TrackSize> columnSizes = <TrackSize>[];
for (var child in children) {
if (child is TableStyleElement) {
// Map <col> tags to predetermined column track sizes
columnSizes = child.children
.where((c) => c.name == "col")
.map((c) {
final span =
int.parse(c.attributes["span"] ?? "1", onError: (_) => 1);
final colWidth = c.attributes["width"];
return List.generate(span, (index) {
if (colWidth != null && colWidth.endsWith("%")) {
final percentageSize = double.tryParse(
colWidth.substring(0, colWidth.length - 1));
return percentageSize != null && !percentageSize.isNaN
? FlexibleTrackSize(percentageSize * 0.01)
: IntrinsicContentTrackSize();
} else if (colWidth != null) {
final fixedPxSize = double.tryParse(colWidth);
return fixedPxSize != null
? FixedTrackSize(fixedPxSize)
: IntrinsicContentTrackSize();
} else {
return IntrinsicContentTrackSize();
}
});
})
.expand((element) => element)
.toList(growable: false);
} else if (child is TableSectionLayoutElement) {
rows.addAll(child.children.whereType());
} else if (child is TableRowLayoutElement) {
rows.add(child);
}
}
// All table rows have a height intrinsic to their (spanned) contents
final rowSizes =
List.generate(rows.length, (_) => IntrinsicContentTrackSize());
// Calculate column bounds
int columnMax = rows
.map((row) => row.children
.whereType<TableCellElement>()
.fold(0, (int value, child) => value + child.colspan))
.fold(0, max);
// Place the cells in the rows/columns
final cells = <GridPlacement>[];
final columnRowOffset = List.generate(columnMax + 1, (_) => 0);
int rowi = 0;
for (var row in rows) {
int columni = 0;
for (var child in row.children) {
if (columnRowOffset[columni] > 0) {
columnRowOffset[columni] = columnRowOffset[columni] - 1;
columni++;
}
if (child is TableCellElement) {
cells.add(GridPlacement(
child: Container(
width: double.infinity,
padding: child.style.padding ?? row.style.padding,
decoration: BoxDecoration(
color: child.style.backgroundColor ?? row.style.backgroundColor,
border: child.style.border ?? row.style.border,
),
child: SizedBox.expand(
child: Container(
alignment: child.style.alignment ??
style.alignment ??
Alignment.centerLeft,
child: StyledText(
textSpan: context.parser.parseTree(context, child),
style: child.style,
renderContext: context,
),
),
),
),
columnStart: columni,
columnSpan: child.colspan,
rowStart: rowi,
rowSpan: child.rowspan,
));
columnRowOffset[columni] = child.rowspan - 1;
columni += child.colspan;
}
}
rowi++;
}
// Create column tracks (insofar there were no colgroups that already defined them)
List<TrackSize> finalColumnSizes =
(columnSizes ?? <TrackSize>[]).take(columnMax).toList();
finalColumnSizes += List.generate(
max(0, columnMax - finalColumnSizes.length),
(_) => IntrinsicContentTrackSize());
return LayoutGrid(
gridFit: GridFit.loose,
templateColumnSizes: finalColumnSizes,
templateRowSizes: rowSizes,
children: cells,
);
}
}
class TableSectionLayoutElement extends LayoutElement {
TableSectionLayoutElement({
String name,
@required List<StyledElement> children,
}) : super(name: name, children: children);
@override
Widget toWidget(RenderContext context) {
// Not rendered; TableLayoutElement will instead consume its children
return Container(child: Text("TABLE SECTION"));
}
}
class TableRowLayoutElement extends LayoutElement {
TableRowLayoutElement({
String name,
@required List<StyledElement> children,
dom.Element node,
}) : super(name: name, children: children, node: node);
@override
Widget toWidget(RenderContext context) {
// Not rendered; TableLayoutElement will instead consume its children
return Container(child: Text("TABLE ROW"));
}
}
class TableCellElement extends StyledElement {
int colspan = 1;
int rowspan = 1;
TableCellElement({
String name,
String elementId,
List<String> elementClasses,
@required List<StyledElement> children,
Style style,
dom.Element node,
}) : super(
name: name,
elementId: elementId,
elementClasses: elementClasses,
children: children,
style: style,
node: node) {
colspan = _parseSpan(this, "colspan");
rowspan = _parseSpan(this, "rowspan");
}
static int _parseSpan(StyledElement element, String attributeName) {
final spanValue = element.attributes[attributeName];
return spanValue == null ? 1 : int.tryParse(spanValue) ?? 1;
}
}
TableCellElement parseTableCellElement(
dom.Element element,
List<StyledElement> children,
) {
final cell = TableCellElement(
name: element.localName,
elementId: element.id,
elementClasses: element.classes.toList(),
children: children,
node: element,
);
if (element.localName == "th") {
cell.style = Style(
fontWeight: FontWeight.bold,
);
}
return cell;
}
class TableStyleElement extends StyledElement {
TableStyleElement({
String name,
List<StyledElement> children,
Style style,
dom.Element node,
}) : super(name: name, children: children, style: style, node: node);
}
TableStyleElement parseTableDefinitionElement(
dom.Element element,
List<StyledElement> children,
) {
switch (element.localName) {
case "colgroup":
case "col":
return TableStyleElement(
name: element.localName,
children: children,
node: element,
);
default:
return TableStyleElement();
}
}
class DetailsContentElement extends LayoutElement {
List<dom.Element> elementList;
DetailsContentElement({
String name,
List<StyledElement> children,
dom.Element node,
this.elementList,
}) : super(name: name, node: node, children: children);
@override
Widget toWidget(RenderContext context) {
List<InlineSpan> childrenList = children?.map((tree) => context.parser.parseTree(context, tree))?.toList();
List<InlineSpan> toRemove = [];
if (childrenList != null) {
for (InlineSpan child in childrenList) {
if (child is TextSpan && child.text != null && child.text.trim().isEmpty) {
toRemove.add(child);
}
}
for (InlineSpan child in toRemove) {
childrenList.remove(child);
}
}
InlineSpan firstChild = childrenList?.isNotEmpty == true ? childrenList.first : null;
return ExpansionTile(
expandedAlignment: Alignment.centerLeft,
title: elementList?.isNotEmpty == true && elementList?.first?.localName == "summary" ? StyledText(
textSpan: TextSpan(
style: style.generateTextStyle(),
children: [firstChild] ?? [],
),
style: style,
renderContext: context,
) : Text("Details"),
children: [
StyledText(
textSpan: TextSpan(
style: style.generateTextStyle(),
children: getChildren(childrenList, context, elementList?.isNotEmpty == true && elementList?.first?.localName == "summary" ? firstChild : null)
),
style: style,
renderContext: context,
),
]
);
}
List<InlineSpan> getChildren(List<InlineSpan> children, RenderContext context, InlineSpan firstChild) {
if (children == null) {
return [];
} else {
if (firstChild != null) children.removeAt(0);
return children;
}
}
}
class EmptyLayoutElement extends LayoutElement {
EmptyLayoutElement({String name = "empty"}) : super(name: name);
@override
Widget toWidget(_) => null;
}
LayoutElement parseLayoutElement(
dom.Element element,
List<StyledElement> children,
) {
switch (element.localName) {
case "details":
if (children?.isEmpty ?? false) {
return EmptyLayoutElement();
}
return DetailsContentElement(
node: element,
name: element.localName,
children: children,
elementList: element.children
);
case "table":
return TableLayoutElement(
name: element.localName,
children: children,
node: element,
);
break;
case "thead":
case "tbody":
case "tfoot":
return TableSectionLayoutElement(
name: element.localName,
children: children,
);
break;
case "tr":
return TableRowLayoutElement(
name: element.localName,
children: children,
node: element,
);
break;
default:
return TableLayoutElement(children: children);
}
}
import 'dart:math';
import 'package:chewie/chewie.dart';
import 'package:chewie_audio/chewie_audio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:html/dom.dart' as dom;
import 'package:video_player/video_player.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered.
///
/// A [ReplacedElement] may use its children nodes to determine relevant information
/// (e.g. <video>'s <source> tags), but the children nodes will not be saved as [children].
abstract class ReplacedElement extends StyledElement {
PlaceholderAlignment alignment;
ReplacedElement(
{String name,
Style style,
dom.Element node,
this.alignment = PlaceholderAlignment.aboveBaseline})
: super(name: name, children: null, style: style, node: node);
static List<String> parseMediaSources(List<dom.Element> elements) {
return elements
.where((element) => element.localName == 'source')
.map((element) {
return element.attributes['src'];
}).toList();
}
Widget toWidget(RenderContext context);
}
/// [TextContentElement] is a [ContentElement] with plaintext as its content.
class TextContentElement extends ReplacedElement {
String text;
TextContentElement({
Style style,
this.text,
}) : super(name: "[text]", style: style);
@override
String toString() {
return "\"${text.replaceAll("\n", "\\n")}\"";
}
@override
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({
String name,
Style style,
this.src,
this.alt,
dom.Element node,
}) : super(
name: name,
style: style,
node: node,
alignment: PlaceholderAlignment.middle,
);
@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);
if (widget != null) {
return widget;
}
}
}
return SizedBox(width: 0, height: 0);
}
}
/// [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({
String name,
Style style,
this.src,
this.width,
this.height,
dom.Element node,
this.navigationDelegate,
}) : super(name: name, style: style, node: node);
@override
Widget toWidget(RenderContext context) {
final sandboxMode = attributes["sandbox"];
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: src,
key: key,
javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts"
? JavascriptMode.unrestricted
: JavascriptMode.disabled,
navigationDelegate: navigationDelegate,
gestureRecognizers: {
Factory<VerticalDragGestureRecognizer>(() => VerticalDragGestureRecognizer())
},
),
);
}
}
/// [AudioContentElement] is a [ContentElement] with an audio file as its content.
class AudioContentElement extends ReplacedElement {
final List<String> src;
final bool showControls;
final bool autoplay;
final bool loop;
final bool muted;
AudioContentElement({
String name,
Style style,
this.src,
this.showControls,
this.autoplay,
this.loop,
this.muted,
dom.Element node,
}) : super(name: name, style: style, node: node);
@override
Widget toWidget(RenderContext context) {
return Container(
width: context.style.width ?? 300,
child: ChewieAudio(
controller: ChewieAudioController(
videoPlayerController: VideoPlayerController.network(
src.first ?? "",
),
autoPlay: autoplay,
looping: loop,
showControls: showControls,
autoInitialize: true,
),
),
);
}
}
/// [VideoContentElement] is a [ContentElement] with a video file as its content.
class VideoContentElement extends ReplacedElement {
final List<String> src;
final String poster;
final bool showControls;
final bool autoplay;
final bool loop;
final bool muted;
final double width;
final double height;
VideoContentElement({
String name,
Style style,
this.src,
this.poster,
this.showControls,
this.autoplay,
this.loop,
this.muted,
this.width,
this.height,
dom.Element node,
}) : super(name: name, style: style, node: node);
@override
Widget toWidget(RenderContext context) {
final double _width = width ?? (height ?? 150) * 2;
final double _height = height ?? (width ?? 300) / 2;
return AspectRatio(
aspectRatio: _width / _height,
child: Container(
child: Chewie(
controller: ChewieController(
videoPlayerController: VideoPlayerController.network(
src.first ?? "",
),
placeholder: poster != null
? Image.network(poster)
: Container(color: Colors.black),
autoPlay: autoplay,
looping: loop,
showControls: showControls,
autoInitialize: true,
aspectRatio: _width / _height,
),
),
),
);
}
}
/// [SvgContentElement] is a [ReplacedElement] with an SVG as its contents.
class SvgContentElement extends ReplacedElement {
final String data;
final double width;
final double height;
SvgContentElement({
this.data,
this.width,
this.height,
});
@override
Widget toWidget(RenderContext context) {
return SvgPicture.string(
data,
width: width,
height: height,
);
}
}
class EmptyContentElement extends ReplacedElement {
EmptyContentElement({String name = "empty"}) : super(name: name);
@override
Widget toWidget(_) => null;
}
class RubyElement extends ReplacedElement {
dom.Element element;
RubyElement({@required this.element, String name = "ruby"})
: super(name: name, alignment: PlaceholderAlignment.middle);
@override
Widget toWidget(RenderContext context) {
dom.Node textNode;
List<Widget> widgets = List<Widget>();
//TODO calculate based off of parent font size.
final rubySize = max(9.0, context.style.fontSize.size / 2);
final rubyYPos = rubySize + rubySize / 2;
element.nodes.forEach((c) {
if (c.nodeType == dom.Node.TEXT_NODE) {
textNode = c;
}
if (c is dom.Element) {
if (c.localName == "rt" && textNode != null) {
final widget = Stack(
alignment: Alignment.center,
children: <Widget>[
Container(
alignment: Alignment.bottomCenter,
child: Center(
child: Transform(
transform:
Matrix4.translationValues(0, -(rubyYPos), 0),
child: Text(c.innerHtml,
style: context.style
.generateTextStyle()
.copyWith(fontSize: rubySize))))),
Container(
child: Text(textNode.text.trim(),
style: context.style.generateTextStyle())),
],
);
widgets.add(widget);
}
}
});
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
textBaseline: TextBaseline.alphabetic,
mainAxisSize: MainAxisSize.min,
children: widgets,
);
}
}
ReplacedElement parseReplacedElement(
dom.Element element,
NavigationDelegate navigationDelegateForIframe,
) {
switch (element.localName) {
case "audio":
final sources = <String>[
if (element.attributes['src'] != null) element.attributes['src'],
...ReplacedElement.parseMediaSources(element.children),
];
if (sources == null || 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",
style: Style(whiteSpace: WhiteSpace.PRE),
);
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 = <String>[
if (element.attributes['src'] != null) element.attributes['src'],
...ReplacedElement.parseMediaSources(element.children),
];
if (sources == null || 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(
data: element.outerHtml,
width: double.tryParse(element.attributes['width'] ?? ""),
height: double.tryParse(element.attributes['height'] ?? ""),
);
case "ruby":
return RubyElement(
element: element,
);
default:
return EmptyContentElement(name: element.localName);
}
}
import 'package:flutter/material.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
//TODO(Sub6Resources): don't use the internal code of the html package as it may change unexpectedly.
import 'package:html/src/query_selector.dart';
/// A [StyledElement] applies a style to all of its children.
class StyledElement {
final String name;
final String elementId;
final List<String> elementClasses;
List<StyledElement> children;
Style style;
final dom.Node _node;
StyledElement({
this.name = "[[No name]]",
this.elementId,
this.elementClasses,
this.children,
this.style,
dom.Element node,
}) : this._node = node;
bool matchesSelector(String selector) =>
_node != null && matches(_node, selector);
Map<String, String> get attributes =>
_node?.attributes?.map((key, value) {
return MapEntry(key, value);
}) ??
Map<String, String>();
dom.Element get element => _node;
@override
String toString() {
String selfData =
"[$name] ${children?.length ?? 0} ${elementClasses?.isNotEmpty == true ? 'C:${elementClasses.toString()}' : ''}${elementId?.isNotEmpty == true ? 'ID: $elementId' : ''}";
children?.forEach((child) {
selfData += ("\n${child.toString()}")
.replaceAll(RegExp("^", multiLine: true), "-");
});
return selfData;
}
}
StyledElement parseStyledElement(
dom.Element element, List<StyledElement> children) {
StyledElement styledElement = StyledElement(
name: element.localName,
elementId: element.id,
elementClasses: element.classes.toList(),
children: children,
node: element,
);
switch (element.localName) {
case "abbr":
case "acronym":
styledElement.style = Style(
textDecoration: TextDecoration.underline,
textDecorationStyle: TextDecorationStyle.dotted,
);
break;
case "address":
continue italics;
case "article":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "aside":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
bold:
case "b":
styledElement.style = Style(
fontWeight: FontWeight.bold,
);
break;
case "bdo":
TextDirection textDirection =
((element.attributes["dir"] ?? "ltr") == "rtl")
? TextDirection.rtl
: TextDirection.ltr;
styledElement.style = Style(
direction: textDirection,
);
break;
case "big":
styledElement.style = Style(
fontSize: FontSize.larger,
);
break;
case "blockquote":
//TODO(Sub6Resources) this is a workaround for collapsing margins. Remove.
if (element.parent.localName == "blockquote") {
styledElement.style = Style(
margin: const EdgeInsets.only(left: 40.0, right: 40.0, bottom: 14.0),
display: Display.BLOCK,
);
} else {
styledElement.style = Style(
margin: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 14.0),
display: Display.BLOCK,
);
}
break;
case "body":
styledElement.style = Style(
margin: EdgeInsets.all(8.0),
display: Display.BLOCK,
);
break;
case "center":
styledElement.style = Style(
alignment: Alignment.center,
display: Display.BLOCK,
);
break;
case "cite":
continue italics;
monospace:
case "code":
styledElement.style = Style(
fontFamily: 'Monospace',
);
break;
case "dd":
styledElement.style = Style(
margin: EdgeInsets.only(left: 40.0),
display: Display.BLOCK,
);
break;
strikeThrough:
case "del":
styledElement.style = Style(
textDecoration: TextDecoration.lineThrough,
);
break;
case "dfn":
continue italics;
case "div":
styledElement.style = Style(
margin: EdgeInsets.all(0),
display: Display.BLOCK,
);
break;
case "dl":
styledElement.style = Style(
margin: EdgeInsets.symmetric(vertical: 14.0),
display: Display.BLOCK,
);
break;
case "dt":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "em":
continue italics;
case "figcaption":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "figure":
styledElement.style = Style(
margin: EdgeInsets.symmetric(vertical: 14.0, horizontal: 40.0),
display: Display.BLOCK,
);
break;
case "footer":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "h1":
styledElement.style = Style(
fontSize: FontSize.xxLarge,
fontWeight: FontWeight.bold,
margin: EdgeInsets.symmetric(vertical: 18.67),
display: Display.BLOCK,
);
break;
case "h2":
styledElement.style = Style(
fontSize: FontSize.xLarge,
fontWeight: FontWeight.bold,
margin: EdgeInsets.symmetric(vertical: 17.5),
display: Display.BLOCK,
);
break;
case "h3":
styledElement.style = Style(
fontSize: FontSize(16.38),
fontWeight: FontWeight.bold,
margin: EdgeInsets.symmetric(vertical: 16.5),
display: Display.BLOCK,
);
break;
case "h4":
styledElement.style = Style(
fontSize: FontSize.medium,
fontWeight: FontWeight.bold,
margin: EdgeInsets.symmetric(vertical: 18.5),
display: Display.BLOCK,
);
break;
case "h5":
styledElement.style = Style(
fontSize: FontSize(11.62),
fontWeight: FontWeight.bold,
margin: EdgeInsets.symmetric(vertical: 19.25),
display: Display.BLOCK,
);
break;
case "h6":
styledElement.style = Style(
fontSize: FontSize(9.38),
fontWeight: FontWeight.bold,
margin: EdgeInsets.symmetric(vertical: 22),
display: Display.BLOCK,
);
break;
case "header":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "hr":
styledElement.style = Style(
margin: EdgeInsets.symmetric(vertical: 7.0),
width: double.infinity,
border: Border(bottom: BorderSide(width: 1.0)),
display: Display.BLOCK,
);
break;
case "html":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
italics:
case "i":
styledElement.style = Style(
fontStyle: FontStyle.italic,
);
break;
case "ins":
continue underline;
case "kbd":
continue monospace;
case "li":
styledElement.style = Style(
display: Display.LIST_ITEM,
);
break;
case "main":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "mark":
styledElement.style = Style(
color: Colors.black,
backgroundColor: Colors.yellow,
);
break;
case "nav":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "noscript":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "ol":
case "ul":
//TODO(Sub6Resources): This is a workaround for collapsed margins. Remove.
if (element.parent.localName == "li") {
styledElement.style = Style(
// margin: EdgeInsets.only(left: 30.0),
display: Display.BLOCK,
listStyleType: element.localName == "ol"
? ListStyleType.DECIMAL
: ListStyleType.DISC,
);
} else {
styledElement.style = Style(
// margin: EdgeInsets.only(left: 30.0, top: 14.0, bottom: 14.0),
display: Display.BLOCK,
listStyleType: element.localName == "ol"
? ListStyleType.DECIMAL
: ListStyleType.DISC,
);
}
break;
case "p":
styledElement.style = Style(
margin: EdgeInsets.symmetric(vertical: 14.0),
display: Display.BLOCK,
);
break;
case "pre":
styledElement.style = Style(
fontFamily: 'monospace',
margin: EdgeInsets.symmetric(vertical: 14.0),
whiteSpace: WhiteSpace.PRE,
display: Display.BLOCK,
);
break;
case "q":
styledElement.style = Style(
before: "\"",
after: "\"",
);
break;
case "s":
continue strikeThrough;
case "samp":
continue monospace;
case "section":
styledElement.style = Style(
display: Display.BLOCK,
);
break;
case "small":
styledElement.style = Style(
fontSize: FontSize.smaller,
);
break;
case "strike":
continue strikeThrough;
case "strong":
continue bold;
case "sub":
styledElement.style = Style(
fontSize: FontSize.smaller,
verticalAlign: VerticalAlign.SUB,
);
break;
case "sup":
styledElement.style = Style(
fontSize: FontSize.smaller,
verticalAlign: VerticalAlign.SUPER,
);
break;
case "tt":
continue monospace;
underline:
case "u":
styledElement.style = Style(
textDecoration: TextDecoration.underline,
);
break;
case "var":
continue italics;
}
return styledElement;
}
typedef ListCharacter = String Function(int i);
import 'package:flutter/gestures.dart';
class Context<T> {
T data;
Context(this.data);
}
// This class is a workaround so that both an image
// and a link can detect taps at the same time.
class MultipleTapGestureRecognizer extends TapGestureRecognizer {
bool _ready = false;
@override
void addAllowedPointer(PointerDownEvent event) {
if (state == GestureRecognizerState.ready) {
_ready = true;
}
super.addAllowedPointer(event);
}
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerCancelEvent) {
_ready = false;
}
super.handlePrimaryPointer(event);
}
@override
void resolve(GestureDisposition disposition) {
if (_ready && disposition == GestureDisposition.rejected) {
_ready = false;
}
super.resolve(disposition);
}
@override
void rejectGesture(int pointer) {
if (_ready) {
acceptGesture(pointer);
_ready = false;
}
}
}
import 'dart:ui';
import 'package:flutter/material.dart';
///This class represents all the available CSS attributes
///for this package.
class Style {
/// CSS attribute "`background-color`"
///
/// Inherited: no,
/// Default: Colors.transparent,
Color backgroundColor;
/// CSS attribute "`color`"
///
/// Inherited: yes,
/// Default: unspecified,
Color color;
/// CSS attribute "`direction`"
///
/// Inherited: yes,
/// Default: TextDirection.ltr,
TextDirection direction;
/// CSS attribute "`display`"
///
/// Inherited: no,
/// Default: unspecified,
Display display;
/// CSS attribute "`font-family`"
///
/// Inherited: yes,
/// Default: Theme.of(context).style.textTheme.body1.fontFamily
String fontFamily;
/// CSS attribute "`font-feature-settings`"
///
/// Inherited: yes,
/// Default: normal
List<FontFeature> fontFeatureSettings;
/// CSS attribute "`font-size`"
///
/// Inherited: yes,
/// Default: FontSize.medium
FontSize fontSize;
/// CSS attribute "`font-style`"
///
/// Inherited: yes,
/// Default: FontStyle.normal,
FontStyle fontStyle;
/// CSS attribute "`font-weight`"
///
/// Inherited: yes,
/// Default: FontWeight.normal,
FontWeight fontWeight;
/// CSS attribute "`height`"
///
/// Inherited: no,
/// Default: Unspecified (null),
double height;
/// CSS attribute "`letter-spacing`"
///
/// Inherited: yes,
/// Default: normal (0),
double letterSpacing;
/// CSS attribute "`list-style-type`"
///
/// Inherited: yes,
/// Default: ListStyleType.DISC
ListStyleType listStyleType;
/// CSS attribute "`list-style-position`"
///
/// Inherited: yes,
/// Default: ListStylePosition.OUTSIDE
ListStylePosition listStylePosition;
/// CSS attribute "`padding`"
///
/// Inherited: no,
/// Default: EdgeInsets.zero
EdgeInsets padding;
/// CSS attribute "`margin`"
///
/// Inherited: no,
/// Default: EdgeInsets.zero
EdgeInsets margin;
/// CSS attribute "`text-align`"
///
/// Inherited: yes,
/// Default: TextAlign.start,
TextAlign textAlign;
/// CSS attribute "`text-decoration`"
///
/// Inherited: no,
/// Default: TextDecoration.none,
TextDecoration textDecoration;
/// CSS attribute "`text-decoration-color`"
///
/// Inherited: no,
/// Default: Current color
Color textDecorationColor;
/// CSS attribute "`text-decoration-style`"
///
/// Inherited: no,
/// Default: TextDecorationStyle.solid,
TextDecorationStyle textDecorationStyle;
/// Loosely based on CSS attribute "`text-decoration-thickness`"
///
/// Uses a percent modifier based on the font size.
///
/// Inherited: no,
/// Default: 1.0 (specified by font size)
// TODO(Sub6Resources): Possibly base this more closely on the CSS attribute.
double textDecorationThickness;
/// CSS attribute "`text-shadow`"
///
/// Inherited: yes,
/// Default: none,
List<Shadow> textShadow;
/// CSS attribute "`vertical-align`"
///
/// Inherited: no,
/// Default: VerticalAlign.BASELINE,
VerticalAlign verticalAlign;
/// CSS attribute "`white-space`"
///
/// Inherited: yes,
/// Default: WhiteSpace.NORMAL,
WhiteSpace whiteSpace;
/// CSS attribute "`width`"
///
/// Inherited: no,
/// Default: unspecified (null)
double width;
/// CSS attribute "`word-spacing`"
///
/// Inherited: yes,
/// Default: normal (0)
double wordSpacing;
/// CSS attribute "`line-height`"
///
/// Supported values: double values
///
/// Unsupported values: normal, 80%, ..
///
/// Inherited: no,
/// Default: Unspecified (null),
LineHeight lineHeight;
//TODO modify these to match CSS styles
String before;
String after;
Border border;
Alignment alignment;
String markerContent;
Style({
this.backgroundColor = Colors.transparent,
this.color,
this.direction,
this.display,
this.fontFamily,
this.fontFeatureSettings,
this.fontSize,
this.fontStyle,
this.fontWeight,
this.height,
this.lineHeight,
this.letterSpacing,
this.listStyleType,
this.listStylePosition,
this.padding,
this.margin,
this.textAlign,
this.textDecoration,
this.textDecorationColor,
this.textDecorationStyle,
this.textDecorationThickness,
this.textShadow,
this.verticalAlign,
this.whiteSpace,
this.width,
this.wordSpacing,
this.before,
this.after,
this.border,
this.alignment,
this.markerContent,
}) {
if (this.alignment == null &&
(display == Display.BLOCK || display == Display.LIST_ITEM)) {
this.alignment = Alignment.centerLeft;
}
}
TextStyle generateTextStyle() {
return TextStyle(
backgroundColor: backgroundColor,
color: color,
decoration: textDecoration,
decorationColor: textDecorationColor,
decorationStyle: textDecorationStyle,
decorationThickness: textDecorationThickness,
fontFamily: fontFamily,
fontFeatures: fontFeatureSettings,
fontSize: fontSize?.size,
fontStyle: fontStyle,
fontWeight: fontWeight,
letterSpacing: letterSpacing,
shadows: textShadow,
wordSpacing: wordSpacing,
height: lineHeight?.size ?? 1.0,
//TODO background
//TODO textBaseline
);
}
@override
String toString() {
return "Style";
}
Style merge(Style other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
color: other.color,
direction: other.direction,
display: other.display,
fontFamily: other.fontFamily,
fontFeatureSettings: other.fontFeatureSettings,
fontSize: other.fontSize,
fontStyle: other.fontStyle,
fontWeight: other.fontWeight,
height: other.height,
lineHeight: other.lineHeight,
letterSpacing: other.letterSpacing,
listStyleType: other.listStyleType,
listStylePosition: other.listStylePosition,
padding: other.padding,
//TODO merge EdgeInsets
margin: other.margin,
//TODO merge EdgeInsets
textAlign: other.textAlign,
textDecoration: other.textDecoration,
textDecorationColor: other.textDecorationColor,
textDecorationStyle: other.textDecorationStyle,
textDecorationThickness: other.textDecorationThickness,
textShadow: other.textShadow,
verticalAlign: other.verticalAlign,
whiteSpace: other.whiteSpace,
width: other.width,
wordSpacing: other.wordSpacing,
before: other.before,
after: other.after,
border: other.border,
//TODO merge border
alignment: other.alignment,
markerContent: other.markerContent,
);
}
Style copyOnlyInherited(Style child) {
if (child == null) return this;
FontSize finalFontSize = child.fontSize != null ?
fontSize != null && child.fontSize?.units == "em" ?
FontSize(child.fontSize.size * fontSize.size) : child.fontSize
: fontSize != null && fontSize.size < 0 ?
FontSize.percent(100) : fontSize;
LineHeight finalLineHeight = child.lineHeight != null ?
child.lineHeight?.units == "length" ?
LineHeight(child.lineHeight.size / (finalFontSize == null ? 14 : finalFontSize.size) * 1.2) : child.lineHeight
: lineHeight;
return child.copyWith(
color: child.color ?? color,
direction: child.direction ?? direction,
display: display == Display.NONE ? display : child.display,
fontFamily: child.fontFamily ?? fontFamily,
fontFeatureSettings: child.fontFeatureSettings ?? fontFeatureSettings,
fontSize: finalFontSize,
fontStyle: child.fontStyle ?? fontStyle,
fontWeight: child.fontWeight ?? fontWeight,
lineHeight: finalLineHeight,
letterSpacing: child.letterSpacing ?? letterSpacing,
listStyleType: child.listStyleType ?? listStyleType,
listStylePosition: child.listStylePosition ?? listStylePosition,
textAlign: child.textAlign ?? textAlign,
textShadow: child.textShadow ?? textShadow,
whiteSpace: child.whiteSpace ?? whiteSpace,
wordSpacing: child.wordSpacing ?? wordSpacing,
);
}
Style copyWith({
Color backgroundColor,
Color color,
TextDirection direction,
Display display,
String fontFamily,
List<FontFeature> fontFeatureSettings,
FontSize fontSize,
FontStyle fontStyle,
FontWeight fontWeight,
double height,
LineHeight lineHeight,
double letterSpacing,
ListStyleType listStyleType,
ListStylePosition listStylePosition,
EdgeInsets padding,
EdgeInsets margin,
TextAlign textAlign,
TextDecoration textDecoration,
Color textDecorationColor,
TextDecorationStyle textDecorationStyle,
double textDecorationThickness,
List<Shadow> textShadow,
VerticalAlign verticalAlign,
WhiteSpace whiteSpace,
double width,
double wordSpacing,
String before,
String after,
Border border,
Alignment alignment,
String markerContent,
}) {
return Style(
backgroundColor: backgroundColor ?? this.backgroundColor,
color: color ?? this.color,
direction: direction ?? this.direction,
display: display ?? this.display,
fontFamily: fontFamily ?? this.fontFamily,
fontFeatureSettings: fontFeatureSettings ?? this.fontFeatureSettings,
fontSize: fontSize ?? this.fontSize,
fontStyle: fontStyle ?? this.fontStyle,
fontWeight: fontWeight ?? this.fontWeight,
height: height ?? this.height,
lineHeight: lineHeight ?? this.lineHeight,
letterSpacing: letterSpacing ?? this.letterSpacing,
listStyleType: listStyleType ?? this.listStyleType,
listStylePosition: listStylePosition ?? this.listStylePosition,
padding: padding ?? this.padding,
margin: margin ?? this.margin,
textAlign: textAlign ?? this.textAlign,
textDecoration: textDecoration ?? this.textDecoration,
textDecorationColor: textDecorationColor ?? this.textDecorationColor,
textDecorationStyle: textDecorationStyle ?? this.textDecorationStyle,
textDecorationThickness:
textDecorationThickness ?? this.textDecorationThickness,
textShadow: textShadow ?? this.textShadow,
verticalAlign: verticalAlign ?? this.verticalAlign,
whiteSpace: whiteSpace ?? this.whiteSpace,
width: width ?? this.width,
wordSpacing: wordSpacing ?? this.wordSpacing,
before: before ?? this.before,
after: after ?? this.after,
border: border ?? this.border,
alignment: alignment ?? this.alignment,
markerContent: markerContent ?? this.markerContent,
);
}
Style.fromTextStyle(TextStyle textStyle) {
this.backgroundColor = textStyle.backgroundColor;
this.color = textStyle.color;
this.textDecoration = textStyle.decoration;
this.textDecorationColor = textStyle.decorationColor;
this.textDecorationStyle = textStyle.decorationStyle;
this.textDecorationThickness = textStyle.decorationThickness;
this.fontFamily = textStyle.fontFamily;
this.fontFeatureSettings = textStyle.fontFeatures;
this.fontSize = FontSize(textStyle.fontSize);
this.fontStyle = textStyle.fontStyle;
this.fontWeight = textStyle.fontWeight;
this.letterSpacing = textStyle.letterSpacing;
this.textShadow = textStyle.shadows;
this.wordSpacing = textStyle.wordSpacing;
this.lineHeight = LineHeight(textStyle.height ?? 1.2);
}
}
enum Display {
BLOCK,
INLINE,
INLINE_BLOCK,
LIST_ITEM,
NONE,
}
class FontSize {
final double size;
final String units;
const FontSize(this.size, {this.units = ""});
/// A percentage of the parent style's font size.
factory FontSize.percent(int percent) {
return FontSize(percent.toDouble() / -100.0, units: "%");
}
factory FontSize.em(double em) {
return FontSize(em, units: "em");
}
factory FontSize.rem(double rem) {
return FontSize(rem * 16 - 2, units: "rem");
}
// These values are calculated based off of the default (`medium`)
// being 14px.
//
// TODO(Sub6Resources): This seems to override Flutter's accessibility text scaling.
//
// Negative values are computed during parsing to be a percentage of
// the parent style's font size.
static const xxSmall = FontSize(7.875);
static const xSmall = FontSize(8.75);
static const small = FontSize(11.375);
static const medium = FontSize(14.0);
static const large = FontSize(15.75);
static const xLarge = FontSize(21.0);
static const xxLarge = FontSize(28.0);
static const smaller = FontSize(-0.83);
static const larger = FontSize(-1.2);
}
class LineHeight {
final double size;
final String units;
const LineHeight(this.size, {this.units = ""});
factory LineHeight.percent(double percent) {
return LineHeight(percent / 100.0 * 1.2, units: "%");
}
factory LineHeight.em(double em) {
return LineHeight(em * 1.2, units: "em");
}
factory LineHeight.rem(double rem) {
return LineHeight(rem * 1.2, units: "rem");
}
factory LineHeight.number(double num) {
return LineHeight(num * 1.2, units: "number");
}
static const normal = LineHeight(1.2);
}
enum ListStyleType {
DISC,
DECIMAL,
}
enum ListStylePosition {
OUTSIDE,
INSIDE,
}
enum VerticalAlign {
BASELINE,
SUB,
SUPER,
}
enum WhiteSpace {
NORMAL,
PRE,
}
name: flutter_html
description: A Flutter widget rendering static HTML and CSS as Flutter widgets.
version: 1.3.0
homepage: https://github.com/Sub6Resources/flutter_html
environment:
sdk: '>=2.2.2 <3.0.0'
flutter: '>=1.17.0'
dependencies:
# Plugin for parsing html
html: ^0.14.0+4
# Plugins for parsing css
csslib: ^0.16.2
css_colors: ^1.0.2
# Plugins for rendering the <table> tag.
flutter_layout_grid: ^0.10.5
# Plugins for rendering the <video> tag.
video_player: ^1.0.1
chewie: ^0.12.2
# Plugin for rendering the <iframe> tag.
webview_flutter: ^1.0.7
# Plugins for rendering the <audio> tag.
chewie_audio: ^1.1.2
# Plugins for rendering the <svg> tag.
flutter_svg: 0.19.1
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_test/flutter_test.dart';
import 'test_data.dart';
class TestApp extends StatelessWidget {
final Widget body;
TestApp(this.body);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: body,
appBar: AppBar(title: Text('flutter_html')),
),
);
}
}
void testHtml(String name, String htmlData) {
testWidgets('$name golden test', (WidgetTester tester) async {
await tester.pumpWidget(
TestApp(
Html(
data: htmlData,
),
),
);
expect(find.byType(Html), findsOneWidget);
// await expectLater(find.byType(Html), matchesGoldenFile('./goldens/$name.png'));
});
}
void main() {
//Test each HTML element
testData.forEach((key, value) {
testHtml(key, value);
});
//Test whitespace processing:
testWidgets('whitespace golden test', (WidgetTester tester) async {
await tester.pumpWidget(
TestApp(
Html(data: """
<p id='whitespace'>
These two lines should have an identical length:<br /><br />
The quick <b> brown </b><u><i> fox </i></u> jumped over the
lazy
dog.<br />
The quick brown fox jumped over the lazy dog.
</p>
"""),
),
);
// await expectLater(find.byType(Html), matchesGoldenFile('./goldens/whitespace.png'));
});
}
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets("Check that default parser does not fail on empty data",
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Html(
data: "",
),
),
),
);
});
testNewParser();
}
void testNewParser() {
test("Html Parser works correctly", () {
HtmlParser.parseHTML("<b>Hello, World!</b>");
});
test("lexDomTree works correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML(
"Hello! <b>Hello, World!</b><i>Hello, New World!</i>"),
[],
[],
null,
);
print(tree.toString());
});
test("InteractableElements work correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML(
"Hello, World! <a href='https://example.com'>This is a link</a>"),
[],
[],
null);
print(tree.toString());
});
test("ContentElements work correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML("<img src='https://image.example.com' />"),
[],
[],
null,
);
print(tree.toString());
});
test("Nesting of elements works correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML(
"<div><div><div><div><a href='link'>Link</a><div>Hello, World! <b>Bold and <i>Italic</i></b></div></div></div></div></div>"),
[],
[],
null,
);
print(tree.toString());
});
test("Video Content Source Parser works correctly", () {
ReplacedElement videoContentElement = parseReplacedElement(
HtmlParser.parseHTML("""
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
Your browser does not support the video tag.
</video>
""").getElementsByTagName("video")[0],
null,
);
expect(videoContentElement, isA<VideoContentElement>());
if (videoContentElement is VideoContentElement) {
expect(videoContentElement.showControls, equals(true),
reason: "Controls isn't working");
expect(videoContentElement.src, hasLength(2),
reason: "Not enough sources...");
}
});
test("Audio Content Source Parser works correctly", () {
ReplacedElement audioContentElement = parseReplacedElement(
HtmlParser.parseHTML("""
<audio controls>
<source src='audio.mp3' type='audio/mpeg'>
<source src='audio.wav' type='audio/wav'>
Your browser does not support the audio tag.
</audio>
""").getElementsByTagName("audio")[0],
null,
);
expect(audioContentElement, isA<AudioContentElement>());
if (audioContentElement is AudioContentElement) {
expect(audioContentElement.showControls, equals(true),
reason: "Controls isn't working");
expect(audioContentElement.src, hasLength(2),
reason: "Not enough sources...");
}
});
test("Test style merging", () {
Style style1 = Style(
display: Display.BLOCK,
fontWeight: FontWeight.bold,
);
Style style2 = Style(
before: "* ",
direction: TextDirection.rtl,
fontStyle: FontStyle.italic,
);
Style finalStyle = style1.merge(style2);
expect(finalStyle.display, equals(Display.BLOCK));
expect(finalStyle.before, equals("* "));
expect(finalStyle.direction, equals(TextDirection.rtl));
expect(finalStyle.fontStyle, equals(FontStyle.italic));
expect(finalStyle.fontWeight, equals(FontWeight.bold));
});
}
import 'package:flutter_html/image_render.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:html/dom.dart' as dom;
void main() {
group("asset uri matcher", () {
ImageSourceMatcher matcher = assetUriMatcher();
test("matches a full asset: uri", () {
expect(_match(matcher, 'asset:some/asset.png'), isTrue);
});
test("matches asset: schema without path", () {
expect(_match(matcher, 'asset:'), isTrue);
});
test("doesn't match literal host 'asset'", () {
expect(_match(matcher, 'asset/faulty.path'), isFalse);
});
test("doesn't match null", () {
expect(_match(matcher, null), isFalse);
});
test("doesn't match empty", () {
expect(_match(matcher, ''), isFalse);
});
});
group("default network source matcher", () {
ImageSourceMatcher matcher = networkSourceMatcher();
test("matches a full http uri", () {
expect(_match(matcher, 'http://legacy.http/uri.png'), isTrue);
});
test("matches a full https uri", () {
expect(_match(matcher, 'https://proper.https/uri'), isTrue);
});
test("matches http: schema without path", () {
expect(_match(matcher, 'http:'), isTrue);
});
test("matches https: schema without path", () {
expect(_match(matcher, 'http:'), isTrue);
});
test("doesn't match null", () {
expect(_match(matcher, null), isFalse);
});
test("doesn't match empty", () {
expect(_match(matcher, ''), isFalse);
});
});
group("custom network source matcher", () {
ImageSourceMatcher matcher = networkSourceMatcher(
schemas: ['https'],
domains: ['www.google.com'],
extension: 'png',
);
test("matches schema, domain and extension", () {
expect(
_match(matcher,
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'),
isTrue);
});
test("doesn't match if schema is different", () {
expect(
_match(matcher,
'http://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'),
isFalse);
});
test("doesn't match if domain is different", () {
expect(
_match(matcher,
'https://google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png'),
isFalse);
});
test("doesn't match if file extension is different", () {
expect(
_match(matcher,
'https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_92x30dppng'),
isFalse);
});
test("doesn't match null", () {
expect(_match(matcher, null), isFalse);
});
test("doesn't match empty", () {
expect(_match(matcher, ''), isFalse);
});
});
group("base64 image data uri matcher", () {
ImageSourceMatcher matcher = base64DataUriMatcher();
test("matches a full png base64 data uri", () {
expect(
_match(matcher,
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='),
isTrue);
});
test("matches a full jpeg base64 data uri", () {
expect(
_match(matcher,
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDADIiJSwlHzIsKSw4NTI7S31RS0VFS5ltc1p9tZ++u7Kfr6zI4f/zyNT/16yv+v/9////////wfD/////////////2wBDATU4OEtCS5NRUZP/zq/O////////////////////////////////////////////////////////////////////wAARCAAYAEADAREAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAQMAAgQF/8QAJRABAAIBBAEEAgMAAAAAAAAAAQIRAAMSITEEEyJBgTORUWFx/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AOgM52xQDrjvAV5Xv0vfKUALlTQfeBm0HThMNHXkL0Lw/swN5qgA8yT4MCS1OEOJV8mBz9Z05yfW8iSx7p4j+jA1aD6Wj7ZMzstsfvAas4UyRHvjrAkC9KhpLMClQntlqFc2X1gUj4viwVObKrddH9YDoHvuujAEuNV+bLwFS8XxdSr+Cq3Vf+4F5RgQl6ZR2p1eAzU/HX80YBYyJLCuexwJCO2O1bwCRidAfWBSctswbI12GAJT3yiwFR7+MBjGK2g/WAJR3FdF84E2rK5VR0YH/9k='),
isTrue);
});
test("matches base64 data uri without data", () {
expect(_match(matcher, 'data:image/png;base64,'), isTrue);
});
test("doesn't match non-base64 image data uri", () {
expect(
_match(matcher,
'data:image/png;hex,89504e470d0a1a0a0000000d49484452000000050000000508060000008d6f26e50000001c4944415408d763f8ffff3fc37f062005c3201284d031f18258cd04000ef535cbd18e0e1f0000000049454e44ae426082'),
isFalse);
});
test("doesn't match base64 non-image data uri", () {
expect(_match(matcher, 'data:text/plain;base64,'), isFalse);
});
test("doesn't non-data schema", () {
expect(_match(matcher, 'http:'), isFalse);
});
test("doesn't match null", () {
expect(_match(matcher, null), isFalse);
});
test("doesn't match empty", () {
expect(_match(matcher, ''), isFalse);
});
});
}
dom.Element _fakeElement(String src) {
return dom.Element.html("""
<img src="$src" />
""");
}
bool _match(ImageSourceMatcher matcher, String src) {
final element = _fakeElement(src);
return matcher.call(
element.attributes.map((key, value) => MapEntry(key.toString(), value)),
element);
}
const testData = <String, String>{
'a': '<a>Hello, World!</a>',
'abbr': '<abbr>HLO-WRLD</abbr>',
'acronym': '<acronym>HW</acronym>',
'address': '<address>123 United States, World</address>',
'article': '<article>123 United States, World</article>',
'aside': '<aside>This is interesting</aside>',
'b': '<b>Hello, World!</b>',
'bdi': '<bdi>Hello, World!</bdi>',
'bdo': '<bdo>Hello, World!</bdo>',
'big': '<big>Hello, World!</big>',
'blockquote': '<blockquote>Hello, World!</blockquote>',
'body': '<body>Hello, World!</body>',
'br': '<span>Hello,<br />World!</span>',
'caption': '<caption>Hello, World!</caption>',
'center': '<center>Hello, World!</center>',
'cite': '<cite>Hello, World!</cite>',
'code': '<code>Hello, World!</code>',
'data': '<data value="value">Hello, World!</data>',
'dd': '<dd>Hello, World!</dd>',
'del': '<del>Hello, World!</del>',
'dfn': '<dfn>Hello, World!</dfn>',
'div': '<div>Hello, World!</div>',
'dl': '<dl>Hello, World!</dl>',
'dt': '<dt>Hello, World!</dt>',
'em': '<em>Hello, World!</em>',
'figcaption_figure':
'<figure><figcaption>Hello, World!</figcaption></figure>',
'font': '<font>Hello, World!</font>',
'footer': '<footer>Hello, World!</footer>',
'h1': '<h1>Hello, World!</h1>',
'h2': '<h2>Hello, World!</h2>',
'h3': '<h3>Hello, World!</h3>',
'h4': '<h4>Hello, World!</h4>',
'h5': '<h5>Hello, World!</h5>',
'h6': '<h6>Hello, World!</h6>',
'header': '<header>Hello, World!</header>',
'hr': '<div>Hello</div><hr /><div>World!</div>',
'i': '<i>Hello, World!</i>',
'img':
'<img alt="Hello, World!" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" />',
'img_alt': '<img alt="Hello, World!" src="" />',
'ins': '<ins>Hello, World!</ins>',
'kbd': '<kbd>Hello, World!</kbd>',
'li_ul': '<ul><li>Hello</li><li>World!</li></ul>',
'li_ol': '<ol><li>Hello</li><li>World!</li></ol>',
'main': '<main>Hello, World!</main>',
'mark': '<mark>Hello, World!</mark>',
'nav': '<nav>Hello, World!</nav>',
'noscript': '<noscript>Hello, World!</noscript>',
'p': '<p>Hello, World!</p>',
'p-with-inline-css-text-align-center': '<p style="text-align: center;">Hello, World!</p>',
'p-with-inline-css-text-align-right': '<p style="text-align: right;">Hello, World!</p>',
'p-with-inline-css-text-align-left': '<p style="text-align: left;">Hello, World!</p>',
'p-with-inline-css-text-align-justify': '<p style="text-align: justify;">Hello, World!</p>',
'p-with-inline-css-text-align-end': '<p style="text-align: end;">Hello, World!</p>',
'p-with-inline-css-text-align-start': '<p style="text-align: start;">Hello, World!</p>',
'pre': '<pre>Hello, World!</pre>',
'q': '<q>Hello, World!</q>',
'rp': '<ruby>漢 <rp> ㄏㄢˋ </rp></ruby>',
'rt': '<ruby>漢 <rt> ㄏㄢˋ </rt></ruby>',
'ruby': '<ruby>漢 <rt> ㄏㄢˋ </rt></ruby>',
's': '<s>Hello, World!</s>',
'samp': '<samp>Hello, World!</samp>',
'section': '<section>Hello, World!</section>',
'small': '<small>Hello, World!</small>',
'span': '<span>Hello, World!</span>',
'span-with-inline-css-color': '<p>Hello, <span style="color: red;">World!</span></p>',
'span-with-inline-css-color-rgb': '<p>Hello, <span style="color: rgb(252, 186, 3);">World!</span></p>',
'span-with-inline-css-color-rgba': '<p>Hello, <span style="color: rgba(252, 186, 3,0.5);">World!</span></p>',
'span-with-inline-css-backgroundcolor': '<p>Hello, <span style="background-color: red; color: rgba(0, 0, 0,0.5);">World!</span></p>',
'span-with-inline-css-backgroundcolor-rgb': '<p>Hello, <span style="background-color: rgb(252, 186, 3); color: rgba(0, 0, 0,0.5);">World!</span></p>',
'span-with-inline-css-backgroundcolor-rgba': '<p>Hello, <span style="background-color: rgba(252, 186, 3,0.5); color: rgba(0, 0, 0,0.5);">World!</span></p>',
'strike': '<strike>Hello, World!</strike>',
'strong': '<strong>Hello, World!</strong>',
'sub': '<sub>Hello, World!</sub>',
'sup': '<sup>Hello, World!</sup>',
'table':
'<table><tr><th>Hello</th><th>World!</th></tr><tr><td>Hello</td><td>World!</td></tr></table>',
'tbody':
'<table><tr><th>Hello</th><th>World!</th></tr><tbody><tr><td>Hello</td><td>World!</td></tr></tbody></table>',
'td':
'<table><tr><th>Hello</th><th>World!</th></tr><tr><td>Hello</td><td>World!</td></tr></table>',
'template': '<template>Hello, World!</template>',
'tfoot':
'<table><tr><th>Hello</th><th>World!</th></tr><tfoot><tr><td>Hello</td><td>World!</td></tr></tfoot></table>',
'th':
'<table><tr><th>Hello</th><th>World!</th></tr><tr><td>Hello</td><td>World!</td></tr></table>',
'thead':
'<table><thead><tr><th>Hello</th><th>World!</th></tr></thead><tr><td>Hello</td><td>World!</td></tr></table>',
'time': '<time>3:00 PM</time>',
'tr':
'<table><tr><th>Hello</th><th>World!</th></tr><tr><td>Hello</td><td>World!</td></tr></table>',
'tt': '<tt>Hello, World!</tt>',
'u': '<u>Hello, World!</u>',
'var': '<var>Hello, World!</var>',
};
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment