Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
136 changes: 79 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,13 @@ Inner links (such as `<a href="#top">Back to the top</a>` will work out of the b

A powerful API that allows you to customize everything when rendering a specific HTML tag. This means you can change the default behaviour or add support for HTML elements that aren't supported natively. You can also make up your own custom tags in your HTML!

`customRender` accepts a `Map<String, CustomRender>`. The `CustomRender` type is a function that requires a `Widget` or `InlineSpan` to be returned. It exposes `RenderContext` and the `Widget` that would have been rendered by `Html` without a `customRender` defined. The `RenderContext` contains the build context, styling and the HTML element, with attrributes and its subtree,.
`customRender` accepts a `Map<CustomRenderMatcher, CustomRender>`.

To use this API, set the key as the tag of the HTML element you wish to provide a custom implementation for, and create a function with the above parameters that returns a `Widget` or `InlineSpan`.
`CustomRenderMatcher` is a function that requires a `bool` to be returned. It exposes the `RenderContext` which provides `BuildContext` and access to the HTML tree.

The `CustomRender` class has two constructors: `CustomRender.widget()` and `CustomRender.inlineSpan()`. Both require a `<Widget/InlineSpan> Function(RenderContext, Function())`. The `Function()` argument is a function that will provide you with the element's children when needed.

To use this API, create a matching function and an instance of `CustomRender`.

Note: If you add any custom tags, you must add these tags to the [`tagsList`](#tagslist) parameter, otherwise they will not be rendered. See below for an example.

Expand All @@ -286,21 +290,21 @@ Widget html = Html(
<flutter horizontal></flutter>
""",
customRender: {
"bird": (RenderContext context, Widget child) {
return TextSpan(text: "🐦");
},
"flutter": (RenderContext context, Widget child) {
return FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
);
},
birdMatcher(): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
flutterMatcher(): 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,
)),
},
tagsList: Html.tags..addAll(["bird", "flutter"]),
);

CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';

CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
```

2. Complex example - wrapping the default widget with your own, in this case placing a horizontal scroll around a (potentially too wide) table.
Expand All @@ -318,14 +322,16 @@ Widget html = Html(
</table>
""",
customRender: {
"table": (context, child) {
tableMatcher(): CustomRender.widget(widget: (context, child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: (context.tree as TableLayoutElement).toWidget(context),
);
}
}),
},
);

CustomRenderMatcher tableMatcher() => (context) => context.tree.element?.localName == "table" ?? false;
```

</details>
Expand All @@ -343,43 +349,52 @@ Widget html = Html(
<iframe src="https://www.youtube.com/embed/tgbNymZ7vqY"></iframe>
""",
customRender: {
"iframe": (RenderContext context, Widget child) {
final attrs = context.tree.element?.attributes;
if (attrs != null) {
double? width = double.tryParse(attrs['width'] ?? "");
double? height = double.tryParse(attrs['height'] ?? "");
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: attrs['src'] ?? "about:blank",
javascriptMode: JavascriptMode.unrestricted,
//no need for scrolling gesture recognizers on embedded youtube, so set gestureRecognizers null
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
gestureRecognizers: attrs['src'] != null && attrs['src']!.contains("youtube.com/embed") ? null : [
Factory(() => VerticalDragGestureRecognizer())
].toSet(),
navigationDelegate: (NavigationRequest request) async {
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
//on other iframe content allow all url loading
if (attrs['src'] != null && attrs['src']!.contains("youtube.com/embed")) {
if (!request.url.contains("youtube.com/embed")) {
return NavigationDecision.prevent;
} else {
return NavigationDecision.navigate;
}
} else {
return NavigationDecision.navigate;
}
},
),
);
} else {
return Container(height: 0);
}
}
}
iframeYT(): CustomRender.widget(widget: (context, buildChildren) {
double? width = double.tryParse(context.tree.attributes['width'] ?? "");
double? height = double.tryParse(context.tree.attributes['height'] ?? "");
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: context.tree.attributes['src']!,
javascriptMode: JavascriptMode.unrestricted,
navigationDelegate: (NavigationRequest request) async {
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
if (!request.url.contains("youtube.com/embed")) {
return NavigationDecision.prevent;
} else {
return NavigationDecision.navigate;
}
},
),
);
}),
iframeOther(): CustomRender.widget(widget: (context, buildChildren) {
double? width = double.tryParse(context.tree.attributes['width'] ?? "");
double? height = double.tryParse(context.tree.attributes['height'] ?? "");
return Container(
width: width ?? (height ?? 150) * 2,
height: height ?? (width ?? 300) / 2,
child: WebView(
initialUrl: context.tree.attributes['src'],
javascriptMode: JavascriptMode.unrestricted,
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
gestureRecognizers: [
Factory(() => VerticalDragGestureRecognizer())
].toSet(),
),
);
}),
iframeNull(): CustomRender.widget(widget: (context, buildChildren) => Container(height: 0, width: 0)),
}
);

CustomRenderMatcher iframeYT() => (context) => context.tree.element?.attributes['src']?.contains("youtube.com/embed") ?? false;

CustomRenderMatcher iframeOther() => (context) => !(context.tree.element?.attributes['src']?.contains("youtube.com/embed")
?? context.tree.element?.attributes['src'] == null);

CustomRenderMatcher iframeNull() => (context) => context.tree.element?.attributes['src'] == null;
```
</details>

Expand Down Expand Up @@ -804,16 +819,23 @@ Then, use the `customRender` parameter to add the widget to render Tex. It could
Widget htmlWidget = Html(
data: r"""<tex>i\hbar\frac{\partial}{\partial t}\Psi(\vec x,t) = -\frac{\hbar}{2m}\nabla^2\Psi(\vec x,t)+ V(\vec x)\Psi(\vec x,t)</tex>""",
customRender: {
"tex": (RenderContext context, _) => Math.tex(
context.tree.element!.text,
texMatcher(): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
context.tree.element?.innerHtml ?? '',
mathStyle: MathStyle.display,
textStyle: context.style.generateTextStyle(),
onErrorFallback: (FlutterMathException e) {
//return your error widget here e.g.
return Text(e.message);
if (context.parser.onMathError != null) {
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
} else {
return Text(e.message);
}
},
),
)),
},
tagsList: Html.tags..add('tex'),
);

CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';
```

### Table
Expand Down
48 changes: 27 additions & 21 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_math_fork/flutter_math.dart';

void main() => runApp(new MyApp());

Expand Down Expand Up @@ -250,7 +251,6 @@ class _MyHomePageState extends State<MyHomePage> {
body: SingleChildScrollView(
child: Html(
data: htmlData,
tagsList: Html.tags..addAll(["bird", "flutter"]),
style: {
"table": Style(
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
Expand All @@ -268,26 +268,32 @@ class _MyHomePageState extends State<MyHomePage> {
),
'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis),
},
customRender: {
"table": (context, child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child:
(context.tree as TableLayoutElement).toWidget(context),
);
},
"bird": (RenderContext context, Widget child) {
return TextSpan(text: "🐦");
},
"flutter": (RenderContext context, Widget child) {
return FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
);
},
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) {
if (context.parser.onMathError != null) {
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
} else {
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: (context.tree as TableLayoutElement).toWidget(context),
)),
},
customImageRenders: {
networkSourceMatcher(domains: ["flutter.dev"]):
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: example
description: flutter_html example app.

publish_to: none
version: 1.0.0+1

environment:
Expand Down
Loading