Skip to content

Commit a38bbd1

Browse files
committed
Merge remote-tracking branch 'upstream/master' into bugfix/913-tables-height-width
2 parents fa9367e + 2e9f2f0 commit a38bbd1

13 files changed

+132
-78
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## [2.2.0] - November 29, 2021:
2+
* Explicitly declare multiplatform support
3+
* Extended and fixed list-style (marker) support
4+
* Basic support for height/width css properties
5+
* Support changing scroll physics of SelectableText.rich
6+
* Support text transform css property
7+
* Bumped minimum flutter_math_fork version for Flutter 2.5 compatibility
8+
* Fix styling of iframes
9+
* Fix nested font tag application
10+
* Fix whitespace rendering between list items
11+
* Prevent crash on empty <table> tag and tables with both colspan/rowspan
12+
* Prevent crash on use of negative margins in css
13+
114
## [2.1.5] - October 7, 2021:
215
* Ignore unsupported custom style selectors when using fromCss
316
* Fix SVG tag usage inside tables

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ A Flutter widget for rendering HTML and CSS as Flutter widgets.
9898
Add the following to your `pubspec.yaml` file:
9999

100100
dependencies:
101-
flutter_html: ^2.1.5
101+
flutter_html: ^2.2.0
102102

103103
## Currently Supported HTML Tags:
104104
| | | | | | | | | | | |

lib/flutter_html.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'package:flutter_html/image_render.dart';
77
import 'package:flutter_html/src/html_elements.dart';
88
import 'package:flutter_html/style.dart';
99
import 'package:html/dom.dart' as dom;
10-
import 'package:webview_flutter/webview_flutter.dart';
10+
import 'package:flutter_html/src/navigation_delegate.dart';
1111

1212
//export render context api
1313
export 'package:flutter_html/html_parser.dart';
@@ -18,6 +18,7 @@ export 'package:flutter_html/src/interactable_element.dart';
1818
export 'package:flutter_html/src/layout_element.dart';
1919
export 'package:flutter_html/src/replaced_element.dart';
2020
export 'package:flutter_html/src/styled_element.dart';
21+
export 'package:flutter_html/src/navigation_delegate.dart';
2122
//export style api
2223
export 'package:flutter_html/style.dart';
2324

lib/html_parser.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ import 'package:flutter_html/src/anchor.dart';
1313
import 'package:flutter_html/src/css_parser.dart';
1414
import 'package:flutter_html/src/html_elements.dart';
1515
import 'package:flutter_html/src/layout_element.dart';
16+
import 'package:flutter_html/src/navigation_delegate.dart';
1617
import 'package:flutter_html/src/utils.dart';
1718
import 'package:flutter_html/style.dart';
1819
import 'package:html/dom.dart' as dom;
1920
import 'package:html/parser.dart' as htmlparser;
2021
import 'package:numerus/numerus.dart';
21-
import 'package:webview_flutter/webview_flutter.dart';
2222

