Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
2 changes: 0 additions & 2 deletions example/lib/generated_plugin_registrant.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@

// ignore_for_file: lines_longer_than_80_chars

import 'package:url_launcher_web/url_launcher_web.dart';
import 'package:video_player_web/video_player_web.dart';
import 'package:wakelock_web/wakelock_web.dart';

import 'package:flutter_web_plugins/flutter_web_plugins.dart';

// ignore: public_member_api_docs
void registerPlugins(Registrar registrar) {
UrlLauncherPlugin.registerWith(registrar);
VideoPlayerPlugin.registerWith(registrar);
WakelockWeb.registerWith(registrar);
registrar.registerMessageHandler();
Expand Down
42 changes: 35 additions & 7 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_math_fork/flutter_math.dart';

void main() => runApp(new MyApp());

Expand Down Expand Up @@ -260,13 +261,32 @@ class _MyHomePageState extends State<MyHomePage> {
alignment: Alignment.topLeft,
),
},
customRender: {
"table": (context, child) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: (context.tree as TableLayoutElement).toWidget(context),
);
}
tagsList: Html.tags..addAll(["tex", "bird", "flutter"]),
customRenders: {
texMatcher(): CustomRender.fromWidget(widget: (context, buildChildren) => Math.tex(
context.tree.element?.innerHtml ?? '',
mathStyle: MathStyle.display,
textStyle: context.style.generateTextStyle(),
onErrorFallback: (FlutterMathException e) {
if (context.parser.onMathError != null) {
return context.parser.onMathError!.call(context.tree.element?.innerHtml ?? '', e.message, e.messageWithType);
} else {
return Text(e.message);
}
},
)),
birdMatcher(): CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => TextSpan(text: "🐦")),
flutterMatcher(): CustomRender.fromWidget(widget: (context, buildChildren) => FlutterLogo(
style: (context.tree.element!.attributes['horizontal'] != null)
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
size: context.style.fontSize!.size! * 5,
)),
tableMatcher(): CustomRender.fromWidget(widget: (context, buildChildren) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: (context.tree as TableLayoutElement).toWidget(context),
)),
},
customImageRenders: {
networkSourceMatcher(domains: ["flutter.dev"]): (context, attributes, element) {
Expand Down Expand Up @@ -298,3 +318,11 @@ class _MyHomePageState extends State<MyHomePage> {
);
}
}

CustomRenderMatcher tableMatcher() => (context) => context.tree.element?.localName == 'table';

CustomRenderMatcher texMatcher() => (context) => context.tree.element?.localName == 'tex';

CustomRenderMatcher birdMatcher() => (context) => context.tree.element?.localName == 'bird';

CustomRenderMatcher flutterMatcher() => (context) => context.tree.element?.localName == 'flutter';
211 changes: 211 additions & 0 deletions lib/custom_render.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/utils.dart';

typedef CustomRenderMatcher = bool Function(RenderContext context);

CustomRenderMatcher blockElementMatcher() => (context) {
return context.tree.style.display == Display.BLOCK;
};

CustomRenderMatcher listElementMatcher() => (context) {
return context.tree.style.display == Display.LIST_ITEM;
};

CustomRenderMatcher replacedElementMatcher() => (context) {
return context.tree is ReplacedElement;
};

CustomRenderMatcher textContentElementMatcher() => (context) {
return context.tree is TextContentElement;
};

CustomRenderMatcher interactableElementMatcher() => (context) {
return context.tree is InteractableElement;
};

CustomRenderMatcher layoutElementMatcher() => (context) {
return context.tree is LayoutElement;
};

CustomRenderMatcher verticalAlignMatcher() => (context) {
return context.tree.style.verticalAlign != null
&& context.tree.style.verticalAlign != VerticalAlign.BASELINE;
};

CustomRenderMatcher fallbackMatcher() => (context) {
return true;
};

class CustomRender {
final InlineSpan Function(RenderContext, Function())? inlineSpan;
final Widget Function(RenderContext, Function())? widget;

CustomRender.fromInlineSpan({
required this.inlineSpan,
}) : widget = null;

CustomRender.fromWidget({
required this.widget,
}) : inlineSpan = null;
}

final CustomRender blockElementRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
child: ContainerSpan(
newContext: context,
style: context.tree.style,
shrinkWrap: context.parser.shrinkWrap,
children: buildChildren.call(),
),
));

final CustomRender listElementRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
child: ContainerSpan(
newContext: context,
style: context.tree.style,
shrinkWrap: context.parser.shrinkWrap,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
textDirection: context.tree.style.direction,
children: [
context.tree.style.listStylePosition == ListStylePosition.OUTSIDE ?
Padding(
padding: context.tree.style.padding ?? EdgeInsets.only(left: context.tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: context.tree.style.direction == TextDirection.rtl ? 10.0 : 0.0),
child: Text(
"${context.style.markerContent}",
textAlign: TextAlign.right,
style: context.style.generateTextStyle()
),
) : Container(height: 0, width: 0),
Text("\t", textAlign: TextAlign.right),
Expanded(
child: Padding(
padding: context.tree.style.listStylePosition == ListStylePosition.INSIDE ?
EdgeInsets.only(left: context.tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: context.tree.style.direction == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero,
child: StyledText(
textSpan: TextSpan(
text: (context.tree.style.listStylePosition ==
ListStylePosition.INSIDE)
? '${context.style.markerContent}'
: null,
children: _getListElementChildren(context.tree.style.listStylePosition, buildChildren),
style: context.style.generateTextStyle(),
),
style: context.style,
renderContext: context,
)
)
)
],
),
),
));

