diff --git a/.circleci/config.yml b/.circleci/config.yml
index 16c40c66b9..010547222b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,13 +1,40 @@
version: 2.1
orbs:
codecov: codecov/codecov@1.0.2
+executors:
+ default-executor:
+ docker:
+ - image: cirrusci/flutter:stable
+ resource_class: large
+ shell: /bin/bash
jobs:
build:
- docker:
- - image: cirrusci/flutter
+ executor: default-executor
steps:
- - checkout
- - run: flutter --version
- - run: flutter test --coverage
- - codecov/upload:
- file: coverage/lcov.info
+ - checkout
+ - run: flutter --version
+ - run:
+ name: Set up environment
+ command: |
+ echo 'export PATH=$HOME/.pub-cache/bin:$PATH' >> $BASH_ENV
+ source $BASH_ENV
+ - run:
+ name: Setup melos
+ command: |
+ flutter pub global activate melos
+ melos --version
+ melos bootstrap
+ - run:
+ name: Run Test Suite
+ command: melos run test
+ - run:
+ name: Generate Coverage Report
+ command: melos run gen_coverage
+ - codecov/upload:
+ file: coverage_report/lcov.info
+ - run:
+ name: Run flutter analyze
+ command: melos run analyze
+ - run:
+ name: Check That Flutter Code is Formatted Correctly
+ command: flutter format -o none --set-exit-if-changed .
diff --git a/.gitignore b/.gitignore
index 34258e6aad..caf0d56dbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -151,3 +151,7 @@ modules.xml
**/flutter_export_environment.sh
/example/ios/Flutter/Flutter.podspec
+
+packages/**/pubspec_overrides.yaml
+./pubspec_overrides.yaml
+/example/pubspec_overrides.yaml
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000000..c3be203555
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,14 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "example",
+ "cwd": "example",
+ "request": "launch",
+ "type": "dart"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000000..bd44fe6c28
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,13 @@
+{
+ "editor.wordWrapColumn": 80,
+ "dart.lineLength": 80, // This weird short line length is used to match source repository code formatting and prevent a lot of merge conflicts in future
+ "editor.rulers": [
+ 80
+ ],
+ "git.mergeEditor": true,
+ "files.exclude": {
+ "**/.git": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true,
+ }
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5879aafab7..122bb26fbb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,32 @@
+## 3.0.0-alpha.6 - *September 2022*
+
+ - **FIX** #731 Resolve newline `
` issue
+ - **FIX** Align the baseline of inline content with the baseline of its parent flow, even if it has padding or borders
+ - **FIX** Improved fontSize inheritance when cascading styles
+ - **FIX** `auto` margins now work for any `Display.BLOCK` element.
+ - **FIX** `auto` width and height is now the default, rather than `null`
+ - **FIX** New CSSBoxWidget that handles calculations of child sizes for a more accurate HTML/CSS layout
+ - **BREAKING** New `Margin`, `Height`, and `Width` classes that allow `em`, `rem`, `px`, `auto`, and `%` values to be given
+ - **FEAT** Negative margins are now allowed
+ - **FIX** Updated default `p` and `h1-6` styles to use `em` for better font scaling
+ - **BREAKING** Package now requires Dart sdk >= Dart 2.17
+ - **FIX**: Apply margins to
properly. (7581ea79)
+ - **FIX**: Use enum instead of const int internally in length.dart. (9dc7f08c)
+ - **FIX**: Change CSSBoxWidget to CssBoxWidget. (a62449a7)
+ - **FIX**: fix textShadow color declaration handler. (77ffe7cb)
+ - **FIX**: ol use default style. (1c2412a2)
+ - **FIX**: Crash when a tr tag includes text node. (ba8301c9)
+ - **FEAT**: exposes fontFamilyFallback parameter. (1d65aafd)
+
+## [3.0.0-alpha.5] - June 9, 2022:
+* Fixed hot reloads, thanks @arjenmels
+* Fixed link taps not working
+* Improvements in README
+
+## [3.0.0-alpha.3] - April 14, 2022:
+* Fixed styling not being applied to list item markers
+* [video] Fixed crash when iframe or video tags used unsupported/incorrect height or width
+
## [3.0.0-alpha.2] - January 5, 2022:
* **BREAKING** Full modularization using split packages; see our upgrade guide or use flutter_html_all
diff --git a/README.md b/README.md
index 326ac6ccb7..9210a56651 100644
--- a/README.md
+++ b/README.md
@@ -90,7 +90,8 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
Add the following to your `pubspec.yaml` file:
dependencies:
- flutter_html: ^3.0.0-alpha.2
+ flutter_html: ^3.0.0-alpha.5
+ // Or flutter_html_all: ^3.0.0-alpha.5 to include table, video, audio, iframe...
## Currently Supported HTML Tags:
| | | | | | | | | | | |
@@ -497,7 +498,7 @@ Widget html = Html(
- Rowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan | Data | Data |
+ Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan | Data | Data |
 |
@@ -566,6 +567,11 @@ The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `wid
#### Registering the `CustomRender`:
+Add the dependency to your pubspec.yaml:
+
+ dependencies:
+ flutter_html_audio: ^3.0.0-alpha.3
+
```dart
Widget html = Html(
customRenders: {
@@ -584,6 +590,11 @@ Sandbox controls the JavaScript mode of the webview - a value of `null` or `allo
#### Registering the `CustomRender`:
+Add the dependency to your pubspec.yaml:
+
+ dependencies:
+ flutter_html_iframe: ^3.0.0-alpha.3
+
```dart
Widget html = Html(
customRenders: {
@@ -620,6 +631,11 @@ Because this package is parsing MathML to Tex, it may not support some functiona
#### Registering the `CustomRender`:
+Add the dependency to your pubspec.yaml:
+
+ dependencies:
+ flutter_html_math: ^3.0.0-alpha.3
+
```dart
Widget html = Html(
customRenders: {
@@ -688,6 +704,11 @@ The package also exposes a few ways to render SVGs within an `
` tag, specif
#### Registering the `CustomRender`:
+Add the dependency to your pubspec.yaml:
+
+ dependencies:
+ flutter_html_svg: ^3.0.0-alpha.3
+
```dart
Widget html = Html(
customRenders: {
@@ -707,6 +728,11 @@ When rendering table elements, the package tries to calculate the best fit for e
#### Registering the `CustomRender`:
+Add the dependency to your pubspec.yaml:
+
+ dependencies:
+ flutter_html_table: ^3.0.0-alpha.3
+
```dart
Widget html = Html(
customRenders: {
@@ -723,6 +749,11 @@ The package considers the attributes `controls`, `loop`, `src`, `autoplay`, `pos
#### Registering the `CustomRender`:
+Add the dependency to your pubspec.yaml:
+
+ dependencies:
+ flutter_html_video: ^3.0.0-alpha.3
+
```dart
Widget html = Html(
customRenders: {
@@ -750,7 +781,8 @@ Widget row = Row(
```
## Migration Guides
-- For Version 1.0 - [Guide](https://github.com/Sub6Resources/flutter_html/wiki/1.0.0-Migration-Guide)
+- For Version 1.0/2.0 - [Guide](https://github.com/Sub6Resources/flutter_html/wiki/1.0.0-Migration-Guide)
+- For Version 3.0 - **TODO**
## Contribution Guide
> Coming soon!
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000000..f065cf1ea4
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
\ No newline at end of file
diff --git a/combine_coverage.sh b/combine_coverage.sh
new file mode 100755
index 0000000000..8831e1fa6a
--- /dev/null
+++ b/combine_coverage.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+escapedPath="$(echo `pwd` | sed 's/\//\\\//g')"
+
+if grep flutter pubspec.yaml > /dev/null; then
+ if [ -d "coverage" ]; then
+ # combine line coverage info from package tests to a common file
+ if [ ! -d "$MELOS_ROOT_PATH/coverage_report" ]; then
+ mkdir "$MELOS_ROOT_PATH/coverage_report"
+ fi
+ sed "s/^SF:lib/SF:$escapedPath\/lib/g" coverage/lcov.info >> "$MELOS_ROOT_PATH/coverage_report/lcov.info"
+ rm -rf "coverage"
+ fi
+fi
\ No newline at end of file
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index 3747830b03..4fcf8de648 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -8,7 +8,7 @@ if (localPropertiesFile.exists()) {
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.")
+ throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
@@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 31
+ compileSdkVersion 33
lintOptions {
disable 'InvalidPackage'
diff --git a/example/lib/generated_plugin_registrant.dart b/example/lib/generated_plugin_registrant.dart
index fd95b4ff6c..fc500d7eff 100644
--- a/example/lib/generated_plugin_registrant.dart
+++ b/example/lib/generated_plugin_registrant.dart
@@ -4,6 +4,7 @@
// ignore_for_file: directives_ordering
// ignore_for_file: lines_longer_than_80_chars
+// ignore_for_file: depend_on_referenced_packages
import 'package:video_player_web/video_player_web.dart';
import 'package:wakelock_web/wakelock_web.dart';
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 550ad15bd1..3392ac14ef 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,31 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html_all/flutter_html_all.dart';
-import 'package:flutter_math_fork/flutter_math.dart';
-void main() => runApp(new MyApp());
+void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
- return new MaterialApp(
+ return MaterialApp(
title: 'Flutter Demo',
- theme: new ThemeData(
+ theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
- home: new MyHomePage(title: 'flutter_html Example'),
+ home: const MyHomePage(title: 'flutter_html Example'),
);
}
}
class MyHomePage extends StatefulWidget {
- MyHomePage({Key? key, required this.title}) : super(key: key);
+ const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
- _MyHomePageState createState() => new _MyHomePageState();
+ MyHomePageState createState() => MyHomePageState();
}
const htmlData = r"""
@@ -55,10 +56,20 @@ const htmlData = r"""
The should be BLACK with 10% alpha style='color: rgba(0, 0, 0, 0.10);
The should be GREEN style='color: rgb(0, 97, 0);
The should be GREEN style='color: rgb(0, 97, 0);
- blasdafjklasdlkjfkl
- blasdafjklasdlkjfkl
- blasdafjklasdlkjfkl
- blasdafjklasdlkjfkl
+ Text Alignment
+ Center Aligned Text
+ Right Aligned Text
+ Justified Text
+ Center Aligned Text
+ Auto Margins
+ Default Div
+ margin: auto
+ margin: 15px auto
+ margin-left: auto
+ With an image - non-block (should not center):
+
+ block image (should center):
+
Table support (with custom styling!):
Famous quote...
@@ -73,7 +84,7 @@ const htmlData = r"""
- Rowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan\nRowspan | Data | Data |
+ Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan Rowspan | Data | Data |
 |
@@ -241,98 +252,126 @@ const htmlData = r"""
Scroll to top
""";
-class _MyHomePageState extends State {
+final staticAnchorKey = GlobalKey();
+
+class MyHomePageState extends State {
@override
Widget build(BuildContext context) {
- return new Scaffold(
+ return Scaffold(
appBar: AppBar(
- title: Text('flutter_html Example'),
+ title: const Text('flutter_html Example'),
centerTitle: true,
),
+ floatingActionButton: FloatingActionButton(
+ child: const Icon(Icons.arrow_downward),
+ onPressed: () {
+ final anchorContext =
+ AnchorKey.forId(staticAnchorKey, "bottom")?.currentContext;
+ if (anchorContext != null) {
+ Scrollable.ensureVisible(anchorContext);
+ }
+ },
+ ),
body: SingleChildScrollView(
child: Html(
+ anchorKey: staticAnchorKey,
data: htmlData,
style: {
"table": Style(
- backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
+ backgroundColor: const Color.fromARGB(0x50, 0xee, 0xee, 0xee),
),
"tr": Style(
- border: Border(bottom: BorderSide(color: Colors.grey)),
+ border: const Border(bottom: BorderSide(color: Colors.grey)),
),
"th": Style(
- padding: EdgeInsets.all(6),
+ padding: const EdgeInsets.all(6),
backgroundColor: Colors.grey,
),
"td": Style(
- padding: EdgeInsets.all(6),
+ padding: const EdgeInsets.all(6),
alignment: Alignment.topLeft,
),
'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis),
},
tagsList: Html.tags..addAll(['tex', 'bird', 'flutter']),
customRenders: {
- tagMatcher("tex"): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
- context.tree.element?.innerHtml ?? '',
- mathStyle: MathStyle.display,
- textStyle: context.style.generateTextStyle(),
- onErrorFallback: (FlutterMathException e) {
- return Text(e.message);
- },
- )),
- tagMatcher("bird"): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
- tagMatcher("flutter"): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
- style: (context.tree.element!.attributes['horizontal'] != null)
- ? FlutterLogoStyle.horizontal
- : FlutterLogoStyle.markOnly,
- textColor: context.style.color!,
- size: context.style.fontSize!.size! * 5,
- )),
- tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: tableRender.call().widget!.call(context, buildChildren),
- )),
+ tagMatcher("tex"): CustomRender.widget(
+ widget: (context, buildChildren) => Math.tex(
+ context.tree.element?.innerHtml ?? '',
+ mathStyle: MathStyle.display,
+ textStyle:
+ context.style.generateTextStyle(context.buildContext),
+ onErrorFallback: (FlutterMathException e) {
+ return Text(e.message);
+ },
+ )),
+ tagMatcher("bird"): CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) =>
+ const TextSpan(text: "🐦")),
+ tagMatcher("flutter"): CustomRender.widget(
+ widget: (context, buildChildren) => FlutterLogo(
+ style: (context.tree.element!.attributes['horizontal'] !=
+ null)
+ ? FlutterLogoStyle.horizontal
+ : FlutterLogoStyle.markOnly,
+ textColor: context.style.color!,
+ size: context.style.fontSize!.value * 5,
+ )),
+ tagMatcher("table"): CustomRender.widget(
+ widget: (context, buildChildren) => SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: tableRender
+ .call()
+ .widget!
+ .call(context, buildChildren),
+ )),
audioMatcher(): audioRender(),
iframeMatcher(): iframeRender(),
- mathMatcher(): mathRender(onMathError: (error, exception, exceptionWithType) {
- print(exception);
+ mathMatcher():
+ mathRender(onMathError: (error, exception, exceptionWithType) {
+ debugPrint(exception);
return Text(exception);
}),
svgTagMatcher(): svgTagRender(),
svgDataUriMatcher(): svgDataImageRender(),
svgAssetUriMatcher(): svgAssetImageRender(),
svgNetworkSourceMatcher(): svgNetworkImageRender(),
- networkSourceMatcher(domains: ["flutter.dev"]): CustomRender.widget(
- widget: (context, buildChildren) {
- return FlutterLogo(size: 36);
- }),
+ networkSourceMatcher(domains: ["flutter.dev"]):
+ CustomRender.widget(widget: (context, buildChildren) {
+ return const FlutterLogo(size: 36);
+ }),
networkSourceMatcher(domains: ["mydomain.com"]): networkImageRender(
headers: {"Custom-Header": "some-value"},
altWidget: (alt) => Text(alt ?? ""),
- loadingWidget: () => Text("Loading..."),
+ loadingWidget: () => const Text("Loading..."),
),
// On relative paths starting with /wiki, prefix with a base url
- (context) => context.tree.element?.attributes["src"] != null
- && context.tree.element!.attributes["src"]!.startsWith("/wiki"):
- networkImageRender(mapUrl: (url) => "https://upload.wikimedia.org" + url!),
+ (context) =>
+ context.tree.element?.attributes["src"] != null &&
+ context.tree.element!.attributes["src"]!
+ .startsWith("/wiki"): networkImageRender(
+ mapUrl: (url) => "https://upload.wikimedia.org${url!}"),
// Custom placeholder image for broken links
- networkSourceMatcher(): networkImageRender(altWidget: (_) => FlutterLogo()),
+ networkSourceMatcher():
+ networkImageRender(altWidget: (_) => const FlutterLogo()),
videoMatcher(): videoRender(),
},
onLinkTap: (url, _, __, ___) {
- print("Opening $url...");
+ debugPrint("Opening $url...");
},
onImageTap: (src, _, __, ___) {
- print(src);
+ debugPrint(src);
},
onImageError: (exception, stackTrace) {
- print(exception);
+ debugPrint(exception.toString());
},
onCssParseError: (css, messages) {
- print("css that errored: $css");
- print("error messages:");
- messages.forEach((element) {
- print(element);
- });
+ debugPrint("css that errored: $css");
+ debugPrint("error messages:");
+ for (var element in messages) {
+ debugPrint(element.toString());
+ }
+ return '';
},
),
),
@@ -340,8 +379,11 @@ class _MyHomePageState extends State {
}
}
-CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';
+CustomRenderMatcher texMatcher() =>
+ (context) => context.tree.element?.localName == 'tex';
-CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';
+CustomRenderMatcher birdMatcher() =>
+ (context) => context.tree.element?.localName == 'bird';
-CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
+CustomRenderMatcher flutterMatcher() =>
+ (context) => context.tree.element?.localName == 'flutter';
diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift
index 1c2c9b0787..8e40e9f075 100644
--- a/example/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,8 +5,12 @@
import FlutterMacOS
import Foundation
-import wakelock_macos
+import package_info_plus
+import video_player_avfoundation
+import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
- WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin"))
+ FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
+ FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
+ WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 79e23feab9..8e1d7d66a5 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -4,7 +4,7 @@ publish_to: none
version: 1.0.0+1
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: ">=3.1.0 <4.0.0"
dependencies:
flutter_html:
@@ -13,15 +13,16 @@ dependencies:
path: ../packages/flutter_html_all
flutter:
sdk: flutter
+ wakelock_web: ^0.4.0
dev_dependencies:
flutter_test:
sdk: flutter
+ flutter_lints: ^2.0.1
flutter:
-
uses-material-design: true
assets:
- assets/html5.png
- - assets/mac.svg
\ No newline at end of file
+ - assets/mac.svg
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
deleted file mode 100644
index 747db1da35..0000000000
--- a/example/test/widget_test.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// 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);
- });
-}
diff --git a/lib/custom_render.dart b/lib/custom_render.dart
index 9b4abd8890..956562e7ad 100644
--- a/lib/custom_render.dart
+++ b/lib/custom_render.dart
@@ -6,44 +6,51 @@ import 'dart:convert';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/utils.dart';
typedef CustomRenderMatcher = bool Function(RenderContext context);
CustomRenderMatcher tagMatcher(String tag) => (context) {
- return context.tree.element?.localName == tag;
-};
+ return context.tree.element?.localName == tag;
+ };
CustomRenderMatcher blockElementMatcher() => (context) {
- return context.tree.style.display == Display.BLOCK &&
- (context.tree.children.isNotEmpty || context.tree.element?.localName == "hr");
-};
+ return (context.tree.style.display == Display.block ||
+ context.tree.style.display == Display.inlineBlock) &&
+ (context.tree.children.isNotEmpty ||
+ context.tree.element?.localName == "hr");
+ };
CustomRenderMatcher listElementMatcher() => (context) {
- return context.tree.style.display == Display.LIST_ITEM;
-};
+ return context.tree.style.display == Display.listItem;
+ };
CustomRenderMatcher replacedElementMatcher() => (context) {
- return context.tree is ReplacedElement;
-};
-
-CustomRenderMatcher dataUriMatcher({String? encoding = 'base64', String? mime}) => (context) {
- if (context.tree.element?.attributes == null
- || _src(context.tree.element!.attributes.cast()) == null) return false;
- final dataUri = _dataUriFormat.firstMatch(_src(context.tree.element!.attributes.cast())!);
- return dataUri != null && dataUri.namedGroup('mime') != "image/svg+xml" &&
- (mime == null || dataUri.namedGroup('mime') == mime) &&
- (encoding == null || dataUri.namedGroup('encoding') == ';$encoding');
-};
+ return context.tree is ReplacedElement;
+ };
+
+CustomRenderMatcher dataUriMatcher(
+ {String? encoding = 'base64', String? mime}) =>
+ (context) {
+ if (context.tree.element?.attributes == null ||
+ _src(context.tree.element!.attributes.cast()) == null) return false;
+ final dataUri = _dataUriFormat
+ .firstMatch(_src(context.tree.element!.attributes.cast())!);
+ return dataUri != null &&
+ dataUri.namedGroup('mime') != "image/svg+xml" &&
+ (mime == null || dataUri.namedGroup('mime') == mime) &&
+ (encoding == null || dataUri.namedGroup('encoding') == ';$encoding');
+ };
CustomRenderMatcher networkSourceMatcher({
- List schemas: const ["https", "http"],
+ List schemas = const ["https", "http"],
List? domains,
String? extension,
}) =>
- (context) {
- if (context.tree.element?.attributes.cast() == null
- || _src(context.tree.element!.attributes.cast()) == null) return false;
+ (context) {
+ if (context.tree.element?.attributes.cast() == null ||
+ _src(context.tree.element!.attributes.cast()) == null) return false;
try {
final src = Uri.parse(_src(context.tree.element!.attributes.cast())!);
return schemas.contains(src.scheme) &&
@@ -55,34 +62,35 @@ CustomRenderMatcher networkSourceMatcher({
};
CustomRenderMatcher assetUriMatcher() => (context) =>
- context.tree.element?.attributes.cast() != null
- && _src(context.tree.element!.attributes.cast()) != null
- && _src(context.tree.element!.attributes.cast())!.startsWith("asset:")
- && !_src(context.tree.element!.attributes.cast())!.endsWith(".svg");
+ context.tree.element?.attributes.cast() != null &&
+ _src(context.tree.element!.attributes.cast()) != null &&
+ _src(context.tree.element!.attributes.cast())!.startsWith("asset:") &&
+ !_src(context.tree.element!.attributes.cast())!.endsWith(".svg");
CustomRenderMatcher textContentElementMatcher() => (context) {
- return context.tree is TextContentElement;
-};
+ return context.tree is TextContentElement;
+ };
CustomRenderMatcher interactableElementMatcher() => (context) {
- return context.tree is InteractableElement;
-};
+ return context.tree is InteractableElement;
+ };
CustomRenderMatcher layoutElementMatcher() => (context) {
- return context.tree is LayoutElement;
-};
+ return context.tree is LayoutElement;
+ };
CustomRenderMatcher verticalAlignMatcher() => (context) {
- return context.tree.style.verticalAlign != null
- && context.tree.style.verticalAlign != VerticalAlign.BASELINE;
-};
+ return context.tree.style.verticalAlign != null &&
+ context.tree.style.verticalAlign != VerticalAlign.baseline;
+ };
CustomRenderMatcher fallbackMatcher() => (context) {
- return true;
-};
+ return true;
+ };
class CustomRender {
- final InlineSpan Function(RenderContext, List Function())? inlineSpan;
+ final InlineSpan Function(RenderContext, List Function())?
+ inlineSpan;
final Widget Function(RenderContext, List Function())? widget;
CustomRender.inlineSpan({
@@ -102,188 +110,177 @@ class SelectableCustomRender extends CustomRender {
}) : super.inlineSpan(inlineSpan: null);
}
-CustomRender blockElementRender({
- Style? style,
- List? children}) =>
+CustomRender blockElementRender({Style? style, List? children}) =>
CustomRender.inlineSpan(inlineSpan: (context, buildChildren) {
- if (context.parser.selectable) {
- return TextSpan(
- style: context.style.generateTextStyle(),
- children: (children as List?) ?? context.tree.children
- .expandIndexed((i, childTree) => [
- if (childTree.style.display == Display.BLOCK &&
- i > 0 &&
- context.tree.children[i - 1] is ReplacedElement)
- TextSpan(text: "\n"),
- context.parser.parseTree(context, childTree),
- if (i != context.tree.children.length - 1 &&
- childTree.style.display == Display.BLOCK &&
- childTree.element?.localName != "html" &&
- childTree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
- );
- }
- return WidgetSpan(
- child: ContainerSpan(
- key: context.key,
- newContext: context,
- style: style ?? context.tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- children: children ?? context.tree.children
- .expandIndexed((i, childTree) => [
- if (context.parser.shrinkWrap &&
- childTree.style.display == Display.BLOCK &&
- i > 0 &&
- context.tree.children[i - 1] is ReplacedElement)
- TextSpan(text: "\n"),
- context.parser.parseTree(context, childTree),
- if (context.parser.shrinkWrap &&
- i != context.tree.children.length - 1 &&
- childTree.style.display == Display.BLOCK &&
- childTree.element?.localName != "html" &&
- childTree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
- ));
+ if (context.parser.selectable) {
+ return TextSpan(
+ style: context.style.generateTextStyle(context.buildContext),
+ children: (children as List?) ??
+ context.tree.children
+ .expandIndexed((i, childTree) => [
+ if (childTree.style.display == Display.block &&
+ i > 0 &&
+ context.tree.children[i - 1] is ReplacedElement)
+ const TextSpan(text: "\n"),
+ context.parser.parseTree(context, childTree),
+ if (i != context.tree.children.length - 1 &&
+ childTree.style.display == Display.block &&
+ childTree.element?.localName != "html" &&
+ childTree.element?.localName != "body")
+ const TextSpan(text: "\n"),
+ ])
+ .toList(),
+ );
+ }
+ return WidgetSpan(
+ alignment: PlaceholderAlignment.baseline,
+ baseline: TextBaseline.alphabetic,
+ child: CssBoxWidget.withInlineSpanChildren(
+ key: context.key,
+ context: context.buildContext,
+ style: style ?? context.tree.style,
+ shrinkWrap: context.parser.shrinkWrap,
+ childIsReplaced:
+ HtmlElements.replacedExternalElements.contains(context.tree.name),
+ children: children ??
+ context.tree.children
+ .expandIndexed((i, childTree) => [
+ if (context.parser.shrinkWrap &&
+ childTree.style.display == Display.block &&
+ i > 0 &&
+ context.tree.children[i - 1] is ReplacedElement)
+ const TextSpan(text: "\n"),
+ context.parser.parseTree(context, childTree),
+ if (context.parser.shrinkWrap &&
+ i != context.tree.children.length - 1 &&
+ childTree.style.display == Display.block &&
+ childTree.element?.localName != "html" &&
+ childTree.element?.localName != "body")
+ const TextSpan(text: "\n"),
+ ])
+ .toList(),
+ ),
+ );
});
-CustomRender listElementRender({
- Style? style,
- Widget? child,
- List? children}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) =>
- WidgetSpan(
- child: ContainerSpan(
- key: context.key,
- newContext: context,
- style: style ?? context.tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- textDirection: style?.direction ?? context.tree.style.direction,
- children: [
- (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.OUTSIDE ?
- Padding(
- padding: style?.padding?.nonNegative ?? context.tree.style.padding?.nonNegative
- ?? EdgeInsets.only(left: (style?.direction ?? context.tree.style.direction) != TextDirection.rtl ? 10.0 : 0.0,
- right: (style?.direction ?? context.tree.style.direction) == TextDirection.rtl ? 10.0 : 0.0),
- child: style?.markerContent ?? context.style.markerContent
- ) : Container(height: 0, width: 0),
- Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
- Expanded(
- child: Padding(
- padding: (style?.listStylePosition ?? context.tree.style.listStylePosition) == ListStylePosition.INSIDE ?
- EdgeInsets.only(left: (style?.direction ?? context.tree.style.direction) != TextDirection.rtl ? 10.0 : 0.0,
- right: (style?.direction ?? context.tree.style.direction) == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero,
- child: StyledText(
- textSpan: TextSpan(
- children: _getListElementChildren(style?.listStylePosition ?? context.tree.style.listStylePosition, buildChildren)
- ..insertAll(0, context.tree.style.listStylePosition == ListStylePosition.INSIDE ?
- [
- WidgetSpan(alignment: PlaceholderAlignment.middle, child: style?.markerContent ?? context.style.markerContent ?? Container(height: 0, width: 0))
- ] : []),
- style: style?.generateTextStyle() ?? context.style.generateTextStyle(),
- ),
- style: style ?? context.style,
- renderContext: context,
- )
- )
- )
- ],
- ),
- ),
-));
-
-CustomRender replacedElementRender({PlaceholderAlignment? alignment, TextBaseline? baseline, Widget? child}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
- alignment: alignment ?? (context.tree as ReplacedElement).alignment,
- baseline: baseline ?? TextBaseline.alphabetic,
- child: child ?? (context.tree as ReplacedElement).toWidget(context)!,
-));
-
-CustomRender textContentElementRender({String? text}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) =>
- TextSpan(text: (text ?? (context.tree as TextContentElement).text).transformed(context.tree.style.textTransform)));
-
-CustomRender base64ImageRender() => CustomRender.widget(widget: (context, buildChildren) {
- final decodedImage = base64.decode(_src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim());
- precacheImage(
- MemoryImage(decodedImage),
- context.buildContext,
- onError: (exception, StackTrace? stackTrace) {
- context.parser.onImageError?.call(exception, stackTrace);
- },
- );
- final widget = Image.memory(
- decodedImage,
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle());
- }
- return child;
+CustomRender listElementRender(
+ {Style? style, Widget? child, List? children}) {
+ return CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) {
+ return WidgetSpan(
+ child: CssBoxWidget.withInlineSpanChildren(
+ key: context.key,
+ context: context.buildContext,
+ style: style ?? context.style,
+ shrinkWrap: context.parser.shrinkWrap,
+ children: buildChildren(),
+ ),
+ );
},
);
- return Builder(
- key: context.key,
- builder: (buildContext) {
- return GestureDetector(
- child: widget,
- onTap: () {
- if (MultipleTapGestureDetector.of(buildContext) != null) {
- MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
- }
- context.parser.onImageTap?.call(
- _src(context.tree.element!.attributes.cast())!.split("base64,")[1].trim(),
- context,
- context.tree.element!.attributes.cast(),
- context.tree.element
+}
+
+CustomRender replacedElementRender(
+ {PlaceholderAlignment? alignment,
+ TextBaseline? baseline,
+ Widget? child}) =>
+ CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) => WidgetSpan(
+ alignment:
+ alignment ?? (context.tree as ReplacedElement).alignment,
+ baseline: baseline ?? TextBaseline.alphabetic,
+ child:
+ child ?? (context.tree as ReplacedElement).toWidget(context)!,
+ ));
+
+CustomRender textContentElementRender({String? text}) =>
+ CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) => TextSpan(
+ text: (text ?? (context.tree as TextContentElement).text)
+ .transformed(context.tree.style.textTransform),
+ ));
+
+CustomRender base64ImageRender() =>
+ CustomRender.widget(widget: (context, buildChildren) {
+ final decodedImage = base64.decode(
+ _src(context.tree.element!.attributes.cast())!
+ .split("base64,")[1]
+ .trim());
+ precacheImage(
+ MemoryImage(decodedImage),
+ context.buildContext,
+ onError: (exception, StackTrace? stackTrace) {
+ context.parser.onImageError?.call(exception, stackTrace);
+ },
+ );
+ final widget = Image.memory(
+ decodedImage,
+ frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ return Text(_alt(context.tree.element!.attributes.cast()) ?? "",
+ style: context.style.generateTextStyle(context.buildContext));
+ }
+ return child;
+ },
+ );
+ return Builder(
+ key: context.key,
+ builder: (buildContext) {
+ return GestureDetector(
+ child: widget,
+ onTap: () {
+ if (MultipleTapGestureDetector.of(buildContext) != null) {
+ MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
+ }
+ context.parser.onImageTap?.call(
+ _src(context.tree.element!.attributes.cast())!
+ .split("base64,")[1]
+ .trim(),
+ context,
+ context.tree.element!.attributes.cast(),
+ context.tree.element);
+ },
);
- },
- );
- }
- );
-});
+ });
+ });
CustomRender assetImageRender({
double? width,
double? height,
-}) => CustomRender.widget(widget: (context, buildChildren) {
- final assetPath = _src(context.tree.element!.attributes.cast())!.replaceFirst('asset:', '');
- final widget = Image.asset(
- assetPath,
- width: width ?? _width(context.tree.element!.attributes.cast()),
- height: height ?? _height(context.tree.element!.attributes.cast()),
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return Text(_alt(context.tree.element!.attributes.cast()) ?? "", style: context.style.generateTextStyle());
- }
- return child;
- },
- );
- return Builder(
- key: context.key,
- builder: (buildContext) {
- return GestureDetector(
- child: widget,
- onTap: () {
- if (MultipleTapGestureDetector.of(buildContext) != null) {
- MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
- }
- context.parser.onImageTap?.call(
- assetPath,
- context,
- context.tree.element!.attributes.cast(),
- context.tree.element
+}) =>
+ CustomRender.widget(widget: (context, buildChildren) {
+ final assetPath = _src(context.tree.element!.attributes.cast())!
+ .replaceFirst('asset:', '');
+ final widget = Image.asset(
+ assetPath,
+ width: width ?? _width(context.tree.element!.attributes.cast()),
+ height: height ?? _height(context.tree.element!.attributes.cast()),
+ frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ return Text(_alt(context.tree.element!.attributes.cast()) ?? "",
+ style: context.style.generateTextStyle(context.buildContext));
+ }
+ return child;
+ },
+ );
+ return Builder(
+ key: context.key,
+ builder: (buildContext) {
+ return GestureDetector(
+ child: widget,
+ onTap: () {
+ if (MultipleTapGestureDetector.of(buildContext) != null) {
+ MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
+ }
+ context.parser.onImageTap?.call(
+ assetPath,
+ context,
+ context.tree.element!.attributes.cast(),
+ context.tree.element);
+ },
);
- },
- );
- }
- );
-});
+ });
+ });
CustomRender networkImageRender({
Map? headers,
@@ -292,205 +289,215 @@ CustomRender networkImageRender({
double? height,
Widget Function(String?)? altWidget,
Widget Function()? loadingWidget,
-}) => CustomRender.widget(widget: (context, buildChildren) {
- final src = mapUrl?.call(_src(context.tree.element!.attributes.cast()))
- ?? _src(context.tree.element!.attributes.cast())!;
- Completer completer = Completer();
- if (context.parser.cachedImageSizes[src] != null) {
- completer.complete(context.parser.cachedImageSizes[src]);
- } else {
- Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- if (!completer.isCompleted) {
- completer.completeError("error");
- }
- return child;
+}) =>
+ CustomRender.widget(widget: (context, buildChildren) {
+ final src = mapUrl?.call(_src(context.tree.element!.attributes.cast())) ??
+ _src(context.tree.element!.attributes.cast())!;
+ Completer completer = Completer();
+ if (context.parser.cachedImageSizes[src] != null) {
+ completer.complete(context.parser.cachedImageSizes[src]);
} else {
- return child;
- }
- });
-
- ImageStreamListener? listener;
- listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
- var myImage = imageInfo.image;
- Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
- if (!completer.isCompleted) {
- context.parser.cachedImageSizes[src] = size;
- completer.complete(size);
- image.image.resolve(ImageConfiguration()).removeListener(listener!);
- }
- }, onError: (object, stacktrace) {
- if (!completer.isCompleted) {
- completer.completeError(object);
- image.image.resolve(ImageConfiguration()).removeListener(listener!);
+ Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ if (!completer.isCompleted) {
+ completer.completeError("error");
+ }
+ return child;
+ } else {
+ return child;
+ }
+ });
+
+ ImageStreamListener? listener;
+ listener =
+ ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
+ var myImage = imageInfo.image;
+ Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
+ if (!completer.isCompleted) {
+ context.parser.cachedImageSizes[src] = size;
+ completer.complete(size);
+ image.image
+ .resolve(const ImageConfiguration())
+ .removeListener(listener!);
+ }
+ }, onError: (object, stacktrace) {
+ if (!completer.isCompleted) {
+ completer.completeError(object);
+ image.image
+ .resolve(const ImageConfiguration())
+ .removeListener(listener!);
+ }
+ });
+
+ image.image.resolve(const ImageConfiguration()).addListener(listener);
}
- });
-
- image.image.resolve(ImageConfiguration()).addListener(listener);
- }
- final attributes = context.tree.element!.attributes.cast();
- final widget = FutureBuilder(
- future: completer.future,
- initialData: context.parser.cachedImageSizes[src],
- builder: (BuildContext buildContext, AsyncSnapshot snapshot) {
- if (snapshot.hasData) {
- return Container(
- constraints: BoxConstraints(
- maxWidth: width ?? _width(attributes) ?? snapshot.data!.width,
- maxHeight:
- (width ?? _width(attributes) ?? snapshot.data!.width) /
- _aspectRatio(attributes, snapshot)),
- child: AspectRatio(
- aspectRatio: _aspectRatio(attributes, snapshot),
- child: Image.network(
- src,
- headers: headers,
- width: width ?? _width(attributes) ?? snapshot.data!.width,
- height: height ?? _height(attributes),
- frameBuilder: (ctx, child, frame, _) {
- if (frame == null) {
- return altWidget?.call(_alt(attributes)) ??
- Text(_alt(attributes) ?? "",
- style: context.style.generateTextStyle());
+ final attributes =
+ context.tree.element!.attributes.cast();
+ final widget = FutureBuilder(
+ future: completer.future,
+ initialData: context.parser.cachedImageSizes[src],
+ builder: (BuildContext buildContext, AsyncSnapshot snapshot) {
+ if (snapshot.hasData) {
+ return Container(
+ constraints: BoxConstraints(
+ maxWidth: width ?? _width(attributes) ?? snapshot.data!.width,
+ maxHeight:
+ (width ?? _width(attributes) ?? snapshot.data!.width) /
+ _aspectRatio(attributes, snapshot)),
+ child: AspectRatio(
+ aspectRatio: _aspectRatio(attributes, snapshot),
+ child: Image.network(
+ src,
+ headers: headers,
+ width: width ?? _width(attributes) ?? snapshot.data!.width,
+ height: height ?? _height(attributes),
+ frameBuilder: (ctx, child, frame, _) {
+ if (frame == null) {
+ return altWidget?.call(_alt(attributes)) ??
+ Text(_alt(attributes) ?? "",
+ style: context.style
+ .generateTextStyle(context.buildContext));
+ }
+ return child;
+ },
+ ),
+ ),
+ );
+ } else if (snapshot.hasError) {
+ return altWidget
+ ?.call(_alt(context.tree.element!.attributes.cast())) ??
+ Text(_alt(context.tree.element!.attributes.cast()) ?? "",
+ style:
+ context.style.generateTextStyle(context.buildContext));
+ } else {
+ return loadingWidget?.call() ?? const CircularProgressIndicator();
+ }
+ },
+ );
+ return Builder(
+ key: context.key,
+ builder: (buildContext) {
+ return GestureDetector(
+ child: widget,
+ onTap: () {
+ if (MultipleTapGestureDetector.of(buildContext) != null) {
+ MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
- return child;
+ context.parser.onImageTap?.call(
+ src,
+ context,
+ context.tree.element!.attributes.cast(),
+ context.tree.element);
},
- ),
- ),
- );
- } else if (snapshot.hasError) {
- return altWidget?.call(_alt(context.tree.element!.attributes.cast())) ??
- Text(_alt(context.tree.element!.attributes.cast())
- ?? "", style: context.style.generateTextStyle());
- } else {
- return loadingWidget?.call() ?? const CircularProgressIndicator();
- }
- },
- );
- return Builder(
- key: context.key,
- builder: (buildContext) {
- return GestureDetector(
- child: widget,
- onTap: () {
- if (MultipleTapGestureDetector.of(buildContext) != null) {
- MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
- }
- context.parser.onImageTap?.call(
- src,
- context,
- context.tree.element!.attributes.cast(),
- context.tree.element
);
- },
- );
- }
- );
-});
+ });
+ });
CustomRender interactableElementRender({List? children}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(
- children: children ?? (context.tree as InteractableElement).children
- .map((tree) => context.parser.parseTree(context, tree))
- .map((childSpan) {
- return _getInteractableChildren(context, context.tree as InteractableElement, childSpan,
- context.style.generateTextStyle().merge(childSpan.style));
- }).toList(),
-));
-
-CustomRender layoutElementRender({Widget? child}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
- child: child ?? (context.tree as LayoutElement).toWidget(context)!,
-));
-
-CustomRender verticalAlignRender({
- double? verticalOffset,
- Style? style,
- List? children}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
- child: Transform.translate(
- key: context.key,
- offset: Offset(0, verticalOffset ?? _getVerticalOffset(context.tree)),
- child: StyledText(
- textSpan: TextSpan(
- style: style?.generateTextStyle() ?? context.style.generateTextStyle(),
- children: children ?? buildChildren.call(),
- ),
- style: context.style,
- renderContext: context,
- ),
- ),
-));
+ CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) => TextSpan(
+ children: children ??
+ (context.tree as InteractableElement)
+ .children
+ .map((tree) => context.parser.parseTree(context, tree))
+ .map((childSpan) {
+ return _getInteractableChildren(
+ context,
+ context.tree as InteractableElement,
+ childSpan,
+ context.style
+ .generateTextStyle(context.buildContext)
+ .merge(childSpan.style));
+ }).toList(),
+ ));
+
+CustomRender layoutElementRender({Widget? child}) => CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) => WidgetSpan(
+ child: child ?? (context.tree as LayoutElement).toWidget(context)!,
+ ));
+
+CustomRender verticalAlignRender(
+ {double? verticalOffset, Style? style, List? children}) =>
+ CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) => WidgetSpan(
+ child: Transform.translate(
+ key: context.key,
+ offset: Offset(
+ 0, verticalOffset ?? _getVerticalOffset(context.tree)),
+ child: CssBoxWidget.withInlineSpanChildren(
+ context: context.buildContext,
+ children: children ?? buildChildren.call(),
+ style: context.style,
+ ),
+ ),
+ ));
CustomRender fallbackRender({Style? style, List? children}) =>
- CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(
- style: style?.generateTextStyle() ?? context.style.generateTextStyle(),
- children: context.tree.children
- .expand((tree) => [
- context.parser.parseTree(context, tree),
- if (tree.style.display == Display.BLOCK &&
- tree.element?.parent?.localName != "th" &&
- tree.element?.parent?.localName != "td" &&
- tree.element?.localName != "html" &&
- tree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
-));
-
-final Map defaultRenders = {
- blockElementMatcher(): blockElementRender(),
- listElementMatcher(): listElementRender(),
- textContentElementMatcher(): textContentElementRender(),
- dataUriMatcher(): base64ImageRender(),
- assetUriMatcher(): assetImageRender(),
- networkSourceMatcher(): networkImageRender(),
- replacedElementMatcher(): replacedElementRender(),
- interactableElementMatcher(): interactableElementRender(),
- layoutElementMatcher(): layoutElementRender(),
- verticalAlignMatcher(): verticalAlignRender(),
- fallbackMatcher(): fallbackRender(),
-};
-
-List _getListElementChildren(ListStylePosition? position, Function() buildChildren) {
- List children = buildChildren.call();
- if (position == ListStylePosition.INSIDE) {
- final tabSpan = WidgetSpan(
- child: Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
- );
- children.insert(0, tabSpan);
- }
- return children;
+ CustomRender.inlineSpan(
+ inlineSpan: (context, buildChildren) => TextSpan(
+ style: style?.generateTextStyle(context.buildContext) ??
+ context.style.generateTextStyle(context.buildContext),
+ children: context.tree.children
+ .expand((tree) => [
+ context.parser.parseTree(context, tree),
+ if (tree.style.display == Display.block &&
+ tree.element?.parent?.localName != "th" &&
+ tree.element?.parent?.localName != "td" &&
+ tree.element?.localName != "html" &&
+ tree.element?.localName != "body")
+ const TextSpan(text: "\n"),
+ ])
+ .toList(),
+ ));
+
+Map generateDefaultRenders() {
+ return {
+ blockElementMatcher(): blockElementRender(),
+ listElementMatcher(): listElementRender(),
+ textContentElementMatcher(): textContentElementRender(),
+ dataUriMatcher(): base64ImageRender(),
+ assetUriMatcher(): assetImageRender(),
+ networkSourceMatcher(): networkImageRender(),
+ replacedElementMatcher(): replacedElementRender(),
+ interactableElementMatcher(): interactableElementRender(),
+ layoutElementMatcher(): layoutElementRender(),
+ verticalAlignMatcher(): verticalAlignRender(),
+ fallbackMatcher(): fallbackRender(),
+ };
}
-InlineSpan _getInteractableChildren(RenderContext context, InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) {
+InlineSpan _getInteractableChildren(RenderContext context,
+ InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) {
if (childSpan is TextSpan) {
return TextSpan(
text: childSpan.text,
children: childSpan.children
- ?.map((e) => _getInteractableChildren(context, tree, e, childStyle.merge(childSpan.style)))
+ ?.map((e) => _getInteractableChildren(
+ context, tree, e, childStyle.merge(childSpan.style)))
.toList(),
- style: context.style.generateTextStyle().merge(
+ style: context.style.generateTextStyle(context.buildContext).merge(
childSpan.style == null
? childStyle
: childStyle.merge(childSpan.style)),
semanticsLabel: childSpan.semanticsLabel,
recognizer: TapGestureRecognizer()
- ..onTap =
- context.parser.internalOnAnchorTap != null ?
- () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element)
- : null,
+ ..onTap = context.parser.internalOnAnchorTap != null
+ ? () => context.parser.internalOnAnchorTap!(
+ tree.href, context, tree.attributes, tree.element)
+ : null,
);
} else {
return WidgetSpan(
child: MultipleTapGestureDetector(
onTap: context.parser.internalOnAnchorTap != null
- ? () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element)
+ ? () => context.parser.internalOnAnchorTap!(
+ tree.href, context, tree.attributes, tree.element)
: null,
child: GestureDetector(
key: context.key,
onTap: context.parser.internalOnAnchorTap != null
- ? () => context.parser.internalOnAnchorTap!(tree.href, context, tree.attributes, tree.element)
+ ? () => context.parser.internalOnAnchorTap!(
+ tree.href, context, tree.attributes, tree.element)
: null,
child: (childSpan as WidgetSpan).child,
),
@@ -499,14 +506,15 @@ InlineSpan _getInteractableChildren(RenderContext context, InteractableElement t
}
}
-final _dataUriFormat = RegExp("^(?data):(?image\/[\\w\+\-\.]+)(?;base64)?\,(?.*)");
+final _dataUriFormat = RegExp(
+ "^(?data):(?image\\/[\\w\\+\\-\\.]+)(?;base64)?\\,(?.*)");
double _getVerticalOffset(StyledElement tree) {
switch (tree.style.verticalAlign) {
- case VerticalAlign.SUB:
- return tree.style.fontSize!.size! / 2.5;
- case VerticalAlign.SUPER:
- return tree.style.fontSize!.size! / -2.5;
+ case VerticalAlign.sub:
+ return tree.style.fontSize!.value / 2.5;
+ case VerticalAlign.sup:
+ return tree.style.fontSize!.value / -2.5;
default:
return 0;
}
@@ -522,12 +530,16 @@ String? _alt(Map attributes) {
double? _height(Map attributes) {
final heightString = attributes["height"];
- return heightString == null ? heightString as double? : double.tryParse(heightString);
+ return heightString == null
+ ? heightString as double?
+ : double.tryParse(heightString);
}
double? _width(Map attributes) {
final widthString = attributes["width"];
- return widthString == null ? widthString as double? : double.tryParse(widthString);
+ return widthString == null
+ ? widthString as double?
+ : double.tryParse(widthString);
}
double _aspectRatio(
@@ -545,5 +557,6 @@ double _aspectRatio(
}
extension ClampedEdgeInsets on EdgeInsetsGeometry {
- EdgeInsetsGeometry get nonNegative => this.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity));
+ EdgeInsetsGeometry get nonNegative =>
+ clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity));
}
diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart
index f0b33d547d..cc84284255 100644
--- a/lib/flutter_html.dart
+++ b/lib/flutter_html.dart
@@ -7,17 +7,18 @@ import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
+export 'package:flutter_html/custom_render.dart';
//export render context api
export 'package:flutter_html/html_parser.dart';
-//export render context api
-export 'package:flutter_html/html_parser.dart';
-export 'package:flutter_html/custom_render.dart';
+
//export src for advanced custom render uses (e.g. casting context.tree)
export 'package:flutter_html/src/anchor.dart';
export 'package:flutter_html/src/interactable_element.dart';
export 'package:flutter_html/src/layout_element.dart';
export 'package:flutter_html/src/replaced_element.dart';
export 'package:flutter_html/src/styled_element.dart';
+//export css_box_widget for use in custom render.
+export 'package:flutter_html/src/css_box_widget.dart';
//export style api
export 'package:flutter_html/style.dart';
@@ -60,8 +61,12 @@ class Html extends StatefulWidget {
this.onImageTap,
this.tagsList = const [],
this.style = const {},
- }) : documentElement = null,
- assert (data != null),
+ this.loadingPlaceholder,
+ this.onContentRendered,
+ this.textScaleFactor,
+ this.shouldSkipStyle,
+ }) : documentElement = null,
+ assert(data != null),
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -78,9 +83,13 @@ class Html extends StatefulWidget {
this.onImageTap,
this.tagsList = const [],
this.style = const {},
- }) : data = null,
+ this.loadingPlaceholder,
+ this.onContentRendered,
+ this.textScaleFactor,
+ this.shouldSkipStyle,
+ }) : data = null,
assert(document != null),
- this.documentElement = document!.documentElement,
+ documentElement = document!.documentElement,
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -97,7 +106,11 @@ class Html extends StatefulWidget {
this.onImageTap,
this.tagsList = const [],
this.style = const {},
- }) : data = null,
+ this.loadingPlaceholder,
+ this.onContentRendered,
+ this.textScaleFactor,
+ this.shouldSkipStyle,
+ }) : data = null,
assert(documentElement != null),
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -141,31 +154,51 @@ class Html extends StatefulWidget {
/// An API that allows you to override the default style for any HTML element
final Map style;
- static List get tags => new List.from(STYLED_ELEMENTS)
- ..addAll(INTERACTABLE_ELEMENTS)
- ..addAll(REPLACED_ELEMENTS)
- ..addAll(LAYOUT_ELEMENTS)
- ..addAll(TABLE_CELL_ELEMENTS)
- ..addAll(TABLE_DEFINITION_ELEMENTS)
- ..addAll(EXTERNAL_ELEMENTS);
+ /// Could be used to control style assignment during parsing HTML tree
+ /// Note that this variable should be set with care, as skipping styles may result in unintended formatting or layout issues.
+ final SkipStyleFunction? shouldSkipStyle;
+
+ final Widget? loadingPlaceholder;
+ final OnContentRendered? onContentRendered;
+ final double? textScaleFactor;
+
+ static List get tags => List.from(HtmlElements.styledElements)
+ ..addAll(HtmlElements.interactableElements)
+ ..addAll(HtmlElements.replacedElements)
+ ..addAll(HtmlElements.layoutElements)
+ ..addAll(HtmlElements.tableCellElements)
+ ..addAll(HtmlElements.tableDefinitionElements)
+ ..addAll(HtmlElements.externalElements);
@override
State createState() => _HtmlState();
}
class _HtmlState extends State {
- late final dom.Element documentElement;
+ late dom.Element documentElement;
@override
void initState() {
super.initState();
- documentElement =
- widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!;
+ documentElement = widget.data != null
+ ? HtmlParser.parseHTML(widget.data!)
+ : widget.documentElement!;
+ }
+
+ @override
+ void didUpdateWidget(Html oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if ((widget.data != null && oldWidget.data != widget.data) ||
+ oldWidget.documentElement != widget.documentElement) {
+ documentElement = widget.data != null
+ ? HtmlParser.parseHTML(widget.data!)
+ : widget.documentElement!;
+ }
}
@override
Widget build(BuildContext context) {
- return Container(
+ return SizedBox(
width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
child: HtmlParser(
key: widget._anchorKey,
@@ -180,8 +213,12 @@ class _HtmlState extends State {
style: widget.style,
customRenders: {}
..addAll(widget.customRenders)
- ..addAll(defaultRenders),
+ ..addAll(generateDefaultRenders()),
tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList,
+ loadingPlaceholder: widget.loadingPlaceholder,
+ onContentRendered: widget.onContentRendered,
+ textScaleFactor: widget.textScaleFactor,
+ shouldSkipStyle: widget.shouldSkipStyle,
),
);
}
@@ -232,7 +269,8 @@ class SelectableHtml extends StatefulWidget {
this.tagsList = const [],
this.selectionControls,
this.scrollPhysics,
- }) : documentElement = null,
+ this.shouldSkipStyle,
+ }) : documentElement = null,
assert(data != null),
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -250,9 +288,10 @@ class SelectableHtml extends StatefulWidget {
this.tagsList = const [],
this.selectionControls,
this.scrollPhysics,
- }) : data = null,
+ this.shouldSkipStyle,
+ }) : data = null,
assert(document != null),
- this.documentElement = document!.documentElement,
+ documentElement = document!.documentElement,
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -269,7 +308,8 @@ class SelectableHtml extends StatefulWidget {
this.tagsList = const [],
this.selectionControls,
this.scrollPhysics,
- }) : data = null,
+ this.shouldSkipStyle,
+ }) : data = null,
assert(documentElement != null),
_anchorKey = anchorKey ?? GlobalKey(),
super(key: key);
@@ -294,7 +334,9 @@ class SelectableHtml extends StatefulWidget {
final OnCssParseError? onCssParseError;
/// A parameter that should be set when the HTML widget is expected to be
- /// flexible
+ /// have a flexible width, that doesn't always fill its maximum width
+ /// constraints. For example, auto horizontal margins are ignored, and
+ /// block-level elements only take up the width they need.
final bool shrinkWrap;
/// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered.
@@ -314,7 +356,12 @@ class SelectableHtml extends StatefulWidget {
/// fallback to the default rendering.
final Map customRenders;
- static List get tags => new List.from(SELECTABLE_ELEMENTS);
+ /// Could be used to control style assignment during parsing HTML tree
+ /// Note that this variable should be set with care, as skipping styles may result in unintended formatting or layout issues.
+ final SkipStyleFunction? shouldSkipStyle;
+
+ static List get tags =>
+ List.from(HtmlElements.selectableElements);
@override
State createState() => _SelectableHtmlState();
@@ -326,12 +373,14 @@ class _SelectableHtmlState extends State {
@override
void initState() {
super.initState();
- documentElement = widget.data != null ? HtmlParser.parseHTML(widget.data!) : widget.documentElement!;
+ documentElement = widget.data != null
+ ? HtmlParser.parseHTML(widget.data!)
+ : widget.documentElement!;
}
@override
Widget build(BuildContext context) {
- return Container(
+ return SizedBox(
width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
child: HtmlParser(
key: widget._anchorKey,
@@ -346,10 +395,12 @@ class _SelectableHtmlState extends State {
style: widget.style,
customRenders: {}
..addAll(widget.customRenders)
- ..addAll(defaultRenders),
- tagsList: widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList,
+ ..addAll(generateDefaultRenders()),
+ tagsList:
+ widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList,
selectionControls: widget.selectionControls,
scrollPhysics: widget.scrollPhysics,
+ shouldSkipStyle: widget.shouldSkipStyle,
),
);
}
diff --git a/lib/html_parser.dart b/lib/html_parser.dart
index 4f15b9a8a1..99809ce576 100644
--- a/lib/html_parser.dart
+++ b/lib/html_parser.dart
@@ -1,31 +1,45 @@
+import 'dart:async';
import 'dart:collection';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:csslib/parser.dart' as cssparser;
import 'package:csslib/visitor.dart' as css;
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/src/css_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
+import 'package:flutter_html/src/style/marker.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as htmlparser;
-import 'package:numerus/numerus.dart';
+import 'package:list_counter/list_counter.dart';
typedef OnTap = void Function(
- String? url,
- RenderContext context,
- Map attributes,
- dom.Element? element,
+ String? url,
+ RenderContext context,
+ Map attributes,
+ dom.Element? element,
);
+
typedef OnCssParseError = String? Function(
String css,
List errors,
);
-class HtmlParser extends StatelessWidget {
- final Key? key;
+typedef OnContentRendered = Function(Size size);
+typedef SkipStyleFunction = bool Function(
+ StyleAssignmentMethod method, StyledElement tree, Style style);
+
+enum StyleAssignmentMethod {
+ external,
+ inline,
+ custom,
+ cascade;
+}
+
+class HtmlParser extends StatefulWidget {
final dom.Element htmlData;
final OnTap? onLinkTap;
final OnTap? onAnchorTap;
@@ -42,11 +56,15 @@ class HtmlParser extends StatelessWidget {
final Html? root;
final TextSelectionControls? selectionControls;
final ScrollPhysics? scrollPhysics;
+ final Widget? loadingPlaceholder;
+ final OnContentRendered? onContentRendered;
+ final double? textScaleFactor;
+ final SkipStyleFunction? shouldSkipStyle;
final Map cachedImageSizes = {};
HtmlParser({
- required this.key,
+ required super.key,
required this.htmlData,
required this.onLinkTap,
required this.onAnchorTap,
@@ -61,71 +79,85 @@ class HtmlParser extends StatelessWidget {
this.root,
this.selectionControls,
this.scrollPhysics,
- }) : this.internalOnAnchorTap = onAnchorTap != null
- ? onAnchorTap
- : key != null
- ? _handleAnchorTap(key, onLinkTap)
- : null,
- super(key: key);
-
+ this.loadingPlaceholder,
+ this.onContentRendered,
+ this.textScaleFactor,
+ this.shouldSkipStyle,
+ }) : internalOnAnchorTap = onAnchorTap ??
+ (key != null ? _handleAnchorTap(key, onLinkTap) : onLinkTap);
+
+ /// As the widget [build]s, the HTML data is processed into a tree of [StyledElement]s,
+ /// which are then parsed into an [InlineSpan] tree that is then rendered to the screen by Flutter
+ //TODO Lazy processing of data. We don't need the processing steps done every build phase unless the data has changed.
@override
- Widget build(BuildContext context) {
- Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError);
- StyledElement lexedTree = lexDomTree(
- htmlData,
- customRenders.keys.toList(),
- tagsList,
- context,
- this,
- );
- StyledElement? externalCssStyledTree;
- if (declarations.isNotEmpty) {
- externalCssStyledTree = _applyExternalCss(declarations, lexedTree);
- }
- StyledElement inlineStyledTree = _applyInlineStyles(externalCssStyledTree ?? lexedTree, onCssParseError);
- StyledElement customStyledTree = _applyCustomStyles(style, inlineStyledTree);
- StyledElement cascadedStyledTree = _cascadeStyles(style, customStyledTree);
- StyledElement cleanedTree = cleanTree(cascadedStyledTree);
- InlineSpan parsedTree = parseTree(
- RenderContext(
- buildContext: context,
- parser: this,
- tree: cleanedTree,
- style: cleanedTree.style,
- ),
- cleanedTree,
+ State createState() => _HtmlParserState();
+
+ static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) => (String? url,
+ RenderContext context,
+ Map attributes,
+ dom.Element? element) {
+ if (url?.startsWith("#") == true) {
+ final anchorContext =
+ AnchorKey.forId(key, url!.substring(1))?.currentContext;
+ if (anchorContext != null) {
+ Scrollable.ensureVisible(anchorContext);
+ }
+ return;
+ }
+ onLinkTap?.call(url, context, attributes, element);
+ };
+
+ /// [parseTree] converts a tree of [StyledElement]s to an [InlineSpan] tree.
+ ///
+ /// [parseTree] is responsible for handling the [customRenders] parameter and
+ /// deciding what different `Style.display` options look like as Widgets.
+ InlineSpan parseTree(RenderContext context, StyledElement tree) {
+ // Merge this element's style into the context so that children
+ // inherit the correct style
+ RenderContext newContext = RenderContext(
+ buildContext: context.buildContext,
+ parser: this,
+ tree: tree,
+ style: context.style.copyOnlyInherited(tree.style),
+ key: AnchorKey.of(key, tree),
);
- // 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.
- if (selectable) {
- return StyledText.selectable(
- textSpan: parsedTree as TextSpan,
- style: cleanedTree.style,
- textScaleFactor: MediaQuery.of(context).textScaleFactor,
- renderContext: RenderContext(
- buildContext: context,
- parser: this,
- tree: cleanedTree,
- style: cleanedTree.style,
- ),
- selectionControls: selectionControls,
- scrollPhysics: scrollPhysics,
- );
+ for (final entry in customRenders.keys) {
+ if (entry.call(newContext)) {
+ buildChildren() =>
+ tree.children.map((tree) => parseTree(newContext, tree)).toList();
+ if (newContext.parser.selectable &&
+ customRenders[entry] is SelectableCustomRender) {
+ selectableBuildChildren() => tree.children
+ .map((tree) => parseTree(newContext, tree) as TextSpan)
+ .toList();
+ return (customRenders[entry] as SelectableCustomRender)
+ .textSpan
+ .call(newContext, selectableBuildChildren);
+ }
+ if (newContext.parser.selectable) {
+ return customRenders[entry]!
+ .inlineSpan!
+ .call(newContext, buildChildren) as TextSpan;
+ }
+ if (customRenders[entry]?.inlineSpan != null) {
+ return customRenders[entry]!
+ .inlineSpan!
+ .call(newContext, buildChildren);
+ }
+ return WidgetSpan(
+ child: CssBoxWidget(
+ style: tree.style,
+ shrinkWrap: newContext.parser.shrinkWrap,
+ textScaleFactor: textScaleFactor,
+ childIsReplaced: true, //TODO is this true?
+ child:
+ customRenders[entry]!.widget!.call(newContext, buildChildren),
+ ),
+ );
+ }
}
- return StyledText(
- textSpan: parsedTree,
- style: cleanedTree.style,
- textScaleFactor: MediaQuery.of(context).textScaleFactor,
- renderContext: RenderContext(
- buildContext: context,
- parser: this,
- tree: cleanedTree,
- style: cleanedTree.style,
- ),
- );
+ return const WidgetSpan(child: SizedBox(height: 0, width: 0));
}
/// [parseHTML] converts a string of HTML to a DOM element using the dart `html` library.
@@ -150,10 +182,11 @@ class HtmlParser extends StatelessWidget {
name: "[Tree Root]",
children: [],
node: html,
- style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
+ //TODO(Sub6Resources): This seems difficult to customize
+ style: Style.fromTextStyle(Theme.of(context).textTheme.bodyMedium!),
);
- html.nodes.forEach((node) {
+ for (var node in html.nodes) {
tree.children.add(_recursiveLexer(
node,
customRenderMatchers,
@@ -161,7 +194,7 @@ class HtmlParser extends StatelessWidget {
context,
parser,
));
- });
+ }
return tree;
}
@@ -179,7 +212,7 @@ class HtmlParser extends StatelessWidget {
) {
List children = [];
- node.nodes.forEach((childNode) {
+ for (var childNode in node.nodes) {
children.add(_recursiveLexer(
childNode,
customRenderMatchers,
@@ -187,49 +220,58 @@ class HtmlParser extends StatelessWidget {
context,
parser,
));
- });
+ }
//TODO(Sub6Resources): There's probably a more efficient way to look this up.
if (node is dom.Element) {
if (!tagsList.contains(node.localName)) {
return EmptyContentElement();
}
- if (STYLED_ELEMENTS.contains(node.localName)) {
+ if (HtmlElements.styledElements.contains(node.localName)) {
return parseStyledElement(node, children);
- } else if (INTERACTABLE_ELEMENTS.contains(node.localName)) {
+ } else if (HtmlElements.interactableElements.contains(node.localName)) {
return parseInteractableElement(node, children);
- } else if (REPLACED_ELEMENTS.contains(node.localName)) {
+ } else if (HtmlElements.replacedElements.contains(node.localName)) {
return parseReplacedElement(node, children);
- } else if (LAYOUT_ELEMENTS.contains(node.localName)) {
+ } else if (HtmlElements.layoutElements.contains(node.localName)) {
return parseLayoutElement(node, children);
- } else if (TABLE_CELL_ELEMENTS.contains(node.localName)) {
+ } else if (HtmlElements.tableCellElements.contains(node.localName)) {
return parseTableCellElement(node, children);
- } else if (TABLE_DEFINITION_ELEMENTS.contains(node.localName)) {
+ } else if (HtmlElements.tableDefinitionElements
+ .contains(node.localName)) {
return parseTableDefinitionElement(node, children);
} else {
final StyledElement tree = parseStyledElement(node, children);
for (final entry in customRenderMatchers) {
if (entry.call(
- RenderContext(
- buildContext: context,
- parser: parser,
- tree: tree,
- style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
- ),
- )) {
+ RenderContext(
+ buildContext: context,
+ parser: parser,
+ tree: tree,
+ style:
+ Style.fromTextStyle(Theme.of(context).textTheme.bodyMedium!),
+ ),
+ )) {
return tree;
}
}
return EmptyContentElement();
}
} else if (node is dom.Text) {
- return TextContentElement(text: node.text, style: Style(), element: node.parent, node: node);
+ return TextContentElement(
+ text: node.text,
+ style: Style(),
+ element: node.parent,
+ node: node,
+ );
} else {
return EmptyContentElement();
}
}
- static Map>> _getExternalCssDeclarations(List styles, OnCssParseError? errorHandler) {
+ static Map>>
+ _getExternalCssDeclarations(
+ List styles, OnCssParseError? errorHandler) {
String fullCss = "";
for (final e in styles) {
fullCss = fullCss + e.innerHtml;
@@ -242,132 +284,141 @@ class HtmlParser extends StatelessWidget {
}
}
- static StyledElement _applyExternalCss(Map>> declarations, StyledElement tree) {
- declarations.forEach((key, style) {
+ static StyledElement _applyExternalCss(
+ Map>> declarations,
+ StyledElement tree,
+ SkipStyleFunction? shouldSkipStyle) {
+ declarations.forEach((key, declaration) {
try {
if (tree.matchesSelector(key)) {
- tree.style = tree.style.merge(declarationsToStyle(style));
+ final style = declarationsToStyle(declaration);
+ final skip = shouldSkipStyle != null
+ ? shouldSkipStyle(StyleAssignmentMethod.external, tree, style)
+ : false;
+ if (!skip) {
+ tree.style = tree.style.merge(style);
+ }
}
} catch (_) {}
});
- tree.children.forEach((e) => _applyExternalCss(declarations, e));
+ for (var element in tree.children) {
+ _applyExternalCss(declarations, element, shouldSkipStyle);
+ }
return tree;
}
- static StyledElement _applyInlineStyles(StyledElement tree, OnCssParseError? errorHandler) {
+ static StyledElement _applyInlineStyles(StyledElement tree,
+ OnCssParseError? errorHandler, SkipStyleFunction? shouldSkipStyle) {
if (tree.attributes.containsKey("style")) {
final newStyle = inlineCssToStyle(tree.attributes['style'], errorHandler);
if (newStyle != null) {
- tree.style = tree.style.merge(newStyle);
+ final skip = shouldSkipStyle != null
+ ? shouldSkipStyle(StyleAssignmentMethod.inline, tree, newStyle)
+ : false;
+ if (!skip) {
+ tree.style = tree.style.merge(newStyle);
+ }
}
}
- tree.children.forEach((e) => _applyInlineStyles(e, errorHandler));
+ for (var element in tree.children) {
+ _applyInlineStyles(element, errorHandler, shouldSkipStyle);
+ }
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.
- static StyledElement _applyCustomStyles(Map style, StyledElement tree) {
+ static StyledElement _applyCustomStyles(Map style,
+ StyledElement tree, SkipStyleFunction? shouldSkipStyle) {
style.forEach((key, style) {
try {
if (tree.matchesSelector(key)) {
- tree.style = tree.style.merge(style);
+ final skip = shouldSkipStyle != null
+ ? shouldSkipStyle(StyleAssignmentMethod.custom, tree, style)
+ : false;
+ if (!skip) {
+ tree.style = tree.style.merge(style);
+ }
}
} catch (_) {}
});
- tree.children.forEach((e) => _applyCustomStyles(style, e));
+ for (var element in tree.children) {
+ _applyCustomStyles(style, element, shouldSkipStyle);
+ }
return tree;
}
/// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each
/// child that doesn't specify a different style.
- static StyledElement _cascadeStyles(Map style, StyledElement tree) {
- tree.children.forEach((child) {
- child.style = tree.style.copyOnlyInherited(child.style);
- _cascadeStyles(style, child);
- });
+ static StyledElement _cascadeStyles(Map style,
+ StyledElement tree, SkipStyleFunction? shouldSkipStyle) {
+ for (var child in tree.children) {
+ final newStyle = tree.style.copyOnlyInherited(child.style);
+ final skip = shouldSkipStyle != null
+ ? shouldSkipStyle(StyleAssignmentMethod.cascade, tree, newStyle)
+ : false;
+ if (!skip) {
+ child.style = newStyle;
+ }
+ _cascadeStyles(style, child, shouldSkipStyle);
+ }
return tree;
}
- /// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are
+ /// [styleTree] takes the lexed [StyleElement] tree and applies external,
+ /// inline, and custom CSS/Flutter styles, and then cascades the styles down the tree.
+ static StyledElement styleTree(
+ StyledElement tree,
+ dom.Element htmlData,
+ Map style,
+ OnCssParseError? onCssParseError,
+ SkipStyleFunction? shouldSkipStyle) {
+ Map>> declarations =
+ _getExternalCssDeclarations(
+ htmlData.getElementsByTagName("style"), onCssParseError);
+
+ StyledElement? externalCssStyledTree;
+ if (declarations.isNotEmpty) {
+ externalCssStyledTree =
+ _applyExternalCss(declarations, tree, shouldSkipStyle);
+ }
+ tree = _applyInlineStyles(
+ externalCssStyledTree ?? tree, onCssParseError, shouldSkipStyle);
+ tree = _applyCustomStyles(style, tree, shouldSkipStyle);
+ tree = _cascadeStyles(style, tree, shouldSkipStyle);
+ return tree;
+ }
+
+ /// [processTree] 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) {
+ static StyledElement processTree(
+ StyledElement tree, double devicePixelRatio) {
tree = _processInternalWhitespace(tree);
tree = _processInlineWhitespace(tree);
tree = _removeEmptyElements(tree);
- tree = _processListCharacters(tree);
+
+ tree = _calculateRelativeValues(tree, devicePixelRatio);
+ tree = _preprocessListMarkers(tree);
+ tree = _processCounters(tree);
+ tree = _processListMarkers(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 [customRenders] parameter and
- /// deciding what different `Style.display` options look like as Widgets.
- InlineSpan parseTree(RenderContext context, StyledElement tree) {
- // Merge this element's style into the context so that children
- // inherit the correct style
- RenderContext newContext = RenderContext(
- buildContext: context.buildContext,
- parser: this,
- tree: tree,
- style: context.style.copyOnlyInherited(tree.style),
- key: AnchorKey.of(key, tree),
- );
-
- for (final entry in customRenders.keys) {
- if (entry.call(newContext)) {
- final buildChildren = () => tree.children.map((tree) => parseTree(newContext, tree)).toList();
- if (newContext.parser.selectable && customRenders[entry] is SelectableCustomRender) {
- final selectableBuildChildren = () => tree.children.map((tree) => parseTree(newContext, tree) as TextSpan).toList();
- return (customRenders[entry] as SelectableCustomRender).textSpan.call(newContext, selectableBuildChildren);
- }
- if (newContext.parser.selectable) {
- return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren) as TextSpan;
- }
- if (customRenders[entry]?.inlineSpan != null) {
- return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren);
- }
- return WidgetSpan(
- child: ContainerSpan(
- newContext: newContext,
- style: tree.style,
- shrinkWrap: newContext.parser.shrinkWrap,
- child: customRenders[entry]!.widget!.call(newContext, buildChildren),
- ),
- );
- }
- }
- return WidgetSpan(child: Container(height: 0, width: 0));
- }
-
- static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) =>
- (String? url, RenderContext context, Map attributes, dom.Element? element) {
- if (url?.startsWith("#") == true) {
- final anchorContext = AnchorKey.forId(key, url!.substring(1))?.currentContext;
- if (anchorContext != null) {
- Scrollable.ensureVisible(anchorContext);
- }
- return;
- }
- onLinkTap?.call(url, context, attributes, element);
- };
-
/// [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) {
+ if ((tree.style.whiteSpace ?? WhiteSpace.normal) == WhiteSpace.pre) {
// Preserve this whitespace
} else if (tree is TextContentElement) {
tree.text = _removeUnnecessaryWhitespace(tree.text!);
@@ -396,24 +447,33 @@ class HtmlParser extends StatelessWidget {
/// initialize indices to negative numbers to make conditionals a little easier
int textIndex = -1;
int elementIndex = -1;
+
/// initialize parent after to a whitespace to account for elements that are
/// the last child in the list of elements
String parentAfterText = " ";
+
/// find the index of the text in the current tree
if ((tree.element?.nodes.length ?? 0) >= 1) {
- textIndex = tree.element?.nodes.indexWhere((element) => element == tree.node) ?? -1;
+ textIndex =
+ tree.element?.nodes.indexWhere((element) => element == tree.node) ??
+ -1;
}
+
/// get the parent nodes
dom.NodeList? parentNodes = tree.element?.parent?.nodes;
+
/// find the index of the tree itself in the parent nodes
if ((parentNodes?.length ?? 0) >= 1) {
- elementIndex = parentNodes?.indexWhere((element) => element == tree.element) ?? -1;
+ elementIndex =
+ parentNodes?.indexWhere((element) => element == tree.element) ?? -1;
}
+
/// if the tree is any node except the last node in the node list and the
/// next node in the node list is a text node, then get its text. Otherwise
/// the next node will be a [dom.Element], so keep unwrapping that until
/// we get the underlying text node, and finally get its text.
- if (elementIndex < (parentNodes?.length ?? 1) - 1 && parentNodes?[elementIndex + 1] is dom.Text) {
+ if (elementIndex < (parentNodes?.length ?? 1) - 1 &&
+ parentNodes?[elementIndex + 1] is dom.Text) {
parentAfterText = parentNodes?[elementIndex + 1].text ?? " ";
} else if (elementIndex < (parentNodes?.length ?? 1) - 1) {
var parentAfter = parentNodes?[elementIndex + 1];
@@ -426,6 +486,7 @@ class HtmlParser extends StatelessWidget {
}
parentAfterText = parentAfter?.text ?? " ";
}
+
/// If the text is the first element in the current tree node list, it
/// starts with a whitespace, it isn't a line break, either the
/// whitespace is unnecessary or it is a block element, and either it is
@@ -435,38 +496,38 @@ class HtmlParser extends StatelessWidget {
/// We should also delete the whitespace at any point in the node list
/// if the previous element is a
because that tag makes the element
/// act like a block element.
- if (textIndex < 1
- && tree.text!.startsWith(' ')
- && tree.element?.localName != "br"
- && (!keepLeadingSpace.data
- || tree.style.display == Display.BLOCK)
- && (elementIndex < 1
- || (elementIndex >= 1
- && parentNodes?[elementIndex - 1] is dom.Text
- && parentNodes![elementIndex - 1].text!.endsWith(" ")))
- ) {
+ if (textIndex < 1 &&
+ tree.text!.startsWith(' ') &&
+ tree.element?.localName != "br" &&
+ (!keepLeadingSpace.data || tree.style.display == Display.block) &&
+ (elementIndex < 1 ||
+ (elementIndex >= 1 &&
+ parentNodes?[elementIndex - 1] is dom.Text &&
+ parentNodes![elementIndex - 1].text!.endsWith(" ")))) {
tree.text = tree.text!.replaceFirst(' ', '');
- } else if (textIndex >= 1
- && tree.text!.startsWith(' ')
- && tree.element?.nodes[textIndex - 1] is dom.Element
- && (tree.element?.nodes[textIndex - 1] as dom.Element).localName == "br"
- ) {
+ } else if (textIndex >= 1 &&
+ tree.text!.startsWith(' ') &&
+ tree.element?.nodes[textIndex - 1] is dom.Element &&
+ (tree.element?.nodes[textIndex - 1] as dom.Element).localName ==
+ "br") {
tree.text = tree.text!.replaceFirst(' ', '');
}
+
/// If the text is the last element in the current tree node list, it isn't
/// a line break, and the next text node starts with a whitespace,
/// update the [Context] to signify to that next text node whether it should
/// keep its whitespace. This is based on whether the current text ends with a
/// whitespace.
- if (textIndex == (tree.element?.nodes.length ?? 1) - 1
- && tree.element?.localName != "br"
- && parentAfterText.startsWith(' ')
- ) {
+ if (textIndex == (tree.element?.nodes.length ?? 1) - 1 &&
+ tree.element?.localName != "br" &&
+ parentAfterText.startsWith(' ')) {
keepLeadingSpace.data = !tree.text!.endsWith(' ');
}
}
- tree.children.forEach((e) => _processInlineWhitespaceRecursive(e, keepLeadingSpace));
+ for (var element in tree.children) {
+ _processInlineWhitespaceRecursive(element, keepLeadingSpace);
+ }
return tree;
}
@@ -480,139 +541,119 @@ class HtmlParser extends StatelessWidget {
/// (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(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();
- tree = _processListCharactersRecursive(tree, olStack);
- return tree;
- }
+ /// [preprocessListMarkers] adds marker pseudo elements to the front of all list
+ /// items.
+ static StyledElement _preprocessListMarkers(StyledElement tree) {
+ tree.style.listStylePosition ??= ListStylePosition.outside;
- /// [_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 olStack) {
- if (tree.style.listStylePosition == null) {
- tree.style.listStylePosition = ListStylePosition.OUTSIDE;
- }
- if (tree.name == 'ol' && tree.style.listStyleType != null && tree.style.listStyleType!.type == "marker") {
- switch (tree.style.listStyleType!) {
- case ListStyleType.LOWER_LATIN:
- case ListStyleType.LOWER_ALPHA:
- case ListStyleType.UPPER_LATIN:
- case ListStyleType.UPPER_ALPHA:
- olStack.add(Context('a'));
- if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
- var start = int.tryParse(tree.attributes['start']!) ?? 1;
- var x = 1;
- while (x < start) {
- olStack.last.data = olStack.last.data.toString().nextLetter();
- x++;
- }
- }
- break;
- default:
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
- break;
+ if (tree.style.display == Display.listItem) {
+ // Add the marker pseudo-element if it doesn't exist
+ tree.style.marker ??= Marker(
+ content: Content.normal,
+ style: tree.style,
+ );
+
+ // Inherit styles from originating widget
+ tree.style.marker!.style =
+ tree.style.copyOnlyInherited(tree.style.marker!.style ?? Style());
+
+ // Add the implicit counter-increment on `list-item` if it isn't set
+ // explicitly already
+ tree.style.counterIncrement ??= {};
+ if (!tree.style.counterIncrement!.containsKey('list-item')) {
+ tree.style.counterIncrement!['list-item'] = 1;
}
- } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "widget") {
- tree.style.markerContent = tree.style.listStyleType!.widget!;
- } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "image") {
- tree.style.markerContent = Image.network(tree.style.listStyleType!.text);
- } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) {
- String marker = "";
- switch (tree.style.listStyleType!) {
- case ListStyleType.NONE:
- break;
- case ListStyleType.CIRCLE:
- marker = '○';
- break;
- case ListStyleType.SQUARE:
- marker = '■';
- break;
- case ListStyleType.DISC:
- marker = '•';
- break;
- case ListStyleType.DECIMAL:
- if (olStack.isEmpty) {
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
- }
- olStack.last.data += 1;
- marker = '${olStack.last.data}.';
- break;
- case ListStyleType.LOWER_LATIN:
- case ListStyleType.LOWER_ALPHA:
- if (olStack.isEmpty) {
- olStack.add(Context('a'));
- if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
- var start = int.tryParse(tree.attributes['start']!) ?? 1;
- var x = 1;
- while (x < start) {
- olStack.last.data = olStack.last.data.toString().nextLetter();
- x++;
- }
- }
- }
- marker = olStack.last.data.toString() + ".";
- olStack.last.data = olStack.last.data.toString().nextLetter();
- break;
- case ListStyleType.UPPER_LATIN:
- case ListStyleType.UPPER_ALPHA:
- if (olStack.isEmpty) {
- olStack.add(Context('a'));
- if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
- var start = int.tryParse(tree.attributes['start']!) ?? 1;
- var x = 1;
- while (x < start) {
- olStack.last.data = olStack.last.data.toString().nextLetter();
- x++;
- }
- }
- }
- marker = olStack.last.data.toString().toUpperCase() + ".";
- olStack.last.data = olStack.last.data.toString().nextLetter();
- break;
- case ListStyleType.LOWER_ROMAN:
- if (olStack.isEmpty) {
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
- }
- olStack.last.data += 1;
- if (olStack.last.data <= 0) {
- marker = '${olStack.last.data}.';
- } else {
- marker = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
- }
- break;
- case ListStyleType.UPPER_ROMAN:
- if (olStack.isEmpty) {
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
- }
- olStack.last.data += 1;
- if (olStack.last.data <= 0) {
- marker = '${olStack.last.data}.';
- } else {
- marker = (olStack.last.data as int).toRomanNumeralString()! + ".";
- }
- break;
+ }
+
+ // Add the counters to ol and ul types.
+ if (tree.name == 'ol' || tree.name == 'ul') {
+ tree.style.counterReset ??= {};
+ if (!tree.style.counterReset!.containsKey('list-item')) {
+ tree.style.counterReset!['list-item'] = 0;
}
- tree.style.markerContent = Text(
- marker,
- textAlign: TextAlign.right,
- );
}
- tree.children.forEach((e) => _processListCharactersRecursive(e, olStack));
+ for (var child in tree.children) {
+ _preprocessListMarkers(child);
+ }
+
+ return tree;
+ }
+
+ /// [_processListCounters] adds the appropriate counter values to each
+ /// StyledElement on the tree.
+ static StyledElement _processCounters(StyledElement tree,
+ [ListQueue? counters]) {
+ // Add the counters for the current scope.
+ tree.counters.addAll(counters?.deepCopy() ?? []);
+
+ // Create any new counters
+ if (tree.style.counterReset != null) {
+ tree.style.counterReset!.forEach((counterName, initialValue) {
+ tree.counters.add(Counter(counterName, initialValue ?? 0));
+ });
+ }
+
+ // Increment any counters that are to be incremented
+ if (tree.style.counterIncrement != null) {
+ tree.style.counterIncrement!.forEach((counterName, increment) {
+ tree.counters
+ .lastWhereOrNull(
+ (counter) => counter.name == counterName,
+ )
+ ?.increment(increment ?? 1);
+
+ // If we didn't newly create the counter, increment the counter in the old copy as well.
+ if (tree.style.counterReset == null ||
+ !tree.style.counterReset!.containsKey(counterName)) {
+ counters
+ ?.lastWhereOrNull(
+ (counter) => counter.name == counterName,
+ )
+ ?.increment(increment ?? 1);
+ }
+ });
+ }
+
+ for (var element in tree.children) {
+ _processCounters(element, tree.counters);
+ }
+
+ return tree;
+ }
+
+ static StyledElement _processListMarkers(StyledElement tree) {
+ if (tree.style.display == Display.listItem) {
+ final listStyleType = tree.style.listStyleType ?? ListStyleType.decimal;
+ final counterStyle = CounterStyleRegistry.lookup(
+ listStyleType.counterStyle,
+ );
+ String counterContent;
+ if (tree.style.marker?.content.isNormal ?? true) {
+ counterContent = counterStyle.generateMarkerContent(
+ tree.counters.lastOrNull?.value ?? 0,
+ );
+ } else if (!(tree.style.marker?.content.display ?? true)) {
+ counterContent = '';
+ } else {
+ counterContent = tree.style.marker?.content.replacementContent ??
+ counterStyle.generateMarkerContent(
+ tree.counters.lastOrNull?.value ?? 0,
+ );
+ }
+ tree.style.marker = Marker(
+ content: Content(counterContent), style: tree.style.marker?.style);
+ }
- if (tree.name == 'ol') {
- olStack.removeLast();
+ for (var child in tree.children) {
+ _processListMarkers(child);
}
return tree;
@@ -624,11 +665,20 @@ class HtmlParser extends StatelessWidget {
static StyledElement _processBeforesAndAfters(StyledElement tree) {
if (tree.style.before != null) {
tree.children.insert(
- 0, TextContentElement(text: tree.style.before, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE)));
+ 0,
+ TextContentElement(
+ text: tree.style.before,
+ style: tree.style
+ .copyWith(beforeAfterNull: true, display: Display.inline),
+ ),
+ );
}
if (tree.style.after != null) {
- tree.children
- .add(TextContentElement(text: tree.style.after, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE)));
+ tree.children.add(TextContentElement(
+ text: tree.style.after,
+ style:
+ tree.style.copyWith(beforeAfterNull: true, display: Display.inline),
+ ));
}
tree.children.forEach(_processBeforesAndAfters);
@@ -636,7 +686,7 @@ class HtmlParser extends StatelessWidget {
return tree;
}
- /// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS21/box.html#collapsing-margins
+ /// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS22/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.
///
@@ -650,8 +700,9 @@ class HtmlParser extends StatelessWidget {
//Short circuit if we've reached a leaf of the tree
if (tree.children.isEmpty) {
// Handle case (4) from above.
- if ((tree.style.height ?? 0) == 0) {
- tree.style.margin = EdgeInsets.zero;
+ if (tree.style.height?.value == 0 &&
+ tree.style.height?.unit != Unit.auto) {
+ tree.style.margin = tree.style.margin?.collapse() ?? Margins.zero;
}
return tree;
}
@@ -667,47 +718,49 @@ class HtmlParser extends StatelessWidget {
// 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 parentTop = tree.style.margin?.top?.value ?? 0;
+ final firstChildTop = tree.children.first.style.margin?.top?.value ?? 0;
final newOuterMarginTop = max(parentTop, firstChildTop);
// Set the parent's margin
if (tree.style.margin == null) {
- tree.style.margin = EdgeInsets.only(top: newOuterMarginTop);
+ tree.style.margin = Margins.only(top: newOuterMarginTop);
} else {
- tree.style.margin = tree.style.margin!.copyWith(top: newOuterMarginTop);
+ tree.style.margin =
+ tree.style.margin!.copyWithEdge(top: newOuterMarginTop);
}
// And remove the child's margin
if (tree.children.first.style.margin == null) {
- tree.children.first.style.margin = EdgeInsets.zero;
+ tree.children.first.style.margin = Margins.zero;
} else {
tree.children.first.style.margin =
- tree.children.first.style.margin!.copyWith(top: 0);
+ tree.children.first.style.margin!.copyWithEdge(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 parentBottom = tree.style.margin?.bottom?.value ?? 0;
+ final lastChildBottom =
+ tree.children.last.style.margin?.bottom?.value ?? 0;
final newOuterMarginBottom = max(parentBottom, lastChildBottom);
// Set the parent's margin
if (tree.style.margin == null) {
- tree.style.margin = EdgeInsets.only(bottom: newOuterMarginBottom);
+ tree.style.margin = Margins.only(bottom: newOuterMarginBottom);
} else {
tree.style.margin =
- tree.style.margin!.copyWith(bottom: newOuterMarginBottom);
+ tree.style.margin!.copyWithEdge(bottom: newOuterMarginBottom);
}
// And remove the child's margin
if (tree.children.last.style.margin == null) {
- tree.children.last.style.margin = EdgeInsets.zero;
+ tree.children.last.style.margin = Margins.zero;
} else {
tree.children.last.style.margin =
- tree.children.last.style.margin!.copyWith(bottom: 0);
+ tree.children.last.style.margin!.copyWithEdge(bottom: 0);
}
}
@@ -715,24 +768,23 @@ class HtmlParser extends StatelessWidget {
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;
+ tree.children[i - 1].style.margin?.bottom?.value ?? 0;
+ final thisTop = tree.children[i].style.margin?.top?.value ?? 0;
+ final newInternalMargin = max(previousSiblingBottom, thisTop);
if (tree.children[i - 1].style.margin == null) {
tree.children[i - 1].style.margin =
- EdgeInsets.only(bottom: newInternalMargin);
+ Margins.only(bottom: newInternalMargin);
} else {
tree.children[i - 1].style.margin = tree.children[i - 1].style.margin!
- .copyWith(bottom: newInternalMargin);
+ .copyWithEdge(bottom: newInternalMargin);
}
if (tree.children[i].style.margin == null) {
- tree.children[i].style.margin =
- EdgeInsets.only(top: newInternalMargin);
+ tree.children[i].style.margin = Margins.only(top: newInternalMargin);
} else {
- tree.children[i].style.margin =
- tree.children[i].style.margin!.copyWith(top: newInternalMargin);
+ tree.children[i].style.margin = tree.children[i].style.margin!
+ .copyWithEdge(top: newInternalMargin);
}
}
}
@@ -751,34 +803,35 @@ class HtmlParser extends StatelessWidget {
tree.children.forEachIndexed((index, child) {
if (child is EmptyContentElement || child is EmptyLayoutElement) {
toRemove.add(child);
- } else if (child is TextContentElement
- && ((tree.name == "body"
- && (index == 0
- || index + 1 == tree.children.length
- || tree.children[index - 1].style.display == Display.BLOCK
- || tree.children[index + 1].style.display == Display.BLOCK))
- || tree.name == "ul")
- && child.text!.replaceAll(' ', '').isEmpty) {
+ } else if (child is TextContentElement &&
+ ((tree.name == "body" &&
+ (index == 0 ||
+ index + 1 == tree.children.length ||
+ tree.children[index - 1].style.display == Display.block ||
+ tree.children[index + 1].style.display ==
+ Display.block)) ||
+ tree.name == "ul") &&
+ child.text!.replaceAll(' ', '').isEmpty) {
toRemove.add(child);
- } else if (child is TextContentElement
- && child.text!.isEmpty
- && child.style.whiteSpace != WhiteSpace.PRE) {
+ } else if (child is TextContentElement &&
+ child.text!.isEmpty &&
+ child.style.whiteSpace != WhiteSpace.pre) {
toRemove.add(child);
} else if (child is TextContentElement &&
- child.style.whiteSpace != WhiteSpace.PRE &&
- tree.style.display == Display.BLOCK &&
+ child.style.whiteSpace != WhiteSpace.pre &&
+ tree.style.display == Display.block &&
child.text!.isEmpty &&
lastChildBlock) {
toRemove.add(child);
- } else if (child.style.display == Display.NONE) {
+ } 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 ||
+ lastChildBlock = (child.style.display == Display.block ||
+ child.style.display == Display.listItem ||
(child is TextContentElement && child.text == '\n'));
});
tree.children.removeWhere((element) => toRemove.contains(element));
@@ -786,20 +839,283 @@ class HtmlParser extends StatelessWidget {
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;
+ /// [_calculateRelativeValues] converts rem values to px sizes and then
+ /// applies relative calculations
+ static StyledElement _calculateRelativeValues(
+ StyledElement tree, double devicePixelRatio) {
+ double remSize = (tree.style.fontSize?.value ?? FontSize.medium.value);
+
+ //If the root element has a rem-based fontSize, then give it the default
+ // font size times the set rem value.
+ if (tree.style.fontSize?.unit == Unit.rem) {
+ tree.style.fontSize = FontSize(FontSize.medium.value * remSize);
+ }
+
+ _applyRelativeValuesRecursive(tree, remSize, devicePixelRatio);
+ tree.style.setRelativeValues(remSize, remSize / devicePixelRatio);
+
+ return tree;
+ }
+
+ /// This is the recursive worker function for [_calculateRelativeValues]
+ static void _applyRelativeValuesRecursive(
+ StyledElement tree, double remFontSize, double devicePixelRatio) {
+ //When we get to this point, there should be a valid fontSize at every level.
+ assert(tree.style.fontSize != null);
+
+ final parentFontSize = tree.style.fontSize!.value;
+
+ for (var child in tree.children) {
+ if (child.style.fontSize == null) {
+ child.style.fontSize = FontSize(parentFontSize);
+ } else {
+ switch (child.style.fontSize!.unit) {
+ case Unit.em:
+ child.style.fontSize =
+ FontSize(parentFontSize * child.style.fontSize!.value);
+ break;
+ case Unit.percent:
+ child.style.fontSize = FontSize(
+ parentFontSize * (child.style.fontSize!.value / 100.0));
+ break;
+ case Unit.rem:
+ child.style.fontSize =
+ FontSize(remFontSize * child.style.fontSize!.value);
+ break;
+ case Unit.px:
+ case Unit.auto:
+ //Ignore
+ break;
+ }
+ }
+
+ // Note: it is necessary to scale down the emSize by the factor of
+ // devicePixelRatio since Flutter seems to calculates font sizes using
+ // physical pixels, but margins/padding using logical pixels.
+ final emSize = child.style.fontSize!.value / devicePixelRatio;
+
+ tree.style.setRelativeValues(remFontSize, emSize);
+
+ _applyRelativeValuesRecursive(child, remFontSize, devicePixelRatio);
+ }
+ }
+}
+
+class _HtmlParserState extends State {
+ static final _renderQueue = [];
+
+ final GlobalKey _htmlGlobalKey = GlobalKey();
+ final GlobalKey _animatedSwitcherKey = GlobalKey();
+
+ Completer? _completer;
+ bool _isOffstage = true;
+ ParseResult? _parseResult;
+
+ InlineSpan? _parsedTree;
+ StyledElement? _processedTree;
+ bool _disposed = false;
+
+ @override
+ void dispose() {
+ _disposed = true;
+
+ // If we're still waiting to run, just cancel and remove us from the render queue
+ if (_completer?.isCompleted == false) {
+ _completer!.completeError(Exception('disposed'));
+ _renderQueue.remove(_completer);
+ }
+
+ super.dispose();
+ }
- tree.children.forEach((child) {
- if ((child.style.fontSize?.size ?? parentFontSize)! < 0) {
- child.style.fontSize =
- FontSize(parentFontSize! * -child.style.fontSize!.size!);
+ @override
+ Widget build(BuildContext context) {
+ if (_parseResult == null) {
+ try {
+ _parseTree(context);
+ } catch (error) {
+ if (kDebugMode) {
+ print(error);
+ }
}
+ }
+
+ if (_parseResult != null && _isOffstage) {
+ WidgetsBinding.instance.addPostFrameCallback(_afterLayout);
+ }
+
+ final children = [];
+
+ children.add(
+ Visibility(
+ visible: _isOffstage || _parseResult == null,
+ maintainSize: true,
+ maintainAnimation: true,
+ maintainState: true,
+ child: widget.loadingPlaceholder ?? Container(height: 1000),
+ ),
+ );
+
+ if (!_isOffstage && _parseResult != null) {
+ children.add(
+ CssBoxWidget.withInlineSpanChildren(
+ key: _htmlGlobalKey,
+ textScaleFactor: widget.textScaleFactor,
+ style: _parseResult!.style,
+ context: context,
+ children: [_parseResult!.inlineSpan],
+ selectable: widget.selectable,
+ scrollPhysics: widget.scrollPhysics,
+ selectionControls: widget.selectionControls,
+ shrinkWrap: widget.shrinkWrap,
+ ),
+ );
+ }
- _processFontSize(child);
+ return Stack(
+ children: [
+ if (_isOffstage && _parseResult != null)
+ Offstage(
+ offstage: true,
+ child: CssBoxWidget.withInlineSpanChildren(
+ key: _htmlGlobalKey,
+ style: _parseResult!.style,
+ textScaleFactor: widget.textScaleFactor,
+ context: context,
+ children: [_parseResult!.inlineSpan],
+ selectable: widget.selectable,
+ scrollPhysics: widget.scrollPhysics,
+ selectionControls: widget.selectionControls,
+ shrinkWrap: widget.shrinkWrap,
+ ),
+ ),
+ AnimatedSwitcher(
+ key: _animatedSwitcherKey,
+ layoutBuilder: (currentChild, previousChildren) {
+ return Stack(
+ alignment: Alignment.topLeft,
+ children: [
+ ...previousChildren,
+ if (currentChild != null) currentChild,
+ ],
+ );
+ },
+ duration: kThemeAnimationDuration,
+ child: Stack(
+ alignment: Alignment.topLeft,
+ fit: StackFit.loose,
+ key: ValueKey(_isOffstage),
+ children: children,
+ ),
+ ),
+ ],
+ );
+ }
+
+ void _afterLayout(Duration timeStamp) {
+ final renderObject = _htmlGlobalKey.currentContext?.findRenderObject();
+ if (renderObject == null) return;
+
+ final size = renderObject.semanticBounds.size;
+ widget.onContentRendered?.call(size);
+
+ setState(() {
+ _isOffstage = false;
});
- return tree;
+ }
+
+ Future _parseTree(BuildContext context) async {
+ if (_completer != null) {
+ if (_renderQueue.isNotEmpty) {
+ _renderQueue.remove(_completer!);
+ }
+
+ if (!_completer!.isCompleted) {
+ _completer!.completeError(Exception('replaced'));
+ }
+ }
+
+ _completer = Completer();
+ _renderQueue.add(_completer!);
+
+ try {
+ if (_renderQueue.length > 1) {
+ if (kDebugMode) {
+ print('_parseTree waiting for queue');
+ }
+ await _completer!.future;
+ } else {
+ _completer!.complete();
+ }
+ } catch (exception) {
+ if (kDebugMode) {
+ print('_parseTree ${exception.toString()}');
+ }
+ return;
+ }
+
+ if (kDebugMode) {
+ print('_parseTree parsing');
+ }
+
+ try {
+ if (_processedTree == null && mounted) {
+ // Lexing Step
+ StyledElement lexedTree = HtmlParser.lexDomTree(
+ widget.htmlData,
+ widget.customRenders.keys.toList(),
+ widget.tagsList,
+ context,
+ widget,
+ );
+ // Styling Step
+ StyledElement styledTree = HtmlParser.styleTree(
+ lexedTree,
+ widget.htmlData,
+ widget.style,
+ widget.onCssParseError,
+ widget.shouldSkipStyle,
+ );
+
+ // Processing Step
+ StyledElement processedTree = HtmlParser.processTree(
+ styledTree, MediaQuery.of(context).devicePixelRatio);
+
+ _processedTree = processedTree;
+
+ _parsedTree = widget.parseTree(
+ RenderContext(
+ buildContext: context,
+ parser: widget,
+ tree: processedTree,
+ style: processedTree.style,
+ ),
+ processedTree,
+ );
+ }
+
+ if (kDebugMode) {
+ print('_parseTree parsed');
+ }
+ } catch (exception) {
+ if (kDebugMode) {
+ print(exception);
+ }
+ return;
+ } finally {
+ _renderQueue.remove(_completer);
+ _completer = null;
+
+ if (_renderQueue.isNotEmpty && !_renderQueue[0].isCompleted) {
+ _renderQueue[0].complete();
+ }
+ }
+
+ if (!_disposed && _parsedTree != null && _processedTree != null) {
+ setState(() {
+ _parseResult = ParseResult(_parsedTree!, _processedTree!.style);
+ });
+ }
}
}
@@ -823,126 +1139,12 @@ class RenderContext {
});
}
-/// 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 AnchorKey? key;
- final Widget? child;
- final List? children;
- final Style style;
- final RenderContext newContext;
- final bool shrinkWrap;
-
- ContainerSpan({
- this.key,
- this.child,
- this.children,
- required this.style,
- required this.newContext,
- this.shrinkWrap = false,
- }): super(key: key);
-
- @override
- Widget build(BuildContext _) {
- return Container(
- decoration: BoxDecoration(
- border: style.border,
- color: style.backgroundColor,
- ),
- height: style.height,
- width: style.width,
- padding: style.padding?.nonNegative,
- margin: style.margin?.nonNegative,
- 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;
- final AnchorKey? key;
- final bool _selectable;
- final TextSelectionControls? selectionControls;
- final ScrollPhysics? scrollPhysics;
-
- const StyledText({
- required this.textSpan,
- required this.style,
- this.textScaleFactor = 1.0,
- required this.renderContext,
- this.key,
- this.selectionControls,
- this.scrollPhysics,
- }) : _selectable = false,
- super(key: key);
-
- const StyledText.selectable({
- required TextSpan textSpan,
- required this.style,
- this.textScaleFactor = 1.0,
- required this.renderContext,
- this.key,
- this.selectionControls,
- this.scrollPhysics,
- }) : textSpan = textSpan,
- _selectable = true,
- super(key: key);
-
- @override
- Widget build(BuildContext context) {
- if (_selectable) {
- return SelectableText.rich(
- textSpan as TextSpan,
- style: style.generateTextStyle(),
- textAlign: style.textAlign,
- textDirection: style.direction,
- textScaleFactor: textScaleFactor,
- maxLines: style.maxLines,
- selectionControls: selectionControls,
- scrollPhysics: scrollPhysics,
- );
- }
- return SizedBox(
- width: consumeExpandedBlock(style.display, renderContext),
- child: Text.rich(
- textSpan,
- style: style.generateTextStyle(),
- textAlign: style.textAlign,
- textDirection: style.direction,
- textScaleFactor: textScaleFactor,
- maxLines: style.maxLines,
- overflow: style.textOverflow,
- ),
- );
- }
-
- double? consumeExpandedBlock(Display? display, RenderContext context) {
- if ((display == Display.BLOCK || display == Display.LIST_ITEM) && !renderContext.parser.shrinkWrap) {
- return double.infinity;
- }
- return null;
- }
-}
-
extension IterateLetters on String {
String nextLetter() {
- String s = this.toLowerCase();
+ String s = toLowerCase();
if (s == "z") {
- return String.fromCharCode(s.codeUnitAt(0) - 25) + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa
+ return String.fromCharCode(s.codeUnitAt(0) - 25) +
+ String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa
} else {
var lastChar = s.substring(s.length - 1);
var sub = s.substring(0, s.length - 1);
@@ -950,7 +1152,7 @@ extension IterateLetters on String {
// If a string of length > 1 ends in Z/z,
// increment the string (excluding the last Z/z) recursively,
// and append A/a (depending on casing) to it
- return sub.nextLetter() + 'a';
+ return '${sub.nextLetter()}a';
} else {
// (take till last char) append with (increment last char)
return sub + String.fromCharCode(lastChar.codeUnitAt(0) + 1);
@@ -958,3 +1160,10 @@ extension IterateLetters on String {
}
}
}
+
+class ParseResult {
+ final InlineSpan inlineSpan;
+ final Style style;
+
+ ParseResult(this.inlineSpan, this.style);
+}
diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart
index bdba172c97..2dbdd7a1bb 100644
--- a/lib/src/anchor.dart
+++ b/lib/src/anchor.dart
@@ -30,7 +30,10 @@ class AnchorKey extends GlobalKey {
@override
bool operator ==(Object other) =>
identical(this, other) ||
- other is AnchorKey && runtimeType == other.runtimeType && parentKey == other.parentKey && id == other.id;
+ other is AnchorKey &&
+ runtimeType == other.runtimeType &&
+ parentKey == other.parentKey &&
+ id == other.id;
@override
int get hashCode => parentKey.hashCode ^ id.hashCode;
diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart
new file mode 100644
index 0000000000..4d2158a134
--- /dev/null
+++ b/lib/src/css_box_widget.dart
@@ -0,0 +1,795 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_html/flutter_html.dart';
+
+class CssBoxWidget extends StatelessWidget {
+ const CssBoxWidget({
+ super.key,
+ required this.child,
+ required this.style,
+ this.textScaleFactor,
+ this.textDirection,
+ this.childIsReplaced = false,
+ this.shrinkWrap = false,
+ });
+
+ /// Generates a CSSBoxWidget that contains a list of InlineSpan children.
+ CssBoxWidget.withInlineSpanChildren({
+ super.key,
+ required BuildContext context,
+ required List children,
+ required this.style,
+ this.textDirection,
+ this.childIsReplaced = false,
+ this.shrinkWrap = false,
+ this.textScaleFactor,
+ bool selectable = false,
+ TextSelectionControls? selectionControls,
+ ScrollPhysics? scrollPhysics,
+ }) : child = selectable
+ ? _generateSelectableWidgetChild(
+ context,
+ children,
+ style,
+ textScaleFactor,
+ selectionControls,
+ scrollPhysics,
+ )
+ : _generateWidgetChild(context, children, style, textScaleFactor);
+
+ /// The child to be rendered within the CSS Box.
+ final Widget child;
+
+ /// The [textScaleFactor] variable stores a value representing the text scale factor. This overrides the system's default text scale factor and is used to scale the text size
+ /// If this variable is not set, the application will use the default text scale factor provided by [MediaQuery.textScaleFactorOf].
+ final double? textScaleFactor;
+
+ /// The style to use to compute this box's margins/padding/box decoration/width/height/etc.
+ ///
+ /// Note that this style will only apply to this box, and will not cascade to its child.
+ final Style style;
+
+ /// Sets the direction the text of this widget should flow. If unset or null,
+ /// the nearest Directionality ancestor is used as a default. If that cannot
+ /// be found, this Widget's renderer will raise an assertion.
+ final TextDirection? textDirection;
+
+ /// Indicates whether this child is a replaced element that manages its own width
+ /// (e.g. img, video, iframe, audio, etc.)
+ final bool childIsReplaced;
+
+ /// Whether or not the content should ignore auto horizontal margins and not
+ /// necessarily take up the full available width unless necessary
+ final bool shrinkWrap;
+
+ @override
+ Widget build(BuildContext context) {
+ final markerBox = style.listStylePosition == ListStylePosition.outside
+ ? _generateMarkerBoxSpan(context, style, textScaleFactor)
+ : null;
+
+ return _CSSBoxRenderer(
+ width: style.width ?? Width.auto(),
+ height: style.height ?? Height.auto(),
+ paddingSize: style.padding?.collapsedSize ?? Size.zero,
+ borderSize: style.border?.dimensions.collapsedSize ?? Size.zero,
+ margins: style.margin ?? Margins.zero,
+ display: style.display ?? Display.inline,
+ childIsReplaced: childIsReplaced,
+ emValue: _calculateEmValue(style, context),
+ textDirection: _checkTextDirection(context, textDirection),
+ shrinkWrap: shrinkWrap,
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ border: style.border,
+ color: style.backgroundColor, //Colors the padding and content boxes
+ ),
+ width: _shouldExpandToFillBlock() ? double.infinity : null,
+ padding: style.padding ?? EdgeInsets.zero,
+ child: child,
+ ),
+ if (markerBox != null)
+ Text.rich(markerBox, textScaleFactor: textScaleFactor),
+ ],
+ );
+ }
+
+ /// Takes a list of InlineSpan children and generates a Text.rich Widget
+ /// containing those children.
+ static Widget _generateWidgetChild(BuildContext context,
+ List children, Style style, double? textScaleFactor) {
+ if (children.isEmpty) {
+ return Container();
+ }
+
+ // Generate an inline marker box if the list-style-position is set to
+ // inside. Otherwise the marker box will be added elsewhere.
+ if (style.listStylePosition == ListStylePosition.inside) {
+ final inlineMarkerBox =
+ _generateMarkerBoxSpan(context, style, textScaleFactor);
+ if (inlineMarkerBox != null) {
+ children.insert(0, inlineMarkerBox);
+ }
+ }
+
+ return RichText(
+ text: TextSpan(
+ style: style.generateTextStyle(context),
+ children: children,
+ ),
+ textScaleFactor: textScaleFactor ?? 1.0,
+ textAlign: style.textAlign ?? TextAlign.start,
+ textDirection: style.direction,
+ maxLines: style.maxLines,
+ overflow: style.textOverflow ?? TextOverflow.clip,
+ );
+ }
+
+ static Widget _generateSelectableWidgetChild(
+ BuildContext context,
+ List children,
+ Style style,
+ double? textScaleFactor,
+ TextSelectionControls? selectionControls,
+ ScrollPhysics? scrollPhysics,
+ ) {
+ if (children.isEmpty) {
+ return Container();
+ }
+
+ return SelectableText.rich(
+ TextSpan(
+ style: style.generateTextStyle(context),
+ children: children,
+ ),
+ textScaleFactor: textScaleFactor,
+ style: style.generateTextStyle(context),
+ textAlign: style.textAlign,
+ textDirection: style.direction,
+ maxLines: style.maxLines,
+ selectionControls: selectionControls,
+ scrollPhysics: scrollPhysics,
+ );
+ }
+
+ static InlineSpan? _generateMarkerBoxSpan(
+ BuildContext context, Style style, double? textScaleFactor) {
+ if (style.display == Display.listItem) {
+ // First handle listStyleImage
+ if (style.listStyleImage != null) {
+ return WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Image.network(
+ style.listStyleImage!.uriText,
+ errorBuilder: (_, __, ___) {
+ if (style.marker?.content.replacementContent?.isNotEmpty ??
+ false) {
+ return Text.rich(
+ textScaleFactor: textScaleFactor,
+ TextSpan(
+ text: style.marker!.content.replacementContent!,
+ style: style.marker!.style?.generateTextStyle(context),
+ ),
+ );
+ }
+
+ return Container();
+ },
+ ),
+ );
+ }
+
+ // Display list marker with given style
+ if (style.marker?.content.replacementContent?.isNotEmpty ?? false) {
+ return TextSpan(
+ text: style.marker!.content.replacementContent!,
+ style: style.marker!.style?.generateTextStyle(context),
+ );
+ }
+ }
+
+ return null;
+ }
+
+ /// Whether or not the content-box should expand its width to fill the
+ /// width available to it or if it should just let its inner content
+ /// determine the content-box's width.
+ bool _shouldExpandToFillBlock() {
+ return (style.display == Display.block ||
+ style.display == Display.listItem) &&
+ !childIsReplaced &&
+ !shrinkWrap;
+ }
+
+ TextDirection _checkTextDirection(
+ BuildContext context, TextDirection? direction) {
+ final textDirection = direction ?? Directionality.maybeOf(context);
+
+ assert(
+ textDirection != null,
+ "CSSBoxWidget needs either a Directionality ancestor or a provided textDirection",
+ );
+
+ return textDirection!;
+ }
+}
+
+class _CSSBoxRenderer extends MultiChildRenderObjectWidget {
+ _CSSBoxRenderer({
+ Key? key,
+ required super.children,
+ required this.display,
+ required this.margins,
+ required this.width,
+ required this.height,
+ required this.borderSize,
+ required this.paddingSize,
+ required this.textDirection,
+ required this.childIsReplaced,
+ required this.emValue,
+ required this.shrinkWrap,
+ }) : super(key: key);
+
+ /// The Display type of the element
+ final Display display;
+
+ /// The computed margin values for this element
+ final Margins margins;
+
+ /// The width of the element
+ final Width width;
+
+ /// The height of the element
+ final Height height;
+
+ /// The collapsed size of the element's border
+ final Size borderSize;
+
+ /// The collapsed size of the element's padding
+ final Size paddingSize;
+
+ /// The direction for this widget's text to flow.
+ final TextDirection textDirection;
+
+ /// Whether or not the child being rendered is a replaced element
+ /// (this changes the rules for rendering)
+ final bool childIsReplaced;
+
+ /// The calculated size of 1em in pixels
+ final double emValue;
+
+ /// Whether or not this container should shrinkWrap its contents.
+ /// (see definition on [CSSBoxWidget])
+ final bool shrinkWrap;
+
+ @override
+ _RenderCSSBox createRenderObject(BuildContext context) {
+ return _RenderCSSBox(
+ display: display,
+ width: width..normalize(emValue),
+ height: height..normalize(emValue),
+ margins: _preProcessMargins(margins, shrinkWrap),
+ borderSize: borderSize,
+ paddingSize: paddingSize,
+ textDirection: textDirection,
+ childIsReplaced: childIsReplaced,
+ shrinkWrap: shrinkWrap,
+ );
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, _RenderCSSBox renderObject) {
+ renderObject
+ ..display = display
+ ..width = (width..normalize(emValue))
+ ..height = (height..normalize(emValue))
+ ..margins = _preProcessMargins(margins, shrinkWrap)
+ ..borderSize = borderSize
+ ..paddingSize = paddingSize
+ ..textDirection = textDirection
+ ..childIsReplaced = childIsReplaced
+ ..shrinkWrap = shrinkWrap;
+ }
+
+ Margins _preProcessMargins(Margins margins, bool shrinkWrap) {
+ Margin leftMargin = margins.left ?? Margin.zero();
+ Margin rightMargin = margins.right ?? Margin.zero();
+ Margin topMargin = margins.top ?? Margin.zero();
+ Margin bottomMargin = margins.bottom ?? Margin.zero();
+
+ //Preprocess margins to a pixel value
+ leftMargin.normalize(emValue);
+ rightMargin.normalize(emValue);
+ topMargin.normalize(emValue);
+ bottomMargin.normalize(emValue);
+
+ // See https://drafts.csswg.org/css2/#inline-width
+ // and https://drafts.csswg.org/css2/#inline-replaced-width
+ // and https://drafts.csswg.org/css2/#inlineblock-width
+ // and https://drafts.csswg.org/css2/#inlineblock-replaced-width
+ if (display == Display.inline || display == Display.inlineBlock) {
+ if (margins.left?.unit == Unit.auto) {
+ leftMargin = Margin.zero();
+ }
+ if (margins.right?.unit == Unit.auto) {
+ rightMargin = Margin.zero();
+ }
+ }
+
+ //Shrink-wrap margins if applicable
+ if (shrinkWrap && leftMargin.unit == Unit.auto) {
+ leftMargin = Margin.zero();
+ }
+
+ if (shrinkWrap && rightMargin.unit == Unit.auto) {
+ rightMargin = Margin.zero();
+ }
+
+ return Margins(
+ top: topMargin,
+ right: rightMargin,
+ bottom: bottomMargin,
+ left: leftMargin,
+ );
+ }
+}
+
+/// Implements the CSS layout algorithm
+class _RenderCSSBox extends RenderBox
+ with
+ ContainerRenderObjectMixin,
+ RenderBoxContainerDefaultsMixin {
+ _RenderCSSBox({
+ required Display display,
+ required Width width,
+ required Height height,
+ required Margins margins,
+ required Size borderSize,
+ required Size paddingSize,
+ required TextDirection textDirection,
+ required bool childIsReplaced,
+ required bool shrinkWrap,
+ }) : _display = display,
+ _width = width,
+ _height = height,
+ _margins = margins,
+ _borderSize = borderSize,
+ _paddingSize = paddingSize,
+ _textDirection = textDirection,
+ _childIsReplaced = childIsReplaced,
+ _shrinkWrap = shrinkWrap;
+
+ Display _display;
+
+ Display get display => _display;
+
+ set display(Display display) {
+ _display = display;
+ markNeedsLayout();
+ }
+
+ Width _width;
+
+ Width get width => _width;
+
+ set width(Width width) {
+ _width = width;
+ markNeedsLayout();
+ }
+
+ Height _height;
+
+ Height get height => _height;
+
+ set height(Height height) {
+ _height = height;
+ markNeedsLayout();
+ }
+
+ Margins _margins;
+
+ Margins get margins => _margins;
+
+ set margins(Margins margins) {
+ _margins = margins;
+ markNeedsLayout();
+ }
+
+ Size _borderSize;
+
+ Size get borderSize => _borderSize;
+
+ set borderSize(Size size) {
+ _borderSize = size;
+ markNeedsLayout();
+ }
+
+ Size _paddingSize;
+
+ Size get paddingSize => _paddingSize;
+
+ set paddingSize(Size size) {
+ _paddingSize = size;
+ markNeedsLayout();
+ }
+
+ TextDirection _textDirection;
+
+ TextDirection get textDirection => _textDirection;
+
+ set textDirection(TextDirection textDirection) {
+ _textDirection = textDirection;
+ markNeedsLayout();
+ }
+
+ bool _childIsReplaced;
+
+ bool get childIsReplaced => _childIsReplaced;
+
+ set childIsReplaced(bool childIsReplaced) {
+ _childIsReplaced = childIsReplaced;
+ markNeedsLayout();
+ }
+
+ bool _shrinkWrap;
+
+ bool get shrinkWrap => _shrinkWrap;
+
+ set shrinkWrap(bool shrinkWrap) {
+ _shrinkWrap = shrinkWrap;
+ markNeedsLayout();
+ }
+
+ @override
+ void setupParentData(RenderBox child) {
+ if (child.parentData is! CSSBoxParentData) {
+ child.parentData = CSSBoxParentData();
+ }
+ }
+
+ static double getIntrinsicDimension(RenderBox? firstChild,
+ double Function(RenderBox child) mainChildSizeGetter) {
+ double extent = 0.0;
+ RenderBox? child = firstChild;
+ while (child != null) {
+ final CSSBoxParentData childParentData =
+ child.parentData! as CSSBoxParentData;
+ extent = math.max(extent, mainChildSizeGetter(child));
+ assert(child.parentData == childParentData);
+ child = childParentData.nextSibling;
+ }
+ return extent;
+ }
+
+ @override
+ double computeMinIntrinsicWidth(double height) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
+ }
+
+ @override
+ double computeMaxIntrinsicWidth(double height) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
+ }
+
+ @override
+ double computeMinIntrinsicHeight(double width) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
+ }
+
+ @override
+ double computeMaxIntrinsicHeight(double width) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
+ }
+
+ @override
+ double? computeDistanceToActualBaseline(TextBaseline baseline) {
+ return firstChild?.getDistanceToActualBaseline(baseline);
+ }
+
+ @override
+ Size computeDryLayout(BoxConstraints constraints) {
+ return _computeSize(
+ constraints: constraints,
+ layoutChild: ChildLayoutHelper.dryLayoutChild,
+ ).parentSize;
+ }
+
+ _Sizes _computeSize(
+ {required BoxConstraints constraints,
+ required ChildLayouter layoutChild}) {
+ if (childCount == 0) {
+ return _Sizes(constraints.biggest, Size.zero);
+ }
+
+ Size containingBlockSize = constraints.biggest;
+ double width = containingBlockSize.width;
+ double height = containingBlockSize.height;
+
+ assert(firstChild != null);
+ RenderBox child = firstChild!;
+
+ final CSSBoxParentData parentData = child.parentData! as CSSBoxParentData;
+ RenderBox? markerBoxChild = parentData.nextSibling;
+
+ // Calculate child size
+ final childConstraints = constraints.copyWith(
+ maxWidth: (this.width.unit != Unit.auto)
+ ? this.width.value
+ : containingBlockSize.width -
+ (margins.left?.value ?? 0) -
+ (margins.right?.value ?? 0),
+ maxHeight: (this.height.unit != Unit.auto)
+ ? this.height.value
+ : containingBlockSize.height -
+ (margins.top?.value ?? 0) -
+ (margins.bottom?.value ?? 0),
+ minWidth: (this.width.unit != Unit.auto) ? this.width.value : 0,
+ minHeight: (this.height.unit != Unit.auto) ? this.height.value : 0,
+ );
+ final Size childSize = layoutChild(child, childConstraints);
+ if (markerBoxChild != null) {
+ layoutChild(markerBoxChild, childConstraints);
+ }
+
+ // Calculate used values of margins based on rules
+ final usedMargins = _calculateUsedMargins(childSize, containingBlockSize);
+ final horizontalMargins =
+ (usedMargins.left?.value ?? 0) + (usedMargins.right?.value ?? 0);
+ final verticalMargins =
+ (usedMargins.top?.value ?? 0) + (usedMargins.bottom?.value ?? 0);
+
+ //Calculate Width and Height of CSS Box
+ height = childSize.height;
+ switch (display) {
+ case Display.block:
+ width = (shrinkWrap || childIsReplaced)
+ ? childSize.width + horizontalMargins
+ : containingBlockSize.width;
+ height = childSize.height + verticalMargins;
+ break;
+ case Display.inline:
+ width = childSize.width + horizontalMargins;
+ height = childSize.height;
+ break;
+ case Display.inlineBlock:
+ width = childSize.width + horizontalMargins;
+ height = childSize.height + verticalMargins;
+ break;
+ case Display.listItem:
+ width = shrinkWrap
+ ? childSize.width + horizontalMargins
+ : containingBlockSize.width;
+ height = childSize.height + verticalMargins;
+ break;
+ case Display.none:
+ width = 0;
+ height = 0;
+ break;
+ }
+
+ return _Sizes(constraints.constrain(Size(width, height)), childSize);
+ }
+
+ @override
+ void performLayout() {
+ final BoxConstraints constraints = this.constraints;
+
+ final sizes = _computeSize(
+ constraints: constraints,
+ layoutChild: ChildLayoutHelper.layoutChild,
+ );
+ size = sizes.parentSize;
+
+ assert(firstChild != null);
+ RenderBox child = firstChild!;
+
+ final CSSBoxParentData childParentData =
+ child.parentData! as CSSBoxParentData;
+
+ // Calculate used margins based on constraints and child size
+ final usedMargins =
+ _calculateUsedMargins(sizes.childSize, constraints.biggest);
+ final leftMargin = usedMargins.left?.value ?? 0;
+ final topMargin = usedMargins.top?.value ?? 0;
+
+ double leftOffset = 0;
+ double topOffset = 0;
+ switch (display) {
+ case Display.block:
+ leftOffset = leftMargin;
+ topOffset = topMargin;
+ break;
+ case Display.inline:
+ leftOffset = leftMargin;
+ break;
+ case Display.inlineBlock:
+ leftOffset = leftMargin;
+ topOffset = topMargin;
+ break;
+ case Display.listItem:
+ leftOffset = leftMargin;
+ topOffset = topMargin;
+ break;
+ case Display.none:
+ //No offset
+ break;
+ }
+ childParentData.offset = Offset(leftOffset, topOffset);
+ assert(child.parentData == childParentData);
+
+ // Now, layout the marker box if it exists:
+ RenderBox? markerBox = childParentData.nextSibling;
+ if (markerBox != null) {
+ final markerBoxParentData = markerBox.parentData! as CSSBoxParentData;
+ final distance = (child.getDistanceToBaseline(TextBaseline.alphabetic,
+ onlyReal: true) ??
+ 0) +
+ topOffset;
+ final offsetHeight = distance -
+ (markerBox.getDistanceToBaseline(TextBaseline.alphabetic) ??
+ markerBox.size.height);
+ markerBoxParentData.offset = Offset(-markerBox.size.width, offsetHeight);
+ }
+ }
+
+ Margins _calculateUsedMargins(Size childSize, Size containingBlockSize) {
+ //We assume that margins have already been preprocessed
+ // (i.e. they are non-null and either px units or auto.
+ assert(margins.left != null && margins.right != null);
+ assert(margins.left!.unit == Unit.px || margins.left!.unit == Unit.auto);
+ assert(margins.right!.unit == Unit.px || margins.right!.unit == Unit.auto);
+
+ Margin marginLeft = margins.left!;
+ Margin marginRight = margins.right!;
+
+ bool widthIsAuto = width.unit == Unit.auto;
+ bool marginLeftIsAuto = marginLeft.unit == Unit.auto;
+ bool marginRightIsAuto = marginRight.unit == Unit.auto;
+
+ if (display == Display.block) {
+ if (childIsReplaced) {
+ widthIsAuto = false;
+ }
+
+ if (shrinkWrap) {
+ widthIsAuto = false;
+ }
+
+ //If width is not auto and the width of the margin box is larger than the
+ // width of the containing block, then consider left and right margins to
+ // have a 0 value.
+ if (!widthIsAuto) {
+ if ((childSize.width + marginLeft.value + marginRight.value) >
+ containingBlockSize.width) {
+ //Treat auto values of margin left and margin right as 0 for following rules
+ marginLeft = Margin(0);
+ marginRight = Margin(0);
+ marginLeftIsAuto = false;
+ marginRightIsAuto = false;
+ }
+ }
+
+ // If all values are non-auto, the box is overconstrained.
+ // One of the margins will need to be adjusted so that the
+ // entire width of the containing block is used.
+ if (!widthIsAuto &&
+ !marginLeftIsAuto &&
+ !marginRightIsAuto &&
+ !shrinkWrap &&
+ !childIsReplaced) {
+ //Ignore either left or right margin based on textDirection.
+
+ switch (textDirection) {
+ case TextDirection.rtl:
+ final difference =
+ containingBlockSize.width - childSize.width - marginRight.value;
+ marginLeft = Margin(difference);
+ break;
+ case TextDirection.ltr:
+ final difference =
+ containingBlockSize.width - childSize.width - marginLeft.value;
+ marginRight = Margin(difference);
+ break;
+ }
+ }
+
+ // If there is exactly one value specified as auto, compute it value from the equality (our widths are already set)
+ if (widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto) {
+ widthIsAuto = false;
+ } else if (!widthIsAuto && marginLeftIsAuto && !marginRightIsAuto) {
+ marginLeft = Margin(
+ containingBlockSize.width - childSize.width - marginRight.value);
+ marginLeftIsAuto = false;
+ } else if (!widthIsAuto && !marginLeftIsAuto && marginRightIsAuto) {
+ marginRight = Margin(
+ containingBlockSize.width - childSize.width - marginLeft.value);
+ marginRightIsAuto = false;
+ }
+
+ //If width is set to auto, any other auto values become 0, and width
+ // follows from the resulting equality.
+ if (widthIsAuto) {
+ if (marginLeftIsAuto) {
+ marginLeft = Margin(0);
+ marginLeftIsAuto = false;
+ }
+ if (marginRightIsAuto) {
+ marginRight = Margin(0);
+ marginRightIsAuto = false;
+ }
+ widthIsAuto = false;
+ }
+
+ //If both margin-left and margin-right are auto, their used values are equal.
+ // This horizontally centers the element within the containing block.
+ if (marginLeftIsAuto && marginRightIsAuto) {
+ final newMargin =
+ Margin((containingBlockSize.width - childSize.width) / 2);
+ marginLeft = newMargin;
+ marginRight = newMargin;
+ marginLeftIsAuto = false;
+ marginRightIsAuto = false;
+ }
+
+ //Assert that all auto values have been assigned.
+ assert(!marginLeftIsAuto && !marginRightIsAuto && !widthIsAuto);
+ }
+
+ return Margins(
+ left: marginLeft,
+ right: marginRight,
+ top: margins.top,
+ bottom: margins.bottom);
+ }
+
+ @override
+ bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+ return defaultHitTestChildren(result, position: position);
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ defaultPaint(context, offset);
+ }
+}
+
+extension Normalize on Dimension {
+ void normalize(double emValue) {
+ switch (unit) {
+ case Unit.rem:
+ // Because CSSBoxWidget doesn't have any information about any
+ // sort of tree structure, treat rem the same as em. The HtmlParser
+ // widget handles rem/em values before they get to CSSBoxWidget.
+ case Unit.em:
+ value *= emValue;
+ unit = Unit.px;
+ return;
+ case Unit.px:
+ case Unit.auto:
+ case Unit.percent:
+ return;
+ }
+ }
+}
+
+double _calculateEmValue(Style style, BuildContext buildContext) {
+ return (style.fontSize?.emValue ?? 16) *
+ MediaQuery.textScaleFactorOf(buildContext) *
+ MediaQuery.of(buildContext).devicePixelRatio;
+}
+
+class CSSBoxParentData extends ContainerBoxParentData {}
+
+class _Sizes {
+ final Size parentSize;
+ final Size childSize;
+
+ const _Sizes(this.parentSize, this.childSize);
+}
diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart
index 1f5bbd90ed..28ef698e17 100644
--- a/lib/src/css_parser.dart
+++ b/lib/src/css_parser.dart
@@ -8,58 +8,110 @@ import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/src/utils.dart';
Style declarationsToStyle(Map> declarations) {
- Style style = new Style();
+ Style style = Style();
declarations.forEach((property, value) {
if (value.isNotEmpty) {
switch (property) {
case 'background-color':
- style.backgroundColor = ExpressionMapping.expressionToColor(value.first) ?? style.backgroundColor;
+ style.backgroundColor =
+ ExpressionMapping.expressionToColor(value.first) ??
+ style.backgroundColor;
break;
case 'border':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- List? borderColors = value.where((element) => ExpressionMapping.expressionToColor(element) != null).toList();
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ element is! css.LengthTerm &&
+ element is! css.PercentageTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm));
+ List? borderColors = value
+ .where((element) =>
+ ExpressionMapping.expressionToColor(element) != null)
+ .toList();
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
List? borderStyles = potentialStyles;
- style.border = ExpressionMapping.expressionToBorder(borderWidths, borderStyles, borderColors);
+ style.border = ExpressionMapping.expressionToBorder(
+ borderWidths, borderStyles, borderColors);
break;
case 'border-left':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ element is! css.LengthTerm &&
+ element is! css.PercentageTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
right: style.border?.right ?? BorderSide.none,
top: style.border?.top ?? BorderSide.none,
bottom: style.border?.bottom ?? BorderSide.none,
@@ -67,217 +119,339 @@ Style declarationsToStyle(Map> declarations) {
style.border = newBorder;
break;
case 'border-right':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ element is! css.LengthTerm &&
+ element is! css.PercentageTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
top: style.border?.top ?? BorderSide.none,
bottom: style.border?.bottom ?? BorderSide.none,
);
style.border = newBorder;
break;
case 'border-top':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ element is! css.LengthTerm &&
+ element is! css.PercentageTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right ?? BorderSide.none,
top: style.border?.top.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
bottom: style.border?.bottom ?? BorderSide.none,
);
style.border = newBorder;
break;
case 'border-bottom':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ element is! css.LengthTerm &&
+ element is! css.PercentageTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right ?? BorderSide.none,
top: style.border?.top ?? BorderSide.none,
bottom: style.border?.bottom.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
);
style.border = newBorder;
break;
case 'color':
- style.color = ExpressionMapping.expressionToColor(value.first) ?? style.color;
+ style.color =
+ ExpressionMapping.expressionToColor(value.first) ?? style.color;
break;
case 'direction':
- style.direction = ExpressionMapping.expressionToDirection(value.first);
+ 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);
+ style.lineHeight =
+ ExpressionMapping.expressionToLineHeight(value.first);
break;
case 'font-family':
- style.fontFamily = ExpressionMapping.expressionToFontFamily(value.first) ?? style.fontFamily;
+ style.fontFamily =
+ ExpressionMapping.expressionToFontFamily(value.first) ??
+ style.fontFamily;
break;
case 'font-feature-settings':
- style.fontFeatureSettings = ExpressionMapping.expressionToFontFeatureSettings(value);
+ style.fontFeatureSettings =
+ ExpressionMapping.expressionToFontFeatureSettings(value);
break;
case 'font-size':
- style.fontSize = ExpressionMapping.expressionToFontSize(value.first) ?? style.fontSize;
+ style.fontSize =
+ ExpressionMapping.expressionToFontSize(value.first) ??
+ style.fontSize;
break;
case 'font-style':
- style.fontStyle = ExpressionMapping.expressionToFontStyle(value.first);
+ style.fontStyle =
+ ExpressionMapping.expressionToFontStyle(value.first);
break;
case 'font-weight':
- style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
+ style.fontWeight =
+ ExpressionMapping.expressionToFontWeight(value.first);
+ break;
+ case '-epub-text-align-last':
+ if (value.first is css.Identifier) {
+ css.Identifier identifier = value.first as css.Identifier;
+ switch (identifier.name) {
+ case 'left':
+ style.alignment = Alignment.centerLeft;
+ break;
+ case 'right':
+ style.alignment = Alignment.centerRight;
+ break;
+ case 'center':
+ style.alignment = Alignment.center;
+ break;
+ }
+ }
break;
case 'list-style':
- css.LiteralTerm? position = value.firstWhereOrNull((e) => e is css.LiteralTerm && (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?;
- css.UriTerm? image = value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?;
- css.LiteralTerm? type = value.firstWhereOrNull((e) => e is css.LiteralTerm && e.text != "outside" && e.text != "inside") as css.LiteralTerm?;
+ css.LiteralTerm? position = value.firstWhereOrNull((e) =>
+ e is css.LiteralTerm &&
+ (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?;
+ css.UriTerm? image =
+ value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?;
+ css.LiteralTerm? type = value.firstWhereOrNull((e) =>
+ e is css.LiteralTerm &&
+ e.text != "outside" &&
+ e.text != "inside") as css.LiteralTerm?;
if (position != null) {
switch (position.text) {
case 'outside':
- style.listStylePosition = ListStylePosition.OUTSIDE;
+ style.listStylePosition = ListStylePosition.outside;
break;
case 'inside':
- style.listStylePosition = ListStylePosition.INSIDE;
+ style.listStylePosition = ListStylePosition.inside;
break;
}
}
if (image != null) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(image) ?? style.listStyleType;
+ style.listStyleImage =
+ ExpressionMapping.expressionToListStyleImage(image) ??
+ style.listStyleImage;
} else if (type != null) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(type) ?? style.listStyleType;
+ style.listStyleType =
+ ExpressionMapping.expressionToListStyleType(type) ??
+ style.listStyleType;
}
break;
case 'list-style-image':
if (value.first is css.UriTerm) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.UriTerm) ?? style.listStyleType;
+ style.listStyleImage = ExpressionMapping.expressionToListStyleImage(
+ value.first as css.UriTerm) ??
+ style.listStyleImage;
}
break;
case 'list-style-position':
if (value.first is css.LiteralTerm) {
switch ((value.first as css.LiteralTerm).text) {
case 'outside':
- style.listStylePosition = ListStylePosition.OUTSIDE;
+ style.listStylePosition = ListStylePosition.outside;
break;
case 'inside':
- style.listStylePosition = ListStylePosition.INSIDE;
+ style.listStylePosition = ListStylePosition.inside;
break;
}
}
break;
case 'height':
- style.height = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.height;
+ style.height =
+ ExpressionMapping.expressionToHeight(value.first) ?? style.height;
break;
case 'list-style-type':
if (value.first is css.LiteralTerm) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType;
+ style.listStyleType = ExpressionMapping.expressionToListStyleType(
+ value.first as css.LiteralTerm) ??
+ style.listStyleType;
}
break;
case 'margin':
- List? marginLengths = value.whereType().toList();
+ List? marginLengths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for margin length, so make sure to remove those before passing it to [ExpressionMapping]
- marginLengths.removeWhere((element) => !(element is css.LengthTerm)
- && !(element is css.EmTerm)
- && !(element is css.RemTerm)
- && !(element is css.NumberTerm)
- );
- List margin = ExpressionMapping.expressionToPadding(marginLengths);
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- left: margin[0],
- right: margin[1],
- top: margin[2],
- bottom: margin[3],
+ marginLengths.removeWhere((element) =>
+ element is! css.LengthTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm &&
+ !(element.text == 'auto'));
+ Margins margin = ExpressionMapping.expressionToMargins(marginLengths);
+ style.margin = (style.margin ?? Margins.all(0)).copyWith(
+ left: margin.left,
+ right: margin.right,
+ top: margin.top,
+ bottom: margin.bottom,
);
break;
case 'margin-left':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- left: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero).copyWith(
+ left: ExpressionMapping.expressionToMargin(value.first));
break;
case 'margin-right':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- right: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero).copyWith(
+ right: ExpressionMapping.expressionToMargin(value.first));
break;
case 'margin-top':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- top: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero)
+ .copyWith(top: ExpressionMapping.expressionToMargin(value.first));
break;
case 'margin-bottom':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- bottom: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero).copyWith(
+ bottom: ExpressionMapping.expressionToMargin(value.first));
break;
case 'padding':
- List? paddingLengths = value.whereType().toList();
+ List? paddingLengths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for padding length, so make sure to remove those before passing it to [ExpressionMapping]
- paddingLengths.removeWhere((element) => !(element is css.LengthTerm)
- && !(element is css.EmTerm)
- && !(element is css.RemTerm)
- && !(element is css.NumberTerm)
- );
- List padding = ExpressionMapping.expressionToPadding(paddingLengths);
+ paddingLengths.removeWhere((element) =>
+ element is! css.LengthTerm &&
+ element is! css.EmTerm &&
+ element is! css.RemTerm &&
+ element is! css.NumberTerm);
+ List padding =
+ ExpressionMapping.expressionToPadding(paddingLengths);
style.padding = (style.padding ?? EdgeInsets.zero).copyWith(
left: padding[0],
right: padding[1],
@@ -302,36 +476,67 @@ Style declarationsToStyle(Map> declarations) {
bottom: ExpressionMapping.expressionToPaddingLength(value.first));
break;
case 'text-align':
- style.textAlign = ExpressionMapping.expressionToTextAlign(value.first);
+ style.textAlign =
+ ExpressionMapping.expressionToTextAlign(value.first);
break;
case 'text-decoration':
- List? textDecorationList = value.whereType().toList();
+ List? textDecorationList =
+ value.whereType().toList();
+
/// List 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 == null || (element.text != "none"
- && element.text != "overline" && element.text != "underline" && element.text != "line-through"));
+ textDecorationList.removeWhere((element) =>
+ element == null ||
+ (element.text != "none" &&
+ element.text != "overline" &&
+ element.text != "underline" &&
+ element.text != "line-through"));
List? nullableList = value;
css.Expression? textDecorationColor;
- textDecorationColor = nullableList.firstWhereOrNull(
- (element) => element is css.HexColorTerm || element is css.FunctionTerm);
- List? potentialStyles = value.whereType().toList();
+ textDecorationColor = nullableList.firstWhereOrNull((element) =>
+ element is css.HexColorTerm || element is css.FunctionTerm);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [textDecorationStyle], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || (element.text != "solid"
- && element.text != "double" && element.text != "dashed" && element.text != "dotted" && element.text != "wavy"));
- css.LiteralTerm? textDecorationStyle = potentialStyles.isNotEmpty ? potentialStyles.last : null;
- style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList);
- if (textDecorationColor != null) style.textDecorationColor = ExpressionMapping.expressionToColor(textDecorationColor)
- ?? style.textDecorationColor;
- if (textDecorationStyle != null) style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(textDecorationStyle);
+ potentialStyles.removeWhere((element) =>
+ element == null ||
+ (element.text != "solid" &&
+ element.text != "double" &&
+ element.text != "dashed" &&
+ element.text != "dotted" &&
+ element.text != "wavy"));
+ css.LiteralTerm? textDecorationStyle =
+ potentialStyles.isNotEmpty ? potentialStyles.last : null;
+ style.textDecoration =
+ ExpressionMapping.expressionToTextDecorationLine(
+ textDecorationList);
+ if (textDecorationColor != null) {
+ style.textDecorationColor =
+ ExpressionMapping.expressionToColor(textDecorationColor) ??
+ style.textDecorationColor;
+ }
+ if (textDecorationStyle != null) {
+ style.textDecorationStyle =
+ ExpressionMapping.expressionToTextDecorationStyle(
+ textDecorationStyle);
+ }
break;
case 'text-decoration-color':
- style.textDecorationColor = ExpressionMapping.expressionToColor(value.first) ?? style.textDecorationColor;
+ style.textDecorationColor =
+ ExpressionMapping.expressionToColor(value.first) ??
+ style.textDecorationColor;
break;
case 'text-decoration-line':
- List? textDecorationList = value.whereType().toList();
- style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList);
+ List? textDecorationList =
+ value.whereType().toList();
+ style.textDecoration =
+ ExpressionMapping.expressionToTextDecorationLine(
+ textDecorationList);
break;
case 'text-decoration-style':
- style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(value.first as css.LiteralTerm);
+ style.textDecorationStyle =
+ ExpressionMapping.expressionToTextDecorationStyle(
+ value.first as css.LiteralTerm);
break;
case 'text-shadow':
style.textShadow = ExpressionMapping.expressionToTextShadow(value);
@@ -348,8 +553,13 @@ Style declarationsToStyle(Map> declarations) {
style.textTransform = TextTransform.none;
}
break;
+ case 'vertical-align':
+ style.verticalAlign = ExpressionMapping.expressionToVerticalAlignTerm(
+ (value.first as css.LiteralTerm));
+ break;
case 'width':
- style.width = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.width;
+ style.width =
+ ExpressionMapping.expressionToWidth(value.first) ?? style.width;
break;
}
}
@@ -372,7 +582,8 @@ Style? inlineCssToStyle(String? inlineStyle, OnCssParseError? errorHandler) {
return null;
}
-Map>> parseExternalCss(String css, OnCssParseError? errorHandler) {
+Map>> parseExternalCss(
+ String css, OnCssParseError? errorHandler) {
var errors = [];
final sheet = cssparser.parse(css, errors: errors);
if (errors.isEmpty) {
@@ -387,30 +598,33 @@ Map>> parseExternalCss(String css, OnCs
}
class DeclarationVisitor extends css.Visitor {
- Map>> _result = {};
- Map> _properties = {};
+ final Map>> _result = {};
+ final Map> _properties = {};
late String _selector;
late String _currentProperty;
- Map>> getDeclarations(css.StyleSheet sheet) {
- sheet.topLevels.forEach((element) {
+ Map>> getDeclarations(
+ css.StyleSheet sheet) {
+ for (var element in sheet.topLevels) {
if (element.span != null) {
_selector = element.span!.text;
element.visit(this);
if (_result[_selector] != null) {
_properties.forEach((key, value) {
if (_result[_selector]![key] != null) {
- _result[_selector]![key]!.addAll(new List.from(value));
+ _result[_selector]![key]!
+ .addAll(List.from(value));
} else {
- _result[_selector]![key] = new List.from(value);
+ _result[_selector]![key] = List.from(value);
}
});
} else {
- _result[_selector] = new Map>.from(_properties);
+ _result[_selector] =
+ Map>.from(_properties);
}
_properties.clear();
}
- });
+ }
return _result;
}
@@ -433,8 +647,12 @@ class DeclarationVisitor extends css.Visitor {
//Mapping functions
class ExpressionMapping {
+ static final _leadingOrTrailingQuoteRegexp = RegExp(r'^"|"$');
- static Border expressionToBorder(List? borderWidths, List? borderStyles, List? borderColors) {
+ static Border expressionToBorder(
+ List? borderWidths,
+ List? borderStyles,
+ List? borderColors) {
CustomBorderSide left = CustomBorderSide();
CustomBorderSide top = CustomBorderSide();
CustomBorderSide right = CustomBorderSide();
@@ -509,11 +727,22 @@ class ExpressionMapping {
}
}
return Border(
- top: BorderSide(width: top.width, color: top.color ?? Colors.black, style: top.style),
- right: BorderSide(width: right.width, color: right.color ?? Colors.black, style: right.style),
- bottom: BorderSide(width: bottom.width, color: bottom.color ?? Colors.black, style: bottom.style),
- left: BorderSide(width: left.width, color: left.color ?? Colors.black, style: left.style)
- );
+ top: BorderSide(
+ width: top.width,
+ color: top.color ?? Colors.black,
+ style: top.style),
+ right: BorderSide(
+ width: right.width,
+ color: right.color ?? Colors.black,
+ style: right.style),
+ bottom: BorderSide(
+ width: bottom.width,
+ color: bottom.color ?? Colors.black,
+ style: bottom.style),
+ left: BorderSide(
+ width: left.width,
+ color: left.color ?? Colors.black,
+ style: left.style));
}
static double expressionToBorderWidth(css.Expression? value) {
@@ -526,7 +755,9 @@ class ExpressionMapping {
} else if (value is css.RemTerm) {
return double.tryParse(value.text) ?? 1.0;
} else if (value is css.LengthTerm) {
- return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? 1.0;
+ return double.tryParse(
+ value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), '')) ??
+ 1.0;
} else if (value is css.LiteralTerm) {
switch (value.text) {
case "thin":
@@ -566,7 +797,7 @@ class ExpressionMapping {
static TextDirection expressionToDirection(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "ltr":
return TextDirection.ltr;
case "rtl":
@@ -578,32 +809,41 @@ class ExpressionMapping {
static Display expressionToDisplay(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case 'block':
- return Display.BLOCK;
+ return Display.block;
case 'inline-block':
- return Display.INLINE_BLOCK;
+ return Display.inlineBlock;
case 'inline':
- return Display.INLINE;
+ return Display.inline;
case 'list-item':
- return Display.LIST_ITEM;
+ return Display.listItem;
case 'none':
- return Display.NONE;
+ return Display.none;
}
}
- return Display.INLINE;
+ return Display.inline;
}
- static List expressionToFontFeatureSettings(List value) {
+ static List expressionToFontFeatureSettings(
+ List value) {
List 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 (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));
+ 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));
}
@@ -619,15 +859,17 @@ class ExpressionMapping {
static FontSize? expressionToFontSize(css.Expression value) {
if (value is css.NumberTerm) {
- return FontSize(double.tryParse(value.text));
+ return FontSize(double.tryParse(value.text) ?? 16, Unit.px);
} else if (value is css.PercentageTerm) {
- return FontSize.percent(double.tryParse(value.text)!);
+ return FontSize(double.tryParse(value.text) ?? 100, Unit.percent);
} 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)!);
+ return FontSize(double.tryParse(value.text) ?? 1, Unit.em);
+ // } else if (value is css.RemTerm) { TODO
+ // return FontSize.rem(double.tryParse(value.text) ?? 1, Unit.em);
} else if (value is css.LengthTerm) {
- return FontSize(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')));
+ return FontSize(double.tryParse(
+ value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), '')) ??
+ 16);
} else if (value is css.LiteralTerm) {
switch (value.text) {
case "xx-small":
@@ -651,7 +893,7 @@ class ExpressionMapping {
static FontStyle expressionToFontStyle(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "italic":
case "oblique":
return FontStyle.italic;
@@ -684,7 +926,7 @@ class ExpressionMapping {
return FontWeight.w900;
}
} else if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "bold":
return FontWeight.bold;
case "bolder":
@@ -698,8 +940,11 @@ class ExpressionMapping {
}
static String? expressionToFontFamily(css.Expression value) {
- if (value is css.LiteralTerm) return value.text;
- return null;
+ if (value is css.LiteralTerm) {
+ return value.text.replaceAll(_leadingOrTrailingQuoteRegexp, '');
+ } else {
+ return value.span?.text.replaceAll(_leadingOrTrailingQuoteRegexp, '');
+ }
}
static LineHeight expressionToLineHeight(css.Expression value) {
@@ -712,40 +957,78 @@ class ExpressionMapping {
} 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(
+ double.tryParse(
+ value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), '')),
+ units: "length");
}
return LineHeight.normal;
}
+ static ListStyleImage? expressionToListStyleImage(css.UriTerm value) {
+ return ListStyleImage(value.text);
+ }
+
static ListStyleType? expressionToListStyleType(css.LiteralTerm value) {
- if (value is css.UriTerm) {
- return ListStyleType.fromImage(value.text);
+ return ListStyleType.fromName(value.text);
+ }
+
+ static Width? expressionToWidth(css.Expression value) {
+ if ((value is css.LiteralTerm) && value.text == 'auto') {
+ return Width.auto();
+ } else {
+ final computedValue = expressionToLengthOrPercent(value);
+ return Width(computedValue.value, computedValue.unit);
}
- switch (value.text) {
- case 'disc':
- return ListStyleType.DISC;
- case 'circle':
- return ListStyleType.CIRCLE;
- case 'decimal':
- return ListStyleType.DECIMAL;
- case 'lower-alpha':
- return ListStyleType.LOWER_ALPHA;
- case 'lower-latin':
- return ListStyleType.LOWER_LATIN;
- case 'lower-roman':
- return ListStyleType.LOWER_ROMAN;
- case 'square':
- return ListStyleType.SQUARE;
- case 'upper-alpha':
- return ListStyleType.UPPER_ALPHA;
- case 'upper-latin':
- return ListStyleType.UPPER_LATIN;
- case 'upper-roman':
- return ListStyleType.UPPER_ROMAN;
- case 'none':
- return ListStyleType.NONE;
+ }
+
+ static Height? expressionToHeight(css.Expression value) {
+ if ((value is css.LiteralTerm) && value.text == 'auto') {
+ return Height.auto();
+ } else {
+ final computedValue = expressionToLengthOrPercent(value);
+ return Height(computedValue.value, computedValue.unit);
}
- return null;
+ }
+
+ static Margin? expressionToMargin(css.Expression value) {
+ if ((value is css.LiteralTerm) && value.text == 'auto') {
+ return Margin.auto();
+ } else {
+ final computedValue = expressionToLengthOrPercent(value);
+ return Margin(computedValue.value, computedValue.unit);
+ }
+ }
+
+ static Margins expressionToMargins(List? lengths) {
+ Margin? left;
+ Margin? right;
+ Margin? top;
+ Margin? bottom;
+ if (lengths != null && lengths.isNotEmpty) {
+ top = expressionToMargin(lengths.first);
+ if (lengths.length == 4) {
+ right = expressionToMargin(lengths[1]);
+ bottom = expressionToMargin(lengths[2]);
+ left = expressionToMargin(lengths.last);
+ }
+ if (lengths.length == 3) {
+ left = expressionToMargin(lengths[1]);
+ right = expressionToMargin(lengths[1]);
+ bottom = expressionToMargin(lengths.last);
+ }
+ if (lengths.length == 2) {
+ bottom = expressionToMargin(lengths.first);
+ left = expressionToMargin(lengths.last);
+ right = expressionToMargin(lengths.last);
+ }
+ if (lengths.length == 1) {
+ bottom = expressionToMargin(lengths.first);
+ left = expressionToMargin(lengths.first);
+ right = expressionToMargin(lengths.first);
+ }
+ }
+ return Margins(left: left, right: right, top: top, bottom: bottom);
}
static List expressionToPadding(List? lengths) {
@@ -787,14 +1070,41 @@ class ExpressionMapping {
} else if (value is css.RemTerm) {
return double.tryParse(value.text);
} else if (value is css.LengthTerm) {
- return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), ''));
+ return double.tryParse(
+ value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), ''));
}
return null;
}
+ static LengthOrPercent expressionToLengthOrPercent(css.Expression value) {
+ if (value is css.NumberTerm) {
+ return LengthOrPercent(double.parse(value.text));
+ } else if (value is css.EmTerm) {
+ return LengthOrPercent(double.parse(value.text), Unit.em);
+ // } else if (value is css.RemTerm) {
+ // return LengthOrPercent(double.parse(value.text), Unit.rem);
+ // TODO there are several other available terms processed by the CSS parser
+ } else if (value is css.LengthTerm) {
+ double number =
+ double.parse(value.text.replaceAll(RegExp(r'\s+(\d+\.\d+)\s+'), ''));
+ Unit unit = _unitMap(value.unit);
+ return LengthOrPercent(number, unit);
+ }
+
+ //Ignore unparsable input
+ return LengthOrPercent(0);
+ }
+
+ static Unit _unitMap(int cssParserUnitToken) {
+ switch (cssParserUnitToken) {
+ default:
+ return Unit.px;
+ }
+ }
+
static TextAlign expressionToTextAlign(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "center":
return TextAlign.center;
case "left":
@@ -812,11 +1122,12 @@ class ExpressionMapping {
return TextAlign.start;
}
- static TextDecoration expressionToTextDecorationLine(List value) {
+ static TextDecoration expressionToTextDecorationLine(
+ List value) {
List decorationList = [];
for (css.LiteralTerm? term in value) {
if (term != null) {
- switch(term.text) {
+ switch (term.text) {
case "overline":
decorationList.add(TextDecoration.overline);
break;
@@ -832,12 +1143,15 @@ class ExpressionMapping {
}
}
}
- if (decorationList.contains(TextDecoration.none)) decorationList = [TextDecoration.none];
+ if (decorationList.contains(TextDecoration.none)) {
+ decorationList = [TextDecoration.none];
+ }
return TextDecoration.combine(decorationList);
}
- static TextDecorationStyle expressionToTextDecorationStyle(css.LiteralTerm value) {
- switch(value.text) {
+ static TextDecorationStyle expressionToTextDecorationStyle(
+ css.LiteralTerm value) {
+ switch (value.text) {
case "wavy":
return TextDecorationStyle.wavy;
case "dotted":
@@ -870,10 +1184,10 @@ class ExpressionMapping {
css.Expression? offsetX;
css.Expression? offsetY;
css.Expression? blurRadius;
- css.HexColorTerm? color;
+ css.Expression? color;
int expressionIndex = 0;
- list.forEach((element) {
- if (element is css.HexColorTerm) {
+ for (var element in list) {
+ if (element is css.HexColorTerm || element is css.FunctionTerm) {
color = element;
} else if (expressionIndex == 0) {
offsetX = element;
@@ -884,23 +1198,32 @@ class ExpressionMapping {
} else {
blurRadius = element;
}
- });
+ }
RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+');
if (offsetX is css.LiteralTerm && offsetY is css.LiteralTerm) {
- if (color != null && ExpressionMapping.expressionToColor(color) != null) {
+ if (color != null &&
+ ExpressionMapping.expressionToColor(color) != null) {
shadow.add(Shadow(
- color: expressionToColor(color)!,
- offset: Offset(
- double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!,
- double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!),
- blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0,
+ color: expressionToColor(color)!,
+ offset: Offset(
+ double.tryParse((offsetX).text.replaceAll(nonNumberRegex, ''))!,
+ double.tryParse(
+ (offsetY).text.replaceAll(nonNumberRegex, ''))!),
+ blurRadius: (blurRadius is css.LiteralTerm)
+ ? double.tryParse(
+ (blurRadius).text.replaceAll(nonNumberRegex, ''))!
+ : 0.0,
));
} else {
shadow.add(Shadow(
- offset: Offset(
- double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!,
- double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!),
- blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0,
+ offset: Offset(
+ double.tryParse((offsetX).text.replaceAll(nonNumberRegex, ''))!,
+ double.tryParse(
+ (offsetY).text.replaceAll(nonNumberRegex, ''))!),
+ blurRadius: (blurRadius is css.LiteralTerm)
+ ? double.tryParse(
+ (blurRadius).text.replaceAll(nonNumberRegex, ''))!
+ : 0.0,
));
}
}
@@ -909,26 +1232,25 @@ class ExpressionMapping {
return finalShadows;
}
- static Color stringToColor(String _text) {
- var text = _text.replaceFirst('#', '');
- if (text.length == 3)
- text = text.replaceAllMapped(
- RegExp(r"[a-f]|\d", caseSensitive: false),
- (match) => '${match.group(0)}${match.group(0)}'
- );
+ static Color stringToColor(String rawText) {
+ var text = rawText.replaceFirst('#', '');
+ if (text.length == 3) {
+ text = text.replaceAllMapped(RegExp(r"[a-f]|\d", caseSensitive: false),
+ (match) => '${match.group(0)}${match.group(0)}');
+ }
if (text.length > 6) {
- text = "0x" + text;
+ text = "0x$text";
} else {
- text = "0xFF" + text;
+ text = "0xFF$text";
}
- return new Color(int.parse(text));
+ return Color(int.parse(text));
}
static Color? rgbOrRgbaToColor(String text) {
final rgbaText = text.replaceAll(')', '').replaceAll(' ', '');
try {
final rgbaValues =
- rgbaText.split(',').map((value) => double.parse(value)).toList();
+ rgbaText.split(',').map((value) => double.parse(value)).toList();
if (rgbaValues.length == 4) {
return Color.fromRGBO(
rgbaValues[0].toInt(),
@@ -954,28 +1276,54 @@ class ExpressionMapping {
final hslText = text.replaceAll(')', '').replaceAll(' ', '');
final hslValues = hslText.split(',').toList();
List parsedHsl = [];
- hslValues.forEach((element) {
- if (element.contains("%") && double.tryParse(element.replaceAll("%", "")) != null) {
+ for (var element in hslValues) {
+ if (element.contains("%") &&
+ double.tryParse(element.replaceAll("%", "")) != null) {
parsedHsl.add(double.tryParse(element.replaceAll("%", ""))! * 0.01);
} else {
- if (element != hslValues.first && (double.tryParse(element) == null || double.tryParse(element)! > 1)) {
+ if (element != hslValues.first &&
+ (double.tryParse(element) == null ||
+ double.tryParse(element)! > 1)) {
parsedHsl.add(null);
} else {
parsedHsl.add(double.tryParse(element));
}
}
- });
+ }
if (parsedHsl.length == 4 && !parsedHsl.contains(null)) {
- return HSLColor.fromAHSL(parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!).toColor();
+ return HSLColor.fromAHSL(
+ parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!)
+ .toColor();
} else if (parsedHsl.length == 3 && !parsedHsl.contains(null)) {
- return HSLColor.fromAHSL(1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!).toColor();
- } else return Colors.black;
+ return HSLColor.fromAHSL(
+ 1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!)
+ .toColor();
+ } else {
+ return Colors.black;
+ }
}
static Color? namedColorToColor(String text) {
- String namedColor = namedColors.keys.firstWhere((element) => element.toLowerCase() == text.toLowerCase(), orElse: () => "");
- if (namedColor != "") {
- return stringToColor(namedColors[namedColor]!);
- } else return null;
+ String namedColor = namedColors.keys.firstWhere(
+ (element) => element.toLowerCase() == text.toLowerCase(),
+ orElse: () => "");
+ if (namedColor != "") {
+ return stringToColor(namedColors[namedColor]!);
+ } else {
+ return null;
+ }
+ }
+
+ static VerticalAlign expressionToVerticalAlignTerm(css.LiteralTerm value) {
+ switch (value.text) {
+ case 'sub':
+ return VerticalAlign.sub;
+ case 'super':
+ return VerticalAlign.sup;
+ case 'baseline':
+ return VerticalAlign.baseline;
+ }
+
+ return VerticalAlign.baseline;
}
}
diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart
index 4b096d8f5d..304c9ff395 100644
--- a/lib/src/html_elements.dart
+++ b/lib/src/html_elements.dart
@@ -2,282 +2,206 @@ 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",
- "rt",
- "s",
- "samp",
- "small",
- "span",
- "strike",
- "strong",
- "sub",
- "sup",
- "time",
- "tt",
- "u",
- "var",
- "wbr",
+class HtmlElements {
+ static const styledElements = [
+ "abbr",
+ "acronym",
+ "address",
+ "b",
+ "bdi",
+ "bdo",
+ "big",
+ "cite",
+ "code",
+ "data",
+ "del",
+ "dfn",
+ "em",
+ "font",
+ "i",
+ "ins",
+ "kbd",
+ "mark",
+ "q",
+ "rt",
+ "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",
-];
+ //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 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",
-];
+ static const blockElements = [
+ "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",
-];
+ static const interactableElements = [
+ "a",
+ ];
-const REPLACED_ELEMENTS = [
- "br",
- "template",
- "rp",
- "rt",
- "ruby",
-];
+ static const replacedElements = [
+ "br",
+ "template",
+ "rp",
+ "rt",
+ "ruby",
+ ];
-const LAYOUT_ELEMENTS = [
- "details",
- "tr",
- "tbody",
- "tfoot",
- "thead",
-];
+ static const layoutElements = [
+ "details",
+ "tr",
+ "tbody",
+ "tfoot",
+ "thead",
+ ];
-const TABLE_CELL_ELEMENTS = ["th", "td"];
+ static const tableCellElements = ["th", "td"];
-const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"];
+ static const tableDefinitionElements = ["col", "colgroup"];
-const EXTERNAL_ELEMENTS = ["audio", "iframe", "img", "math", "svg", "table", "video"];
+ static const externalElements = [
+ "audio",
+ "iframe",
+ "img",
+ "math",
+ "svg",
+ "table",
+ "video"
+ ];
-const SELECTABLE_ELEMENTS = [
- "br",
- "a",
- "article",
- "aside",
- "blockquote",
- "body",
- "center",
- "dd",
- "div",
- "dl",
- "dt",
- "figcaption",
- "figure",
- "footer",
- "h1",
- "h2",
- "h3",
- "h4",
- "h5",
- "h6",
- "header",
- "hr",
- "html",
- "main",
- "nav",
- "noscript",
- "p",
- "pre",
- "section",
- "summary",
- "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",
- "time",
- "tt",
- "u",
- "var",
- "wbr",
-];
+ static const replacedExternalElements = ["iframe", "img", "video", "audio"];
-/**
- 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]
- */
+ static const selectableElements = [
+ "br",
+ "a",
+ "article",
+ "aside",
+ "blockquote",
+ "body",
+ "center",
+ "dd",
+ "div",
+ "dl",
+ "dt",
+ "figcaption",
+ "figure",
+ "footer",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "header",
+ "hr",
+ "html",
+ "main",
+ "nav",
+ "noscript",
+ "p",
+ "pre",
+ "section",
+ "summary",
+ "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",
+ "time",
+ "tt",
+ "u",
+ "var",
+ "wbr",
+ ];
+}
diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart
index 2aab878a64..6a1bf90a4f 100644
--- a/lib/src/interactable_element.dart
+++ b/lib/src/interactable_element.dart
@@ -8,35 +8,37 @@ class InteractableElement extends StyledElement {
String? href;
InteractableElement({
- required String name,
- required List children,
- required Style style,
+ required super.name,
+ required super.children,
+ required super.style,
required this.href,
required dom.Node node,
- required String elementId,
- }) : super(name: name, children: children, style: style, node: node as dom.Element?, elementId: elementId);
+ required super.elementId,
+ }) : super(node: node as dom.Element?);
}
/// A [Gesture] indicates the type of interaction by a user.
enum Gesture {
- TAP,
+ tap,
}
StyledElement parseInteractableElement(
- dom.Element element, List children) {
+ dom.Element element,
+ List children,
+) {
switch (element.localName) {
case "a":
if (element.attributes.containsKey('href')) {
return InteractableElement(
- name: element.localName!,
- children: children,
- href: element.attributes['href'],
- style: Style(
- color: Colors.blue,
- textDecoration: TextDecoration.underline,
- ),
- node: element,
- elementId: element.id
+ name: element.localName!,
+ children: children,
+ href: element.attributes['href'],
+ style: Style(
+ color: Colors.blue,
+ textDecoration: TextDecoration.underline,
+ ),
+ node: element,
+ elementId: element.id,
);
}
// When tag have no href, it must be non clickable and without decoration.
@@ -47,6 +49,7 @@ StyledElement parseInteractableElement(
node: element,
elementId: element.id,
);
+
/// will never be called, just to suppress missing return warning
default:
return InteractableElement(
@@ -55,7 +58,7 @@ StyledElement parseInteractableElement(
node: element,
href: '',
style: Style(),
- elementId: "[[No ID]]"
+ elementId: "[[No ID]]",
);
}
}
diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart
index 33093e7493..cd0e67677c 100644
--- a/lib/src/layout_element.dart
+++ b/lib/src/layout_element.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/anchor.dart';
+import 'package:flutter_html/src/css_box_widget.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/styled_element.dart';
import 'package:flutter_html/style.dart';
@@ -11,10 +12,10 @@ import 'package:html/dom.dart' as dom;
abstract class LayoutElement extends StyledElement {
LayoutElement({
String name = "[[No Name]]",
- required List children,
+ required super.children,
String? elementId,
- dom.Element? node,
- }) : super(name: name, children: children, style: Style(), node: node, elementId: elementId ?? "[[No ID]]");
+ super.node,
+ }) : super(name: name, style: Style(), elementId: elementId ?? "[[No ID]]");
Widget? toWidget(RenderContext context);
}
@@ -28,21 +29,21 @@ class TableSectionLayoutElement extends LayoutElement {
@override
Widget toWidget(RenderContext context) {
// Not rendered; TableLayoutElement will instead consume its children
- return Container(child: Text("TABLE SECTION"));
+ return const Text("TABLE SECTION");
}
}
class TableRowLayoutElement extends LayoutElement {
TableRowLayoutElement({
- required String name,
- required List children,
- required dom.Element node,
- }) : super(name: name, children: children, node: node);
+ required super.name,
+ required super.children,
+ required super.node,
+ });
@override
Widget toWidget(RenderContext context) {
// Not rendered; TableLayoutElement will instead consume its children
- return Container(child: Text("TABLE ROW"));
+ return const Text("TABLE ROW");
}
}
@@ -51,13 +52,13 @@ class TableCellElement extends StyledElement {
int rowspan = 1;
TableCellElement({
- required String name,
- required String elementId,
- required List elementClasses,
- required List children,
- required Style style,
- required dom.Element node,
- }) : super(name: name, elementId: elementId, elementClasses: elementClasses, children: children, style: style, node: node) {
+ required super.name,
+ required super.elementId,
+ required super.elementClasses,
+ required super.children,
+ required super.style,
+ required super.node,
+ }) {
colspan = _parseSpan(this, "colspan");
rowspan = _parseSpan(this, "rowspan");
}
@@ -90,11 +91,11 @@ TableCellElement parseTableCellElement(
class TableStyleElement extends StyledElement {
TableStyleElement({
- required String name,
- required List children,
- required Style style,
- required dom.Element node,
- }) : super(name: name, children: children, style: style, node: node);
+ required super.name,
+ required super.children,
+ required super.style,
+ required super.node,
+ });
}
TableStyleElement parseTableDefinitionElement(
@@ -124,65 +125,77 @@ class DetailsContentElement extends LayoutElement {
List elementList;
DetailsContentElement({
- required String name,
- required List children,
+ required super.name,
+ required super.children,
required dom.Element node,
required this.elementList,
- }) : super(name: name, node: node, children: children, elementId: node.id);
+ }) : super(node: node, elementId: node.id);
@override
Widget toWidget(RenderContext context) {
- List? childrenList = children.map((tree) => context.parser.parseTree(context, tree)).toList();
+ List? childrenList = children
+ .map((tree) => context.parser.parseTree(context, tree))
+ .toList();
List toRemove = [];
for (InlineSpan child in childrenList) {
- if (child is TextSpan && child.text != null && child.text!.trim().isEmpty) {
+ 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;
+ InlineSpan? firstChild =
+ childrenList.isNotEmpty == true ? childrenList.first : null;
return ExpansionTile(
key: AnchorKey.of(context.parser.key, this),
expandedAlignment: Alignment.centerLeft,
- title: elementList.isNotEmpty == true && elementList.first.localName == "summary" ? StyledText(
- textSpan: TextSpan(
- style: style.generateTextStyle(),
- children: firstChild == null ? [] : [firstChild],
- ),
- style: style,
- renderContext: context,
- ) : Text("Details"),
+ title: elementList.isNotEmpty == true &&
+ elementList.first.localName == "summary"
+ ? CssBoxWidget.withInlineSpanChildren(
+ context: context.buildContext,
+ children: firstChild == null ? [] : [firstChild],
+ style: style,
+ )
+ : const Text("Details"),
children: [
- StyledText(
- textSpan: TextSpan(
- style: style.generateTextStyle(),
- children: getChildren(childrenList, context, elementList.isNotEmpty == true && elementList.first.localName == "summary" ? firstChild : null)
- ),
+ CssBoxWidget.withInlineSpanChildren(
+ context: context.buildContext,
+ children: getChildren(
+ childrenList,
+ context,
+ elementList.isNotEmpty == true &&
+ elementList.first.localName == "summary"
+ ? firstChild
+ : null),
style: style,
- renderContext: context,
),
- ]
- );
+ ]);
}
- List getChildren(List children, RenderContext context, InlineSpan? firstChild) {
+ List getChildren(List children, RenderContext context,
+ InlineSpan? firstChild) {
if (firstChild != null) children.removeAt(0);
return children;
}
}
class EmptyLayoutElement extends LayoutElement {
- EmptyLayoutElement({required String name}) : super(name: name, children: []);
+ EmptyLayoutElement({required String name})
+ : super(
+ name: name,
+ children: [],
+ );
@override
- Widget? toWidget(_) => null;
+ Widget? toWidget(context) => null;
}
LayoutElement parseLayoutElement(
- dom.Element element,
- List children,
+ dom.Element element,
+ List