Skip to content

Commit 2ffa1dd

Browse files
feat: Add WrapperExtension helper (#1264)
1 parent 8ac444b commit 2ffa1dd

22 files changed

+296
-62
lines changed

example/lib/main.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,14 @@ class MyHomePageState extends State<MyHomePage> {
317317
),
318318
},
319319
extensions: [
320+
TagWrapExtension(
321+
tagsToWrap: {"table"},
322+
builder: (child) {
323+
return SingleChildScrollView(
324+
scrollDirection: Axis.horizontal,
325+
child: child,
326+
);
327+
}),
320328
TagExtension(
321329
tagsToExtend: {"tex"},
322330
builder: (context) => Math.tex(

lib/src/builtins/details_element_builtin.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class DetailsElementBuiltIn extends HtmlExtension {
2424

2525
@override
2626
InlineSpan build(ExtensionContext context,
27-
Map<StyledElement, InlineSpan> Function() parseChildren) {
28-
final childList = parseChildren();
27+
Map<StyledElement, InlineSpan> Function() buildChildren) {
28+
final childList = buildChildren();
2929
final children = childList.values;
3030

3131
InlineSpan? firstChild = children.isNotEmpty ? children.first : null;

lib/src/builtins/image_builtin.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class ImageBuiltIn extends HtmlExtension {
7272

7373
@override
7474
InlineSpan build(ExtensionContext context,
75-
Map<StyledElement, InlineSpan> Function() parseChildren) {
75+
Map<StyledElement, InlineSpan> Function() buildChildren) {
7676
final element = context.styledElement as ImageElement;
7777

7878
final imageStyle = Style(

lib/src/builtins/interactive_element_builtin.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ class InteractiveElementBuiltIn extends HtmlExtension {
3939

4040
@override
4141
InlineSpan build(ExtensionContext context,
42-
Map<StyledElement, InlineSpan> Function() parseChildren) {
42+
Map<StyledElement, InlineSpan> Function() buildChildren) {
4343
return TextSpan(
44-
children: parseChildren().values.map((childSpan) {
44+
children: buildChildren().values.map((childSpan) {
4545
return _processInteractableChild(context, childSpan);
4646
}).toList(),
4747
);

lib/src/builtins/ruby_builtin.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class RubyBuiltIn extends HtmlExtension {
4040

4141
@override
4242
InlineSpan build(ExtensionContext context,
43-
Map<StyledElement, InlineSpan> Function() parseChildren) {
43+
Map<StyledElement, InlineSpan> Function() buildChildren) {
4444
StyledElement? node;
4545
List<Widget> widgets = <Widget>[];
4646
final rubySize = context.parser.style['rt']?.fontSize?.value ??

lib/src/builtins/styled_element_builtin.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ class StyledElementBuiltIn extends HtmlExtension {
409409

410410
@override
411411
InlineSpan build(ExtensionContext context,
412-
Map<StyledElement, InlineSpan> Function() parseChildren) {
412+
Map<StyledElement, InlineSpan> Function() buildChildren) {
413413
if (context.styledElement!.style.display == Display.listItem ||
414414
((context.styledElement!.style.display == Display.block ||
415415
context.styledElement!.style.display == Display.inlineBlock) &&
@@ -424,7 +424,7 @@ class StyledElementBuiltIn extends HtmlExtension {
424424
shrinkWrap: context.parser.shrinkWrap,
425425
childIsReplaced: ["iframe", "img", "video", "audio"]
426426
.contains(context.styledElement!.name),
427-
children: parseChildren()
427+
children: buildChildren()
428428
.entries
429429
.expandIndexed((i, child) => [
430430
child.value,
@@ -441,7 +441,7 @@ class StyledElementBuiltIn extends HtmlExtension {
441441

442442
return TextSpan(
443443
style: context.styledElement!.style.generateTextStyle(),
444-
children: parseChildren()
444+
children: buildChildren()
445445
.entries
446446
.expand((child) => [
447447
child.value,

lib/src/builtins/text_builtin.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class TextBuiltIn extends HtmlExtension {
4444

4545
@override
4646
InlineSpan build(ExtensionContext context,
47-
Map<StyledElement, InlineSpan> Function() parseChildren) {
47+
Map<StyledElement, InlineSpan> Function() buildChildren) {
4848
final element = context.styledElement! as TextContentElement;
4949
return TextSpan(
5050
style: element.style.generateTextStyle(),

lib/src/builtins/vertical_align_builtin.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ class VerticalAlignBuiltIn extends HtmlExtension {
2323
}
2424

2525
@override
26-
InlineSpan build(ExtensionContext context, parseChildren) {
26+
InlineSpan build(ExtensionContext context, buildChildren) {
2727
return WidgetSpan(
2828
child: Transform.translate(
2929
offset: Offset(0, _getVerticalOffset(context.styledElement!)),
3030
child: CssBoxWidget.withInlineSpanChildren(
31-
children: parseChildren().values.toList(),
31+
children: buildChildren().values.toList(),
3232
style: context.styledElement!.style,
3333
),
3434
),

lib/src/extension/helpers/image_extension.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ class ImageExtension extends ImageBuiltIn {
5959
}
6060

6161
@override
62-
InlineSpan build(ExtensionContext context, parseChildren) {
62+
InlineSpan build(ExtensionContext context, buildChildren) {
6363
if (builder != null) {
6464
return builder!.call(context);
6565
} else {
66-
return super.build(context, parseChildren);
66+
return super.build(context, buildChildren);
6767
}
6868
}
6969
}

lib/src/extension/helpers/image_tap_extension.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ class OnImageTapExtension extends ImageBuiltIn {
4444
}
4545

4646
@override
47-
InlineSpan build(ExtensionContext context, parseChildren) {
48-
final children = parseChildren();
47+
InlineSpan build(ExtensionContext context, buildChildren) {
48+
final children = buildChildren();
4949

5050
assert(children.keys.isNotEmpty,
5151
"The OnImageTapExtension has been thwarted! It no longer has an `img` child");

lib/src/extension/helpers/matcher_extension.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class MatcherExtension extends HtmlExtension {
3838
}
3939

4040
@override
41-
InlineSpan build(ExtensionContext context, parseChildren) {
41+
InlineSpan build(ExtensionContext context, buildChildren) {
4242
return builder(context);
4343
}
4444
}

lib/src/extension/helpers/tag_extension.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ class TagExtension extends HtmlExtension {
88
late final InlineSpan Function(ExtensionContext) builder;
99

1010
/// [TagExtension] allows you to extend the functionality of flutter_html
11-
/// by defining the behavior of custom tags to return a child widget.
11+
/// by defining a mapping from a custom or existing tag to a widget.
12+
///
13+
/// If instead you'd like to wrap a tag (or custom tag) in a widget,
14+
/// see [TagWrapExtension].
1215
TagExtension({
1316
required this.tagsToExtend,
1417
Widget? child,
@@ -23,8 +26,8 @@ class TagExtension extends HtmlExtension {
2326
}
2427

2528
/// [TagExtension.inline] allows you to extend the functionality of
26-
/// flutter_html by defining the behavior of custom tags to return
27-
/// a child InlineSpan.
29+
/// flutter_html by defining a mapping from a custom or existing tag
30+
/// to an InlineSpan.
2831
TagExtension.inline({
2932
required this.tagsToExtend,
3033
InlineSpan? child,
@@ -42,7 +45,7 @@ class TagExtension extends HtmlExtension {
4245
Set<String> get supportedTags => tagsToExtend;
4346

4447
@override
45-
InlineSpan build(ExtensionContext context, parseChildren) {
48+
InlineSpan build(ExtensionContext context, buildChildren) {
4649
return builder(context);
4750
}
4851
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:flutter_html/src/css_box_widget.dart';
3+
import 'package:flutter_html/src/extension/html_extension.dart';
4+
import 'package:flutter_html/src/style.dart';
5+
import 'package:flutter_html/src/tree/styled_element.dart';
6+
import 'package:html/dom.dart' as html;
7+
8+
class TagWrapExtension extends HtmlExtension {
9+
final Set<String> tagsToWrap;
10+
final Widget Function(Widget child) builder;
11+
12+
/// [TagWrapExtension] allows you to easily wrap a specific tag (or tags)
13+
/// in another element. For example, you could wrap `<table>` in a
14+
/// `SingleChildScrollView`:
15+
///
16+
/// ```dart
17+
/// extensions: [
18+
/// WrapperExtension(
19+
/// tagsToWrap: {"table"},
20+
/// builder: (child) {
21+
/// return SingleChildScrollView(
22+
/// scrollDirection: Axis.horizontal,
23+
/// child: child,
24+
/// );
25+
/// },
26+
/// ),
27+
/// ],
28+
/// ```
29+
TagWrapExtension({
30+
required this.tagsToWrap,
31+
required this.builder,
32+
});
33+
34+
@override
35+
Set<String> get supportedTags => tagsToWrap;
36+
37+
@override
38+
bool matches(ExtensionContext context) {
39+
switch (context.currentStep) {
40+
case CurrentStep.preparing:
41+
return super.matches(context);
42+
case CurrentStep.preStyling:
43+
case CurrentStep.preProcessing:
44+
return false;
45+
case CurrentStep.building:
46+
return context.styledElement is WrapperElement;
47+
}
48+
}
49+
50+
@override
51+
StyledElement prepare(
52+
ExtensionContext context, List<StyledElement> children) {
53+
return WrapperElement(
54+
child: context.parser.prepareFromExtension(
55+
context,
56+
children,
57+
extensionsToIgnore: {this},
58+
),
59+
);
60+
}
61+
62+
@override
63+
InlineSpan build(ExtensionContext context, buildChildren) {
64+
final children = buildChildren();
65+
final child = CssBoxWidget.withInlineSpanChildren(
66+
children: children.values.toList(),
67+
style: context.styledElement!.style,
68+
);
69+
70+
return WidgetSpan(
71+
child: builder.call(child),
72+
);
73+
}
74+
}
75+
76+
class WrapperElement extends StyledElement {
77+
WrapperElement({
78+
required StyledElement child,
79+
}) : super(
80+
node: html.Element.tag("wrapper-element"),
81+
style: Style(),
82+
children: [child],
83+
name: "[wrapper-element]",
84+
);
85+
}

lib/src/extension/html_extension.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export 'package:flutter_html/src/extension/helpers/tag_extension.dart';
88
export 'package:flutter_html/src/extension/helpers/matcher_extension.dart';
99
export 'package:flutter_html/src/extension/helpers/image_extension.dart';
1010
export 'package:flutter_html/src/extension/helpers/image_tap_extension.dart';
11+
export 'package:flutter_html/src/extension/helpers/tag_wrap_extension.dart';
1112

1213
/// The [HtmlExtension] class allows you to customize the behavior of flutter_html
1314
/// or add additional functionality.
@@ -58,7 +59,7 @@ abstract class HtmlExtension {
5859
/// attached `Style` elements, into an `InlineSpan` tree that includes
5960
/// Widget/TextSpans that can be rendered in a RichText widget.
6061
InlineSpan build(ExtensionContext context,
61-
Map<StyledElement, InlineSpan> Function() parseChildren) {
62+
Map<StyledElement, InlineSpan> Function() buildChildren) {
6263
throw UnimplementedError(
6364
"Extension `$runtimeType` matched `${context.styledElement!.name}` but didn't implement `parse`");
6465
}

lib/src/html_parser.dart

Lines changed: 59 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,61 @@ class HtmlParser extends StatefulWidget {
9292
}
9393
onLinkTap?.call(url, attributes, element);
9494
};
95+
96+
/// Prepares the html node using one of the built-ins or HtmlExtensions
97+
/// available. If none of the extensions matches, returns an
98+
/// EmptyContentElement
99+
StyledElement prepareFromExtension(
100+
ExtensionContext extensionContext,
101+
List<StyledElement> children, {
102+
Set<HtmlExtension> extensionsToIgnore = const {},
103+
}) {
104+
// Loop through every extension and see if it can handle this node
105+
for (final extension in extensions) {
106+
if (!extensionsToIgnore.contains(extension) &&
107+
extension.matches(extensionContext)) {
108+
return extension.prepare(extensionContext, children);
109+
}
110+
}
111+
112+
// Loop through built in elements and see if they can handle this node.
113+
for (final builtIn in builtIns) {
114+
if (!extensionsToIgnore.contains(builtIn) &&
115+
builtIn.matches(extensionContext)) {
116+
return builtIn.prepare(extensionContext, children);
117+
}
118+
}
119+
120+
// If no extension or built-in matches, then return an empty content element.
121+
return EmptyContentElement(node: extensionContext.node);
122+
}
123+
124+
/// Builds the StyledElement into an InlineSpan using one of the built-ins
125+
/// or HtmlExtensions available. If none of the extensions matches, returns
126+
/// an empty TextSpan.
127+
InlineSpan buildFromExtension(
128+
ExtensionContext extensionContext,
129+
Map<StyledElement, InlineSpan> Function() buildChildren, {
130+
Set<HtmlExtension> extensionsToIgnore = const {},
131+
}) {
132+
// Loop through every extension and see if it can handle this node
133+
for (final extension in extensions) {
134+
if (!extensionsToIgnore.contains(extension) &&
135+
extension.matches(extensionContext)) {
136+
return extension.build(extensionContext, buildChildren);
137+
}
138+
}
139+
140+
// Loop through built in elements and see if they can handle this node.
141+
for (final builtIn in builtIns) {
142+
if (!extensionsToIgnore.contains(builtIn) &&
143+
builtIn.matches(extensionContext)) {
144+
return builtIn.build(extensionContext, buildChildren);
145+
}
146+
}
147+
148+
return const TextSpan(text: "");
149+
}
95150
}
96151

97152
class _HtmlParserState extends State<HtmlParser> {
@@ -189,22 +244,8 @@ class _HtmlParserState extends State<HtmlParser> {
189244
// Lex this element's children
190245
final children = node.nodes.map(_prepareHtmlTreeRecursive).toList();
191246

192-
// Loop through every extension and see if it can handle this node
193-
for (final extension in widget.extensions) {
194-
if (extension.matches(extensionContext)) {
195-
return extension.prepare(extensionContext, children);
196-
}
197-
}
198-
199-
// Loop through built in elements and see if they can handle this node.
200-
for (final builtIn in HtmlParser.builtIns) {
201-
if (builtIn.matches(extensionContext)) {
202-
return builtIn.prepare(extensionContext, children);
203-
}
204-
}
205-
206-
// If no extension or built-in matches, then return an empty content element.
207-
return EmptyContentElement(node: node);
247+
// Prepare the element from one of the extensions
248+
return widget.prepareFromExtension(extensionContext, children);
208249
}
209250

210251
/// Called before any styling is cascaded on the tree
@@ -353,26 +394,12 @@ class _HtmlParserState extends State<HtmlParser> {
353394
}
354395

355396
// Generate a function that allows children to be generated
356-
Map<StyledElement, InlineSpan> parseChildren() {
397+
Map<StyledElement, InlineSpan> buildChildren() {
357398
return Map.fromEntries(tree.children.map((child) {
358399
return MapEntry(child, _buildTreeRecursive(child));
359400
}));
360401
}
361402

362-
// Loop through every extension and see if it can handle this node
363-
for (final extension in widget.extensions) {
364-
if (extension.matches(extensionContext)) {
365-
return extension.build(extensionContext, parseChildren);
366-
}
367-
}
368-
369-
// Loop through built in elements and see if they can handle this node.
370-
for (final builtIn in HtmlParser.builtIns) {
371-
if (builtIn.matches(extensionContext)) {
372-
return builtIn.build(extensionContext, parseChildren);
373-
}
374-
}
375-
376-
return const TextSpan(text: "");
403+
return widget.buildFromExtension(extensionContext, buildChildren);
377404
}
378405
}

0 commit comments

Comments
 (0)