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 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