Skip to content

Commit b5aa467

Browse files
authored
Merge pull request Sub6Resources#632 from tneotia/feature/upgrade-custom-render
Upgrade customRender to imitate customImageRender & add support for customRender for SelectableHtml
2 parents 6de34b9 + 742a20c commit b5aa467

File tree

8 files changed

+552
-327
lines changed

8 files changed

+552
-327
lines changed

README.md

Lines changed: 79 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,13 @@ Inner links (such as `<a href="#top">Back to the top</a>` will work out of the b
269269

270270
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!
271271

272-
`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,.
272+
`customRender` accepts a `Map<CustomRenderMatcher, CustomRender>`.
273273

274-
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`.
274+
`CustomRenderMatcher` is a function that requires a `bool` to be returned. It exposes the `RenderContext` which provides `BuildContext` and access to the HTML tree.
275+
276+
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.
277+
278+
To use this API, create a matching function and an instance of `CustomRender`.
275279

276280
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.
277281

@@ -286,21 +290,21 @@ Widget html = Html(
286290
<flutter horizontal></flutter>
287291
""",
288292
customRender: {
289-
"bird": (RenderContext context, Widget child) {
290-
return TextSpan(text: "🐦");
291-
},
292-
"flutter": (RenderContext context, Widget child) {
293-
return FlutterLogo(
294-
style: (context.tree.element!.attributes['horizontal'] != null)
295-
? FlutterLogoStyle.horizontal
296-
: FlutterLogoStyle.markOnly,
297-
textColor: context.style.color!,
298-
size: context.style.fontSize!.size! * 5,
299-
);
300-
},
293+
birdMatcher(): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
294+
flutterMatcher(): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
295+
style: (context.tree.element!.attributes['horizontal'] != null)
296+
? FlutterLogoStyle.horizontal
297+
: FlutterLogoStyle.markOnly,
298+
textColor: context.style.color!,
299+
size: context.style.fontSize!.size! * 5,
300+
)),
301301
},
302302
tagsList: Html.tags..addAll(["bird", "flutter"]),
303303
);
304+
305+
CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';
306+
307+
CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
304308
```
305309

306310
2. Complex example - wrapping the default widget with your own, in this case placing a horizontal scroll around a (potentially too wide) table.
@@ -318,14 +322,16 @@ Widget html = Html(
318322
</table>
319323
""",
320324
customRender: {
321-
"table": (context, child) {
325+
tableMatcher(): CustomRender.widget(widget: (context, child) {
322326
return SingleChildScrollView(
323327
scrollDirection: Axis.horizontal,
324328
child: (context.tree as TableLayoutElement).toWidget(context),
325329
);
326-
}
330+
}),
327331
},
328332
);
333+
334+
CustomRenderMatcher tableMatcher() => (context) => context.tree.element?.localName == "table" ?? false;
329335
```
330336

331337
</details>
@@ -343,43 +349,52 @@ Widget html = Html(
343349
<iframe src="https://www.youtube.com/embed/tgbNymZ7vqY"></iframe>
344350
""",
345351
customRender: {
346-
"iframe": (RenderContext context, Widget child) {
347-
final attrs = context.tree.element?.attributes;
348-
if (attrs != null) {
349-
double? width = double.tryParse(attrs['width'] ?? "");
350-
double? height = double.tryParse(attrs['height'] ?? "");
351-
return Container(
352-
width: width ?? (height ?? 150) * 2,
353-
height: height ?? (width ?? 300) / 2,
354-
child: WebView(
355-
initialUrl: attrs['src'] ?? "about:blank",
356-
javascriptMode: JavascriptMode.unrestricted,
357-
//no need for scrolling gesture recognizers on embedded youtube, so set gestureRecognizers null
358-
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
359-
gestureRecognizers: attrs['src'] != null && attrs['src']!.contains("youtube.com/embed") ? null : [
360-
Factory(() => VerticalDragGestureRecognizer())
361-
].toSet(),
362-
navigationDelegate: (NavigationRequest request) async {
363-
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
364-
//on other iframe content allow all url loading
365-
if (attrs['src'] != null && attrs['src']!.contains("youtube.com/embed")) {
366-
if (!request.url.contains("youtube.com/embed")) {
367-
return NavigationDecision.prevent;
368-
} else {
369-
return NavigationDecision.navigate;
370-
}
371-
} else {
372-
return NavigationDecision.navigate;
373-
}
374-
},
375-
),
376-
);
377-
} else {
378-
return Container(height: 0);
379-
}
380-
}
381-
}
352+
iframeYT(): CustomRender.widget(widget: (context, buildChildren) {
353+
double? width = double.tryParse(context.tree.attributes['width'] ?? "");
354+
double? height = double.tryParse(context.tree.attributes['height'] ?? "");
355+
return Container(
356+
width: width ?? (height ?? 150) * 2,
357+
height: height ?? (width ?? 300) / 2,
358+
child: WebView(
359+
initialUrl: context.tree.attributes['src']!,
360+
javascriptMode: JavascriptMode.unrestricted,
361+
navigationDelegate: (NavigationRequest request) async {
362+
//no need to load any url besides the embedded youtube url when displaying embedded youtube, so prevent url loading
363+
if (!request.url.contains("youtube.com/embed")) {
364+
return NavigationDecision.prevent;
365+
} else {
366+
return NavigationDecision.navigate;
367+
}
368+
},
369+
),
370+
);
371+
}),
372+
iframeOther(): CustomRender.widget(widget: (context, buildChildren) {
373+
double? width = double.tryParse(context.tree.attributes['width'] ?? "");
374+
double? height = double.tryParse(context.tree.attributes['height'] ?? "");
375+
return Container(
376+
width: width ?? (height ?? 150) * 2,
377+
height: height ?? (width ?? 300) / 2,
378+
child: WebView(
379+
initialUrl: context.tree.attributes['src'],
380+
javascriptMode: JavascriptMode.unrestricted,
381+
//on other iframe content scrolling might be necessary, so use VerticalDragGestureRecognizer
382+
gestureRecognizers: [
383+
Factory(() => VerticalDragGestureRecognizer())
384+
].toSet(),
385+
),
386+
);
387+
}),
388+
iframeNull(): CustomRender.widget(widget: (context, buildChildren) => Container(height: 0, width: 0)),
389+
}
382390
);
391+
392+
CustomRenderMatcher iframeYT() => (context) => context.tree.element?.attributes['src']?.contains("youtube.com/embed") ?? false;
393+
394+
CustomRenderMatcher iframeOther() => (context) => !(context.tree.element?.attributes['src']?.contains("youtube.com/embed")
395+
?? context.tree.element?.attributes['src'] == null);
396+
397+
CustomRenderMatcher iframeNull() => (context) => context.tree.element?.attributes['src'] == null;
383398
```
384399
</details>
385400

@@ -804,16 +819,23 @@ Then, use the `customRender` parameter to add the widget to render Tex. It could
804819
Widget htmlWidget = Html(
805820
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>""",
806821
customRender: {
807-
"tex": (RenderContext context, _) => Math.tex(
808-
context.tree.element!.text,
822+
texMatcher(): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
823+
context.tree.element?.innerHtml ?? '',
824+
mathStyle: MathStyle.display,
825+
textStyle: context.style.generateTextStyle(),
809826
onErrorFallback: (FlutterMathException e) {
810-
//return your error widget here e.g.
811-
return Text(e.message);
827+
if (context.parser.onMathError != null) {
828+
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
829+
} else {
830+
return Text(e.message);
831+
}
812832
},
813-
),
833+
)),
814834
},
815835
tagsList: Html.tags..add('tex'),
816836
);
837+
838+
CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';
817839
```
818840

819841
### Table

example/lib/main.dart

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_html/flutter_html.dart';
3+
import 'package:flutter_math_fork/flutter_math.dart';
34

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

@@ -250,7 +251,6 @@ class _MyHomePageState extends State<MyHomePage> {
250251
body: SingleChildScrollView(
251252
child: Html(
252253
data: htmlData,
253-
tagsList: Html.tags..addAll(["bird", "flutter"]),
254254
style: {
255255
"table": Style(
256256
backgroundColor: Color.fromARGB(0x50, 0xee, 0xee, 0xee),
@@ -268,26 +268,32 @@ class _MyHomePageState extends State<MyHomePage> {
268268
),
269269
'h5': Style(maxLines: 2, textOverflow: TextOverflow.ellipsis),
270270
},
271-
customRender: {
272-
"table": (context, child) {
273-
return SingleChildScrollView(
274-
scrollDirection: Axis.horizontal,
275-
child:
276-
(context.tree as TableLayoutElement).toWidget(context),
277-
);
278-
},
279-
"bird": (RenderContext context, Widget child) {
280-
return TextSpan(text: "🐦");
281-
},
282-
"flutter": (RenderContext context, Widget child) {
283-
return FlutterLogo(
284-
style: (context.tree.element!.attributes['horizontal'] != null)
285-
? FlutterLogoStyle.horizontal
286-
: FlutterLogoStyle.markOnly,
287-
textColor: context.style.color!,
288-
size: context.style.fontSize!.size! * 5,
289-
);
290-
},
271+
tagsList: Html.tags..addAll(["tex", "bird", "flutter"]),
272+
customRenders: {
273+
tagMatcher("tex"): CustomRender.widget(widget: (context, buildChildren) => Math.tex(
274+
context.tree.element?.innerHtml ?? '',
275+
mathStyle: MathStyle.display,
276+
textStyle: context.style.generateTextStyle(),
277+
onErrorFallback: (FlutterMathException e) {
278+
if (context.parser.onMathError != null) {
279+
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
280+
} else {
281+
return Text(e.message);
282+
}
283+
},
284+
)),
285+
tagMatcher("bird"): CustomRender.inlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
286+
tagMatcher("flutter"): CustomRender.widget(widget: (context, buildChildren) => FlutterLogo(
287+
style: (context.tree.element!.attributes['horizontal'] != null)
288+
? FlutterLogoStyle.horizontal
289+
: FlutterLogoStyle.markOnly,
290+
textColor: context.style.color!,
291+
size: context.style.fontSize!.size! * 5,
292+
)),
293+
tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView(
294+
scrollDirection: Axis.horizontal,
295+
child: (context.tree as TableLayoutElement).toWidget(context),
296+
)),
291297
},
292298
customImageRenders: {
293299
networkSourceMatcher(domains: ["flutter.dev"]):

example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: example
22
description: flutter_html example app.
3-
3+
publish_to: none
44
version: 1.0.0+1
55

66
environment:

0 commit comments

Comments
 (0)