final CustomRender replacedElementRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
alignment: (context.tree as ReplacedElement).alignment,
baseline: TextBaseline.alphabetic,
child: (context.tree as ReplacedElement).toWidget(context)!,
));

final CustomRender textContentElementRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) =>
TextSpan(text: (context.tree as TextContentElement).text));

final CustomRender interactableElementRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => TextSpan(
children: (context.tree as InteractableElement).children
.map((tree) => context.parser.parseTree(context, tree))
.map((childSpan) {
return _getInteractableChildren(context, context.tree as InteractableElement, childSpan,
context.style.generateTextStyle().merge(childSpan.style));
}).toList(),
));

final CustomRender layoutElementRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
child: (context.tree as LayoutElement).toWidget(context)!,
));

final CustomRender verticalAlignRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => WidgetSpan(
child: Transform.translate(
offset: Offset(0, _getVerticalOffset(context.tree)),
child: StyledText(
textSpan: TextSpan(
style: context.style.generateTextStyle(),
children: buildChildren.call(),
),
style: context.style,
renderContext: context,
),
),
));

final CustomRender fallbackRender = CustomRender.fromInlineSpan(inlineSpan: (context, buildChildren) => TextSpan(
style: context.style.generateTextStyle(),
children: buildChildren.call(),
));

final Map<CustomRenderMatcher, CustomRender> defaultRenders = {
blockElementMatcher(): blockElementRender,
listElementMatcher(): listElementRender,
textContentElementMatcher(): textContentElementRender,
replacedElementMatcher(): replacedElementRender,
interactableElementMatcher(): interactableElementRender,
layoutElementMatcher(): layoutElementRender,
verticalAlignMatcher(): verticalAlignRender,
fallbackMatcher(): fallbackRender,
};

List<InlineSpan> _getListElementChildren(ListStylePosition? position, Function() buildChildren) {
InlineSpan tabSpan = WidgetSpan(child: Text("\t", textAlign: TextAlign.right));
List<InlineSpan> children = buildChildren.call();
if (position == ListStylePosition.INSIDE) {
children.insert(0, tabSpan);
}
return children;
}

InlineSpan _getInteractableChildren(RenderContext context, InteractableElement tree, InlineSpan childSpan, TextStyle childStyle) {
if (childSpan is TextSpan) {
return TextSpan(
text: childSpan.text,
children: childSpan.children
?.map((e) => _getInteractableChildren(context, tree, e, childStyle.merge(childSpan.style)))
.toList(),
style: context.style.generateTextStyle().merge(
childSpan.style == null
? childStyle
: childStyle.merge(childSpan.style)),
semanticsLabel: childSpan.semanticsLabel,
recognizer: TapGestureRecognizer()
..onTap = () => context.parser.onLinkTap?.call(tree.href, context, tree.attributes, tree.element),
);
} else {
return WidgetSpan(
child: RawGestureDetector(
gestures: {
MultipleTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<MultipleTapGestureRecognizer>(
() => MultipleTapGestureRecognizer(),
(instance) {
instance..onTap = () => context.parser.onLinkTap?.call(tree.href, context, tree.attributes, tree.element);
},
),
},
child: (childSpan as WidgetSpan).child,
),
);
}
}

double _getVerticalOffset(StyledElement tree) {
switch (tree.style.verticalAlign) {
case VerticalAlign.SUB:
return tree.style.fontSize!.size! / 2.5;
case VerticalAlign.SUPER:
return tree.style.fontSize!.size! / -2.5;
default:
return 0;
}
}
12 changes: 8 additions & 4 deletions lib/flutter_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ library flutter_html;

//export image render api
export 'package:flutter_html/image_render.dart';
export 'package:flutter_html/custom_render.dart';
//export style api
export 'package:flutter_html/style.dart';
//export render context api
Expand All @@ -13,6 +14,7 @@ export 'package:flutter_html/src/styled_element.dart';
export 'package:flutter_html/src/interactable_element.dart';

import 'package:flutter/material.dart';
import 'package:flutter_html/custom_render.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/image_render.dart';
import 'package:flutter_html/src/html_elements.dart';
Expand Down Expand Up @@ -50,7 +52,7 @@ class Html extends StatelessWidget {
Key? key,
required this.data,
this.onLinkTap,
this.customRender = const {},
this.customRenders = const {},
this.customImageRenders = const {},
this.onImageError,
this.onMathError,
Expand All @@ -67,7 +69,7 @@ class Html extends StatelessWidget {
Key? key,
@required this.document,
this.onLinkTap,
this.customRender = const {},
this.customRenders = const {},
this.customImageRenders = const {},
this.onImageError,
this.onMathError,
Expand Down Expand Up @@ -113,7 +115,7 @@ class Html extends StatelessWidget {

/// Either return a custom widget for specific node types or return null to
/// fallback to the default rendering.
final Map<String, CustomRender> customRender;
final Map<CustomRenderMatcher, CustomRender> customRenders;

/// An API that allows you to override the default style for any HTML element
final Map<String, Style> style;
Expand Down Expand Up @@ -145,7 +147,9 @@ class Html extends StatelessWidget {
onMathError: onMathError,
shrinkWrap: shrinkWrap,
style: style,
customRender: customRender,
customRenders: {}
..addAll(customRenders)
..addAll(defaultRenders),
imageRenders: {}
..addAll(customImageRenders)
..addAll(defaultImageRenders),
Expand Down
Loading