Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/flutter_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ library flutter_html;
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/style.dart';
import 'package:webview_flutter/webview_flutter.dart';

class Html extends StatelessWidget {
/// The `Html` widget takes HTML as input and displays a RichText
Expand Down Expand Up @@ -40,6 +41,7 @@ class Html extends StatelessWidget {
this.onImageTap,
this.blacklistedElements = const [],
this.style,
this.navigationDelegateForIframe,
}) : super(key: key);

final String data;
Expand All @@ -59,6 +61,11 @@ class Html extends StatelessWidget {
/// Fancy New Parser parameters
final Map<String, Style> style;

/// Decides how to handle a specific navigation request in the WebView of an
/// Iframe. It's necessary to use the webview_flutter package inside the app
/// to use NavigationDelegate.
final NavigationDelegate navigationDelegateForIframe;

@override
Widget build(BuildContext context) {
final double width = shrinkWrap ? null : MediaQuery.of(context).size.width;
Expand All @@ -74,6 +81,7 @@ class Html extends StatelessWidget {
style: style,
customRender: customRender,
blacklistedElements: blacklistedElements,
navigationDelegateForIframe: navigationDelegateForIframe,
),
);
}
Expand Down
29 changes: 22 additions & 7 deletions lib/html_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as htmlparser;
import 'package:webview_flutter/webview_flutter.dart';