2323
typedef OnTap = void Function(
2424
String? url,
@@ -61,6 +61,8 @@ class HtmlParser extends StatelessWidget {
6161
final TextSelectionControls? selectionControls;
6262
final ScrollPhysics? scrollPhysics;
6363

64+
final Map<String, Size> cachedImageSizes = {};
65+
6466
HtmlParser({
6567
required this.key,
6668
required this.htmlData,
@@ -401,9 +403,11 @@ class HtmlParser extends StatelessWidget {
401403
);
402404
} else if (tree.style.display == Display.LIST_ITEM) {
403405
List<InlineSpan> getChildren(StyledElement tree) {
404-
InlineSpan tabSpan = WidgetSpan(child: Text("\t", textAlign: TextAlign.right));
405406
List<InlineSpan> children = tree.children.map((tree) => parseTree(newContext, tree)).toList();
406407
if (tree.style.listStylePosition == ListStylePosition.INSIDE) {
408+
final tabSpan = WidgetSpan(
409+
child: Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
410+
);
407411
children.insert(0, tabSpan);
408412
}
409413
return children;
@@ -425,7 +429,7 @@ class HtmlParser extends StatelessWidget {
425429
padding: tree.style.padding ?? EdgeInsets.only(left: tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style.direction == TextDirection.rtl ? 10.0 : 0.0),
426430
child: newContext.style.markerContent
427431
) : Container(height: 0, width: 0),
428-
Text("\t", textAlign: TextAlign.right),
432+
Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
429433
Expanded(
430434
child: Padding(
431435
padding: tree.style.listStylePosition == ListStylePosition.INSIDE ?
@@ -737,7 +741,6 @@ class HtmlParser extends StatelessWidget {
737741
String marker = "";
738742
switch (tree.style.listStyleType!) {
739743
case ListStyleType.NONE:
740-
tree.style.markerContent = '';
741744
break;
742745
case ListStyleType.CIRCLE:
743746
marker = '○';
@@ -1054,7 +1057,7 @@ class ContainerSpan extends StatelessWidget {
10541057
height: style.height,
10551058
width: style.width,
10561059
padding: style.padding,
1057-
margin: style.margin,
1060+
margin: style.margin?.clamp(EdgeInsets.zero, const EdgeInsets.all(double.infinity)),
10581061
alignment: shrinkWrap ? null : style.alignment,
10591062
child: child ??
10601063
StyledText(

lib/image_render.dart

Lines changed: 32 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -109,48 +109,44 @@ ImageRender networkImageRender({
109109
}) =>
110110
(context, attributes, element) {
111111
final src = mapUrl?.call(_src(attributes)) ?? _src(attributes)!;
112-
precacheImage(
113-
NetworkImage(
114-
src,
115-
headers: headers,
116-
),
117-
context.buildContext,
118-
onError: (exception, StackTrace? stackTrace) {
119-
context.parser.onImageError?.call(exception, stackTrace);
120-
},
121-
);
122112
Completer<Size> completer = Completer();
123-
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
124-
if (frame == null) {
113+
if (context.parser.cachedImageSizes[src] != null) {
114+
completer.complete(context.parser.cachedImageSizes[src]);
115+
} else {
116+
Image image = Image.network(src, frameBuilder: (ctx, child, frame, _) {
117+
if (frame == null) {
118+
if (!completer.isCompleted) {
119+
completer.completeError("error");
120+
}
121+
return child;
122+
} else {
123+
return child;
124+
}
125+
});
126+
127+
ImageStreamListener? listener;
128+
listener = ImageStreamListener((ImageInfo imageInfo, bool synchronousCall) {
129+
var myImage = imageInfo.image;
130+
Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
125131
if (!completer.isCompleted) {
126-
completer.completeError("error");
132+
context.parser.cachedImageSizes[src] = size;
133+
completer.complete(size);
134+
image.image.resolve(ImageConfiguration()).removeListener(listener!);
127135
}
128-
return child;
129-
} else {
130-
return child;
131-
}
132-
});
133-
134-
var listener =
135-
ImageStreamListener((ImageInfo image, bool synchronousCall) {
136-
var myImage = image.image;
137-
Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
138-
if (!completer.isCompleted) {
139-
completer.complete(size);
140-
}
141-
}, onError: (object, stacktrace) {
142-
if (!completer.isCompleted) {
143-
completer.completeError(object);
144-
}
145-
});
146-
147-
image.image.resolve(ImageConfiguration()).addListener(listener);
136+
}, onError: (object, stacktrace) {
137+
if (!completer.isCompleted) {
138+
completer.completeError(object);
139+
image.image.resolve(ImageConfiguration()).removeListener(listener!);
140+
}
141+
});
142+
143+
image.image.resolve(ImageConfiguration()).addListener(listener);
144+
}
145+
148146
return FutureBuilder<Size>(
149147
future: completer.future,
148+
initialData: context.parser.cachedImageSizes[src],
150149
builder: (BuildContext buildContext, AsyncSnapshot<Size> snapshot) {
151-
if (completer.isCompleted) {
152-
image.image.resolve(ImageConfiguration()).removeListener(listener);
153-
}
154150
if (snapshot.hasData) {
155151
return Container(
156152
constraints: BoxConstraints(

lib/src/layout_element.dart

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class TableLayoutElement extends LayoutElement {
3535
key: AnchorKey.of(context.parser.key, this),
3636
margin: style.margin,
3737
padding: style.padding,
38+
alignment: style.alignment,
3839
decoration: BoxDecoration(
3940
color: style.backgroundColor,
4041
border: style.border,
@@ -87,31 +88,37 @@ class TableLayoutElement extends LayoutElement {
8788
}
8889

8990
// All table rows have a height intrinsic to their (spanned) contents
90-
final rowSizes =
91-
List.generate(rows.length, (_) => IntrinsicContentTrackSize());
91+
final rowSizes = List.generate(rows.length, (_) => IntrinsicContentTrackSize());
9292

9393
// Calculate column bounds
94-
int columnMax = rows
95-
.map((row) => row.children
96-
.whereType<TableCellElement>()
97-
.fold(0, (int value, child) => value + child.colspan))
98-
.fold(0, max);
94+
int columnMax = 0;
95+
List<int> rowSpanOffsets = [];
96+
for (final row in rows) {
97+
final cols = row.children.whereType<TableCellElement>().fold(0, (int value, child) => value + child.colspan) +
98+
rowSpanOffsets.fold<int>(0, (int offset, child) => child);
99+
columnMax = max(cols, columnMax);
100+
rowSpanOffsets = [
101+
...rowSpanOffsets.map((value) => value - 1).where((value) => value > 0),
102+
...row.children.whereType<TableCellElement>().map((cell) => cell.rowspan - 1),
103+
];
104+
}
99105

100106
// Place the cells in the rows/columns
101107
final cells = <GridPlacement>[];
102108
final columnRowOffset = List.generate(columnMax, (_) => 0);
109+
final columnColspanOffset = List.generate(columnMax, (_) => 0);
103110
int rowi = 0;
104111
for (var row in rows) {
105112
int columni = 0;
106113
for (var child in row.children) {
107114
if (columni > columnMax - 1 ) {
108115
break;
109116
}
110-
while (columnRowOffset[columni] > 0) {
111-
columnRowOffset[columni] = columnRowOffset[columni] - 1;
112-
columni++;
113-
}
114117
if (child is TableCellElement) {
118+
while (columnRowOffset[columni] > 0) {
119+
columnRowOffset[columni] = columnRowOffset[columni] - 1;
120+
columni += columnColspanOffset[columni].clamp(1, columnMax - columni - 1);
121+
}
115122
cells.add(GridPlacement(
116123
child: Container(
117124
width: child.style.width ?? double.infinity,
@@ -140,6 +147,7 @@ class TableLayoutElement extends LayoutElement {
140147
rowSpan: min(child.rowspan, rows.length - rowi),
141148
));
142149
columnRowOffset[columni] = child.rowspan - 1;
150+
columnColspanOffset[columni] = child.colspan;
143151
columni += child.colspan;
144152
}
145153
}

lib/src/navigation_delegate.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'dart:async';
2+
3+
/// Information about a navigation action that is about to be executed.
4+
class NavigationRequest {
5+
NavigationRequest({required this.url, required this.isForMainFrame});
6+
7+
/// The URL that will be loaded if the navigation is executed.
8+
final String url;
9+
10+
/// Whether the navigation request is to be loaded as the main frame.
11+
final bool isForMainFrame;
12+
13+
@override
14+
String toString() {
15+
return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)';
16+
}
17+
}
18+
19+
/// A decision on how to handle a navigation request.
20+
enum NavigationDecision {
21+
/// Prevent the navigation from taking place.
22+
prevent,
23+
24+
/// Allow the navigation to take place.
25+
navigate,
26+
}
27+
28+
/// Decides how to handle a specific navigation request.
29+
///
30+
/// The returned [NavigationDecision] determines how the navigation described by
31+
/// `navigation` should be handled.
32+
///
33+
/// See also: [WebView.navigationDelegate].
34+
typedef FutureOr<NavigationDecision> NavigationDelegate(
35+
NavigationRequest navigation);

lib/src/replaced_element.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
77
import 'package:flutter_html/html_parser.dart';
88
import 'package:flutter_html/src/anchor.dart';
99
import 'package:flutter_html/src/html_elements.dart';
10+
import 'package:flutter_html/src/navigation_delegate.dart';
1011
import 'package:flutter_html/src/utils.dart';
1112
import 'package:flutter_html/src/widgets/iframe_unsupported.dart'
1213
if (dart.library.io) 'package:flutter_html/src/widgets/iframe_mobile.dart'
@@ -16,7 +17,6 @@ import 'package:flutter_math_fork/flutter_math.dart';
1617
import 'package:flutter_svg/flutter_svg.dart';
1718
import 'package:html/dom.dart' as dom;
1819
import 'package:video_player/video_player.dart';
19-
import 'package:webview_flutter/webview_flutter.dart';
2020

2121
/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered.
2222
///

lib/src/widgets/iframe_mobile.dart

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import 'package:flutter/foundation.dart';
22
import 'package:flutter/gestures.dart';
33
import 'package:flutter/material.dart';
44
import 'package:flutter_html/html_parser.dart';
5+
import 'package:flutter_html/src/navigation_delegate.dart';
56
import 'package:flutter_html/src/replaced_element.dart';
67
import 'package:flutter_html/style.dart';
7-
import 'package:webview_flutter/webview_flutter.dart';
8+
import 'package:webview_flutter/webview_flutter.dart' as webview;
89
import 'package:html/dom.dart' as dom;
910

1011
/// [IframeContentElement is a [ReplacedElement] with web content.
@@ -33,13 +34,23 @@ class IframeContentElement extends ReplacedElement {
3334
child: ContainerSpan(
3435
style: context.style,
3536
newContext: context,
36-
child: WebView(
37+
child: webview.WebView(
3738
initialUrl: src,
3839
key: key,
3940
javascriptMode: sandboxMode == null || sandboxMode == "allow-scripts"
40-
? JavascriptMode.unrestricted
41-
: JavascriptMode.disabled,
42-
navigationDelegate: navigationDelegate,
41+
? webview.JavascriptMode.unrestricted
42+
: webview.JavascriptMode.disabled,
43+
navigationDelegate: (request) async {
44+
final result = await navigationDelegate!(NavigationRequest(
45+
url: request.url,
46+
isForMainFrame: request.isForMainFrame,
47+
));
48+
if (result == NavigationDecision.prevent) {
49+
return webview.NavigationDecision.prevent;
50+
} else {
51+
return webview.NavigationDecision.navigate;
52+
}
53+
},
4354
gestureRecognizers: {
4455
Factory<VerticalDragGestureRecognizer>(() => VerticalDragGestureRecognizer())
4556
},

lib/src/widgets/iframe_unsupported.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_html/html_parser.dart';
3+
import 'package:flutter_html/src/navigation_delegate.dart';
34
import 'package:flutter_html/src/replaced_element.dart';
45
import 'package:flutter_html/style.dart';
5-
import 'package:webview_flutter/webview_flutter.dart';
66
import 'package:html/dom.dart' as dom;
77

88
/// [IframeContentElement is a [ReplacedElement] with web content.
@@ -30,4 +30,4 @@ class IframeContentElement extends ReplacedElement {
3030
child: Text("Iframes are currently not supported in this environment"),
3131
);
3232
}
33-
}
33+
}

lib/src/widgets/iframe_web.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_html/html_parser.dart';
33
import 'package:flutter_html/shims/dart_ui.dart' as ui;
4+
import 'package:flutter_html/src/navigation_delegate.dart';
45
import 'package:flutter_html/src/replaced_element.dart';
56
import 'package:flutter_html/src/utils.dart';
67
import 'package:flutter_html/style.dart';
7-
import 'package:webview_flutter/webview_flutter.dart';
88
import 'package:html/dom.dart' as dom;
99
// ignore: avoid_web_libraries_in_flutter
1010
import 'dart:html' as html;

lib/style.dart

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -548,20 +548,7 @@ class ListStyleType {
548548
static const LOWER_ROMAN = ListStyleType("LOWER_ROMAN");
549549
static const UPPER_ROMAN = ListStyleType("UPPER_ROMAN");
550550
static const SQUARE = ListStyleType("SQUARE");
551-
}
552-
553-
enum ListStyleType {
554-
LOWER_ALPHA,
555-
UPPER_ALPHA,
556-
LOWER_LATIN,
557-
UPPER_LATIN,
558-
CIRCLE,
559-
DISC,
560-
DECIMAL,
561-
LOWER_ROMAN,
562-
UPPER_ROMAN,
563-
SQUARE,
564-
NONE,
551+
static const NONE = ListStyleType("NONE");
565552
}
566553

567554
enum ListStylePosition {

0 commit comments

Comments
 (0)