Skip to content

Commit 13b76f5

Browse files
committed
Merge branch 'master' of https://github.com/Sub6Resources/flutter_html into bugfix/audio-video-disposing
� Conflicts: � lib/flutter_html.dart � lib/src/replaced_element.dart � lib/src/styled_element.dart
2 parents a51d0c9 + 6de34b9 commit 13b76f5

18 files changed

+216
-117
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
## [2.2.1] - December 8, 2021:
2+
* Allow styling on ruby tags
3+
* Allow width/height/alignment styling on table/tr/td tags
4+
* Prevent images causing rebuilding and leaking memory
5+
* Fixes display of list items on iOS with font weights below 400
6+
* Prevent crash on negative margins or paddings
7+
8+
## [2.2.0] - November 29, 2021:
9+
* Explicitly declare multiplatform support
10+
* Extended and fixed list-style (marker) support
11+
* Basic support for height/width css properties
12+
* Support changing scroll physics of SelectableText.rich
13+
* Support text transform css property
14+
* Bumped minimum flutter_math_fork version for Flutter 2.5 compatibility
15+
* Fix styling of iframes
16+
* Fix nested font tag application
17+
* Fix whitespace rendering between list items
18+
* Prevent crash on empty <table> tag and tables with both colspan/rowspan
19+
* Prevent crash on use of negative margins in css
20+
121
## [2.1.5] - October 7, 2021:
222
* Ignore unsupported custom style selectors when using fromCss
323
* Fix SVG tag usage inside tables

README.md

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

102102
dependencies:
103-
flutter_html: ^2.1.5
103+
flutter_html: ^2.2.1
104104

105105
## Currently Supported HTML Tags:
106106
| | | | | | | | | | | |

lib/flutter_html.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import 'package:flutter_html/src/html_elements.dart';
1010
import 'package:flutter_html/src/utils.dart';
1111
import 'package:flutter_html/style.dart';
1212
import 'package:html/dom.dart' as dom;
13+
import 'package:flutter_html/src/navigation_delegate.dart';
1314
import 'package:video_player/video_player.dart';
14-
import 'package:webview_flutter/webview_flutter.dart';
1515

1616
//export render context api
1717
export 'package:flutter_html/html_parser.dart';
@@ -22,6 +22,7 @@ export 'package:flutter_html/src/interactable_element.dart';
2222
export 'package:flutter_html/src/layout_element.dart';
2323
export 'package:flutter_html/src/replaced_element.dart';
2424
export 'package:flutter_html/src/styled_element.dart';
25+
export 'package:flutter_html/src/navigation_delegate.dart';
2526
//export style api
2627
export 'package:flutter_html/style.dart';
2728

@@ -67,8 +68,7 @@ class Html extends StatefulWidget {
6768
this.tagsList = const [],
6869
this.style = const {},
6970
this.navigationDelegateForIframe,
70-
})
71-
: document = null,
71+
}) : document = null,
7272
assert(data != null),
7373
_anchorKey = anchorKey ?? GlobalKey(),
7474
super(key: key);
@@ -89,8 +89,7 @@ class Html extends StatefulWidget {
8989
this.tagsList = const [],
9090
this.style = const {},
9191
this.navigationDelegateForIframe,
92-
})
93-
: data = null,
92+
}) : data = null,
9493
assert(document != null),
9594
_anchorKey = anchorKey ?? GlobalKey(),
9695
super(key: key);

lib/html_parser.dart

