diff --git a/lib/src/builtins/details_element_builtin.dart b/lib/src/builtins/details_element_builtin.dart index d3a48a7db6..1a2957f9b7 100644 --- a/lib/src/builtins/details_element_builtin.dart +++ b/lib/src/builtins/details_element_builtin.dart @@ -23,9 +23,8 @@ class DetailsElementBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { - final childList = buildChildren(); + InlineSpan build(ExtensionContext context) { + final childList = context.builtChildrenMap!; final children = childList.values; InlineSpan? firstChild = children.isNotEmpty ? children.first : null; diff --git a/lib/src/builtins/image_builtin.dart b/lib/src/builtins/image_builtin.dart index 89da7c8589..08bfd1b70d 100644 --- a/lib/src/builtins/image_builtin.dart +++ b/lib/src/builtins/image_builtin.dart @@ -71,8 +71,7 @@ class ImageBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { final element = context.styledElement as ImageElement; final imageStyle = Style( diff --git a/lib/src/builtins/interactive_element_builtin.dart b/lib/src/builtins/interactive_element_builtin.dart index a6fc71cde5..e8486b0701 100644 --- a/lib/src/builtins/interactive_element_builtin.dart +++ b/lib/src/builtins/interactive_element_builtin.dart @@ -37,10 +37,9 @@ class InteractiveElementBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { return TextSpan( - children: buildChildren().values.map((childSpan) { + children: context.inlineSpanChildren!.map((childSpan) { return _processInteractableChild(context, childSpan); }).toList(), ); diff --git a/lib/src/builtins/ruby_builtin.dart b/lib/src/builtins/ruby_builtin.dart index f91b8c5279..4ecc6ed744 100644 --- a/lib/src/builtins/ruby_builtin.dart +++ b/lib/src/builtins/ruby_builtin.dart @@ -39,8 +39,7 @@ class RubyBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { StyledElement? node; List widgets = []; final rubySize = context.parser.style['rt']?.fontSize?.value ?? diff --git a/lib/src/builtins/styled_element_builtin.dart b/lib/src/builtins/styled_element_builtin.dart index 226eb165f1..08a9f49db1 100644 --- a/lib/src/builtins/styled_element_builtin.dart +++ b/lib/src/builtins/styled_element_builtin.dart @@ -414,8 +414,7 @@ class StyledElementBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { if (context.styledElement!.style.display == Display.listItem || ((context.styledElement!.style.display == Display.block || context.styledElement!.style.display == Display.inlineBlock) && @@ -430,8 +429,7 @@ class StyledElementBuiltIn extends HtmlExtension { shrinkWrap: context.parser.shrinkWrap, childIsReplaced: ["iframe", "img", "video", "audio"] .contains(context.styledElement!.name), - children: buildChildren() - .entries + children: context.builtChildrenMap!.entries .expandIndexed((i, child) => [ child.value, if (context.parser.shrinkWrap && @@ -448,8 +446,7 @@ class StyledElementBuiltIn extends HtmlExtension { return TextSpan( style: context.styledElement!.style.generateTextStyle(), - children: buildChildren() - .entries + children: context.builtChildrenMap!.entries .expandIndexed((index, child) => [ child.value, if (context.parser.shrinkWrap && diff --git a/lib/src/builtins/text_builtin.dart b/lib/src/builtins/text_builtin.dart index 2221c3a7bd..09eca0d75a 100644 --- a/lib/src/builtins/text_builtin.dart +++ b/lib/src/builtins/text_builtin.dart @@ -41,8 +41,7 @@ class TextBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { if (context.styledElement is LinebreakContentElement) { return TextSpan( text: '\n', diff --git a/lib/src/builtins/vertical_align_builtin.dart b/lib/src/builtins/vertical_align_builtin.dart index 81617c37f4..69259c89ed 100644 --- a/lib/src/builtins/vertical_align_builtin.dart +++ b/lib/src/builtins/vertical_align_builtin.dart @@ -23,12 +23,12 @@ class VerticalAlignBuiltIn extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { return WidgetSpan( child: Transform.translate( offset: Offset(0, _getVerticalOffset(context.styledElement!)), child: CssBoxWidget.withInlineSpanChildren( - children: buildChildren().values.toList(), + children: context.inlineSpanChildren!, style: context.styledElement!.style, ), ), diff --git a/lib/src/extension/extension_context.dart b/lib/src/extension/extension_context.dart index 6cd86a04ad..b7a7a044a6 100644 --- a/lib/src/extension/extension_context.dart +++ b/lib/src/extension/extension_context.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'package:flutter/widgets.dart'; import 'package:flutter_html/src/html_parser.dart'; +import 'package:flutter_html/src/style.dart'; import 'package:flutter_html/src/tree/styled_element.dart'; import 'package:html/dom.dart' as html; @@ -59,7 +60,8 @@ class ExtensionContext { })); } - /// Returns the id of the element, or an empty string if it is not present + /// Returns the id of the element, or an empty string if it is not present or + /// this Node is not an html Element. String get id { if (node is html.Element) { return (node as html.Element).id; @@ -68,8 +70,8 @@ class ExtensionContext { return ''; } - /// Returns a set of classes on the element, or an empty set if none are - /// present. + /// Returns a set of classes on this Element, or an empty set if none are + /// present or this Node is not an html Element. Set get classes { if (node is html.Element) { return (node as html.Element).classes; @@ -83,38 +85,58 @@ class ExtensionContext { final HtmlParser parser; /// A reference to the [StyledElement] representation of this node. - /// Guaranteed to be non-null only after the lexing step + /// Guaranteed to be non-null only after the preparing step final StyledElement? styledElement; - /// Guaranteed only when in the `parse` method of an Extension, but it might not necessarily be the nearest BuildContext. Probably should use a `Builder` Widget if you absolutely need the most relevant BuildContext. + /// A reference to the [Style] on the [StyledElement] representation of this + /// node. Guaranteed to be non-null only after the preparing step. + Style? get style { + return styledElement?.style; + } + + /// The [StyledElement] version of this node's children. Guaranteed to be + /// non-null only after the preparing step. + List get styledElementChildren { + return styledElement!.children; + } + + final BuildChildrenCallback? _callbackToBuildChildren; + Map? _builtChildren; + + /// A map between the original [StyledElement] children of this node and the + /// fully built [InlineSpan] children of this node. + Map? get builtChildrenMap { + _builtChildren ??= _callbackToBuildChildren?.call(); + + return _builtChildren; + } + + /// The [InlineSpan] version of this node's children. Constructed lazily. + /// Guaranteed to be non-null only when `currentStep` is `building`. + List? get inlineSpanChildren { + _builtChildren ??= _callbackToBuildChildren?.call(); + + return _builtChildren?.values.toList(); + } + + /// Guaranteed to be non-null only when `currentStep` is `building`, + /// but it might not necessarily be the nearest BuildContext. Probably should + /// use a `Builder` Widget if you need the most relevant BuildContext. final BuildContext? buildContext; /// Constructs a new [ExtensionContext] object with the given information. - const ExtensionContext({ + ExtensionContext({ + required this.currentStep, required this.node, required this.parser, this.styledElement, this.buildContext, - required this.currentStep, - }); - - ExtensionContext copyWith({ - html.Node? node, - HtmlParser? parser, - StyledElement? styledElement, - BuildContext? buildContext, - CurrentStep? currentStep, - }) { - return ExtensionContext( - node: node ?? this.node, - parser: parser ?? this.parser, - styledElement: styledElement ?? this.styledElement, - buildContext: buildContext ?? this.buildContext, - currentStep: currentStep ?? this.currentStep, - ); - } + BuildChildrenCallback? buildChildrenCallback, + }) : _callbackToBuildChildren = buildChildrenCallback; } +typedef BuildChildrenCallback = Map Function(); + enum CurrentStep { preparing, preStyling, diff --git a/lib/src/extension/helpers/image_extension.dart b/lib/src/extension/helpers/image_extension.dart index 373c4cf22c..76ce26e933 100644 --- a/lib/src/extension/helpers/image_extension.dart +++ b/lib/src/extension/helpers/image_extension.dart @@ -59,11 +59,11 @@ class ImageExtension extends ImageBuiltIn { } @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { if (builder != null) { return builder!.call(context); } else { - return super.build(context, buildChildren); + return super.build(context); } } } diff --git a/lib/src/extension/helpers/image_tap_extension.dart b/lib/src/extension/helpers/image_tap_extension.dart index 1d7bcf7491..a3c913ab44 100644 --- a/lib/src/extension/helpers/image_tap_extension.dart +++ b/lib/src/extension/helpers/image_tap_extension.dart @@ -44,11 +44,13 @@ class OnImageTapExtension extends ImageBuiltIn { } @override - InlineSpan build(ExtensionContext context, buildChildren) { - final children = buildChildren(); + InlineSpan build(ExtensionContext context) { + final children = context.builtChildrenMap!; - assert(children.keys.isNotEmpty, - "The OnImageTapExtension has been thwarted! It no longer has an `img` child"); + assert( + children.keys.isNotEmpty, + "The OnImageTapExtension has been thwarted! It no longer has an `img` child", + ); final actualImage = children.keys.first; diff --git a/lib/src/extension/helpers/matcher_extension.dart b/lib/src/extension/helpers/matcher_extension.dart index c0b96ba823..331b095aa9 100644 --- a/lib/src/extension/helpers/matcher_extension.dart +++ b/lib/src/extension/helpers/matcher_extension.dart @@ -38,7 +38,7 @@ class MatcherExtension extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { return builder(context); } } diff --git a/lib/src/extension/helpers/tag_extension.dart b/lib/src/extension/helpers/tag_extension.dart index e36a42c910..4390146686 100644 --- a/lib/src/extension/helpers/tag_extension.dart +++ b/lib/src/extension/helpers/tag_extension.dart @@ -45,7 +45,7 @@ class TagExtension extends HtmlExtension { Set get supportedTags => tagsToExtend; @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { return builder(context); } } diff --git a/lib/src/extension/helpers/tag_wrap_extension.dart b/lib/src/extension/helpers/tag_wrap_extension.dart index 827729dd0b..e155998863 100644 --- a/lib/src/extension/helpers/tag_wrap_extension.dart +++ b/lib/src/extension/helpers/tag_wrap_extension.dart @@ -60,11 +60,10 @@ class TagWrapExtension extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, buildChildren) { - final children = buildChildren(); + InlineSpan build(ExtensionContext context) { final child = CssBoxWidget.withInlineSpanChildren( - children: children.values.toList(), - style: context.styledElement!.style, + children: context.inlineSpanChildren!, + style: context.style!, ); return WidgetSpan( diff --git a/lib/src/extension/html_extension.dart b/lib/src/extension/html_extension.dart index 85f1dccca5..80d7870838 100644 --- a/lib/src/extension/html_extension.dart +++ b/lib/src/extension/html_extension.dart @@ -58,10 +58,10 @@ abstract class HtmlExtension { /// The final step in the chain. Converts the StyledElement tree, with its /// attached `Style` elements, into an `InlineSpan` tree that includes /// Widget/TextSpans that can be rendered in a RichText widget. - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { throw UnimplementedError( - "Extension `$runtimeType` matched `${context.styledElement!.name}` but didn't implement `parse`"); + "Extension `$runtimeType` matched `${context.styledElement!.name}` but didn't implement `parse`", + ); } /// Called when the Html widget is being destroyed. This would be a very diff --git a/lib/src/html_parser.dart b/lib/src/html_parser.dart index 7e5775bafc..287063fb6a 100644 --- a/lib/src/html_parser.dart +++ b/lib/src/html_parser.dart @@ -125,15 +125,14 @@ class HtmlParser extends StatefulWidget { /// or HtmlExtensions available. If none of the extensions matches, returns /// an empty TextSpan. InlineSpan buildFromExtension( - ExtensionContext extensionContext, - Map Function() buildChildren, { + ExtensionContext extensionContext, { Set 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); + return extension.build(extensionContext); } } @@ -141,7 +140,7 @@ class HtmlParser extends StatefulWidget { for (final builtIn in builtIns) { if (!extensionsToIgnore.contains(builtIn) && builtIn.matches(extensionContext)) { - return builtIn.build(extensionContext, buildChildren); + return builtIn.build(extensionContext); } } @@ -380,6 +379,13 @@ class _HtmlParserState extends State { } InlineSpan _buildTreeRecursive(StyledElement tree) { + // Generate a function that allows children to be built lazily + Map buildChildren() { + return Map.fromEntries(tree.children.map((child) { + return MapEntry(child, _buildTreeRecursive(child)); + })); + } + // Set the extension context for this node. final extensionContext = ExtensionContext( parser: widget, @@ -387,6 +393,7 @@ class _HtmlParserState extends State { node: tree.node, styledElement: tree, currentStep: CurrentStep.building, + buildChildrenCallback: buildChildren, ); // Block restricted tags from getting sent to extensions @@ -394,13 +401,6 @@ class _HtmlParserState extends State { return const TextSpan(text: ""); } - // Generate a function that allows children to be generated - Map buildChildren() { - return Map.fromEntries(tree.children.map((child) { - return MapEntry(child, _buildTreeRecursive(child)); - })); - } - - return widget.buildFromExtension(extensionContext, buildChildren); + return widget.buildFromExtension(extensionContext); } } diff --git a/lib/src/processing/lists.dart b/lib/src/processing/lists.dart index e8859a228e..d52403f060 100644 --- a/lib/src/processing/lists.dart +++ b/lib/src/processing/lists.dart @@ -2,7 +2,6 @@ import 'dart:collection'; import 'package:collection/collection.dart'; import 'package:flutter_html/flutter_html.dart'; -import 'package:flutter_html/src/style/marker.dart'; import 'package:list_counter/list_counter.dart'; class ListProcessing { diff --git a/lib/src/style.dart b/lib/src/style.dart index 367c969053..18d82a97a8 100644 --- a/lib/src/style.dart +++ b/lib/src/style.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_html/src/css_parser.dart'; -import 'package:flutter_html/src/style/marker.dart'; //Export Style value-unit APIs export 'package:flutter_html/src/style/margin.dart'; @@ -11,6 +10,7 @@ export 'package:flutter_html/src/style/length.dart'; export 'package:flutter_html/src/style/size.dart'; export 'package:flutter_html/src/style/fontsize.dart'; export 'package:flutter_html/src/style/lineheight.dart'; +export 'package:flutter_html/src/style/marker.dart'; ///This class represents all the available CSS attributes ///for this package. diff --git a/packages/flutter_html_audio/lib/flutter_html_audio.dart b/packages/flutter_html_audio/lib/flutter_html_audio.dart index 607da1812d..ecd781d337 100644 --- a/packages/flutter_html_audio/lib/flutter_html_audio.dart +++ b/packages/flutter_html_audio/lib/flutter_html_audio.dart @@ -19,7 +19,7 @@ class AudioHtmlExtension extends HtmlExtension { Set get supportedTags => {"audio"}; @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { return WidgetSpan( child: AudioWidget( context: context, diff --git a/packages/flutter_html_iframe/lib/flutter_html_iframe.dart b/packages/flutter_html_iframe/lib/flutter_html_iframe.dart index 4522626528..18ff5dead8 100644 --- a/packages/flutter_html_iframe/lib/flutter_html_iframe.dart +++ b/packages/flutter_html_iframe/lib/flutter_html_iframe.dart @@ -19,7 +19,7 @@ class IframeHtmlExtension extends HtmlExtension { Set get supportedTags => {"iframe"}; @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { return WidgetSpan( child: IframeWidget( extensionContext: context, diff --git a/packages/flutter_html_math/lib/flutter_html_math.dart b/packages/flutter_html_math/lib/flutter_html_math.dart index c4b5ea5ec7..afe0e62843 100644 --- a/packages/flutter_html_math/lib/flutter_html_math.dart +++ b/packages/flutter_html_math/lib/flutter_html_math.dart @@ -18,7 +18,7 @@ class MathHtmlExtension extends HtmlExtension { Set get supportedTags => {"math"}; @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { String texStr = _parseMathRecursive(context.styledElement!.element!, ''); return WidgetSpan( child: CssBoxWidget( diff --git a/packages/flutter_html_svg/lib/flutter_html_svg.dart b/packages/flutter_html_svg/lib/flutter_html_svg.dart index 61fb0d97b3..93dfa849f9 100644 --- a/packages/flutter_html_svg/lib/flutter_html_svg.dart +++ b/packages/flutter_html_svg/lib/flutter_html_svg.dart @@ -133,7 +133,7 @@ class SvgHtmlExtension extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { late final Widget widget; if (context.elementName == "svg") { diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart index 37571244d8..7f4278d6f0 100644 --- a/packages/flutter_html_table/lib/flutter_html_table.dart +++ b/packages/flutter_html_table/lib/flutter_html_table.dart @@ -102,8 +102,7 @@ class TableHtmlExtension extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, - Map Function() buildChildren) { + InlineSpan build(ExtensionContext context) { if (context.elementName == "table") { return WidgetSpan( child: CssBoxWidget( @@ -112,7 +111,7 @@ class TableHtmlExtension extends HtmlExtension { builder: (_, constraints) { return _layoutCells( context.styledElement as TableElement, - buildChildren(), + context.builtChildrenMap!, context, constraints, ); @@ -124,7 +123,7 @@ class TableHtmlExtension extends HtmlExtension { return WidgetSpan( child: CssBoxWidget.withInlineSpanChildren( - children: buildChildren().values.toList(), + children: context.inlineSpanChildren!, style: Style(), ), ); diff --git a/packages/flutter_html_video/lib/flutter_html_video.dart b/packages/flutter_html_video/lib/flutter_html_video.dart index 1ab41638a3..3675b173a0 100644 --- a/packages/flutter_html_video/lib/flutter_html_video.dart +++ b/packages/flutter_html_video/lib/flutter_html_video.dart @@ -21,7 +21,7 @@ class VideoHtmlExtension extends HtmlExtension { Set get supportedTags => {"video"}; @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { return WidgetSpan( child: VideoWidget( context: context, diff --git a/test/test_utils.dart b/test/test_utils.dart index 53a6afad8a..e31617caf6 100644 --- a/test/test_utils.dart +++ b/test/test_utils.dart @@ -217,11 +217,10 @@ class TestExtension extends HtmlExtension { } @override - InlineSpan build(ExtensionContext context, buildChildren) { + InlineSpan build(ExtensionContext context) { finalCallback?.call(context.styledElement!); return context.parser.buildFromExtension( context, - buildChildren, extensionsToIgnore: {this}, ); }