You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This is an idea for a simplified version of our public-facing API. I've been thinking a lot about the customRender API. I like the matchers, but the interface can be confusing. These suggestions should make interfacing with our widget easier, so basic users don't have to know the internals of how the project works, while still allowing advanced users to get everything they need to done.
@erickok any thoughts or feedback? Any feedback from any community members?
To start, here's what the Html widget would look like in the example with my proposed changes:
Html(
anchorKey: staticAnchorKey,
data: htmlData,
style: { ... }, // No changes here, omitted for the sake of space
extensions: [
TagExtension("tex", builder: (ExtensionContext context) {
returnMath.tex(
context.innerHtml,
mathStyle:MathStyle.display,
textStyle: context.style.generateTextStyle(),
onErrorFallback: (FlutterMathException e) {
returnText(e.message);
},
);
}),
TagExtension.inline("bird", inlineSpan:TextSpan(text:"🐦")),
TagExtension("flutter", builder: (context) {
returnFlutterLogo(
style: context.attributes['horizontal'] !=null?FlutterLogoStyle.horizontal
:FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.value *5,
);
}),
MyCustomTableExtension(), //Read more about this belowAudioExtension(),
IframeExtension(),
MathExtension(),
SvgExtension(),
MatcherExtension(
matcher: (context) {
return context.attributes['src'] !=null&& context.attributes['src']!.startsWith("/wiki");
},
builder: (context) {
// Map from "/wiki" to "https://upload.wikimedia.org/wiki". I haven't thought up a clean solution for this network image src filtering yet, but there is lots of demand for it. Any feedback would be welcome!
}
),
VideoExtension(),
],
),
Changes
tagsList is no longer required when adding custom widgets. It'll still be there as a parameter for black/white-listing tags, but Html will automatically search through the given extensions and enable support for the custom tags by default. This requires that the Html.tags member be turned into a getter that has that same behavior.
customRenders replaced with extensions. This is the major shift. Currently customRenders takes in a Map<bool Function(RenderContext), CustomRender>, which is a bit confusing. extensions will just take in a List<Extension>. More details on the Extension class below.
RenderContext in builder replaced by ExtensionContext. ExtensionContext will store and provide direct access to some of the most commonly used attributes, as well as all the extra stuff RenderContext holds.
New Extension Class
This new class encapsulates the "matcher"/"renderer" logic into a container. Keeping it in a class also means that the object can persist and receive data across the various phases of the Html widget's lexing/styling/processing/parsing/rendering/disposing lifecycle. This also makes the Extensions tightly coupled with the tree, rather than a hacky afterthought.
Here's what the basic signature of the Extension class might look like. By default, the only thing that needs to be overwritten by child classes is the matches method. Everything else will work out of the box, with default behavior:
abstractclassExtension {
// Tells the HtmlParser what additional tags to add to the default supported tag list (the user can still override this by setting an explicit tagList on the Html widget)List<String> get supportedTags;
// Whether or not this extension needs to do any work in this contextboolmatches(ExtensionContext context);
// Converts parsed HTML to a StyledElement. Need to define default behavior, or perhaps defer this step back to the Html widget by defaultStyledElementlex(dom.Node element);
// Called before styles are applied to the tree. Default behavior: return tree;StyledElementbeforeStyle(ExtensionContext context, StyledElement tree);
// Called after styling, but before extra elements/whitespace has been removed, margins collapsed, list characters processed, or relative values calculated. Default behavior: return tree;StyledElementbeforeProcessing(ExtensionContext context, StyledElement tree);
//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 or Text.rich widget. Need to define default behavior, or perhaps defer this step back to the Html widget by defaultInlineSpanparse(ExtensionContext context, StyledElement tree);
//Called when the Html widget is being destroyed. This would be a very good place to dispose() any controllers or free any resources that the extension uses. Default behavior: do nothing. voidonDispose();
}
And then there's the ExtensionContext class, which has the following members available:
classExtensionContext {
// Guaranteed to always be present
dom.Node node;
// Shortcut getters for `node`String elementName; // Returns an empty string if the node is a text content node, comment, or any other non-Element node.String innerHtml; // Returns an empty string if there is no inner HTMLList<dom.Node> nodeChildren; //Returns an empty list if there are no childrenLinkedHashMap<String, String> attributes; // The attributes of the node. Empty Map if none exist.String? id; //Null if not presentList<String> classes; //Empty if element has no classes// Guaranteed to be non-null after the lexing stepStyledElement? 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.BuildContext? context;
// Guaranteed to always be present. Useful for calling callbacks on the `Html` widget like `onImageTap`.HtmlParser parser;
}
These classes haven't actually been written or implemented, so things may be subject to change as specific implementation requirements open or close different doors.
Benefit: Clearer Path Towards First and Third-Party Extensions
This new approach would make the modular design a little more intuitive.
Each of our first-party packages would now just need to override the Extension class, and including them is a bit more intuitive than, say, svgAssetUriMatcher(): svgAssetImageRender(), etc. They would still be able to support custom options, error callbacks, etc, in their constructors.
AudioExtension( //Or do we prefer AudioTagExtension() or AudioHtmlExtension()?//Options for this extension. Pass in a controller, error handling, etc.
),
IframeExtension(),
MathExtension(),
SvgExtension(), //This would include matchers for svg tag and data/network/asset uri's. We might consider providing some options to turn certain features on or offTableExtension(),
In addition, this opens the door more widely for third-party packages to extend flutter_html's functionality in a way that is easy to use and doesn't affect existing users.
For example, a third-party could write and publish support for:
YoutubeExtension(),
MarkdownExtension(),
TexExtension(),
ImageDisplaysFullscreenOnTapExtension(),
LazyLoadingHtmlExtension(), //Which we would look at and seriously consider adding to the main project, with the permission of the extension owner, of courseJavascriptExtension(), //Someone is feeling extremely ambitious and really doesn't want to just use webview for some reason :)
Included Extension Helper Classes
Since the average use case isn't worth creating a whole class to override Extension for, flutter_html will provide a couple helper class constructors for basic use cases, as used in the example above. Here's what their signatures might look like:
TagExtension(String tagName, {Widget? child, WidgetFunction(ExtensionContext)? builder}); //Takes either a widget or a builderTagExtension.inline(String tagName, {InlineSpan? child, InlineSpanFunction(ExtensionContext)? builder)); //Takes either an InlineSpan or a builderMatcherExtension({requiredboolFunction(ExtensionContext) matcher, requiredWidgetFunction(ExtensionContext) builder}), // Similar to the current "matcher", "renderer" API.MatcherExtension.inline({requiredboolFunction(ExtensionContext) matcher, requiredInlineSpanFunction(ExtensionContext) builder}),
Hopefully it's fairly obvious how these would be implemented!
Example Extension:
Here's a somewhat silly example of what an extension subclass might look like and how it would be used by the user (I'm coding this in the GitHub text editor, so forgive any typos or errors 😅):
classNumberOfElementsExtensionextendsExtension {
finalString tagToCount;
bool _counting =false;
int _counter =0;
NumberOfElementsExtension({
this.tagToCount ="*",
});
@overrideList<String> supportedTags => ["start-count", "end-count", "display-count"];
@overrideboolmatches(ExtensionContext context) {
if(_counting) {
if(context.elementName =="end-count"&& context.styledElement ==null) {
_counting =false;
}
if(context.elementName == tagToCount || tagToCount =="*") {
_counter++;
}
} else {
if(context.elementName =="start-count"&& context.styledElement ==null) {
_counting =true;
}
}
// The only element this extension actually renders is "display-count"return context.elementName =="display-count";
}
@overrideInlineSpanparse(ExtensionContext context, StyledElement tree) {
//There's a lot we could do here (process children, update styles, etc.), but we'll just show the simplest case. If we want a block-level element where styling is handled for us, it is recommended to wrap our widget in CssBoxWidget.returnTextSpan(
text:"There are $_counter elements!",
style: tree.style.generateTextStyle(),
);
}
}
I think this is a fantastic improvement over the current state. It extends what is possible while making the API easier to understand. We could apply the same idea to our image renders as well, though it remains to be seen if we can merge the two entirely into one 'extension' feature.
Perhaps some sort of ImageExtension helper class (and perhaps a few helper subclasses) might make that possible. I'll look the current implementation over and see what could be done
Maybe I'm a bit pedantic but the naming of "Extension" might be confusing to some, especially since extension is already a reserved keyword in dart, and might clash with other classes since it's such a common term. Intellisense may also add to confusion if you don't explicitly check where the import is coming from.
HtmlExtensions or something that better differentiates it might be an improvement imo :)
This is an idea for a simplified version of our public-facing API. I've been thinking a lot about the customRender API. I like the matchers, but the interface can be confusing. These suggestions should make interfacing with our widget easier, so basic users don't have to know the internals of how the project works, while still allowing advanced users to get everything they need to done.
@erickok any thoughts or feedback? Any feedback from any community members?
To start, here's what the
Html
widget would look like in the example with my proposed changes:Changes
tagsList
is no longer required when adding custom widgets. It'll still be there as a parameter for black/white-listing tags, butHtml
will automatically search through the given extensions and enable support for the custom tags by default. This requires that theHtml.tags
member be turned into a getter that has that same behavior.customRenders
replaced withextensions
. This is the major shift. CurrentlycustomRenders
takes in aMap<bool Function(RenderContext), CustomRender>
, which is a bit confusing.extensions
will just take in aList<Extension>
. More details on theExtension
class below.RenderContext
in builder replaced byExtensionContext
.ExtensionContext
will store and provide direct access to some of the most commonly used attributes, as well as all the extra stuffRenderContext
holds.New
Extension
ClassThis new class encapsulates the "matcher"/"renderer" logic into a container. Keeping it in a class also means that the object can persist and receive data across the various phases of the Html widget's lexing/styling/processing/parsing/rendering/disposing lifecycle. This also makes the Extensions tightly coupled with the tree, rather than a hacky afterthought.
Here's what the basic signature of the
Extension
class might look like. By default, the only thing that needs to be overwritten by child classes is thematches
method. Everything else will work out of the box, with default behavior:And then there's the
ExtensionContext
class, which has the following members available:These classes haven't actually been written or implemented, so things may be subject to change as specific implementation requirements open or close different doors.
Benefit: Clearer Path Towards First and Third-Party Extensions
This new approach would make the modular design a little more intuitive.
Each of our first-party packages would now just need to override the
Extension
class, and including them is a bit more intuitive than, say,svgAssetUriMatcher(): svgAssetImageRender()
, etc. They would still be able to support custom options, error callbacks, etc, in their constructors.In addition, this opens the door more widely for third-party packages to extend flutter_html's functionality in a way that is easy to use and doesn't affect existing users.
For example, a third-party could write and publish support for:
Included
Extension
Helper ClassesSince the average use case isn't worth creating a whole class to override
Extension
for, flutter_html will provide a couple helper class constructors for basic use cases, as used in the example above. Here's what their signatures might look like:Hopefully it's fairly obvious how these would be implemented!
Example
Extension
:Here's a somewhat silly example of what an extension subclass might look like and how it would be used by the user (I'm coding this in the GitHub text editor, so forgive any typos or errors 😅):
And, here's the usage in an app:
Which would output something along the lines of
One...
Two...
Three...
The text was updated successfully, but these errors were encountered: