Skip to content

Commit

Permalink
feat: Add WrapperExtension helper (#1264)
Browse files Browse the repository at this point in the history
  • Loading branch information
Sub6Resources committed May 15, 2023
1 parent 8ac444b commit 2ffa1dd
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 62 deletions.
8 changes: 8 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ class MyHomePageState extends State<MyHomePage> {
),
},
extensions: [
TagWrapExtension(
tagsToWrap: {"table"},
builder: (child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: child,
);
}),
TagExtension(
tagsToExtend: {"tex"},
builder: (context) => Math.tex(
Expand Down
4 changes: 2 additions & 2 deletions lib/src/builtins/details_element_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ class DetailsElementBuiltIn extends HtmlExtension {

@override
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
final childList = parseChildren();
Map<StyledElement, InlineSpan> Function() buildChildren) {
final childList = buildChildren();
final children = childList.values;

InlineSpan? firstChild = children.isNotEmpty ? children.first : null;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/builtins/image_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ImageBuiltIn extends HtmlExtension {

@override
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
Map<StyledElement, InlineSpan> Function() buildChildren) {
final element = context.styledElement as ImageElement;

final imageStyle = Style(
Expand Down
4 changes: 2 additions & 2 deletions lib/src/builtins/interactive_element_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ class InteractiveElementBuiltIn extends HtmlExtension {

@override
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
Map<StyledElement, InlineSpan> Function() buildChildren) {
return TextSpan(
children: parseChildren().values.map((childSpan) {
children: buildChildren().values.map((childSpan) {
return _processInteractableChild(context, childSpan);
}).toList(),
);
Expand Down
2 changes: 1 addition & 1 deletion lib/src/builtins/ruby_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class RubyBuiltIn extends HtmlExtension {

@override
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
Map<StyledElement, InlineSpan> Function() buildChildren) {
StyledElement? node;
List<Widget> widgets = <Widget>[];
final rubySize = context.parser.style['rt']?.fontSize?.value ??
Expand Down
6 changes: 3 additions & 3 deletions lib/src/builtins/styled_element_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ class StyledElementBuiltIn extends HtmlExtension {

@override
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
Map<StyledElement, InlineSpan> Function() buildChildren) {
if (context.styledElement!.style.display == Display.listItem ||
((context.styledElement!.style.display == Display.block ||
context.styledElement!.style.display == Display.inlineBlock) &&
Expand All @@ -424,7 +424,7 @@ class StyledElementBuiltIn extends HtmlExtension {
shrinkWrap: context.parser.shrinkWrap,
childIsReplaced: ["iframe", "img", "video", "audio"]
.contains(context.styledElement!.name),
children: parseChildren()
children: buildChildren()
.entries
.expandIndexed((i, child) => [
child.value,
Expand All @@ -441,7 +441,7 @@ class StyledElementBuiltIn extends HtmlExtension {

return TextSpan(
style: context.styledElement!.style.generateTextStyle(),
children: parseChildren()
children: buildChildren()
.entries
.expand((child) => [
child.value,
Expand Down
2 changes: 1 addition & 1 deletion lib/src/builtins/text_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class TextBuiltIn extends HtmlExtension {

@override
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
Map<StyledElement, InlineSpan> Function() buildChildren) {
final element = context.styledElement! as TextContentElement;
return TextSpan(
style: element.style.generateTextStyle(),
Expand Down
4 changes: 2 additions & 2 deletions lib/src/builtins/vertical_align_builtin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ class VerticalAlignBuiltIn extends HtmlExtension {
}

@override
InlineSpan build(ExtensionContext context, parseChildren) {
InlineSpan build(ExtensionContext context, buildChildren) {
return WidgetSpan(
child: Transform.translate(
offset: Offset(0, _getVerticalOffset(context.styledElement!)),
child: CssBoxWidget.withInlineSpanChildren(
children: parseChildren().values.toList(),
children: buildChildren().values.toList(),
style: context.styledElement!.style,
),
),
Expand Down
4 changes: 2 additions & 2 deletions lib/src/extension/helpers/image_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ class ImageExtension extends ImageBuiltIn {
}

@override
InlineSpan build(ExtensionContext context, parseChildren) {
InlineSpan build(ExtensionContext context, buildChildren) {
if (builder != null) {
return builder!.call(context);
} else {
return super.build(context, parseChildren);
return super.build(context, buildChildren);
}
}
}
4 changes: 2 additions & 2 deletions lib/src/extension/helpers/image_tap_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ class OnImageTapExtension extends ImageBuiltIn {
}

@override
InlineSpan build(ExtensionContext context, parseChildren) {
final children = parseChildren();
InlineSpan build(ExtensionContext context, buildChildren) {
final children = buildChildren();

assert(children.keys.isNotEmpty,
"The OnImageTapExtension has been thwarted! It no longer has an `img` child");
Expand Down
2 changes: 1 addition & 1 deletion lib/src/extension/helpers/matcher_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class MatcherExtension extends HtmlExtension {
}

@override
InlineSpan build(ExtensionContext context, parseChildren) {
InlineSpan build(ExtensionContext context, buildChildren) {
return builder(context);
}
}
11 changes: 7 additions & 4 deletions lib/src/extension/helpers/tag_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ class TagExtension extends HtmlExtension {
late final InlineSpan Function(ExtensionContext) builder;

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

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

@override
InlineSpan build(ExtensionContext context, parseChildren) {
InlineSpan build(ExtensionContext context, buildChildren) {
return builder(context);
}
}
85 changes: 85 additions & 0 deletions lib/src/extension/helpers/tag_wrap_extension.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_html/src/css_box_widget.dart';
import 'package:flutter_html/src/extension/html_extension.dart';
import 'package:flutter_html/src/style.dart';
import 'package:flutter_html/src/tree/styled_element.dart';
import 'package:html/dom.dart' as html;

class TagWrapExtension extends HtmlExtension {
final Set<String> tagsToWrap;
final Widget Function(Widget child) builder;

/// [TagWrapExtension] allows you to easily wrap a specific tag (or tags)
/// in another element. For example, you could wrap `<table>` in a
/// `SingleChildScrollView`:
///
/// ```dart
/// extensions: [
/// WrapperExtension(
/// tagsToWrap: {"table"},
/// builder: (child) {
/// return SingleChildScrollView(
/// scrollDirection: Axis.horizontal,
/// child: child,
/// );
/// },
/// ),
/// ],
/// ```
TagWrapExtension({
required this.tagsToWrap,
required this.builder,
});

@override
Set<String> get supportedTags => tagsToWrap;

@override
bool matches(ExtensionContext context) {
switch (context.currentStep) {
case CurrentStep.preparing:
return super.matches(context);
case CurrentStep.preStyling:
case CurrentStep.preProcessing:
return false;
case CurrentStep.building:
return context.styledElement is WrapperElement;
}
}

@override
StyledElement prepare(
ExtensionContext context, List<StyledElement> children) {
return WrapperElement(
child: context.parser.prepareFromExtension(
context,
children,
extensionsToIgnore: {this},
),
);
}

@override
InlineSpan build(ExtensionContext context, buildChildren) {
final children = buildChildren();
final child = CssBoxWidget.withInlineSpanChildren(
children: children.values.toList(),
style: context.styledElement!.style,
);

return WidgetSpan(
child: builder.call(child),
);
}
}

class WrapperElement extends StyledElement {
WrapperElement({
required StyledElement child,
}) : super(
node: html.Element.tag("wrapper-element"),
style: Style(),
children: [child],
name: "[wrapper-element]",
);
}
3 changes: 2 additions & 1 deletion lib/src/extension/html_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export 'package:flutter_html/src/extension/helpers/tag_extension.dart';
export 'package:flutter_html/src/extension/helpers/matcher_extension.dart';
export 'package:flutter_html/src/extension/helpers/image_extension.dart';
export 'package:flutter_html/src/extension/helpers/image_tap_extension.dart';
export 'package:flutter_html/src/extension/helpers/tag_wrap_extension.dart';

/// The [HtmlExtension] class allows you to customize the behavior of flutter_html
/// or add additional functionality.
Expand Down Expand Up @@ -58,7 +59,7 @@ abstract class HtmlExtension {
/// attached `Style` elements, into an `InlineSpan` tree that includes
/// Widget/TextSpans that can be rendered in a RichText widget.
InlineSpan build(ExtensionContext context,
Map<StyledElement, InlineSpan> Function() parseChildren) {
Map<StyledElement, InlineSpan> Function() buildChildren) {
throw UnimplementedError(
"Extension `$runtimeType` matched `${context.styledElement!.name}` but didn't implement `parse`");
}
Expand Down
91 changes: 59 additions & 32 deletions lib/src/html_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,61 @@ class HtmlParser extends StatefulWidget {
}
onLinkTap?.call(url, attributes, element);
};

/// Prepares the html node using one of the built-ins or HtmlExtensions
/// available. If none of the extensions matches, returns an
/// EmptyContentElement
StyledElement prepareFromExtension(
ExtensionContext extensionContext,
List<StyledElement> children, {
Set<HtmlExtension> extensionsToIgnore = const {},
}) {
// Loop through every extension and see if it can handle this node
for (final extension in extensions) {
if (!extensionsToIgnore.contains(extension) &&
extension.matches(extensionContext)) {
return extension.prepare(extensionContext, children);
}
}

// Loop through built in elements and see if they can handle this node.
for (final builtIn in builtIns) {
if (!extensionsToIgnore.contains(builtIn) &&
builtIn.matches(extensionContext)) {
return builtIn.prepare(extensionContext, children);
}
}

// If no extension or built-in matches, then return an empty content element.
return EmptyContentElement(node: extensionContext.node);
}

/// Builds the StyledElement into an InlineSpan using one of the built-ins
/// or HtmlExtensions available. If none of the extensions matches, returns
/// an empty TextSpan.
InlineSpan buildFromExtension(
ExtensionContext extensionContext,
Map<StyledElement, InlineSpan> Function() buildChildren, {
Set<HtmlExtension> extensionsToIgnore = const {},
}) {
// Loop through every extension and see if it can handle this node
for (final extension in extensions) {
if (!extensionsToIgnore.contains(extension) &&
extension.matches(extensionContext)) {
return extension.build(extensionContext, buildChildren);
}
}

// Loop through built in elements and see if they can handle this node.
for (final builtIn in builtIns) {
if (!extensionsToIgnore.contains(builtIn) &&
builtIn.matches(extensionContext)) {
return builtIn.build(extensionContext, buildChildren);
}
}

return const TextSpan(text: "");
}
}

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

// Loop through every extension and see if it can handle this node
for (final extension in widget.extensions) {
if (extension.matches(extensionContext)) {
return extension.prepare(extensionContext, children);
}
}

// Loop through built in elements and see if they can handle this node.
for (final builtIn in HtmlParser.builtIns) {
if (builtIn.matches(extensionContext)) {
return builtIn.prepare(extensionContext, children);
}
}

// If no extension or built-in matches, then return an empty content element.
return EmptyContentElement(node: node);
// Prepare the element from one of the extensions
return widget.prepareFromExtension(extensionContext, children);
}

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

// Generate a function that allows children to be generated
Map<StyledElement, InlineSpan> parseChildren() {
Map<StyledElement, InlineSpan> buildChildren() {
return Map.fromEntries(tree.children.map((child) {
return MapEntry(child, _buildTreeRecursive(child));
}));
}

// Loop through every extension and see if it can handle this node
for (final extension in widget.extensions) {
if (extension.matches(extensionContext)) {
return extension.build(extensionContext, parseChildren);
}
}

// Loop through built in elements and see if they can handle this node.
for (final builtIn in HtmlParser.builtIns) {
if (builtIn.matches(extensionContext)) {
return builtIn.build(extensionContext, parseChildren);
}
}

return const TextSpan(text: "");
return widget.buildFromExtension(extensionContext, buildChildren);
}
}
Loading

0 comments on commit 2ffa1dd

Please sign in to comment.