Lines changed: 12 additions & 9 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,
@@ -62,6 +62,8 @@ class HtmlParser extends StatelessWidget {
6262
final TextSelectionControls? selectionControls;
6363
final ScrollPhysics? scrollPhysics;
6464

65+
final Map<String, Size> cachedImageSizes = {};
66+
6567
HtmlParser({
6668
required this.key,
6769
required this.htmlData,
@@ -216,7 +218,7 @@ class HtmlParser extends StatelessWidget {
216218
} else if (INTERACTABLE_ELEMENTS.contains(node.localName)) {
217219
return parseInteractableElement(node, children);
218220
} else if (REPLACED_ELEMENTS.contains(node.localName)) {
219-
return parseReplacedElement(node, navigationDelegateForIframe);
221+
return parseReplacedElement(node, children, navigationDelegateForIframe);
220222
} else if (LAYOUT_ELEMENTS.contains(node.localName)) {
221223
return parseLayoutElement(node, children);
222224
} else if (TABLE_CELL_ELEMENTS.contains(node.localName)) {
@@ -403,9 +405,11 @@ class HtmlParser extends StatelessWidget {
403405
);
404406
} else if (tree.style.display == Display.LIST_ITEM) {
405407
List<InlineSpan> getChildren(StyledElement tree) {
406-
InlineSpan tabSpan = WidgetSpan(child: Text("\t", textAlign: TextAlign.right));
407408
List<InlineSpan> children = tree.children.map((tree) => parseTree(newContext, tree)).toList();
408409
if (tree.style.listStylePosition == ListStylePosition.INSIDE) {
410+
final tabSpan = WidgetSpan(
411+
child: Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
412+
);
409413
children.insert(0, tabSpan);
410414
}
411415
return children;
@@ -424,10 +428,10 @@ class HtmlParser extends StatelessWidget {
424428
children: [
425429
tree.style.listStylePosition == ListStylePosition.OUTSIDE ?
426430
Padding(
427-
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),
431+
padding: tree.style.padding?.nonNegative ?? EdgeInsets.only(left: tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style.direction == TextDirection.rtl ? 10.0 : 0.0),
428432
child: newContext.style.markerContent
429433
) : Container(height: 0, width: 0),
430-
Text("\t", textAlign: TextAlign.right),
434+
Text("\t", textAlign: TextAlign.right, style: TextStyle(fontWeight: FontWeight.w400)),
431435
Expanded(
432436
child: Padding(
433437
padding: tree.style.listStylePosition == ListStylePosition.INSIDE ?
@@ -739,7 +743,6 @@ class HtmlParser extends StatelessWidget {
739743
String marker = "";
740744
switch (tree.style.listStyleType!) {
741745
case ListStyleType.NONE:
742-
tree.style.markerContent = '';
743746
break;
744747
case ListStyleType.CIRCLE:
745748
marker = '○';
@@ -961,7 +964,7 @@ class HtmlParser extends StatelessWidget {
961964
if (child is EmptyContentElement || child is EmptyLayoutElement) {
962965
toRemove.add(child);
963966
} else if (child is TextContentElement
964-
&& tree.name == "body"
967+
&& (tree.name == "body" || tree.name == "ul")
965968
&& child.text!.replaceAll(' ', '').isEmpty) {
966969
toRemove.add(child);
967970
} else if (child is TextContentElement
@@ -1055,8 +1058,8 @@ class ContainerSpan extends StatelessWidget {
10551058
),
10561059
height: style.height,
10571060
width: style.width,
1058-
padding: style.padding,
1059-
margin: style.margin,
1061+
padding: style.padding?.nonNegative,
1062+
margin: style.margin?.nonNegative,
10601063
alignment: shrinkWrap ? null : style.alignment,
10611064
child: child ??
10621065
StyledText(

lib/image_render.dart

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

lib/src/html_elements.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const STYLED_ELEMENTS = [
2222
"kbd",
2323
"mark",
2424
"q",
25+
"rt",
2526
"s",
2627
"samp",
2728
"small",

lib/src/interactable_element.dart

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,30 @@ enum Gesture {
2222
TAP,
2323
}
2424

25-
InteractableElement parseInteractableElement(
25+
StyledElement parseInteractableElement(
2626
dom.Element element, List<StyledElement> children) {
2727
switch (element.localName) {
2828
case "a":
29-
return InteractableElement(
29+
if (element.attributes.containsKey('href')) {
30+
return InteractableElement(
31+
name: element.localName!,
32+
children: children,
33+
href: element.attributes['href'],
34+
style: Style(
35+
color: Colors.blue,
36+
textDecoration: TextDecoration.underline,
37+
),
38+
node: element,
39+
elementId: element.id
40+
);
41+
}
42+
// When <a> tag have no href, it must be non clickable and without decoration.
43+
return StyledElement(
3044
name: element.localName!,
3145
children: children,
32-
href: element.attributes['href'],
33-
style: Style(
34-
color: Colors.blue,
35-
textDecoration: TextDecoration.underline,
36-
),
46+
style: Style(),
3747
node: element,
38-
elementId: element.id
48+
elementId: element.id,
3949
);
4050
/// will never be called, just to suppress missing return warning
4151
default:
@@ -48,4 +58,4 @@ InteractableElement parseInteractableElement(
4858
elementId: "[[No ID]]"
4959
);
5060
}
51-
}
61+
}

lib/src/layout_element.dart

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:flutter_html/html_parser.dart';
55
import 'package:flutter_html/src/anchor.dart';
66
import 'package:flutter_html/src/html_elements.dart';
77
import 'package:flutter_html/src/styled_element.dart';
8+
import 'package:flutter_html/src/utils.dart';
89
import 'package:flutter_html/style.dart';
910
import 'package:flutter_layout_grid/flutter_layout_grid.dart';
1011
import 'package:html/dom.dart' as dom;
@@ -33,8 +34,9 @@ class TableLayoutElement extends LayoutElement {
3334
Widget toWidget(RenderContext context) {
3435
return Container(
3536
key: AnchorKey.of(context.parser.key, this),
36-
margin: style.margin,
37-
padding: style.padding,
37+
padding: style.padding?.nonNegative,
38+
margin: style.margin?.nonNegative,
39+
alignment: style.alignment,
3840
decoration: BoxDecoration(
3941
color: style.backgroundColor,
4042
border: style.border,
@@ -87,35 +89,42 @@ class TableLayoutElement extends LayoutElement {
8789
}
8890

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

9394
// 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);
95+
int columnMax = 0;
96+
List<int> rowSpanOffsets = [];
97+
for (final row in rows) {
98+
final cols = row.children.whereType<TableCellElement>().fold(0, (int value, child) => value + child.colspan) +
99+
rowSpanOffsets.fold<int>(0, (int offset, child) => child);
100+
columnMax = max(cols, columnMax);
101+
rowSpanOffsets = [
102+
...rowSpanOffsets.map((value) => value - 1).where((value) => value > 0),
103+
...row.children.whereType<TableCellElement>().map((cell) => cell.rowspan - 1),
104+
];
105+
}
99106

100107
// Place the cells in the rows/columns
101108
final cells = <GridPlacement>[];
102109
final columnRowOffset = List.generate(columnMax, (_) => 0);
110+
final columnColspanOffset = List.generate(columnMax, (_) => 0);
103111
int rowi = 0;
104112
for (var row in rows) {
105113
int columni = 0;
106114
for (var child in row.children) {
107115
if (columni > columnMax - 1 ) {
108116
break;
109117
}
110-
while (columnRowOffset[columni] > 0) {
111-
columnRowOffset[columni] = columnRowOffset[columni] - 1;
112-
columni++;
113-
}
114118
if (child is TableCellElement) {
119+
while (columnRowOffset[columni] > 0) {
120+
columnRowOffset[columni] = columnRowOffset[columni] - 1;
121+
columni += columnColspanOffset[columni].clamp(1, columnMax - columni - 1);
122+
}
115123
cells.add(GridPlacement(
116124
child: Container(
117-
width: double.infinity,
118-
padding: child.style.padding ?? row.style.padding,
125+
width: child.style.width ?? double.infinity,
126+
height: child.style.height,
127+
padding: child.style.padding?.nonNegative ?? row.style.padding?.nonNegative,
119128
decoration: BoxDecoration(
120129
color: child.style.backgroundColor ?? row.style.backgroundColor,
121130
border: child.style.border ?? row.style.border,
@@ -139,6 +148,7 @@ class TableLayoutElement extends LayoutElement {
139148
rowSpan: min(child.rowspan, rows.length - rowi),
140149
));
141150
columnRowOffset[columni] = child.rowspan - 1;
151+
columnColspanOffset[columni] = child.colspan;
142152
columni += child.colspan;
143153
}
144154
}
@@ -155,6 +165,11 @@ class TableLayoutElement extends LayoutElement {
155165
max(0, columnMax - finalColumnSizes.length),
156166
(_) => IntrinsicContentTrackSize());
157167

168+
if (finalColumnSizes.isEmpty || rowSizes.isEmpty) {
169+
// No actual cells to show
170+
return SizedBox();
171+
}
172+
158173
return LayoutGrid(
159174
gridFit: GridFit.loose,
160175
columnSizes: finalColumnSizes,

0 commit comments

Comments
 (0)