typedef OnTap = void Function(String url);
typedef CustomRender = Widget Function(
Expand All @@ -30,6 +31,7 @@ class HtmlParser extends StatelessWidget {
final Map<String, Style> style;
final Map<String, CustomRender> customRender;
final List<String> blacklistedElements;
final NavigationDelegate navigationDelegateForIframe;

HtmlParser({
@required this.htmlData,
Expand All @@ -40,6 +42,7 @@ class HtmlParser extends StatelessWidget {
this.style,
this.customRender,
this.blacklistedElements,
this.navigationDelegateForIframe,
});

@override
Expand All @@ -49,6 +52,7 @@ class HtmlParser extends StatelessWidget {
document,
customRender?.keys?.toList() ?? [],
blacklistedElements,
navigationDelegateForIframe,
);
StyledElement styledTree = applyCSS(lexedTree);
StyledElement inlineStyledTree = applyInlineStyles(styledTree);
Expand All @@ -69,7 +73,7 @@ class HtmlParser extends StatelessWidget {
// scaling is used, but relies on https://github.com/flutter/flutter/pull/59711
// to wrap everything when larger accessibility fonts are used.
return StyledText(
textSpan: parsedTree,
textSpan: parsedTree,
style: cleanedTree.style,
textScaleFactor: MediaQuery.of(context).textScaleFactor,
);
Expand All @@ -90,6 +94,7 @@ class HtmlParser extends StatelessWidget {
dom.Document html,
List<String> customRenderTags,
List<String> blacklistedElements,
NavigationDelegate navigationDelegateForIframe,
) {
StyledElement tree = StyledElement(
name: "[Tree Root]",
Expand All @@ -98,8 +103,12 @@ class HtmlParser extends StatelessWidget {
);

html.nodes.forEach((node) {
tree.children
.add(_recursiveLexer(node, customRenderTags, blacklistedElements));
tree.children.add(_recursiveLexer(
node,
customRenderTags,
blacklistedElements,
navigationDelegateForIframe,
));
});

return tree;
Expand All @@ -113,12 +122,17 @@ class HtmlParser extends StatelessWidget {
dom.Node node,
List<String> customRenderTags,
List<String> blacklistedElements,
NavigationDelegate navigationDelegateForIframe,
) {
List<StyledElement> children = List<StyledElement>();

node.nodes.forEach((childNode) {
children.add(
_recursiveLexer(childNode, customRenderTags, blacklistedElements));
children.add(_recursiveLexer(
childNode,
customRenderTags,
blacklistedElements,
navigationDelegateForIframe,
));
});

//TODO(Sub6Resources): There's probably a more efficient way to look this up.
Expand All @@ -131,7 +145,7 @@ class HtmlParser extends StatelessWidget {
} else if (INTERACTABLE_ELEMENTS.contains(node.localName)) {
return parseInteractableElement(node, children);
} else if (REPLACED_ELEMENTS.contains(node.localName)) {
return parseReplacedElement(node);
return parseReplacedElement(node, navigationDelegateForIframe);
} else if (LAYOUT_ELEMENTS.contains(node.localName)) {
return parseLayoutElement(node, children);
} else if (TABLE_STYLE_ELEMENTS.contains(node.localName)) {
Expand Down Expand Up @@ -268,7 +282,8 @@ class HtmlParser extends StatelessWidget {
shrinkWrap: context.parser.shrinkWrap,
child: Stack(
children: [
if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE || tree.style?.listStylePosition == null)
if (tree.style?.listStylePosition == ListStylePosition.OUTSIDE ||
tree.style?.listStylePosition == null)
PositionedDirectional(
width: 30, //TODO derive this from list padding.
start: 0,
Expand Down
17 changes: 12 additions & 5 deletions lib/src/replaced_element.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ import 'package:chewie/chewie.dart';
import 'package:chewie_audio/chewie_audio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:video_player/video_player.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/utils.dart';
import 'package:flutter_html/style.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:html/dom.dart' as dom;
import 'package:video_player/video_player.dart';
import 'package:webview_flutter/webview_flutter.dart';

/// A [ReplacedElement] is a type of [StyledElement] that does not require its [children] to be rendered.
///
Expand Down Expand Up @@ -158,6 +158,7 @@ class IframeContentElement extends ReplacedElement {
final String src;
final double width;
final double height;
final NavigationDelegate navigationDelegate;

IframeContentElement({
String name,
Expand All @@ -166,6 +167,7 @@ class IframeContentElement extends ReplacedElement {
this.width,
this.height,
dom.Element node,
this.navigationDelegate,
}) : super(name: name, style: style, node: node);

@override
Expand All @@ -176,6 +178,7 @@ class IframeContentElement extends ReplacedElement {
child: WebView(
initialUrl: src,
javascriptMode: JavascriptMode.unrestricted,
navigationDelegate: navigationDelegate,
gestureRecognizers: {
Factory(() => PlatformViewVerticalGestureRecognizer())
},
Expand Down Expand Up @@ -350,7 +353,10 @@ class RubyElement extends ReplacedElement {
}
}

ReplacedElement parseReplacedElement(dom.Element element) {
ReplacedElement parseReplacedElement(
dom.Element element,
NavigationDelegate navigationDelegateForIframe,
) {
switch (element.localName) {
case "audio":
final sources = <String>[
Expand All @@ -377,6 +383,7 @@ ReplacedElement parseReplacedElement(dom.Element element) {
src: element.attributes['src'],
width: double.tryParse(element.attributes['width'] ?? ""),
height: double.tryParse(element.attributes['height'] ?? ""),
navigationDelegate: navigationDelegateForIframe,
);
case "img":
return ImageContentElement(
Expand Down
51 changes: 31 additions & 20 deletions test/html_parser_test.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_html/flutter_html.dart';

void main() {
testWidgets("Check that default parser does not fail on empty data",
Expand All @@ -29,10 +29,12 @@ void testNewParser() {

test("lexDomTree works correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML(
"Hello! <b>Hello, World!</b><i>Hello, New World!</i>"),
[],
[]);
HtmlParser.parseHTML(
"Hello! <b>Hello, World!</b><i>Hello, New World!</i>"),
[],
[],
null,
);
print(tree.toString());
});

Expand All @@ -41,36 +43,43 @@ void testNewParser() {
HtmlParser.parseHTML(
"Hello, World! <a href='https://example.com'>This is a link</a>"),
[],
[]);
[],
null);
print(tree.toString());
});

test("ContentElements work correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML("<img src='https://image.example.com' />"),
[],
[]);
HtmlParser.parseHTML("<img src='https://image.example.com' />"),
[],
[],
null,
);
print(tree.toString());
});

test("Nesting of elements works correctly", () {
StyledElement tree = HtmlParser.lexDomTree(
HtmlParser.parseHTML(
"<div><div><div><div><a href='link'>Link</a><div>Hello, World! <b>Bold and <i>Italic</i></b></div></div></div></div></div>"),
[],
[]);
HtmlParser.parseHTML(
"<div><div><div><div><a href='link'>Link</a><div>Hello, World! <b>Bold and <i>Italic</i></b></div></div></div></div></div>"),
[],
[],
null,
);
print(tree.toString());
});

test("Video Content Source Parser works correctly", () {
ReplacedElement videoContentElement =
parseReplacedElement(HtmlParser.parseHTML("""
ReplacedElement videoContentElement = parseReplacedElement(
HtmlParser.parseHTML("""
<video width="320" height="240" controls>
<source src="movie.mp4" type="video/mp4">
<source src="movie.ogg" type="video/ogg">
Your browser does not support the video tag.
</video>
""").getElementsByTagName("video")[0]);
""").getElementsByTagName("video")[0],
null,
);

expect(videoContentElement, isA<VideoContentElement>());
if (videoContentElement is VideoContentElement) {
Expand All @@ -82,14 +91,16 @@ void testNewParser() {
});

test("Audio Content Source Parser works correctly", () {
ReplacedElement audioContentElement =
parseReplacedElement(HtmlParser.parseHTML("""
ReplacedElement audioContentElement = parseReplacedElement(
HtmlParser.parseHTML("""
<audio controls>
<source src='audio.mp3' type='audio/mpeg'>
<source src='audio.wav' type='audio/wav'>
Your browser does not support the audio tag.
</audio>
""").getElementsByTagName("audio")[0]);
""").getElementsByTagName("audio")[0],
null,
);
expect(audioContentElement, isA<AudioContentElement>());
if (audioContentElement is AudioContentElement) {
expect(audioContentElement.showControls, equals(true),
Expand Down