diff --git a/example/lib/main.dart b/example/lib/main.dart
index e0489a248c..eea3fc5cc1 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -55,10 +55,20 @@ const htmlData = r"""
The should be BLACK with 10% alpha style='color: rgba(0, 0, 0, 0.10);
The should be GREEN style='color: rgb(0, 97, 0);
The should be GREEN style='color: rgb(0, 97, 0);
- blasdafjklasdlkjfkl
- blasdafjklasdlkjfkl
- blasdafjklasdlkjfkl
- blasdafjklasdlkjfkl
+ Text Alignment
+ Center Aligned Text
+ Right Aligned Text
+ Justified Text
+ Center Aligned Text
+ Auto Margins
+ Default Div
+ margin: auto
+ margin: 15px auto
+ margin-left: auto
+ With an image - non-block (should not center):
+
+ block image (should center):
+
Table support (with custom styling!):
Famous quote...
@@ -297,7 +307,7 @@ class _MyHomePageState extends State {
? FlutterLogoStyle.horizontal
: FlutterLogoStyle.markOnly,
textColor: context.style.color!,
- size: context.style.fontSize!.size! * 5,
+ size: context.style.fontSize!.value * 5,
)),
tagMatcher("table"): CustomRender.widget(widget: (context, buildChildren) => SingleChildScrollView(
scrollDirection: Axis.horizontal,
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 79e23feab9..ffde7654f9 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -4,7 +4,7 @@ publish_to: none
version: 1.0.0+1
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: '>=2.17.0 <3.0.0'
dependencies:
flutter_html:
diff --git a/lib/custom_render.dart b/lib/custom_render.dart
index e421f3c931..27d15f13ff 100644
--- a/lib/custom_render.dart
+++ b/lib/custom_render.dart
@@ -6,6 +6,7 @@ import 'dart:convert';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/utils.dart';
typedef CustomRenderMatcher = bool Function(RenderContext context);
@@ -15,7 +16,8 @@ CustomRenderMatcher tagMatcher(String tag) => (context) {
};
CustomRenderMatcher blockElementMatcher() => (context) {
- return context.tree.style.display == Display.BLOCK &&
+ return (context.tree.style.display == Display.BLOCK ||
+ context.tree.style.display == Display.INLINE_BLOCK) &&
(context.tree.children.isNotEmpty ||
context.tree.element?.localName == "hr");
};
@@ -116,10 +118,6 @@ CustomRender blockElementRender({Style? style, List? children}) =>
children: (children as List?) ??
context.tree.children
.expandIndexed((i, childTree) => [
- if (childTree.style.display == Display.BLOCK &&
- i > 0 &&
- context.tree.children[i - 1] is ReplacedElement)
- TextSpan(text: "\n"),
context.parser.parseTree(context, childTree),
if (i != context.tree.children.length - 1 &&
childTree.style.display == Display.BLOCK &&
@@ -131,117 +129,109 @@ CustomRender blockElementRender({Style? style, List? children}) =>
);
}
return WidgetSpan(
- child: ContainerSpan(
- key: context.key,
- newContext: context,
- style: style ?? context.tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- children: children ??
- context.tree.children
- .expandIndexed((i, childTree) => [
- if (context.parser.shrinkWrap &&
- childTree.style.display == Display.BLOCK &&
- i > 0 &&
- context.tree.children[i - 1] is ReplacedElement)
- TextSpan(text: "\n"),
- context.parser.parseTree(context, childTree),
- if (i != context.tree.children.length - 1 &&
- childTree.style.display == Display.BLOCK &&
- childTree.element?.localName != "html" &&
- childTree.element?.localName != "body")
- TextSpan(text: "\n"),
- ])
- .toList(),
- ));
+ alignment: PlaceholderAlignment.baseline,
+ baseline: TextBaseline.alphabetic,
+ child: CssBoxWidget.withInlineSpanChildren(
+ key: context.key,
+ style: style ?? context.tree.style,
+ shrinkWrap: context.parser.shrinkWrap,
+ childIsReplaced:
+ REPLACED_EXTERNAL_ELEMENTS.contains(context.tree.name),
+ children: children ??
+ context.tree.children
+ .expandIndexed((i, childTree) => [
+ context.parser.parseTree(context, childTree),
+ //TODO can this newline be added in a different step?
+ if (i != context.tree.children.length - 1 &&
+ childTree.style.display == Display.BLOCK &&
+ childTree.element?.localName != "html" &&
+ childTree.element?.localName != "body")
+ TextSpan(text: "\n"),
+ ])
+ .toList(),
+ ),
+ );
});
CustomRender listElementRender(
{Style? style, Widget? child, List? children}) =>
CustomRender.inlineSpan(
- inlineSpan: (context, buildChildren) => WidgetSpan(
- child: ContainerSpan(
- key: context.key,
- newContext: context,
- style: style ?? context.tree.style,
- shrinkWrap: context.parser.shrinkWrap,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- textDirection:
- style?.direction ?? context.tree.style.direction,
- children: [
- (style?.listStylePosition ??
- context.tree.style.listStylePosition) ==
- ListStylePosition.OUTSIDE
- ? Padding(
- padding: style?.padding?.nonNegative ??
- context.tree.style.padding?.nonNegative ??
- EdgeInsets.only(
- left: (style?.direction ??
- context.tree.style.direction) !=
- TextDirection.rtl
- ? 10.0
- : 0.0,
- right: (style?.direction ??
- context.tree.style.direction) ==
- TextDirection.rtl
- ? 10.0
- : 0.0),
- child: style?.markerContent ??
- context.style.markerContent)
- : Container(height: 0, width: 0),
- Text("\u0020",
- textAlign: TextAlign.right,
- style: TextStyle(fontWeight: FontWeight.w400)),
- Expanded(
- child: Padding(
- padding: (style?.listStylePosition ??
- context.tree.style.listStylePosition) ==
- ListStylePosition.INSIDE
- ? EdgeInsets.only(
- left: (style?.direction ??
- context.tree.style.direction) !=
- TextDirection.rtl
- ? 10.0
- : 0.0,
- right: (style?.direction ??
- context.tree.style.direction) ==
- TextDirection.rtl
- ? 10.0
- : 0.0)
- : EdgeInsets.zero,
- child: StyledText(
- textSpan: TextSpan(
- children: _getListElementChildren(
- style?.listStylePosition ??
- context.tree.style.listStylePosition,
- buildChildren)
- ..insertAll(
- 0,
- context.tree.style.listStylePosition ==
- ListStylePosition.INSIDE
- ? [
- WidgetSpan(
- alignment:
- PlaceholderAlignment
- .middle,
- child: style?.markerContent ??
- context.style
- .markerContent ??
- Container(
- height: 0, width: 0))
- ]
- : []),
- style: style?.generateTextStyle() ??
- context.style.generateTextStyle(),
- ),
- style: style ?? context.style,
- renderContext: context,
- )))
- ],
+ inlineSpan: (context, buildChildren) => WidgetSpan(
+ child: CssBoxWidget(
+ key: context.key,
+ style: style ?? context.tree.style,
+ shrinkWrap: context.parser.shrinkWrap,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ textDirection: style?.direction ?? context.tree.style.direction,
+ children: [
+ (style?.listStylePosition ??
+ context.tree.style.listStylePosition) ==
+ ListStylePosition.OUTSIDE
+ ? Padding(
+ padding: style?.padding?.nonNegative ??
+ context.tree.style.padding?.nonNegative ??
+ EdgeInsets.only(
+ left: (style?.direction ??
+ context.tree.style.direction) !=
+ TextDirection.rtl
+ ? 10.0
+ : 0.0,
+ right: (style?.direction ??
+ context.tree.style.direction) ==
+ TextDirection.rtl
+ ? 10.0
+ : 0.0),
+ child:
+ style?.markerContent ?? context.style.markerContent)
+ : Container(height: 0, width: 0),
+ Text("\u0020",
+ textAlign: TextAlign.right,
+ style: TextStyle(fontWeight: FontWeight.w400)),
+ Expanded(
+ child: Padding(
+ padding: (style?.listStylePosition ??
+ context.tree.style.listStylePosition) ==
+ ListStylePosition.INSIDE
+ ? EdgeInsets.only(
+ left: (style?.direction ??
+ context.tree.style.direction) !=
+ TextDirection.rtl
+ ? 10.0
+ : 0.0,
+ right: (style?.direction ??
+ context.tree.style.direction) ==
+ TextDirection.rtl
+ ? 10.0
+ : 0.0)
+ : EdgeInsets.zero,
+ child: CssBoxWidget.withInlineSpanChildren(
+ children: _getListElementChildren(
+ style?.listStylePosition ??
+ context.tree.style.listStylePosition,
+ buildChildren)
+ ..insertAll(
+ 0,
+ context.tree.style.listStylePosition ==
+ ListStylePosition.INSIDE
+ ? [
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: style?.markerContent ??
+ context.style.markerContent ??
+ Container(height: 0, width: 0))
+ ]
+ : []),
+ style: style ?? context.style,
+ ),
),
),
- ));
+ ],
+ ),
+ ),
+ ),
+ );
CustomRender replacedElementRender(
{PlaceholderAlignment? alignment,
@@ -482,14 +472,9 @@ CustomRender verticalAlignRender(
key: context.key,
offset: Offset(
0, verticalOffset ?? _getVerticalOffset(context.tree)),
- child: StyledText(
- textSpan: TextSpan(
- style: style?.generateTextStyle() ??
- context.style.generateTextStyle(),
- children: children ?? buildChildren.call(),
- ),
+ child: CssBoxWidget.withInlineSpanChildren(
+ children: children ?? buildChildren.call(),
style: context.style,
- renderContext: context,
),
),
));
@@ -512,19 +497,21 @@ CustomRender fallbackRender({Style? style, List? children}) =>
.toList(),
));
-final Map defaultRenders = {
- blockElementMatcher(): blockElementRender(),
- listElementMatcher(): listElementRender(),
- textContentElementMatcher(): textContentElementRender(),
- dataUriMatcher(): base64ImageRender(),
- assetUriMatcher(): assetImageRender(),
- networkSourceMatcher(): networkImageRender(),
- replacedElementMatcher(): replacedElementRender(),
- interactableElementMatcher(): interactableElementRender(),
- layoutElementMatcher(): layoutElementRender(),
- verticalAlignMatcher(): verticalAlignRender(),
- fallbackMatcher(): fallbackRender(),
-};
+Map generateDefaultRenders() {
+ return {
+ blockElementMatcher(): blockElementRender(),
+ listElementMatcher(): listElementRender(),
+ textContentElementMatcher(): textContentElementRender(),
+ dataUriMatcher(): base64ImageRender(),
+ assetUriMatcher(): assetImageRender(),
+ networkSourceMatcher(): networkImageRender(),
+ replacedElementMatcher(): replacedElementRender(),
+ interactableElementMatcher(): interactableElementRender(),
+ layoutElementMatcher(): layoutElementRender(),
+ verticalAlignMatcher(): verticalAlignRender(),
+ fallbackMatcher(): fallbackRender(),
+ };
+}
List _getListElementChildren(
ListStylePosition? position, Function() buildChildren) {
@@ -585,9 +572,9 @@ final _dataUriFormat = RegExp(
double _getVerticalOffset(StyledElement tree) {
switch (tree.style.verticalAlign) {
case VerticalAlign.SUB:
- return tree.style.fontSize!.size! / 2.5;
+ return tree.style.fontSize!.value / 2.5;
case VerticalAlign.SUPER:
- return tree.style.fontSize!.size! / -2.5;
+ return tree.style.fontSize!.value / -2.5;
default:
return 0;
}
diff --git a/lib/flutter_html.dart b/lib/flutter_html.dart
index 3db68efb2b..ec60476178 100644
--- a/lib/flutter_html.dart
+++ b/lib/flutter_html.dart
@@ -18,6 +18,8 @@ export 'package:flutter_html/src/interactable_element.dart';
export 'package:flutter_html/src/layout_element.dart';
export 'package:flutter_html/src/replaced_element.dart';
export 'package:flutter_html/src/styled_element.dart';
+//export css_box_widget for use in custom render.
+export 'package:flutter_html/src/css_box_widget.dart';
//export style api
export 'package:flutter_html/style.dart';
@@ -177,24 +179,21 @@ class _HtmlState extends State {
@override
Widget build(BuildContext context) {
- return Container(
- width: widget.shrinkWrap ? null : MediaQuery.of(context).size.width,
- child: HtmlParser(
- key: widget._anchorKey,
- htmlData: documentElement,
- onLinkTap: widget.onLinkTap,
- onAnchorTap: widget.onAnchorTap,
- onImageTap: widget.onImageTap,
- onCssParseError: widget.onCssParseError,
- onImageError: widget.onImageError,
- shrinkWrap: widget.shrinkWrap,
- selectable: false,
- style: widget.style,
- customRenders: {}
- ..addAll(widget.customRenders)
- ..addAll(defaultRenders),
- tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList,
- ),
+ return HtmlParser(
+ key: widget._anchorKey,
+ htmlData: documentElement,
+ onLinkTap: widget.onLinkTap,
+ onAnchorTap: widget.onAnchorTap,
+ onImageTap: widget.onImageTap,
+ onCssParseError: widget.onCssParseError,
+ onImageError: widget.onImageError,
+ shrinkWrap: widget.shrinkWrap,
+ selectable: false,
+ style: widget.style,
+ customRenders: {}
+ ..addAll(widget.customRenders)
+ ..addAll(generateDefaultRenders()),
+ tagsList: widget.tagsList.isEmpty ? Html.tags : widget.tagsList,
);
}
}
@@ -306,7 +305,9 @@ class SelectableHtml extends StatefulWidget {
final OnCssParseError? onCssParseError;
/// A parameter that should be set when the HTML widget is expected to be
- /// flexible
+ /// have a flexible width, that doesn't always fill its maximum width
+ /// constraints. For example, auto horizontal margins are ignored, and
+ /// block-level elements only take up the width they need.
final bool shrinkWrap;
/// A list of HTML tags that are the only tags that are rendered. By default, this list is empty and all supported HTML tags are rendered.
@@ -360,7 +361,7 @@ class _SelectableHtmlState extends State {
style: widget.style,
customRenders: {}
..addAll(widget.customRenders)
- ..addAll(defaultRenders),
+ ..addAll(generateDefaultRenders()),
tagsList:
widget.tagsList.isEmpty ? SelectableHtml.tags : widget.tagsList,
selectionControls: widget.selectionControls,
diff --git a/lib/html_parser.dart b/lib/html_parser.dart
index 2f66cc9a36..7ed4e959af 100644
--- a/lib/html_parser.dart
+++ b/lib/html_parser.dart
@@ -14,10 +14,10 @@ import 'package:html/parser.dart' as htmlparser;
import 'package:numerus/numerus.dart';
typedef OnTap = void Function(
- String? url,
- RenderContext context,
- Map attributes,
- dom.Element? element,
+ String? url,
+ RenderContext context,
+ Map attributes,
+ dom.Element? element,
);
typedef OnCssParseError = String? Function(
String css,
@@ -62,15 +62,18 @@ class HtmlParser extends StatelessWidget {
this.selectionControls,
this.scrollPhysics,
}) : this.internalOnAnchorTap = onAnchorTap != null
- ? onAnchorTap
- : key != null
- ? _handleAnchorTap(key, onLinkTap)
- : onLinkTap,
+ ? onAnchorTap
+ : key != null
+ ? _handleAnchorTap(key, onLinkTap)
+ : onLinkTap,
super(key: key);
+ /// As the widget [build]s, the HTML data is processed into a tree of [StyledElement]s,
+ /// which are then parsed into an [InlineSpan] tree that is then rendered to the screen by Flutter
+ //TODO Lazy processing of data. We don't need the processing steps done every build phase unless the data has changed.
@override
Widget build(BuildContext context) {
- Map>> declarations = _getExternalCssDeclarations(htmlData.getElementsByTagName("style"), onCssParseError);
+ // Lexing Step
StyledElement lexedTree = lexDomTree(
htmlData,
customRenders.keys.toList(),
@@ -78,53 +81,33 @@ class HtmlParser extends StatelessWidget {
context,
this,
);
- StyledElement? externalCssStyledTree;
- if (declarations.isNotEmpty) {
- externalCssStyledTree = _applyExternalCss(declarations, lexedTree);
- }
- StyledElement inlineStyledTree = _applyInlineStyles(externalCssStyledTree ?? lexedTree, onCssParseError);
- StyledElement customStyledTree = _applyCustomStyles(style, inlineStyledTree);
- StyledElement cascadedStyledTree = _cascadeStyles(style, customStyledTree);
- StyledElement cleanedTree = cleanTree(cascadedStyledTree);
+
+ // Styling Step
+ StyledElement styledTree =
+ styleTree(lexedTree, htmlData, style, onCssParseError);
+
+ // Processing Step
+ StyledElement processedTree =
+ processTree(styledTree, MediaQuery.of(context).devicePixelRatio);
+
+ // Parsing Step
InlineSpan parsedTree = parseTree(
RenderContext(
buildContext: context,
parser: this,
- tree: cleanedTree,
- style: cleanedTree.style,
+ tree: processedTree,
+ style: processedTree.style,
),
- cleanedTree,
+ processedTree,
);
- // This is the final scaling that assumes any other StyledText instances are
- // using textScaleFactor = 1.0 (which is the default). This ensures the correct
- // scaling is used, but relies on https://github.com/flutter/flutter/pull/59711
- // to wrap everything when larger accessibility fonts are used.
- if (selectable) {
- return StyledText.selectable(
- textSpan: parsedTree as TextSpan,
- style: cleanedTree.style,
- textScaleFactor: MediaQuery.of(context).textScaleFactor,
- renderContext: RenderContext(
- buildContext: context,
- parser: this,
- tree: cleanedTree,
- style: cleanedTree.style,
- ),
- selectionControls: selectionControls,
- scrollPhysics: scrollPhysics,
- );
- }
- return StyledText(
- textSpan: parsedTree,
- style: cleanedTree.style,
- textScaleFactor: MediaQuery.of(context).textScaleFactor,
- renderContext: RenderContext(
- buildContext: context,
- parser: this,
- tree: cleanedTree,
- style: cleanedTree.style,
- ),
+ return CssBoxWidget.withInlineSpanChildren(
+ style: processedTree.style,
+ children: [parsedTree],
+ selectable: selectable,
+ scrollPhysics: scrollPhysics,
+ selectionControls: selectionControls,
+ shrinkWrap: shrinkWrap,
);
}
@@ -150,6 +133,7 @@ class HtmlParser extends StatelessWidget {
name: "[Tree Root]",
children: [],
node: html,
+ //TODO(Sub6Resources): This seems difficult to customize
style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
);
@@ -210,26 +194,34 @@ class HtmlParser extends StatelessWidget {
final StyledElement tree = parseStyledElement(node, children);
for (final entry in customRenderMatchers) {
if (entry.call(
- RenderContext(
- buildContext: context,
- parser: parser,
- tree: tree,
- style: Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
- ),
- )) {
+ RenderContext(
+ buildContext: context,
+ parser: parser,
+ tree: tree,
+ style:
+ Style.fromTextStyle(Theme.of(context).textTheme.bodyText2!),
+ ),
+ )) {
return tree;
}
}
return EmptyContentElement();
}
} else if (node is dom.Text) {
- return TextContentElement(text: node.text, style: Style(), element: node.parent, node: node);
+ return TextContentElement(
+ text: node.text,
+ style: Style(),
+ element: node.parent,
+ node: node,
+ );
} else {
return EmptyContentElement();
}
}
- static Map>> _getExternalCssDeclarations(List styles, OnCssParseError? errorHandler) {
+ static Map>>
+ _getExternalCssDeclarations(
+ List styles, OnCssParseError? errorHandler) {
String fullCss = "";
for (final e in styles) {
fullCss = fullCss + e.innerHtml;
@@ -242,7 +234,9 @@ class HtmlParser extends StatelessWidget {
}
}
- static StyledElement _applyExternalCss(Map>> declarations, StyledElement tree) {
+ static StyledElement _applyExternalCss(
+ Map>> declarations,
+ StyledElement tree) {
declarations.forEach((key, style) {
try {
if (tree.matchesSelector(key)) {
@@ -256,7 +250,8 @@ class HtmlParser extends StatelessWidget {
return tree;
}
- static StyledElement _applyInlineStyles(StyledElement tree, OnCssParseError? errorHandler) {
+ static StyledElement _applyInlineStyles(
+ StyledElement tree, OnCssParseError? errorHandler) {
if (tree.attributes.containsKey("style")) {
final newStyle = inlineCssToStyle(tree.attributes['style'], errorHandler);
if (newStyle != null) {
@@ -270,7 +265,8 @@ class HtmlParser extends StatelessWidget {
/// [applyCustomStyles] applies the [Style] objects passed into the [Html]
/// widget onto the [StyledElement] tree, no cascading of styles is done at this point.
- static StyledElement _applyCustomStyles(Map style, StyledElement tree) {
+ static StyledElement _applyCustomStyles(
+ Map style, StyledElement tree) {
style.forEach((key, style) {
try {
if (tree.matchesSelector(key)) {
@@ -285,7 +281,8 @@ class HtmlParser extends StatelessWidget {
/// [_cascadeStyles] cascades all of the inherited styles down the tree, applying them to each
/// child that doesn't specify a different style.
- static StyledElement _cascadeStyles(Map style, StyledElement tree) {
+ static StyledElement _cascadeStyles(
+ Map style, StyledElement tree) {
tree.children.forEach((child) {
child.style = tree.style.copyOnlyInherited(child.style);
_cascadeStyles(style, child);
@@ -294,17 +291,37 @@ class HtmlParser extends StatelessWidget {
return tree;
}
- /// [cleanTree] optimizes the [StyledElement] tree so all [BlockElement]s are
+ /// [styleTree] takes the lexed [StyleElement] tree and applies external,
+ /// inline, and custom CSS/Flutter styles, and then cascades the styles down the tree.
+ static StyledElement styleTree(StyledElement tree, dom.Element htmlData,
+ Map style, OnCssParseError? onCssParseError) {
+ Map>> declarations =
+ _getExternalCssDeclarations(
+ htmlData.getElementsByTagName("style"), onCssParseError);
+
+ StyledElement? externalCssStyledTree;
+ if (declarations.isNotEmpty) {
+ externalCssStyledTree = _applyExternalCss(declarations, tree);
+ }
+ tree = _applyInlineStyles(externalCssStyledTree ?? tree, onCssParseError);
+ tree = _applyCustomStyles(style, tree);
+ tree = _cascadeStyles(style, tree);
+ return tree;
+ }
+
+ /// [processTree] optimizes the [StyledElement] tree so all [BlockElement]s are
/// on the first level, redundant levels are collapsed, empty elements are
/// removed, and specialty elements are processed.
- static StyledElement cleanTree(StyledElement tree) {
+ static StyledElement processTree(
+ StyledElement tree, double devicePixelRatio) {
tree = _processInternalWhitespace(tree);
tree = _processInlineWhitespace(tree);
tree = _removeEmptyElements(tree);
+
+ tree = _calculateRelativeValues(tree, devicePixelRatio);
tree = _processListCharacters(tree);
tree = _processBeforesAndAfters(tree);
tree = _collapseMargins(tree);
- tree = _processFontSize(tree);
return tree;
}
@@ -325,23 +342,34 @@ class HtmlParser extends StatelessWidget {
for (final entry in customRenders.keys) {
if (entry.call(newContext)) {
- final buildChildren = () => tree.children.map((tree) => parseTree(newContext, tree)).toList();
- if (newContext.parser.selectable && customRenders[entry] is SelectableCustomRender) {
- final selectableBuildChildren = () => tree.children.map((tree) => parseTree(newContext, tree) as TextSpan).toList();
- return (customRenders[entry] as SelectableCustomRender).textSpan.call(newContext, selectableBuildChildren);
+ final buildChildren = () =>
+ tree.children.map((tree) => parseTree(newContext, tree)).toList();
+ if (newContext.parser.selectable &&
+ customRenders[entry] is SelectableCustomRender) {
+ final selectableBuildChildren = () => tree.children
+ .map((tree) => parseTree(newContext, tree) as TextSpan)
+ .toList();
+ return (customRenders[entry] as SelectableCustomRender)
+ .textSpan
+ .call(newContext, selectableBuildChildren);
}
if (newContext.parser.selectable) {
- return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren) as TextSpan;
+ return customRenders[entry]!
+ .inlineSpan!
+ .call(newContext, buildChildren) as TextSpan;
}
if (customRenders[entry]?.inlineSpan != null) {
- return customRenders[entry]!.inlineSpan!.call(newContext, buildChildren);
+ return customRenders[entry]!
+ .inlineSpan!
+ .call(newContext, buildChildren);
}
return WidgetSpan(
- child: ContainerSpan(
- newContext: newContext,
+ child: CssBoxWidget(
style: tree.style,
shrinkWrap: newContext.parser.shrinkWrap,
- child: customRenders[entry]!.widget!.call(newContext, buildChildren),
+ child:
+ customRenders[entry]!.widget!.call(newContext, buildChildren),
+ childIsReplaced: true, //TODO is this true?
),
);
}
@@ -349,10 +377,13 @@ class HtmlParser extends StatelessWidget {
return WidgetSpan(child: Container(height: 0, width: 0));
}
- static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) =>
- (String? url, RenderContext context, Map attributes, dom.Element? element) {
+ static OnTap _handleAnchorTap(Key key, OnTap? onLinkTap) => (String? url,
+ RenderContext context,
+ Map attributes,
+ dom.Element? element) {
if (url?.startsWith("#") == true) {
- final anchorContext = AnchorKey.forId(key, url!.substring(1))?.currentContext;
+ final anchorContext =
+ AnchorKey.forId(key, url!.substring(1))?.currentContext;
if (anchorContext != null) {
Scrollable.ensureVisible(anchorContext);
}
@@ -396,24 +427,33 @@ class HtmlParser extends StatelessWidget {
/// initialize indices to negative numbers to make conditionals a little easier
int textIndex = -1;
int elementIndex = -1;
+
/// initialize parent after to a whitespace to account for elements that are
/// the last child in the list of elements
String parentAfterText = " ";
+
/// find the index of the text in the current tree
if ((tree.element?.nodes.length ?? 0) >= 1) {
- textIndex = tree.element?.nodes.indexWhere((element) => element == tree.node) ?? -1;
+ textIndex =
+ tree.element?.nodes.indexWhere((element) => element == tree.node) ??
+ -1;
}
+
/// get the parent nodes
dom.NodeList? parentNodes = tree.element?.parent?.nodes;
+
/// find the index of the tree itself in the parent nodes
if ((parentNodes?.length ?? 0) >= 1) {
- elementIndex = parentNodes?.indexWhere((element) => element == tree.element) ?? -1;
+ elementIndex =
+ parentNodes?.indexWhere((element) => element == tree.element) ?? -1;
}
+
/// if the tree is any node except the last node in the node list and the
/// next node in the node list is a text node, then get its text. Otherwise
/// the next node will be a [dom.Element], so keep unwrapping that until
/// we get the underlying text node, and finally get its text.
- if (elementIndex < (parentNodes?.length ?? 1) - 1 && parentNodes?[elementIndex + 1] is dom.Text) {
+ if (elementIndex < (parentNodes?.length ?? 1) - 1 &&
+ parentNodes?[elementIndex + 1] is dom.Text) {
parentAfterText = parentNodes?[elementIndex + 1].text ?? " ";
} else if (elementIndex < (parentNodes?.length ?? 1) - 1) {
var parentAfter = parentNodes?[elementIndex + 1];
@@ -426,6 +466,7 @@ class HtmlParser extends StatelessWidget {
}
parentAfterText = parentAfter?.text ?? " ";
}
+
/// If the text is the first element in the current tree node list, it
/// starts with a whitespace, it isn't a line break, either the
/// whitespace is unnecessary or it is a block element, and either it is
@@ -435,38 +476,37 @@ class HtmlParser extends StatelessWidget {
/// We should also delete the whitespace at any point in the node list
/// if the previous element is a
because that tag makes the element
/// act like a block element.
- if (textIndex < 1
- && tree.text!.startsWith(' ')
- && tree.element?.localName != "br"
- && (!keepLeadingSpace.data
- || tree.style.display == Display.BLOCK)
- && (elementIndex < 1
- || (elementIndex >= 1
- && parentNodes?[elementIndex - 1] is dom.Text
- && parentNodes![elementIndex - 1].text!.endsWith(" ")))
- ) {
+ if (textIndex < 1 &&
+ tree.text!.startsWith(' ') &&
+ tree.element?.localName != "br" &&
+ (!keepLeadingSpace.data || tree.style.display == Display.BLOCK) &&
+ (elementIndex < 1 ||
+ (elementIndex >= 1 &&
+ parentNodes?[elementIndex - 1] is dom.Text &&
+ parentNodes![elementIndex - 1].text!.endsWith(" ")))) {
tree.text = tree.text!.replaceFirst(' ', '');
- } else if (textIndex >= 1
- && tree.text!.startsWith(' ')
- && tree.element?.nodes[textIndex - 1] is dom.Element
- && (tree.element?.nodes[textIndex - 1] as dom.Element).localName == "br"
- ) {
+ } else if (textIndex >= 1 &&
+ tree.text!.startsWith(' ') &&
+ tree.element?.nodes[textIndex - 1] is dom.Element &&
+ (tree.element?.nodes[textIndex - 1] as dom.Element).localName ==
+ "br") {
tree.text = tree.text!.replaceFirst(' ', '');
}
+
/// If the text is the last element in the current tree node list, it isn't
/// a line break, and the next text node starts with a whitespace,
/// update the [Context] to signify to that next text node whether it should
/// keep its whitespace. This is based on whether the current text ends with a
/// whitespace.
- if (textIndex == (tree.element?.nodes.length ?? 1) - 1
- && tree.element?.localName != "br"
- && parentAfterText.startsWith(' ')
- ) {
+ if (textIndex == (tree.element?.nodes.length ?? 1) - 1 &&
+ tree.element?.localName != "br" &&
+ parentAfterText.startsWith(' ')) {
keepLeadingSpace.data = !tree.text!.endsWith(' ');
}
}
- tree.children.forEach((e) => _processInlineWhitespaceRecursive(e, keepLeadingSpace));
+ tree.children
+ .forEach((e) => _processInlineWhitespaceRecursive(e, keepLeadingSpace));
return tree;
}
@@ -503,14 +543,19 @@ class HtmlParser extends StatelessWidget {
if (tree.style.listStylePosition == null) {
tree.style.listStylePosition = ListStylePosition.OUTSIDE;
}
- if (tree.name == 'ol' && tree.style.listStyleType != null && tree.style.listStyleType!.type == "marker") {
+ if (tree.name == 'ol' &&
+ tree.style.listStyleType != null &&
+ tree.style.listStyleType!.type == "marker") {
switch (tree.style.listStyleType!) {
case ListStyleType.LOWER_LATIN:
case ListStyleType.LOWER_ALPHA:
case ListStyleType.UPPER_LATIN:
case ListStyleType.UPPER_ALPHA:
olStack.add(Context('a'));
- if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
+ if ((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start']!)
+ : null) !=
+ null) {
var start = int.tryParse(tree.attributes['start']!) ?? 1;
var x = 1;
while (x < start) {
@@ -520,14 +565,22 @@ class HtmlParser extends StatelessWidget {
}
break;
default:
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
+ olStack.add(Context((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start'] ?? "") ?? 1
+ : 1) -
+ 1));
break;
}
- } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "widget") {
+ } else if (tree.style.display == Display.LIST_ITEM &&
+ tree.style.listStyleType != null &&
+ tree.style.listStyleType!.type == "widget") {
tree.style.markerContent = tree.style.listStyleType!.widget!;
- } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "image") {
+ } else if (tree.style.display == Display.LIST_ITEM &&
+ tree.style.listStyleType != null &&
+ tree.style.listStyleType!.type == "image") {
tree.style.markerContent = Image.network(tree.style.listStyleType!.text);
- } else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) {
+ } else if (tree.style.display == Display.LIST_ITEM &&
+ tree.style.listStyleType != null) {
String marker = "";
switch (tree.style.listStyleType!) {
case ListStyleType.NONE:
@@ -543,7 +596,10 @@ class HtmlParser extends StatelessWidget {
break;
case ListStyleType.DECIMAL:
if (olStack.isEmpty) {
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
+ olStack.add(Context((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start'] ?? "") ?? 1
+ : 1) -
+ 1));
}
olStack.last.data += 1;
marker = '${olStack.last.data}.';
@@ -552,7 +608,10 @@ class HtmlParser extends StatelessWidget {
case ListStyleType.LOWER_ALPHA:
if (olStack.isEmpty) {
olStack.add(Context('a'));
- if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
+ if ((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start']!)
+ : null) !=
+ null) {
var start = int.tryParse(tree.attributes['start']!) ?? 1;
var x = 1;
while (x < start) {
@@ -568,7 +627,10 @@ class HtmlParser extends StatelessWidget {
case ListStyleType.UPPER_ALPHA:
if (olStack.isEmpty) {
olStack.add(Context('a'));
- if ((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start']!) : null) != null) {
+ if ((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start']!)
+ : null) !=
+ null) {
var start = int.tryParse(tree.attributes['start']!) ?? 1;
var x = 1;
while (x < start) {
@@ -582,18 +644,27 @@ class HtmlParser extends StatelessWidget {
break;
case ListStyleType.LOWER_ROMAN:
if (olStack.isEmpty) {
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
+ olStack.add(Context((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start'] ?? "") ?? 1
+ : 1) -
+ 1));
}
olStack.last.data += 1;
if (olStack.last.data <= 0) {
marker = '${olStack.last.data}.';
} else {
- marker = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
+ marker = (olStack.last.data as int)
+ .toRomanNumeralString()!
+ .toLowerCase() +
+ ".";
}
break;
case ListStyleType.UPPER_ROMAN:
if (olStack.isEmpty) {
- olStack.add(Context((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
+ olStack.add(Context((tree.attributes['start'] != null
+ ? int.tryParse(tree.attributes['start'] ?? "") ?? 1
+ : 1) -
+ 1));
}
olStack.last.data += 1;
if (olStack.last.data <= 0) {
@@ -604,9 +675,9 @@ class HtmlParser extends StatelessWidget {
break;
}
tree.style.markerContent = Text(
- marker,
- textAlign: TextAlign.right,
- style: tree.style.generateTextStyle(),
+ marker,
+ textAlign: TextAlign.right,
+ style: tree.style.generateTextStyle(),
);
}
@@ -625,11 +696,20 @@ class HtmlParser extends StatelessWidget {
static StyledElement _processBeforesAndAfters(StyledElement tree) {
if (tree.style.before != null) {
tree.children.insert(
- 0, TextContentElement(text: tree.style.before, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE)));
+ 0,
+ TextContentElement(
+ text: tree.style.before,
+ style: tree.style
+ .copyWith(beforeAfterNull: true, display: Display.INLINE),
+ ),
+ );
}
if (tree.style.after != null) {
- tree.children
- .add(TextContentElement(text: tree.style.after, style: tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE)));
+ tree.children.add(TextContentElement(
+ text: tree.style.after,
+ style:
+ tree.style.copyWith(beforeAfterNull: true, display: Display.INLINE),
+ ));
}
tree.children.forEach(_processBeforesAndAfters);
@@ -637,7 +717,7 @@ class HtmlParser extends StatelessWidget {
return tree;
}
- /// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS21/box.html#collapsing-margins
+ /// [collapseMargins] follows the specifications at https://www.w3.org/TR/CSS22/box.html#collapsing-margins
/// for collapsing margins of block-level boxes. This prevents the doubling of margins between
/// boxes, and makes for a more correct rendering of the html content.
///
@@ -651,8 +731,9 @@ class HtmlParser extends StatelessWidget {
//Short circuit if we've reached a leaf of the tree
if (tree.children.isEmpty) {
// Handle case (4) from above.
- if ((tree.style.height ?? 0) == 0) {
- tree.style.margin = EdgeInsets.zero;
+ if (tree.style.height?.value == 0 &&
+ tree.style.height?.unit != Unit.auto) {
+ tree.style.margin = tree.style.margin?.collapse() ?? Margins.zero;
}
return tree;
}
@@ -668,47 +749,49 @@ class HtmlParser extends StatelessWidget {
// Handle case (1) from above.
// Top margins cannot collapse if the element has padding
if ((tree.style.padding?.top ?? 0) == 0) {
- final parentTop = tree.style.margin?.top ?? 0;
- final firstChildTop = tree.children.first.style.margin?.top ?? 0;
+ final parentTop = tree.style.margin?.top?.value ?? 0;
+ final firstChildTop = tree.children.first.style.margin?.top?.value ?? 0;
final newOuterMarginTop = max(parentTop, firstChildTop);
// Set the parent's margin
if (tree.style.margin == null) {
- tree.style.margin = EdgeInsets.only(top: newOuterMarginTop);
+ tree.style.margin = Margins.only(top: newOuterMarginTop);
} else {
- tree.style.margin = tree.style.margin!.copyWith(top: newOuterMarginTop);
+ tree.style.margin =
+ tree.style.margin!.copyWithEdge(top: newOuterMarginTop);
}
// And remove the child's margin
if (tree.children.first.style.margin == null) {
- tree.children.first.style.margin = EdgeInsets.zero;
+ tree.children.first.style.margin = Margins.zero;
} else {
tree.children.first.style.margin =
- tree.children.first.style.margin!.copyWith(top: 0);
+ tree.children.first.style.margin!.copyWithEdge(top: 0);
}
}
// Handle case (3) from above.
// Bottom margins cannot collapse if the element has padding
if ((tree.style.padding?.bottom ?? 0) == 0) {
- final parentBottom = tree.style.margin?.bottom ?? 0;
- final lastChildBottom = tree.children.last.style.margin?.bottom ?? 0;
+ final parentBottom = tree.style.margin?.bottom?.value ?? 0;
+ final lastChildBottom =
+ tree.children.last.style.margin?.bottom?.value ?? 0;
final newOuterMarginBottom = max(parentBottom, lastChildBottom);
// Set the parent's margin
if (tree.style.margin == null) {
- tree.style.margin = EdgeInsets.only(bottom: newOuterMarginBottom);
+ tree.style.margin = Margins.only(bottom: newOuterMarginBottom);
} else {
tree.style.margin =
- tree.style.margin!.copyWith(bottom: newOuterMarginBottom);
+ tree.style.margin!.copyWithEdge(bottom: newOuterMarginBottom);
}
// And remove the child's margin
if (tree.children.last.style.margin == null) {
- tree.children.last.style.margin = EdgeInsets.zero;
+ tree.children.last.style.margin = Margins.zero;
} else {
tree.children.last.style.margin =
- tree.children.last.style.margin!.copyWith(bottom: 0);
+ tree.children.last.style.margin!.copyWithEdge(bottom: 0);
}
}
@@ -716,24 +799,23 @@ class HtmlParser extends StatelessWidget {
if (tree.children.length > 1) {
for (int i = 1; i < tree.children.length; i++) {
final previousSiblingBottom =
- tree.children[i - 1].style.margin?.bottom ?? 0;
- final thisTop = tree.children[i].style.margin?.top ?? 0;
- final newInternalMargin = max(previousSiblingBottom, thisTop) / 2;
+ tree.children[i - 1].style.margin?.bottom?.value ?? 0;
+ final thisTop = tree.children[i].style.margin?.top?.value ?? 0;
+ final newInternalMargin = max(previousSiblingBottom, thisTop);
if (tree.children[i - 1].style.margin == null) {
tree.children[i - 1].style.margin =
- EdgeInsets.only(bottom: newInternalMargin);
+ Margins.only(bottom: newInternalMargin);
} else {
tree.children[i - 1].style.margin = tree.children[i - 1].style.margin!
- .copyWith(bottom: newInternalMargin);
+ .copyWithEdge(bottom: newInternalMargin);
}
if (tree.children[i].style.margin == null) {
- tree.children[i].style.margin =
- EdgeInsets.only(top: newInternalMargin);
+ tree.children[i].style.margin = Margins.only(top: newInternalMargin);
} else {
- tree.children[i].style.margin =
- tree.children[i].style.margin!.copyWith(top: newInternalMargin);
+ tree.children[i].style.margin = tree.children[i].style.margin!
+ .copyWithEdge(top: newInternalMargin);
}
}
}
@@ -752,18 +834,19 @@ class HtmlParser extends StatelessWidget {
tree.children.forEachIndexed((index, child) {
if (child is EmptyContentElement || child is EmptyLayoutElement) {
toRemove.add(child);
- } else if (child is TextContentElement
- && ((tree.name == "body"
- && (index == 0
- || index + 1 == tree.children.length
- || tree.children[index - 1].style.display == Display.BLOCK
- || tree.children[index + 1].style.display == Display.BLOCK))
- || tree.name == "ul")
- && child.text!.replaceAll(' ', '').isEmpty) {
+ } else if (child is TextContentElement &&
+ ((tree.name == "body" &&
+ (index == 0 ||
+ index + 1 == tree.children.length ||
+ tree.children[index - 1].style.display == Display.BLOCK ||
+ tree.children[index + 1].style.display ==
+ Display.BLOCK)) ||
+ tree.name == "ul") &&
+ child.text!.replaceAll(' ', '').isEmpty) {
toRemove.add(child);
- } else if (child is TextContentElement
- && child.text!.isEmpty
- && child.style.whiteSpace != WhiteSpace.PRE) {
+ } else if (child is TextContentElement &&
+ child.text!.isEmpty &&
+ child.style.whiteSpace != WhiteSpace.PRE) {
toRemove.add(child);
} else if (child is TextContentElement &&
child.style.whiteSpace != WhiteSpace.PRE &&
@@ -787,20 +870,65 @@ class HtmlParser extends StatelessWidget {
return tree;
}
- /// [_processFontSize] changes percent-based font sizes (negative numbers in this implementation)
- /// to pixel-based font sizes.
- static StyledElement _processFontSize(StyledElement tree) {
- double? parentFontSize = tree.style.fontSize?.size ?? FontSize.medium.size;
+ /// [_calculateRelativeValues] converts rem values to px sizes and then
+ /// applies relative calculations
+ static StyledElement _calculateRelativeValues(
+ StyledElement tree, double devicePixelRatio) {
+ double remSize = (tree.style.fontSize?.value ?? FontSize.medium.value);
+
+ //If the root element has a rem-based fontSize, then give it the default
+ // font size times the set rem value.
+ if (tree.style.fontSize?.unit == Unit.rem) {
+ tree.style.fontSize = FontSize(FontSize.medium.value * remSize);
+ }
+
+ _applyRelativeValuesRecursive(tree, remSize, devicePixelRatio);
+ tree.style.setRelativeValues(remSize, remSize / devicePixelRatio);
+
+ return tree;
+ }
+
+ /// This is the recursive worker function for [_calculateRelativeValues]
+ static void _applyRelativeValuesRecursive(
+ StyledElement tree, double remFontSize, double devicePixelRatio) {
+ //When we get to this point, there should be a valid fontSize at every level.
+ assert(tree.style.fontSize != null);
+
+ final parentFontSize = tree.style.fontSize!.value;
tree.children.forEach((child) {
- if ((child.style.fontSize?.size ?? parentFontSize)! < 0) {
- child.style.fontSize =
- FontSize(parentFontSize! * -child.style.fontSize!.size!);
+ if (child.style.fontSize == null) {
+ child.style.fontSize = FontSize(parentFontSize);
+ } else {
+ switch (child.style.fontSize!.unit) {
+ case Unit.em:
+ child.style.fontSize =
+ FontSize(parentFontSize * child.style.fontSize!.value);
+ break;
+ case Unit.percent:
+ child.style.fontSize = FontSize(
+ parentFontSize * (child.style.fontSize!.value / 100.0));
+ break;
+ case Unit.rem:
+ child.style.fontSize =
+ FontSize(remFontSize * child.style.fontSize!.value);
+ break;
+ case Unit.px:
+ case Unit.auto:
+ //Ignore
+ break;
+ }
}
- _processFontSize(child);
+ // Note: it is necessary to scale down the emSize by the factor of
+ // devicePixelRatio since Flutter seems to calculates font sizes using
+ // physical pixels, but margins/padding using logical pixels.
+ final emSize = child.style.fontSize!.value / devicePixelRatio;
+
+ tree.style.setRelativeValues(remFontSize, emSize);
+
+ _applyRelativeValuesRecursive(child, remFontSize, devicePixelRatio);
});
- return tree;
}
}
@@ -824,126 +952,12 @@ class RenderContext {
});
}
-/// A [ContainerSpan] is a widget with an [InlineSpan] child or children.
-///
-/// A [ContainerSpan] can have a border, background color, height, width, padding, and margin
-/// and can represent either an INLINE or BLOCK-level element.
-class ContainerSpan extends StatelessWidget {
- final AnchorKey? key;
- final Widget? child;
- final List? children;
- final Style style;
- final RenderContext newContext;
- final bool shrinkWrap;
-
- ContainerSpan({
- this.key,
- this.child,
- this.children,
- required this.style,
- required this.newContext,
- this.shrinkWrap = false,
- }): super(key: key);
-
- @override
- Widget build(BuildContext _) {
- return Container(
- decoration: BoxDecoration(
- border: style.border,
- color: style.backgroundColor,
- ),
- height: style.height,
- width: style.width,
- padding: style.padding?.nonNegative,
- margin: style.margin?.nonNegative,
- alignment: shrinkWrap ? null : style.alignment,
- child: child ??
- StyledText(
- textSpan: TextSpan(
- style: newContext.style.generateTextStyle(),
- children: children,
- ),
- style: newContext.style,
- renderContext: newContext,
- ),
- );
- }
-}
-
-class StyledText extends StatelessWidget {
- final InlineSpan textSpan;
- final Style style;
- final double textScaleFactor;
- final RenderContext renderContext;
- final AnchorKey? key;
- final bool _selectable;
- final TextSelectionControls? selectionControls;
- final ScrollPhysics? scrollPhysics;
-
- const StyledText({
- required this.textSpan,
- required this.style,
- this.textScaleFactor = 1.0,
- required this.renderContext,
- this.key,
- this.selectionControls,
- this.scrollPhysics,
- }) : _selectable = false,
- super(key: key);
-
- const StyledText.selectable({
- required TextSpan textSpan,
- required this.style,
- this.textScaleFactor = 1.0,
- required this.renderContext,
- this.key,
- this.selectionControls,
- this.scrollPhysics,
- }) : textSpan = textSpan,
- _selectable = true,
- super(key: key);
-
- @override
- Widget build(BuildContext context) {
- if (_selectable) {
- return SelectableText.rich(
- textSpan as TextSpan,
- style: style.generateTextStyle(),
- textAlign: style.textAlign,
- textDirection: style.direction,
- textScaleFactor: textScaleFactor,
- maxLines: style.maxLines,
- selectionControls: selectionControls,
- scrollPhysics: scrollPhysics,
- );
- }
- return SizedBox(
- width: consumeExpandedBlock(style.display, renderContext),
- child: Text.rich(
- textSpan,
- style: style.generateTextStyle(),
- textAlign: style.textAlign,
- textDirection: style.direction,
- textScaleFactor: textScaleFactor,
- maxLines: style.maxLines,
- overflow: style.textOverflow,
- ),
- );
- }
-
- double? consumeExpandedBlock(Display? display, RenderContext context) {
- if ((display == Display.BLOCK || display == Display.LIST_ITEM) && !renderContext.parser.shrinkWrap) {
- return double.infinity;
- }
- return null;
- }
-}
-
extension IterateLetters on String {
String nextLetter() {
String s = this.toLowerCase();
if (s == "z") {
- return String.fromCharCode(s.codeUnitAt(0) - 25) + String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa
+ return String.fromCharCode(s.codeUnitAt(0) - 25) +
+ String.fromCharCode(s.codeUnitAt(0) - 25); // AA or aa
} else {
var lastChar = s.substring(s.length - 1);
var sub = s.substring(0, s.length - 1);
diff --git a/lib/src/anchor.dart b/lib/src/anchor.dart
index bdba172c97..2dbdd7a1bb 100644
--- a/lib/src/anchor.dart
+++ b/lib/src/anchor.dart
@@ -30,7 +30,10 @@ class AnchorKey extends GlobalKey {
@override
bool operator ==(Object other) =>
identical(this, other) ||
- other is AnchorKey && runtimeType == other.runtimeType && parentKey == other.parentKey && id == other.id;
+ other is AnchorKey &&
+ runtimeType == other.runtimeType &&
+ parentKey == other.parentKey &&
+ id == other.id;
@override
int get hashCode => parentKey.hashCode ^ id.hashCode;
diff --git a/lib/src/css_box_widget.dart b/lib/src/css_box_widget.dart
new file mode 100644
index 0000000000..5bfffd82da
--- /dev/null
+++ b/lib/src/css_box_widget.dart
@@ -0,0 +1,711 @@
+import 'dart:math' as math;
+
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_html/flutter_html.dart';
+
+class CssBoxWidget extends StatelessWidget {
+ CssBoxWidget({
+ this.key,
+ required this.child,
+ required this.style,
+ this.textDirection,
+ this.childIsReplaced = false,
+ this.shrinkWrap = false,
+ }) : super(key: key);
+
+ /// Generates a CSSBoxWidget that contains a list of InlineSpan children.
+ CssBoxWidget.withInlineSpanChildren({
+ this.key,
+ required List children,
+ required this.style,
+ this.textDirection,
+ this.childIsReplaced = false,
+ this.shrinkWrap = false,
+ bool selectable = false,
+ TextSelectionControls? selectionControls,
+ ScrollPhysics? scrollPhysics,
+ }) : this.child = selectable
+ ? _generateSelectableWidgetChild(
+ children,
+ style,
+ selectionControls,
+ scrollPhysics,
+ )
+ : _generateWidgetChild(children, style),
+ super(key: key);
+
+ /// An optional anchor key to use in finding this box
+ final AnchorKey? key;
+
+ /// The child to be rendered within the CSS Box.
+ final Widget child;
+
+ /// The style to use to compute this box's margins/padding/box decoration/width/height/etc.
+ ///
+ /// Note that this style will only apply to this box, and will not cascade to its child.
+ final Style style;
+
+ /// Sets the direction the text of this widget should flow. If unset or null,
+ /// the nearest Directionality ancestor is used as a default. If that cannot
+ /// be found, this Widget's renderer will raise an assertion.
+ final TextDirection? textDirection;
+
+ /// Indicates whether this child is a replaced element that manages its own width
+ /// (e.g. img, video, iframe, audio, etc.)
+ final bool childIsReplaced;
+
+ /// Whether or not the content should ignore auto horizontal margins and not
+ /// necessarily take up the full available width unless necessary
+ final bool shrinkWrap;
+
+ @override
+ Widget build(BuildContext context) {
+ return _CSSBoxRenderer(
+ width: style.width ?? Width.auto(),
+ height: style.height ?? Height.auto(),
+ paddingSize: style.padding?.collapsedSize ?? Size.zero,
+ borderSize: style.border?.dimensions.collapsedSize ?? Size.zero,
+ margins: style.margin ?? Margins.zero,
+ display: style.display ?? Display.INLINE,
+ childIsReplaced: childIsReplaced,
+ emValue: _calculateEmValue(style, context),
+ textDirection: _checkTextDirection(context, textDirection),
+ shrinkWrap: shrinkWrap,
+ child: Container(
+ decoration: BoxDecoration(
+ border: style.border,
+ color: style.backgroundColor, //Colors the padding and content boxes
+ ),
+ width: _shouldExpandToFillBlock() ? double.infinity : null,
+ padding: style.padding ?? EdgeInsets.zero,
+ child: child,
+ ),
+ );
+ }
+
+ /// Takes a list of InlineSpan children and generates a Text.rich Widget
+ /// containing those children.
+ static Widget _generateWidgetChild(List children, Style style) {
+ if (children.isEmpty) {
+ return Container();
+ }
+
+ return Text.rich(
+ TextSpan(
+ style: style.generateTextStyle(),
+ children: children,
+ ),
+ style: style.generateTextStyle(),
+ textAlign: style.textAlign,
+ textDirection: style.direction,
+ maxLines: style.maxLines,
+ overflow: style.textOverflow,
+ );
+ }
+
+ static Widget _generateSelectableWidgetChild(
+ List children,
+ Style style,
+ TextSelectionControls? selectionControls,
+ ScrollPhysics? scrollPhysics,
+ ) {
+ if (children.isEmpty) {
+ return Container();
+ }
+
+ return SelectableText.rich(
+ TextSpan(
+ style: style.generateTextStyle(),
+ children: children,
+ ),
+ style: style.generateTextStyle(),
+ textAlign: style.textAlign,
+ textDirection: style.direction,
+ maxLines: style.maxLines,
+ selectionControls: selectionControls,
+ scrollPhysics: scrollPhysics,
+ );
+ }
+
+ /// Whether or not the content-box should expand its width to fill the
+ /// width available to it or if it should just let its inner content
+ /// determine the content-box's width.
+ bool _shouldExpandToFillBlock() {
+ return (style.display == Display.BLOCK ||
+ style.display == Display.LIST_ITEM) &&
+ !childIsReplaced &&
+ !shrinkWrap;
+ }
+
+ TextDirection _checkTextDirection(
+ BuildContext context, TextDirection? direction) {
+ final textDirection = direction ?? Directionality.maybeOf(context);
+
+ assert(
+ textDirection != null,
+ "CSSBoxWidget needs either a Directionality ancestor or a provided textDirection",
+ );
+
+ return textDirection!;
+ }
+}
+
+class _CSSBoxRenderer extends MultiChildRenderObjectWidget {
+ _CSSBoxRenderer({
+ Key? key,
+ required Widget child,
+ required this.display,
+ required this.margins,
+ required this.width,
+ required this.height,
+ required this.borderSize,
+ required this.paddingSize,
+ required this.textDirection,
+ required this.childIsReplaced,
+ required this.emValue,
+ required this.shrinkWrap,
+ }) : super(key: key, children: [child]);
+
+ /// The Display type of the element
+ final Display display;
+
+ /// The computed margin values for this element
+ final Margins margins;
+
+ /// The width of the element
+ final Width width;
+
+ /// The height of the element
+ final Height height;
+
+ /// The collapsed size of the element's border
+ final Size borderSize;
+
+ /// The collapsed size of the element's padding
+ final Size paddingSize;
+
+ /// The direction for this widget's text to flow.
+ final TextDirection textDirection;
+
+ /// Whether or not the child being rendered is a replaced element
+ /// (this changes the rules for rendering)
+ final bool childIsReplaced;
+
+ /// The calculated size of 1em in pixels
+ final double emValue;
+
+ /// Whether or not this container should shrinkWrap its contents.
+ /// (see definition on [CSSBoxWidget])
+ final bool shrinkWrap;
+
+ @override
+ _RenderCSSBox createRenderObject(BuildContext context) {
+ return _RenderCSSBox(
+ display: display,
+ width: width..normalize(emValue),
+ height: height..normalize(emValue),
+ margins: _preProcessMargins(margins, shrinkWrap),
+ borderSize: borderSize,
+ paddingSize: paddingSize,
+ textDirection: textDirection,
+ childIsReplaced: childIsReplaced,
+ shrinkWrap: shrinkWrap,
+ );
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, _RenderCSSBox renderObject) {
+ renderObject
+ ..display = display
+ ..width = (width..normalize(emValue))
+ ..height = (height..normalize(emValue))
+ ..margins = _preProcessMargins(margins, shrinkWrap)
+ ..borderSize = borderSize
+ ..paddingSize = paddingSize
+ ..textDirection = textDirection
+ ..childIsReplaced = childIsReplaced
+ ..shrinkWrap = shrinkWrap;
+ }
+
+ Margins _preProcessMargins(Margins margins, bool shrinkWrap) {
+ Margin leftMargin = margins.left ?? Margin.zero();
+ Margin rightMargin = margins.right ?? Margin.zero();
+ Margin topMargin = margins.top ?? Margin.zero();
+ Margin bottomMargin = margins.bottom ?? Margin.zero();
+
+ //Preprocess margins to a pixel value
+ leftMargin.normalize(emValue);
+ rightMargin.normalize(emValue);
+ topMargin.normalize(emValue);
+ bottomMargin.normalize(emValue);
+
+ // See https://drafts.csswg.org/css2/#inline-width
+ // and https://drafts.csswg.org/css2/#inline-replaced-width
+ // and https://drafts.csswg.org/css2/#inlineblock-width
+ // and https://drafts.csswg.org/css2/#inlineblock-replaced-width
+ if (display == Display.INLINE || display == Display.INLINE_BLOCK) {
+ if (margins.left?.unit == Unit.auto) {
+ leftMargin = Margin.zero();
+ }
+ if (margins.right?.unit == Unit.auto) {
+ rightMargin = Margin.zero();
+ }
+ }
+
+ //Shrink-wrap margins if applicable
+ if (shrinkWrap && leftMargin.unit == Unit.auto) {
+ leftMargin = Margin.zero();
+ }
+
+ if (shrinkWrap && rightMargin.unit == Unit.auto) {
+ rightMargin = Margin.zero();
+ }
+
+ return Margins(
+ top: topMargin,
+ right: rightMargin,
+ bottom: bottomMargin,
+ left: leftMargin,
+ );
+ }
+}
+
+/// Implements the CSS layout algorithm
+class _RenderCSSBox extends RenderBox
+ with
+ ContainerRenderObjectMixin,
+ RenderBoxContainerDefaultsMixin {
+ _RenderCSSBox({
+ required Display display,
+ required Width width,
+ required Height height,
+ required Margins margins,
+ required Size borderSize,
+ required Size paddingSize,
+ required TextDirection textDirection,
+ required bool childIsReplaced,
+ required bool shrinkWrap,
+ }) : _display = display,
+ _width = width,
+ _height = height,
+ _margins = margins,
+ _borderSize = borderSize,
+ _paddingSize = paddingSize,
+ _textDirection = textDirection,
+ _childIsReplaced = childIsReplaced,
+ _shrinkWrap = shrinkWrap;
+
+ Display _display;
+
+ Display get display => _display;
+
+ set display(Display display) {
+ _display = display;
+ markNeedsLayout();
+ }
+
+ Width _width;
+
+ Width get width => _width;
+
+ set width(Width width) {
+ _width = width;
+ markNeedsLayout();
+ }
+
+ Height _height;
+
+ Height get height => _height;
+
+ set height(Height height) {
+ _height = height;
+ markNeedsLayout();
+ }
+
+ Margins _margins;
+
+ Margins get margins => _margins;
+
+ set margins(Margins margins) {
+ _margins = margins;
+ markNeedsLayout();
+ }
+
+ Size _borderSize;
+
+ Size get borderSize => _borderSize;
+
+ set borderSize(Size size) {
+ _borderSize = size;
+ markNeedsLayout();
+ }
+
+ Size _paddingSize;
+
+ Size get paddingSize => _paddingSize;
+
+ set paddingSize(Size size) {
+ _paddingSize = size;
+ markNeedsLayout();
+ }
+
+ TextDirection _textDirection;
+
+ TextDirection get textDirection => _textDirection;
+
+ set textDirection(TextDirection textDirection) {
+ _textDirection = textDirection;
+ markNeedsLayout();
+ }
+
+ bool _childIsReplaced;
+
+ bool get childIsReplaced => _childIsReplaced;
+
+ set childIsReplaced(bool childIsReplaced) {
+ _childIsReplaced = childIsReplaced;
+ markNeedsLayout();
+ }
+
+ bool _shrinkWrap;
+
+ bool get shrinkWrap => _shrinkWrap;
+
+ set shrinkWrap(bool shrinkWrap) {
+ _shrinkWrap = shrinkWrap;
+ markNeedsLayout();
+ }
+
+ @override
+ void setupParentData(RenderBox child) {
+ if (child.parentData is! CSSBoxParentData)
+ child.parentData = CSSBoxParentData();
+ }
+
+ static double getIntrinsicDimension(RenderBox? firstChild,
+ double Function(RenderBox child) mainChildSizeGetter) {
+ double extent = 0.0;
+ RenderBox? child = firstChild;
+ while (child != null) {
+ final CSSBoxParentData childParentData =
+ child.parentData! as CSSBoxParentData;
+ extent = math.max(extent, mainChildSizeGetter(child));
+ assert(child.parentData == childParentData);
+ child = childParentData.nextSibling;
+ }
+ return extent;
+ }
+
+ @override
+ double computeMinIntrinsicWidth(double height) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMinIntrinsicWidth(height));
+ }
+
+ @override
+ double computeMaxIntrinsicWidth(double height) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMaxIntrinsicWidth(height));
+ }
+
+ @override
+ double computeMinIntrinsicHeight(double width) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMinIntrinsicHeight(width));
+ }
+
+ @override
+ double computeMaxIntrinsicHeight(double width) {
+ return getIntrinsicDimension(
+ firstChild, (RenderBox child) => child.getMaxIntrinsicHeight(width));
+ }
+
+ @override
+ double? computeDistanceToActualBaseline(TextBaseline baseline) {
+ return firstChild?.getDistanceToActualBaseline(baseline);
+ }
+
+ @override
+ Size computeDryLayout(BoxConstraints constraints) {
+ return _computeSize(
+ constraints: constraints,
+ layoutChild: ChildLayoutHelper.dryLayoutChild,
+ ).parentSize;
+ }
+
+ _Sizes _computeSize(
+ {required BoxConstraints constraints,
+ required ChildLayouter layoutChild}) {
+ if (childCount == 0) {
+ return _Sizes(constraints.biggest, Size.zero);
+ }
+
+ Size containingBlockSize = constraints.biggest;
+ double width = containingBlockSize.width;
+ double height = containingBlockSize.height;
+
+ RenderBox? child = firstChild;
+ assert(child != null);
+
+ // Calculate child size
+ final childConstraints = constraints.copyWith(
+ maxWidth: (this.width.unit != Unit.auto)
+ ? this.width.value
+ : containingBlockSize.width -
+ (this.margins.left?.value ?? 0) -
+ (this.margins.right?.value ?? 0),
+ maxHeight: (this.height.unit != Unit.auto)
+ ? this.height.value
+ : containingBlockSize.height -
+ (this.margins.top?.value ?? 0) -
+ (this.margins.bottom?.value ?? 0),
+ minWidth: (this.width.unit != Unit.auto) ? this.width.value : 0,
+ minHeight: (this.height.unit != Unit.auto) ? this.height.value : 0,
+ );
+ final Size childSize = layoutChild(child!, childConstraints);
+
+ // Calculate used values of margins based on rules
+ final usedMargins = _calculateUsedMargins(childSize, containingBlockSize);
+ final horizontalMargins =
+ (usedMargins.left?.value ?? 0) + (usedMargins.right?.value ?? 0);
+ final verticalMargins =
+ (usedMargins.top?.value ?? 0) + (usedMargins.bottom?.value ?? 0);
+
+ //Calculate Width and Height of CSS Box
+ height = childSize.height;
+ switch (display) {
+ case Display.BLOCK:
+ width = (shrinkWrap || childIsReplaced)
+ ? childSize.width + horizontalMargins
+ : containingBlockSize.width;
+ height = childSize.height + verticalMargins;
+ break;
+ case Display.INLINE:
+ width = childSize.width + horizontalMargins;
+ height = childSize.height;
+ break;
+ case Display.INLINE_BLOCK:
+ width = childSize.width + horizontalMargins;
+ height = childSize.height + verticalMargins;
+ break;
+ case Display.LIST_ITEM:
+ width = shrinkWrap
+ ? childSize.width + horizontalMargins
+ : containingBlockSize.width;
+ height = childSize.height + verticalMargins;
+ break;
+ case Display.NONE:
+ width = 0;
+ height = 0;
+ break;
+ }
+
+ return _Sizes(constraints.constrain(Size(width, height)), childSize);
+ }
+
+ @override
+ void performLayout() {
+ final BoxConstraints constraints = this.constraints;
+
+ final sizes = _computeSize(
+ constraints: constraints,
+ layoutChild: ChildLayoutHelper.layoutChild,
+ );
+ size = sizes.parentSize;
+
+ RenderBox? child = firstChild;
+ while (child != null) {
+ final CSSBoxParentData childParentData =
+ child.parentData! as CSSBoxParentData;
+
+ // Calculate used margins based on constraints and child size
+ final usedMargins =
+ _calculateUsedMargins(sizes.childSize, constraints.biggest);
+ final leftMargin = usedMargins.left?.value ?? 0;
+ final topMargin = usedMargins.top?.value ?? 0;
+
+ double leftOffset = 0;
+ double topOffset = 0;
+ switch (display) {
+ case Display.BLOCK:
+ leftOffset = leftMargin;
+ topOffset = topMargin;
+ break;
+ case Display.INLINE:
+ leftOffset = leftMargin;
+ break;
+ case Display.INLINE_BLOCK:
+ leftOffset = leftMargin;
+ topOffset = topMargin;
+ break;
+ case Display.LIST_ITEM:
+ leftOffset = leftMargin;
+ topOffset = topMargin;
+ break;
+ case Display.NONE:
+ //No offset
+ break;
+ }
+ childParentData.offset = Offset(leftOffset, topOffset);
+
+ assert(child.parentData == childParentData);
+ child = childParentData.nextSibling;
+ }
+ }
+
+ Margins _calculateUsedMargins(Size childSize, Size containingBlockSize) {
+ //We assume that margins have already been preprocessed
+ // (i.e. they are non-null and either px units or auto.
+ assert(margins.left != null && margins.right != null);
+ assert(margins.left!.unit == Unit.px || margins.left!.unit == Unit.auto);
+ assert(margins.right!.unit == Unit.px || margins.right!.unit == Unit.auto);
+
+ Margin marginLeft = margins.left!;
+ Margin marginRight = margins.right!;
+
+ bool widthIsAuto = width.unit == Unit.auto;
+ bool marginLeftIsAuto = marginLeft.unit == Unit.auto;
+ bool marginRightIsAuto = marginRight.unit == Unit.auto;
+
+ if (display == Display.BLOCK) {
+ if (childIsReplaced) {
+ widthIsAuto = false;
+ }
+
+ if (shrinkWrap) {
+ widthIsAuto = false;
+ }
+
+ //If width is not auto and the width of the margin box is larger than the
+ // width of the containing block, then consider left and right margins to
+ // have a 0 value.
+ if (!widthIsAuto) {
+ if ((childSize.width + marginLeft.value + marginRight.value) >
+ containingBlockSize.width) {
+ //Treat auto values of margin left and margin right as 0 for following rules
+ marginLeft = Margin(0);
+ marginRight = Margin(0);
+ marginLeftIsAuto = false;
+ marginRightIsAuto = false;
+ }
+ }
+
+ // If all values are non-auto, the box is overconstrained.
+ // One of the margins will need to be adjusted so that the
+ // entire width of the containing block is used.
+ if (!widthIsAuto &&
+ !marginLeftIsAuto &&
+ !marginRightIsAuto &&
+ !shrinkWrap &&
+ !childIsReplaced) {
+ //Ignore either left or right margin based on textDirection.
+
+ switch (textDirection) {
+ case TextDirection.rtl:
+ final difference =
+ containingBlockSize.width - childSize.width - marginRight.value;
+ marginLeft = Margin(difference);
+ break;
+ case TextDirection.ltr:
+ final difference =
+ containingBlockSize.width - childSize.width - marginLeft.value;
+ marginRight = Margin(difference);
+ break;
+ }
+ }
+
+ // If there is exactly one value specified as auto, compute it value from the equality (our widths are already set)
+ if (widthIsAuto && !marginLeftIsAuto && !marginRightIsAuto) {
+ widthIsAuto = false;
+ } else if (!widthIsAuto && marginLeftIsAuto && !marginRightIsAuto) {
+ marginLeft = Margin(
+ containingBlockSize.width - childSize.width - marginRight.value);
+ marginLeftIsAuto = false;
+ } else if (!widthIsAuto && !marginLeftIsAuto && marginRightIsAuto) {
+ marginRight = Margin(
+ containingBlockSize.width - childSize.width - marginLeft.value);
+ marginRightIsAuto = false;
+ }
+
+ //If width is set to auto, any other auto values become 0, and width
+ // follows from the resulting equality.
+ if (widthIsAuto) {
+ if (marginLeftIsAuto) {
+ marginLeft = Margin(0);
+ marginLeftIsAuto = false;
+ }
+ if (marginRightIsAuto) {
+ marginRight = Margin(0);
+ marginRightIsAuto = false;
+ }
+ widthIsAuto = false;
+ }
+
+ //If both margin-left and margin-right are auto, their used values are equal.
+ // This horizontally centers the element within the containing block.
+ if (marginLeftIsAuto && marginRightIsAuto) {
+ final newMargin =
+ Margin((containingBlockSize.width - childSize.width) / 2);
+ marginLeft = newMargin;
+ marginRight = newMargin;
+ marginLeftIsAuto = false;
+ marginRightIsAuto = false;
+ }
+
+ //Assert that all auto values have been assigned.
+ assert(!marginLeftIsAuto && !marginRightIsAuto && !widthIsAuto);
+ }
+
+ return Margins(
+ left: marginLeft,
+ right: marginRight,
+ top: margins.top,
+ bottom: margins.bottom);
+ }
+
+ @override
+ bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+ return defaultHitTestChildren(result, position: position);
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ defaultPaint(context, offset);
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ }
+}
+
+extension Normalize on Dimension {
+ void normalize(double emValue) {
+ switch (this.unit) {
+ case Unit.em:
+ this.value *= emValue;
+ this.unit = Unit.px;
+ return;
+ case Unit.px:
+ case Unit.auto:
+ case Unit.percent:
+ return;
+ }
+ }
+}
+
+double _calculateEmValue(Style style, BuildContext buildContext) {
+ return (style.fontSize?.emValue ?? 16) *
+ MediaQuery.textScaleFactorOf(buildContext) *
+ MediaQuery.of(buildContext).devicePixelRatio;
+}
+
+class CSSBoxParentData extends ContainerBoxParentData {}
+
+class _Sizes {
+ final Size parentSize;
+ final Size childSize;
+
+ const _Sizes(this.parentSize, this.childSize);
+}
diff --git a/lib/src/css_parser.dart b/lib/src/css_parser.dart
index acc4724cc2..2d709187e9 100644
--- a/lib/src/css_parser.dart
+++ b/lib/src/css_parser.dart
@@ -13,53 +13,105 @@ Style declarationsToStyle(Map> declarations) {
if (value.isNotEmpty) {
switch (property) {
case 'background-color':
- style.backgroundColor = ExpressionMapping.expressionToColor(value.first) ?? style.backgroundColor;
+ style.backgroundColor =
+ ExpressionMapping.expressionToColor(value.first) ??
+ style.backgroundColor;
break;
case 'border':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- List? borderColors = value.where((element) => ExpressionMapping.expressionToColor(element) != null).toList();
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ !(element is css.LengthTerm) &&
+ !(element is css.PercentageTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm)));
+ List? borderColors = value
+ .where((element) =>
+ ExpressionMapping.expressionToColor(element) != null)
+ .toList();
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
List? borderStyles = potentialStyles;
- style.border = ExpressionMapping.expressionToBorder(borderWidths, borderStyles, borderColors);
+ style.border = ExpressionMapping.expressionToBorder(
+ borderWidths, borderStyles, borderColors);
break;
case 'border-left':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ !(element is css.LengthTerm) &&
+ !(element is css.PercentageTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm)));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
right: style.border?.right ?? BorderSide.none,
top: style.border?.top ?? BorderSide.none,
bottom: style.border?.bottom ?? BorderSide.none,
@@ -67,135 +119,226 @@ Style declarationsToStyle(Map> declarations) {
style.border = newBorder;
break;
case 'border-right':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ !(element is css.LengthTerm) &&
+ !(element is css.PercentageTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm)));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
top: style.border?.top ?? BorderSide.none,
bottom: style.border?.bottom ?? BorderSide.none,
);
style.border = newBorder;
break;
case 'border-top':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ !(element is css.LengthTerm) &&
+ !(element is css.PercentageTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm)));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right ?? BorderSide.none,
top: style.border?.top.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
bottom: style.border?.bottom ?? BorderSide.none,
);
style.border = newBorder;
break;
case 'border-bottom':
- List? borderWidths = value.whereType().toList();
+ List? borderWidths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [BorderSide.width], so make sure to remove those before passing it to [ExpressionMapping]
- borderWidths.removeWhere((element) => element == null || (element.text != "thin"
- && element.text != "medium" && element.text != "thick"
- && !(element is css.LengthTerm) && !(element is css.PercentageTerm)
- && !(element is css.EmTerm) && !(element is css.RemTerm)
- && !(element is css.NumberTerm))
- );
- css.LiteralTerm? borderWidth = borderWidths.firstWhereOrNull((element) => element != null);
- css.Expression? borderColor = value.firstWhereOrNull((element) => ExpressionMapping.expressionToColor(element) != null);
- List? potentialStyles = value.whereType().toList();
+ borderWidths.removeWhere((element) =>
+ element == null ||
+ (element.text != "thin" &&
+ element.text != "medium" &&
+ element.text != "thick" &&
+ !(element is css.LengthTerm) &&
+ !(element is css.PercentageTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm)));
+ css.LiteralTerm? borderWidth =
+ borderWidths.firstWhereOrNull((element) => element != null);
+ css.Expression? borderColor = value.firstWhereOrNull((element) =>
+ ExpressionMapping.expressionToColor(element) != null);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// Currently doesn't matter, as Flutter only supports "solid" or "none", but may support more in the future.
- List possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
+ List possibleBorderValues = [
+ "dotted",
+ "dashed",
+ "solid",
+ "double",
+ "groove",
+ "ridge",
+ "inset",
+ "outset",
+ "none",
+ "hidden"
+ ];
+
/// List might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
+ potentialStyles.removeWhere((element) =>
+ element == null || !possibleBorderValues.contains(element.text));
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
Border newBorder = Border(
left: style.border?.left ?? BorderSide.none,
right: style.border?.right ?? BorderSide.none,
top: style.border?.top ?? BorderSide.none,
bottom: style.border?.bottom.copyWith(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor),
- ) ?? BorderSide(
- width: ExpressionMapping.expressionToBorderWidth(borderWidth),
- style: ExpressionMapping.expressionToBorderStyle(borderStyle),
- color: ExpressionMapping.expressionToColor(borderColor) ?? Colors.black,
- ),
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor),
+ ) ??
+ BorderSide(
+ width: ExpressionMapping.expressionToBorderWidth(borderWidth),
+ style: ExpressionMapping.expressionToBorderStyle(borderStyle),
+ color: ExpressionMapping.expressionToColor(borderColor) ??
+ Colors.black,
+ ),
);
style.border = newBorder;
break;
case 'color':
- style.color = ExpressionMapping.expressionToColor(value.first) ?? style.color;
+ style.color =
+ ExpressionMapping.expressionToColor(value.first) ?? style.color;
break;
case 'direction':
- style.direction = ExpressionMapping.expressionToDirection(value.first);
+ style.direction =
+ ExpressionMapping.expressionToDirection(value.first);
break;
case 'display':
style.display = ExpressionMapping.expressionToDisplay(value.first);
break;
case 'line-height':
- style.lineHeight = ExpressionMapping.expressionToLineHeight(value.first);
+ style.lineHeight =
+ ExpressionMapping.expressionToLineHeight(value.first);
break;
case 'font-family':
- style.fontFamily = ExpressionMapping.expressionToFontFamily(value.first) ?? style.fontFamily;
+ style.fontFamily =
+ ExpressionMapping.expressionToFontFamily(value.first) ??
+ style.fontFamily;
break;
case 'font-feature-settings':
- style.fontFeatureSettings = ExpressionMapping.expressionToFontFeatureSettings(value);
+ style.fontFeatureSettings =
+ ExpressionMapping.expressionToFontFeatureSettings(value);
break;
case 'font-size':
- style.fontSize = ExpressionMapping.expressionToFontSize(value.first) ?? style.fontSize;
+ style.fontSize =
+ ExpressionMapping.expressionToFontSize(value.first) ??
+ style.fontSize;
break;
case 'font-style':
- style.fontStyle = ExpressionMapping.expressionToFontStyle(value.first);
+ style.fontStyle =
+ ExpressionMapping.expressionToFontStyle(value.first);
break;
case 'font-weight':
- style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
+ style.fontWeight =
+ ExpressionMapping.expressionToFontWeight(value.first);
break;
case 'list-style':
- css.LiteralTerm? position = value.firstWhereOrNull((e) => e is css.LiteralTerm && (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?;
- css.UriTerm? image = value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?;
- css.LiteralTerm? type = value.firstWhereOrNull((e) => e is css.LiteralTerm && e.text != "outside" && e.text != "inside") as css.LiteralTerm?;
+ css.LiteralTerm? position = value.firstWhereOrNull((e) =>
+ e is css.LiteralTerm &&
+ (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?;
+ css.UriTerm? image =
+ value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?;
+ css.LiteralTerm? type = value.firstWhereOrNull((e) =>
+ e is css.LiteralTerm &&
+ e.text != "outside" &&
+ e.text != "inside") as css.LiteralTerm?;
if (position != null) {
switch (position.text) {
case 'outside':
@@ -207,14 +350,20 @@ Style declarationsToStyle(Map> declarations) {
}
}
if (image != null) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(image) ?? style.listStyleType;
+ style.listStyleType =
+ ExpressionMapping.expressionToListStyleType(image) ??
+ style.listStyleType;
} else if (type != null) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(type) ?? style.listStyleType;
+ style.listStyleType =
+ ExpressionMapping.expressionToListStyleType(type) ??
+ style.listStyleType;
}
break;
case 'list-style-image':
if (value.first is css.UriTerm) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.UriTerm) ?? style.listStyleType;
+ style.listStyleType = ExpressionMapping.expressionToListStyleType(
+ value.first as css.UriTerm) ??
+ style.listStyleType;
}
break;
case 'list-style-position':
@@ -230,54 +379,63 @@ Style declarationsToStyle(Map> declarations) {
}
break;
case 'height':
- style.height = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.height;
+ style.height =
+ ExpressionMapping.expressionToHeight(value.first) ?? style.height;
break;
case 'list-style-type':
if (value.first is css.LiteralTerm) {
- style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.LiteralTerm) ?? style.listStyleType;
+ style.listStyleType = ExpressionMapping.expressionToListStyleType(
+ value.first as css.LiteralTerm) ??
+ style.listStyleType;
}
break;
case 'margin':
- List? marginLengths = value.whereType().toList();
+ List? marginLengths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for margin length, so make sure to remove those before passing it to [ExpressionMapping]
- marginLengths.removeWhere((element) => !(element is css.LengthTerm)
- && !(element is css.EmTerm)
- && !(element is css.RemTerm)
- && !(element is css.NumberTerm)
- );
- List margin = ExpressionMapping.expressionToPadding(marginLengths);
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- left: margin[0],
- right: margin[1],
- top: margin[2],
- bottom: margin[3],
+ marginLengths.removeWhere((element) =>
+ !(element is css.LengthTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm) &&
+ !(element.text == 'auto'));
+ Margins margin = ExpressionMapping.expressionToMargins(marginLengths);
+ style.margin = (style.margin ?? Margins.all(0)).copyWith(
+ left: margin.left,
+ right: margin.right,
+ top: margin.top,
+ bottom: margin.bottom,
);
break;
case 'margin-left':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- left: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero).copyWith(
+ left: ExpressionMapping.expressionToMargin(value.first));
break;
case 'margin-right':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- right: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero).copyWith(
+ right: ExpressionMapping.expressionToMargin(value.first));
break;
case 'margin-top':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- top: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero)
+ .copyWith(top: ExpressionMapping.expressionToMargin(value.first));
break;
case 'margin-bottom':
- style.margin = (style.margin ?? EdgeInsets.zero).copyWith(
- bottom: ExpressionMapping.expressionToPaddingLength(value.first));
+ style.margin = (style.margin ?? Margins.zero).copyWith(
+ bottom: ExpressionMapping.expressionToMargin(value.first));
break;
case 'padding':
- List? paddingLengths = value.whereType().toList();
+ List? paddingLengths =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for padding length, so make sure to remove those before passing it to [ExpressionMapping]
- paddingLengths.removeWhere((element) => !(element is css.LengthTerm)
- && !(element is css.EmTerm)
- && !(element is css.RemTerm)
- && !(element is css.NumberTerm)
- );
- List padding = ExpressionMapping.expressionToPadding(paddingLengths);
+ paddingLengths.removeWhere((element) =>
+ !(element is css.LengthTerm) &&
+ !(element is css.EmTerm) &&
+ !(element is css.RemTerm) &&
+ !(element is css.NumberTerm));
+ List padding =
+ ExpressionMapping.expressionToPadding(paddingLengths);
style.padding = (style.padding ?? EdgeInsets.zero).copyWith(
left: padding[0],
right: padding[1],
@@ -302,36 +460,65 @@ Style declarationsToStyle(Map> declarations) {
bottom: ExpressionMapping.expressionToPaddingLength(value.first));
break;
case 'text-align':
- style.textAlign = ExpressionMapping.expressionToTextAlign(value.first);
+ style.textAlign =
+ ExpressionMapping.expressionToTextAlign(value.first);
break;
case 'text-decoration':
- List? textDecorationList = value.whereType().toList();
+ List? textDecorationList =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [textDecorationList], so make sure to remove those before passing it to [ExpressionMapping]
- textDecorationList.removeWhere((element) => element == null || (element.text != "none"
- && element.text != "overline" && element.text != "underline" && element.text != "line-through"));
+ textDecorationList.removeWhere((element) =>
+ element == null ||
+ (element.text != "none" &&
+ element.text != "overline" &&
+ element.text != "underline" &&
+ element.text != "line-through"));
List? nullableList = value;
css.Expression? textDecorationColor;
- textDecorationColor = nullableList.firstWhereOrNull(
- (element) => element is css.HexColorTerm || element is css.FunctionTerm);
- List? potentialStyles = value.whereType().toList();
+ textDecorationColor = nullableList.firstWhereOrNull((element) =>
+ element is css.HexColorTerm || element is css.FunctionTerm);
+ List? potentialStyles =
+ value.whereType().toList();
+
/// List might include other values than the ones we want for [textDecorationStyle], so make sure to remove those before passing it to [ExpressionMapping]
- potentialStyles.removeWhere((element) => element == null || (element.text != "solid"
- && element.text != "double" && element.text != "dashed" && element.text != "dotted" && element.text != "wavy"));
- css.LiteralTerm? textDecorationStyle = potentialStyles.isNotEmpty ? potentialStyles.last : null;
- style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList);
- if (textDecorationColor != null) style.textDecorationColor = ExpressionMapping.expressionToColor(textDecorationColor)
- ?? style.textDecorationColor;
- if (textDecorationStyle != null) style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(textDecorationStyle);
+ potentialStyles.removeWhere((element) =>
+ element == null ||
+ (element.text != "solid" &&
+ element.text != "double" &&
+ element.text != "dashed" &&
+ element.text != "dotted" &&
+ element.text != "wavy"));
+ css.LiteralTerm? textDecorationStyle =
+ potentialStyles.isNotEmpty ? potentialStyles.last : null;
+ style.textDecoration =
+ ExpressionMapping.expressionToTextDecorationLine(
+ textDecorationList);
+ if (textDecorationColor != null)
+ style.textDecorationColor =
+ ExpressionMapping.expressionToColor(textDecorationColor) ??
+ style.textDecorationColor;
+ if (textDecorationStyle != null)
+ style.textDecorationStyle =
+ ExpressionMapping.expressionToTextDecorationStyle(
+ textDecorationStyle);
break;
case 'text-decoration-color':
- style.textDecorationColor = ExpressionMapping.expressionToColor(value.first) ?? style.textDecorationColor;
+ style.textDecorationColor =
+ ExpressionMapping.expressionToColor(value.first) ??
+ style.textDecorationColor;
break;
case 'text-decoration-line':
- List? textDecorationList = value.whereType().toList();
- style.textDecoration = ExpressionMapping.expressionToTextDecorationLine(textDecorationList);
+ List? textDecorationList =
+ value.whereType().toList();
+ style.textDecoration =
+ ExpressionMapping.expressionToTextDecorationLine(
+ textDecorationList);
break;
case 'text-decoration-style':
- style.textDecorationStyle = ExpressionMapping.expressionToTextDecorationStyle(value.first as css.LiteralTerm);
+ style.textDecorationStyle =
+ ExpressionMapping.expressionToTextDecorationStyle(
+ value.first as css.LiteralTerm);
break;
case 'text-shadow':
style.textShadow = ExpressionMapping.expressionToTextShadow(value);
@@ -349,7 +536,8 @@ Style declarationsToStyle(Map> declarations) {
}
break;
case 'width':
- style.width = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.width;
+ style.width =
+ ExpressionMapping.expressionToWidth(value.first) ?? style.width;
break;
}
}
@@ -372,7 +560,8 @@ Style? inlineCssToStyle(String? inlineStyle, OnCssParseError? errorHandler) {
return null;
}
-Map>> parseExternalCss(String css, OnCssParseError? errorHandler) {
+Map>> parseExternalCss(
+ String css, OnCssParseError? errorHandler) {
var errors = [];
final sheet = cssparser.parse(css, errors: errors);
if (errors.isEmpty) {
@@ -392,7 +581,8 @@ class DeclarationVisitor extends css.Visitor {
late String _selector;
late String _currentProperty;
- Map>> getDeclarations(css.StyleSheet sheet) {
+ Map>> getDeclarations(
+ css.StyleSheet sheet) {
sheet.topLevels.forEach((element) {
if (element.span != null) {
_selector = element.span!.text;
@@ -400,13 +590,15 @@ class DeclarationVisitor extends css.Visitor {
if (_result[_selector] != null) {
_properties.forEach((key, value) {
if (_result[_selector]![key] != null) {
- _result[_selector]![key]!.addAll(new List.from(value));
+ _result[_selector]![key]!
+ .addAll(new List.from(value));
} else {
_result[_selector]![key] = new List.from(value);
}
});
} else {
- _result[_selector] = new Map>.from(_properties);
+ _result[_selector] =
+ new Map>.from(_properties);
}
_properties.clear();
}
@@ -433,8 +625,10 @@ class DeclarationVisitor extends css.Visitor {
//Mapping functions
class ExpressionMapping {
-
- static Border expressionToBorder(List? borderWidths, List? borderStyles, List? borderColors) {
+ static Border expressionToBorder(
+ List? borderWidths,
+ List? borderStyles,
+ List? borderColors) {
CustomBorderSide left = CustomBorderSide();
CustomBorderSide top = CustomBorderSide();
CustomBorderSide right = CustomBorderSide();
@@ -509,11 +703,22 @@ class ExpressionMapping {
}
}
return Border(
- top: BorderSide(width: top.width, color: top.color ?? Colors.black, style: top.style),
- right: BorderSide(width: right.width, color: right.color ?? Colors.black, style: right.style),
- bottom: BorderSide(width: bottom.width, color: bottom.color ?? Colors.black, style: bottom.style),
- left: BorderSide(width: left.width, color: left.color ?? Colors.black, style: left.style)
- );
+ top: BorderSide(
+ width: top.width,
+ color: top.color ?? Colors.black,
+ style: top.style),
+ right: BorderSide(
+ width: right.width,
+ color: right.color ?? Colors.black,
+ style: right.style),
+ bottom: BorderSide(
+ width: bottom.width,
+ color: bottom.color ?? Colors.black,
+ style: bottom.style),
+ left: BorderSide(
+ width: left.width,
+ color: left.color ?? Colors.black,
+ style: left.style));
}
static double expressionToBorderWidth(css.Expression? value) {
@@ -526,7 +731,9 @@ class ExpressionMapping {
} else if (value is css.RemTerm) {
return double.tryParse(value.text) ?? 1.0;
} else if (value is css.LengthTerm) {
- return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ?? 1.0;
+ return double.tryParse(
+ value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ??
+ 1.0;
} else if (value is css.LiteralTerm) {
switch (value.text) {
case "thin":
@@ -566,7 +773,7 @@ class ExpressionMapping {
static TextDirection expressionToDirection(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "ltr":
return TextDirection.ltr;
case "rtl":
@@ -578,7 +785,7 @@ class ExpressionMapping {
static Display expressionToDisplay(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case 'block':
return Display.BLOCK;
case 'inline-block':
@@ -594,16 +801,25 @@ class ExpressionMapping {
return Display.INLINE;
}
- static List expressionToFontFeatureSettings(List value) {
+ static List expressionToFontFeatureSettings(
+ List value) {
List fontFeatures = [];
for (int i = 0; i < value.length; i++) {
css.Expression exp = value[i];
if (exp is css.LiteralTerm) {
- if (exp.text != "on" && exp.text != "off" && exp.text != "1" && exp.text != "0") {
+ if (exp.text != "on" &&
+ exp.text != "off" &&
+ exp.text != "1" &&
+ exp.text != "0") {
if (i < value.length - 1) {
- css.Expression nextExp = value[i+1];
- if (nextExp is css.LiteralTerm && (nextExp.text == "on" || nextExp.text == "off" || nextExp.text == "1" || nextExp.text == "0")) {
- fontFeatures.add(FontFeature(exp.text, nextExp.text == "on" || nextExp.text == "1" ? 1 : 0));
+ css.Expression nextExp = value[i + 1];
+ if (nextExp is css.LiteralTerm &&
+ (nextExp.text == "on" ||
+ nextExp.text == "off" ||
+ nextExp.text == "1" ||
+ nextExp.text == "0")) {
+ fontFeatures.add(FontFeature(exp.text,
+ nextExp.text == "on" || nextExp.text == "1" ? 1 : 0));
} else {
fontFeatures.add(FontFeature.enable(exp.text));
}
@@ -619,15 +835,17 @@ class ExpressionMapping {
static FontSize? expressionToFontSize(css.Expression value) {
if (value is css.NumberTerm) {
- return FontSize(double.tryParse(value.text));
+ return FontSize(double.tryParse(value.text) ?? 16, Unit.px);
} else if (value is css.PercentageTerm) {
- return FontSize.percent(double.tryParse(value.text)!);
+ return FontSize(double.tryParse(value.text) ?? 100, Unit.percent);
} else if (value is css.EmTerm) {
- return FontSize.em(double.tryParse(value.text));
- } else if (value is css.RemTerm) {
- return FontSize.rem(double.tryParse(value.text)!);
+ return FontSize(double.tryParse(value.text) ?? 1, Unit.em);
+ // } else if (value is css.RemTerm) { TODO
+ // return FontSize.rem(double.tryParse(value.text) ?? 1, Unit.em);
} else if (value is css.LengthTerm) {
- return FontSize(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')));
+ return FontSize(double.tryParse(
+ value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')) ??
+ 16);
} else if (value is css.LiteralTerm) {
switch (value.text) {
case "xx-small":
@@ -651,7 +869,7 @@ class ExpressionMapping {
static FontStyle expressionToFontStyle(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "italic":
case "oblique":
return FontStyle.italic;
@@ -684,7 +902,7 @@ class ExpressionMapping {
return FontWeight.w900;
}
} else if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "bold":
return FontWeight.bold;
case "bolder":
@@ -712,7 +930,10 @@ class ExpressionMapping {
} else if (value is css.RemTerm) {
return LineHeight.rem(double.tryParse(value.text)!);
} else if (value is css.LengthTerm) {
- return LineHeight(double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')), units: "length");
+ return LineHeight(
+ double.tryParse(
+ value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), '')),
+ units: "length");
}
return LineHeight.normal;
}
@@ -748,6 +969,64 @@ class ExpressionMapping {
return null;
}
+ static Width? expressionToWidth(css.Expression value) {
+ if ((value is css.LiteralTerm) && value.text == 'auto') {
+ return Width.auto();
+ } else {
+ final computedValue = expressionToLengthOrPercent(value);
+ return Width(computedValue.value, computedValue.unit);
+ }
+ }
+
+ static Height? expressionToHeight(css.Expression value) {
+ if ((value is css.LiteralTerm) && value.text == 'auto') {
+ return Height.auto();
+ } else {
+ final computedValue = expressionToLengthOrPercent(value);
+ return Height(computedValue.value, computedValue.unit);
+ }
+ }
+
+ static Margin? expressionToMargin(css.Expression value) {
+ if ((value is css.LiteralTerm) && value.text == 'auto') {
+ return Margin.auto();
+ } else {
+ final computedValue = expressionToLengthOrPercent(value);
+ return Margin(computedValue.value, computedValue.unit);
+ }
+ }
+
+ static Margins expressionToMargins(List? lengths) {
+ Margin? left;
+ Margin? right;
+ Margin? top;
+ Margin? bottom;
+ if (lengths != null && lengths.isNotEmpty) {
+ top = expressionToMargin(lengths.first);
+ if (lengths.length == 4) {
+ right = expressionToMargin(lengths[1]);
+ bottom = expressionToMargin(lengths[2]);
+ left = expressionToMargin(lengths.last);
+ }
+ if (lengths.length == 3) {
+ left = expressionToMargin(lengths[1]);
+ right = expressionToMargin(lengths[1]);
+ bottom = expressionToMargin(lengths.last);
+ }
+ if (lengths.length == 2) {
+ bottom = expressionToMargin(lengths.first);
+ left = expressionToMargin(lengths.last);
+ right = expressionToMargin(lengths.last);
+ }
+ if (lengths.length == 1) {
+ bottom = expressionToMargin(lengths.first);
+ left = expressionToMargin(lengths.first);
+ right = expressionToMargin(lengths.first);
+ }
+ }
+ return Margins(left: left, right: right, top: top, bottom: bottom);
+ }
+
static List expressionToPadding(List? lengths) {
double? left;
double? right;
@@ -787,14 +1066,41 @@ class ExpressionMapping {
} else if (value is css.RemTerm) {
return double.tryParse(value.text);
} else if (value is css.LengthTerm) {
- return double.tryParse(value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), ''));
+ return double.tryParse(
+ value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), ''));
}
return null;
}
+ static LengthOrPercent expressionToLengthOrPercent(css.Expression value) {
+ if (value is css.NumberTerm) {
+ return LengthOrPercent(double.parse(value.text));
+ } else if (value is css.EmTerm) {
+ return LengthOrPercent(double.parse(value.text), Unit.em);
+ // } else if (value is css.RemTerm) {
+ // return LengthOrPercent(double.parse(value.text), Unit.rem);
+ // TODO there are several other available terms processed by the CSS parser
+ } else if (value is css.LengthTerm) {
+ double number = double.parse(
+ value.text.replaceAll(new RegExp(r'\s+(\d+\.\d+)\s+'), ''));
+ Unit unit = _unitMap(value.unit);
+ return LengthOrPercent(number, unit);
+ }
+
+ //Ignore unparsable input
+ return LengthOrPercent(0);
+ }
+
+ static Unit _unitMap(int cssParserUnitToken) {
+ switch (cssParserUnitToken) {
+ default:
+ return Unit.px;
+ }
+ }
+
static TextAlign expressionToTextAlign(css.Expression value) {
if (value is css.LiteralTerm) {
- switch(value.text) {
+ switch (value.text) {
case "center":
return TextAlign.center;
case "left":
@@ -812,11 +1118,12 @@ class ExpressionMapping {
return TextAlign.start;
}
- static TextDecoration expressionToTextDecorationLine(List value) {
+ static TextDecoration expressionToTextDecorationLine(
+ List value) {
List decorationList = [];
for (css.LiteralTerm? term in value) {
if (term != null) {
- switch(term.text) {
+ switch (term.text) {
case "overline":
decorationList.add(TextDecoration.overline);
break;
@@ -832,12 +1139,14 @@ class ExpressionMapping {
}
}
}
- if (decorationList.contains(TextDecoration.none)) decorationList = [TextDecoration.none];
+ if (decorationList.contains(TextDecoration.none))
+ decorationList = [TextDecoration.none];
return TextDecoration.combine(decorationList);
}
- static TextDecorationStyle expressionToTextDecorationStyle(css.LiteralTerm value) {
- switch(value.text) {
+ static TextDecorationStyle expressionToTextDecorationStyle(
+ css.LiteralTerm value) {
+ switch (value.text) {
case "wavy":
return TextDecorationStyle.wavy;
case "dotted":
@@ -887,20 +1196,37 @@ class ExpressionMapping {
});
RegExp nonNumberRegex = RegExp(r'\s+(\d+\.\d+)\s+');
if (offsetX is css.LiteralTerm && offsetY is css.LiteralTerm) {
- if (color != null && ExpressionMapping.expressionToColor(color) != null) {
+ if (color != null &&
+ ExpressionMapping.expressionToColor(color) != null) {
shadow.add(Shadow(
- color: expressionToColor(color)!,
- offset: Offset(
- double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!,
- double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!),
- blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0,
+ color: expressionToColor(color)!,
+ offset: Offset(
+ double.tryParse((offsetX as css.LiteralTerm)
+ .text
+ .replaceAll(nonNumberRegex, ''))!,
+ double.tryParse((offsetY as css.LiteralTerm)
+ .text
+ .replaceAll(nonNumberRegex, ''))!),
+ blurRadius: (blurRadius is css.LiteralTerm)
+ ? double.tryParse((blurRadius as css.LiteralTerm)
+ .text
+ .replaceAll(nonNumberRegex, ''))!
+ : 0.0,
));
} else {
shadow.add(Shadow(
- offset: Offset(
- double.tryParse((offsetX as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!,
- double.tryParse((offsetY as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))!),
- blurRadius: (blurRadius is css.LiteralTerm) ? double.tryParse((blurRadius as css.LiteralTerm).text.replaceAll(nonNumberRegex, ''))! : 0.0,
+ offset: Offset(
+ double.tryParse((offsetX as css.LiteralTerm)
+ .text
+ .replaceAll(nonNumberRegex, ''))!,
+ double.tryParse((offsetY as css.LiteralTerm)
+ .text
+ .replaceAll(nonNumberRegex, ''))!),
+ blurRadius: (blurRadius is css.LiteralTerm)
+ ? double.tryParse((blurRadius as css.LiteralTerm)
+ .text
+ .replaceAll(nonNumberRegex, ''))!
+ : 0.0,
));
}
}
@@ -912,10 +1238,8 @@ class ExpressionMapping {
static Color stringToColor(String _text) {
var text = _text.replaceFirst('#', '');
if (text.length == 3)
- text = text.replaceAllMapped(
- RegExp(r"[a-f]|\d", caseSensitive: false),
- (match) => '${match.group(0)}${match.group(0)}'
- );
+ text = text.replaceAllMapped(RegExp(r"[a-f]|\d", caseSensitive: false),
+ (match) => '${match.group(0)}${match.group(0)}');
if (text.length > 6) {
text = "0x" + text;
} else {
@@ -928,7 +1252,7 @@ class ExpressionMapping {
final rgbaText = text.replaceAll(')', '').replaceAll(' ', '');
try {
final rgbaValues =
- rgbaText.split(',').map((value) => double.parse(value)).toList();
+ rgbaText.split(',').map((value) => double.parse(value)).toList();
if (rgbaValues.length == 4) {
return Color.fromRGBO(
rgbaValues[0].toInt(),
@@ -955,10 +1279,13 @@ class ExpressionMapping {
final hslValues = hslText.split(',').toList();
List parsedHsl = [];
hslValues.forEach((element) {
- if (element.contains("%") && double.tryParse(element.replaceAll("%", "")) != null) {
+ if (element.contains("%") &&
+ double.tryParse(element.replaceAll("%", "")) != null) {
parsedHsl.add(double.tryParse(element.replaceAll("%", ""))! * 0.01);
} else {
- if (element != hslValues.first && (double.tryParse(element) == null || double.tryParse(element)! > 1)) {
+ if (element != hslValues.first &&
+ (double.tryParse(element) == null ||
+ double.tryParse(element)! > 1)) {
parsedHsl.add(null);
} else {
parsedHsl.add(double.tryParse(element));
@@ -966,16 +1293,24 @@ class ExpressionMapping {
}
});
if (parsedHsl.length == 4 && !parsedHsl.contains(null)) {
- return HSLColor.fromAHSL(parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!).toColor();
+ return HSLColor.fromAHSL(
+ parsedHsl.last!, parsedHsl.first!, parsedHsl[1]!, parsedHsl[2]!)
+ .toColor();
} else if (parsedHsl.length == 3 && !parsedHsl.contains(null)) {
- return HSLColor.fromAHSL(1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!).toColor();
- } else return Colors.black;
+ return HSLColor.fromAHSL(
+ 1.0, parsedHsl.first!, parsedHsl[1]!, parsedHsl.last!)
+ .toColor();
+ } else
+ return Colors.black;
}
static Color? namedColorToColor(String text) {
- String namedColor = namedColors.keys.firstWhere((element) => element.toLowerCase() == text.toLowerCase(), orElse: () => "");
- if (namedColor != "") {
- return stringToColor(namedColors[namedColor]!);
- } else return null;
+ String namedColor = namedColors.keys.firstWhere(
+ (element) => element.toLowerCase() == text.toLowerCase(),
+ orElse: () => "");
+ if (namedColor != "") {
+ return stringToColor(namedColors[namedColor]!);
+ } else
+ return null;
}
}
diff --git a/lib/src/html_elements.dart b/lib/src/html_elements.dart
index 4b096d8f5d..bf4e363024 100644
--- a/lib/src/html_elements.dart
+++ b/lib/src/html_elements.dart
@@ -129,7 +129,17 @@ const TABLE_CELL_ELEMENTS = ["th", "td"];
const TABLE_DEFINITION_ELEMENTS = ["col", "colgroup"];
-const EXTERNAL_ELEMENTS = ["audio", "iframe", "img", "math", "svg", "table", "video"];
+const EXTERNAL_ELEMENTS = [
+ "audio",
+ "iframe",
+ "img",
+ "math",
+ "svg",
+ "table",
+ "video"
+];
+
+const REPLACED_EXTERNAL_ELEMENTS = ["iframe", "img", "video", "audio"];
const SELECTABLE_ELEMENTS = [
"br",
diff --git a/lib/src/interactable_element.dart b/lib/src/interactable_element.dart
index 2aab878a64..57a918fdbe 100644
--- a/lib/src/interactable_element.dart
+++ b/lib/src/interactable_element.dart
@@ -8,13 +8,13 @@ class InteractableElement extends StyledElement {
String? href;
InteractableElement({
- required String name,
- required List children,
- required Style style,
+ required super.name,
+ required super.children,
+ required super.style,
required this.href,
required dom.Node node,
- required String elementId,
- }) : super(name: name, children: children, style: style, node: node as dom.Element?, elementId: elementId);
+ required super.elementId,
+ }) : super(node: node as dom.Element?);
}
/// A [Gesture] indicates the type of interaction by a user.
@@ -23,20 +23,22 @@ enum Gesture {
}
StyledElement parseInteractableElement(
- dom.Element element, List children) {
+ dom.Element element,
+ List children,
+) {
switch (element.localName) {
case "a":
if (element.attributes.containsKey('href')) {
return InteractableElement(
- name: element.localName!,
- children: children,
- href: element.attributes['href'],
- style: Style(
- color: Colors.blue,
- textDecoration: TextDecoration.underline,
- ),
- node: element,
- elementId: element.id
+ name: element.localName!,
+ children: children,
+ href: element.attributes['href'],
+ style: Style(
+ color: Colors.blue,
+ textDecoration: TextDecoration.underline,
+ ),
+ node: element,
+ elementId: element.id,
);
}
// When tag have no href, it must be non clickable and without decoration.
@@ -47,6 +49,7 @@ StyledElement parseInteractableElement(
node: element,
elementId: element.id,
);
+
/// will never be called, just to suppress missing return warning
default:
return InteractableElement(
@@ -55,7 +58,7 @@ StyledElement parseInteractableElement(
node: element,
href: '',
style: Style(),
- elementId: "[[No ID]]"
+ elementId: "[[No ID]]",
);
}
}
diff --git a/lib/src/layout_element.dart b/lib/src/layout_element.dart
index 33093e7493..d366ce1fc3 100644
--- a/lib/src/layout_element.dart
+++ b/lib/src/layout_element.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/anchor.dart';
+import 'package:flutter_html/src/css_box_widget.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/src/styled_element.dart';
import 'package:flutter_html/style.dart';
@@ -11,10 +12,10 @@ import 'package:html/dom.dart' as dom;
abstract class LayoutElement extends StyledElement {
LayoutElement({
String name = "[[No Name]]",
- required List children,
+ required super.children,
String? elementId,
- dom.Element? node,
- }) : super(name: name, children: children, style: Style(), node: node, elementId: elementId ?? "[[No ID]]");
+ super.node,
+ }) : super(name: name, style: Style(), elementId: elementId ?? "[[No ID]]");
Widget? toWidget(RenderContext context);
}
@@ -34,10 +35,10 @@ class TableSectionLayoutElement extends LayoutElement {
class TableRowLayoutElement extends LayoutElement {
TableRowLayoutElement({
- required String name,
- required List children,
- required dom.Element node,
- }) : super(name: name, children: children, node: node);
+ required super.name,
+ required super.children,
+ required super.node,
+ });
@override
Widget toWidget(RenderContext context) {
@@ -51,13 +52,13 @@ class TableCellElement extends StyledElement {
int rowspan = 1;
TableCellElement({
- required String name,
- required String elementId,
- required List elementClasses,
- required List children,
- required Style style,
- required dom.Element node,
- }) : super(name: name, elementId: elementId, elementClasses: elementClasses, children: children, style: style, node: node) {
+ required super.name,
+ required super.elementId,
+ required super.elementClasses,
+ required super.children,
+ required super.style,
+ required super.node,
+ }) {
colspan = _parseSpan(this, "colspan");
rowspan = _parseSpan(this, "rowspan");
}
@@ -90,11 +91,11 @@ TableCellElement parseTableCellElement(
class TableStyleElement extends StyledElement {
TableStyleElement({
- required String name,
- required List children,
- required Style style,
- required dom.Element node,
- }) : super(name: name, children: children, style: style, node: node);
+ required super.name,
+ required super.children,
+ required super.style,
+ required super.node,
+ });
}
TableStyleElement parseTableDefinitionElement(
@@ -124,65 +125,75 @@ class DetailsContentElement extends LayoutElement {
List elementList;
DetailsContentElement({
- required String name,
- required List children,
+ required super.name,
+ required super.children,
required dom.Element node,
required this.elementList,
- }) : super(name: name, node: node, children: children, elementId: node.id);
+ }) : super(node: node, elementId: node.id);
@override
Widget toWidget(RenderContext context) {
- List? childrenList = children.map((tree) => context.parser.parseTree(context, tree)).toList();
+ List? childrenList = children
+ .map((tree) => context.parser.parseTree(context, tree))
+ .toList();
List toRemove = [];
for (InlineSpan child in childrenList) {
- if (child is TextSpan && child.text != null && child.text!.trim().isEmpty) {
+ if (child is TextSpan &&
+ child.text != null &&
+ child.text!.trim().isEmpty) {
toRemove.add(child);
}
}
for (InlineSpan child in toRemove) {
childrenList.remove(child);
}
- InlineSpan? firstChild = childrenList.isNotEmpty == true ? childrenList.first : null;
+ InlineSpan? firstChild =
+ childrenList.isNotEmpty == true ? childrenList.first : null;
return ExpansionTile(
key: AnchorKey.of(context.parser.key, this),
expandedAlignment: Alignment.centerLeft,
- title: elementList.isNotEmpty == true && elementList.first.localName == "summary" ? StyledText(
- textSpan: TextSpan(
- style: style.generateTextStyle(),
- children: firstChild == null ? [] : [firstChild],
- ),
- style: style,
- renderContext: context,
- ) : Text("Details"),
+ title: elementList.isNotEmpty == true &&
+ elementList.first.localName == "summary"
+ ? CssBoxWidget.withInlineSpanChildren(
+ children: firstChild == null ? [] : [firstChild],
+ style: style,
+ )
+ : Text("Details"),
children: [
- StyledText(
- textSpan: TextSpan(
- style: style.generateTextStyle(),
- children: getChildren(childrenList, context, elementList.isNotEmpty == true && elementList.first.localName == "summary" ? firstChild : null)
- ),
+ CssBoxWidget.withInlineSpanChildren(
+ children: getChildren(
+ childrenList,
+ context,
+ elementList.isNotEmpty == true &&
+ elementList.first.localName == "summary"
+ ? firstChild
+ : null),
style: style,
- renderContext: context,
),
- ]
- );
+ ]);
}
- List getChildren(List children, RenderContext context, InlineSpan? firstChild) {
+ List getChildren(List children, RenderContext context,
+ InlineSpan? firstChild) {
if (firstChild != null) children.removeAt(0);
return children;
}
}
class EmptyLayoutElement extends LayoutElement {
- EmptyLayoutElement({required String name}) : super(name: name, children: []);
+ EmptyLayoutElement({required String name})
+ : super(
+ name: name,
+ children: [],
+ );
@override
Widget? toWidget(_) => null;
}
LayoutElement parseLayoutElement(
- dom.Element element,
- List children,
+ dom.Element element,
+ List children,
) {
switch (element.localName) {
case "details":
@@ -190,10 +201,10 @@ LayoutElement parseLayoutElement(
return EmptyLayoutElement(name: "empty");
}
return DetailsContentElement(
- node: element,
- name: element.localName!,
- children: children,
- elementList: element.children
+ node: element,
+ name: element.localName!,
+ children: children,
+ elementList: element.children,
);
case "thead":
case "tbody":
diff --git a/lib/src/replaced_element.dart b/lib/src/replaced_element.dart
index 81cc5d58ee..3234da0f1d 100644
--- a/lib/src/replaced_element.dart
+++ b/lib/src/replaced_element.dart
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_html/html_parser.dart';
import 'package:flutter_html/src/anchor.dart';
+import 'package:flutter_html/src/css_box_widget.dart';
import 'package:flutter_html/src/html_elements.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
@@ -17,13 +18,13 @@ abstract class ReplacedElement extends StyledElement {
PlaceholderAlignment alignment;
ReplacedElement({
- required String name,
- required Style style,
- required String elementId,
+ required super.name,
+ required super.style,
+ required super.elementId,
List? children,
- dom.Element? node,
+ super.node,
this.alignment = PlaceholderAlignment.aboveBaseline,
- }) : super(name: name, children: children ?? [], style: style, node: node, elementId: elementId);
+ }) : super(children: children ?? []);
static List parseMediaSources(List elements) {
return elements
@@ -46,7 +47,11 @@ class TextContentElement extends ReplacedElement {
required this.text,
this.node,
dom.Element? element,
- }) : super(name: "[text]", style: style, node: element, elementId: "[[No ID]]");
+ }) : super(
+ name: "[text]",
+ style: style,
+ node: element,
+ elementId: "[[No ID]]");
@override
String toString() {
@@ -58,7 +63,8 @@ class TextContentElement extends ReplacedElement {
}
class EmptyContentElement extends ReplacedElement {
- EmptyContentElement({String name = "empty"}) : super(name: name, style: Style(), elementId: "[[No ID]]");
+ EmptyContentElement({String name = "empty"})
+ : super(name: name, style: Style(), elementId: "[[No ID]]");
@override
Widget? toWidget(_) => null;
@@ -70,23 +76,29 @@ class RubyElement extends ReplacedElement {
RubyElement({
required this.element,
required List children,
- String name = "ruby"
- }) : super(name: name, alignment: PlaceholderAlignment.middle, style: Style(), elementId: element.id, children: children);
+ String name = "ruby",
+ }) : super(
+ name: name,
+ alignment: PlaceholderAlignment.middle,
+ style: Style(),
+ elementId: element.id,
+ children: children);
@override
Widget toWidget(RenderContext context) {
StyledElement? node;
List widgets = [];
- final rubySize = context.parser.style['rt']?.fontSize?.size ?? max(9.0, context.style.fontSize!.size! / 2);
+ final rubySize = context.parser.style['rt']?.fontSize?.value ??
+ max(9.0, context.style.fontSize!.value / 2);
final rubyYPos = rubySize + rubySize / 2;
List children = [];
context.tree.children.forEachIndexed((index, element) {
- if (!((element is TextContentElement)
- && (element.text ?? "").trim().isEmpty
- && index > 0
- && index + 1 < context.tree.children.length
- && !(context.tree.children[index - 1] is TextContentElement)
- && !(context.tree.children[index + 1] is TextContentElement))) {
+ if (!((element is TextContentElement) &&
+ (element.text ?? "").trim().isEmpty &&
+ index > 0 &&
+ index + 1 < context.tree.children.length &&
+ !(context.tree.children[index - 1] is TextContentElement) &&
+ !(context.tree.children[index + 1] is TextContentElement))) {
children.add(element);
}
});
@@ -96,30 +108,31 @@ class RubyElement extends ReplacedElement {
alignment: Alignment.center,
children: [
Container(
- alignment: Alignment.bottomCenter,
- child: Center(
- child: Transform(
- transform:
- Matrix4.translationValues(0, -(rubyYPos), 0),
- child: ContainerSpan(
- newContext: RenderContext(
- buildContext: context.buildContext,
- parser: context.parser,
- style: c.style,
- tree: c,
- ),
- style: c.style,
- child: Text(c.element!.innerHtml,
- style: c.style
- .generateTextStyle()
- .copyWith(fontSize: rubySize)),
- )))),
- ContainerSpan(
- newContext: context,
- style: context.style,
- child: node is TextContentElement ? Text((node as TextContentElement).text?.trim() ?? "",
- style: context.style.generateTextStyle()) : null,
- children: node is TextContentElement ? null : [context.parser.parseTree(context, node!)]),
+ alignment: Alignment.bottomCenter,
+ child: Center(
+ child: Transform(
+ transform: Matrix4.translationValues(0, -(rubyYPos), 0),
+ child: CssBoxWidget(
+ style: c.style,
+ child: Text(
+ c.element!.innerHtml,
+ style: c.style
+ .generateTextStyle()
+ .copyWith(fontSize: rubySize),
+ ),
+ ),
+ ),
+ ),
+ ),
+ CssBoxWidget(
+ style: context.style,
+ child: node is TextContentElement
+ ? Text(
+ (node as TextContentElement).text?.trim() ?? "",
+ style: context.style.generateTextStyle(),
+ )
+ : RichText(text: context.parser.parseTree(context, node!)),
+ ),
],
);
widgets.add(widget);
@@ -132,12 +145,14 @@ class RubyElement extends ReplacedElement {
child: Wrap(
key: AnchorKey.of(context.parser.key, this),
runSpacing: rubySize,
- children: widgets.map((e) => Row(
- crossAxisAlignment: CrossAxisAlignment.end,
- textBaseline: TextBaseline.alphabetic,
- mainAxisSize: MainAxisSize.min,
- children: [e],
- )).toList(),
+ children: widgets
+ .map((e) => Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ textBaseline: TextBaseline.alphabetic,
+ mainAxisSize: MainAxisSize.min,
+ children: [e],
+ ))
+ .toList(),
),
);
}
@@ -153,7 +168,7 @@ ReplacedElement parseReplacedElement(
text: "\n",
style: Style(whiteSpace: WhiteSpace.PRE),
element: element,
- node: element
+ node: element,
);
case "ruby":
return RubyElement(
@@ -161,6 +176,7 @@ ReplacedElement parseReplacedElement(
children: children,
);
default:
- return EmptyContentElement(name: element.localName == null ? "[[No Name]]" : element.localName!);
+ return EmptyContentElement(
+ name: element.localName == null ? "[[No Name]]" : element.localName!);
}
}
diff --git a/lib/src/style/fontsize.dart b/lib/src/style/fontsize.dart
new file mode 100644
index 0000000000..5d766c630a
--- /dev/null
+++ b/lib/src/style/fontsize.dart
@@ -0,0 +1,39 @@
+//TODO implement dimensionality
+
+import 'length.dart';
+
+class FontSize extends LengthOrPercent {
+ FontSize(double size, [Unit unit = Unit.px]) : super(size, unit);
+
+ // These values are calculated based off of the default (`medium`)
+ // being 14px.
+ //
+ // TODO(Sub6Resources): This seems to override Flutter's accessibility text scaling.
+ //
+ // Negative values are computed during parsing to be a percentage of
+ // the parent style's font size.
+ static final xxSmall = FontSize(7.875);
+ static final xSmall = FontSize(8.75);
+ static final small = FontSize(11.375);
+ static final medium = FontSize(14.0);
+ static final large = FontSize(15.75);
+ static final xLarge = FontSize(21.0);
+ static final xxLarge = FontSize(28.0);
+ static final smaller = FontSize(83, Unit.percent);
+ static final larger = FontSize(120, Unit.percent);
+
+ static FontSize? inherit(FontSize? parent, FontSize? child) {
+ if (child != null && parent != null) {
+ if (child.unit == Unit.em) {
+ return FontSize(child.value * parent.value);
+ } else if (child.unit == Unit.percent) {
+ return FontSize(child.value / 100.0 * parent.value);
+ }
+ return child;
+ }
+
+ return parent;
+ }
+
+ double get emValue => this.value;
+}
diff --git a/lib/src/style/length.dart b/lib/src/style/length.dart
new file mode 100644
index 0000000000..3fab69263d
--- /dev/null
+++ b/lib/src/style/length.dart
@@ -0,0 +1,62 @@
+/// These are the base unit types
+enum _UnitType {
+ percent,
+ length,
+ auto,
+ lengthPercent(children: [_UnitType.length, _UnitType.percent]),
+ lengthPercentAuto(children: [_UnitType.length, _UnitType.percent, _UnitType.auto]);
+
+ final List<_UnitType> children;
+
+ const _UnitType({this.children = const []});
+
+ bool matches(_UnitType other) {
+ return this == other || children.contains(other);
+ }
+}
+
+/// A Unit represents a CSS unit
+enum Unit {
+ //ch,
+ em(_UnitType.length),
+ //ex,
+ percent(_UnitType.percent),
+ px(_UnitType.length),
+ rem(_UnitType.length),
+ //Q,
+ //vh,
+ //vw,
+ auto(_UnitType.auto);
+
+ const Unit(this.unitType);
+ final _UnitType unitType;
+}
+
+/// Represents a CSS dimension https://drafts.csswg.org/css-values/#dimensions
+abstract class Dimension {
+ double value;
+ Unit unit;
+
+ Dimension(this.value, this.unit, _UnitType _dimensionUnitType)
+ : assert(_dimensionUnitType.matches(unit.unitType),
+ "This Dimension was given a Unit that isn't specified.");
+}
+
+/// This dimension takes a value with a length unit such as px or em. Note that
+/// these can be fixed or relative (but they must not be a percent)
+class Length extends Dimension {
+ Length(double value, [Unit unit = Unit.px]) : super(value, unit, _UnitType.length);
+}
+
+/// This dimension takes a value with a length-percent unit such as px or em
+/// or %. Note that these can be fixed or relative (but they must not be a
+/// percent)
+class LengthOrPercent extends Dimension {
+ LengthOrPercent(double value, [Unit unit = Unit.px])
+ : super(value, unit, _UnitType.lengthPercent);
+}
+
+class AutoOrLengthOrPercent extends Dimension {
+ AutoOrLengthOrPercent(double value, [Unit unit = Unit.px])
+ : super(value, unit, _UnitType.lengthPercentAuto);
+}
diff --git a/lib/src/style/lineheight.dart b/lib/src/style/lineheight.dart
new file mode 100644
index 0000000000..0550ee1a7a
--- /dev/null
+++ b/lib/src/style/lineheight.dart
@@ -0,0 +1,25 @@
+//TODO implement dimensionality
+class LineHeight {
+ final double? size;
+ final String units;
+
+ const LineHeight(this.size, {this.units = ""});
+
+ factory LineHeight.percent(double percent) {
+ return LineHeight(percent / 100.0 * 1.2, units: "%");
+ }
+
+ factory LineHeight.em(double em) {
+ return LineHeight(em * 1.2, units: "em");
+ }
+
+ factory LineHeight.rem(double rem) {
+ return LineHeight(rem * 1.2, units: "rem");
+ }
+
+ factory LineHeight.number(double num) {
+ return LineHeight(num * 1.2, units: "number");
+ }
+
+ static const normal = LineHeight(1.2);
+}
diff --git a/lib/src/style/margin.dart b/lib/src/style/margin.dart
new file mode 100644
index 0000000000..4df4e7b890
--- /dev/null
+++ b/lib/src/style/margin.dart
@@ -0,0 +1,73 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_html/src/style/length.dart';
+
+class Margin extends AutoOrLengthOrPercent {
+ Margin(double value, [Unit? unit = Unit.px]) : super(value, unit ?? Unit.px);
+
+ Margin.auto() : super(0, Unit.auto);
+
+ Margin.zero() : super(0, Unit.px);
+}
+
+class Margins {
+ final Margin? left;
+ final Margin? right;
+ final Margin? top;
+ final Margin? bottom;
+
+ const Margins({this.left, this.right, this.top, this.bottom});
+
+ /// Auto margins already have a "value" of zero so can be considered collapsed.
+ Margins collapse() => Margins(
+ left: left?.unit == Unit.auto ? left : Margin(0, Unit.px),
+ right: right?.unit == Unit.auto ? right : Margin(0, Unit.px),
+ top: top?.unit == Unit.auto ? top : Margin(0, Unit.px),
+ bottom: bottom?.unit == Unit.auto ? bottom : Margin(0, Unit.px),
+ );
+
+ Margins copyWith(
+ {Margin? left, Margin? right, Margin? top, Margin? bottom}) =>
+ Margins(
+ left: left ?? this.left,
+ right: right ?? this.right,
+ top: top ?? this.top,
+ bottom: bottom ?? this.bottom,
+ );
+
+ Margins copyWithEdge(
+ {double? left, double? right, double? top, double? bottom}) =>
+ Margins(
+ left: left != null ? Margin(left, this.left?.unit) : this.left,
+ right: right != null ? Margin(right, this.right?.unit) : this.right,
+ top: top != null ? Margin(top, this.top?.unit) : this.top,
+ bottom:
+ bottom != null ? Margin(bottom, this.bottom?.unit) : this.bottom,
+ );
+
+ // bool get isAutoHorizontal => (left is MarginAuto) || (right is MarginAuto);
+
+ /// Analogous to [EdgeInsets.zero]
+ static Margins get zero => Margins.all(0);
+
+ /// Analogous to [EdgeInsets.all]
+ Margins.all(double value, {Unit? unit})
+ : left = Margin(value, unit),
+ right = Margin(value, unit),
+ top = Margin(value, unit),
+ bottom = Margin(value, unit);
+
+ /// Analogous to [EdgeInsets.only]
+ Margins.only(
+ {double? left, double? right, double? top, double? bottom, Unit? unit})
+ : left = Margin(left ?? 0, unit),
+ right = Margin(right ?? 0, unit),
+ top = Margin(top ?? 0, unit),
+ bottom = Margin(bottom ?? 0, unit);
+
+ /// Analogous to [EdgeInsets.symmetric]
+ Margins.symmetric({double? horizontal, double? vertical, Unit? unit})
+ : left = Margin(horizontal ?? 0, unit),
+ right = Margin(horizontal ?? 0, unit),
+ top = Margin(vertical ?? 0, unit),
+ bottom = Margin(vertical ?? 0, unit);
+}
diff --git a/lib/src/style/size.dart b/lib/src/style/size.dart
new file mode 100644
index 0000000000..1b73663793
--- /dev/null
+++ b/lib/src/style/size.dart
@@ -0,0 +1,18 @@
+import 'package:flutter_html/flutter_html.dart';
+
+/// The [Width] class takes in a value and units, and defaults to px if no
+/// units are provided. A helper constructor, [Width.auto] constructor is
+/// provided for convenience.
+class Width extends AutoOrLengthOrPercent {
+ Width(super.value, [super.unit = Unit.px])
+ : assert(value >= 0, 'Width value must be non-negative');
+
+ Width.auto() : super(0, Unit.auto);
+}
+
+class Height extends AutoOrLengthOrPercent {
+ Height(super.value, [super.unit = Unit.px])
+ : assert(value >= 0, 'Height value must be non-negative');
+
+ Height.auto() : super(0, Unit.auto);
+}
diff --git a/lib/src/styled_element.dart b/lib/src/styled_element.dart
index 9561d8f228..8434544c50 100644
--- a/lib/src/styled_element.dart
+++ b/lib/src/styled_element.dart
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/src/css_parser.dart';
+import 'package:flutter_html/src/style/margin.dart';
import 'package:flutter_html/style.dart';
import 'package:html/dom.dart' as dom;
//TODO(Sub6Resources): don't use the internal code of the html package as it may change unexpectedly.
@@ -48,7 +49,9 @@ class StyledElement {
}
StyledElement parseStyledElement(
- dom.Element element, List children) {
+ dom.Element element,
+ List children,
+) {
StyledElement styledElement = StyledElement(
name: element.localName!,
elementId: element.id,
@@ -102,19 +105,19 @@ StyledElement parseStyledElement(
//TODO(Sub6Resources) this is a workaround for collapsing margins. Remove.
if (element.parent!.localName == "blockquote") {
styledElement.style = Style(
- margin: const EdgeInsets.only(left: 40.0, right: 40.0, bottom: 14.0),
+ margin: Margins.only(left: 40.0, right: 40.0, bottom: 14.0),
display: Display.BLOCK,
);
} else {
styledElement.style = Style(
- margin: const EdgeInsets.symmetric(horizontal: 40.0, vertical: 14.0),
+ margin: Margins.symmetric(horizontal: 40.0, vertical: 14.0),
display: Display.BLOCK,
);
}
break;
case "body":
styledElement.style = Style(
- margin: EdgeInsets.all(8.0),
+ margin: Margins.all(8.0),
display: Display.BLOCK,
);
break;
@@ -134,7 +137,7 @@ StyledElement parseStyledElement(
break;
case "dd":
styledElement.style = Style(
- margin: EdgeInsets.only(left: 40.0),
+ margin: Margins.only(left: 40.0),
display: Display.BLOCK,
);
break;
@@ -148,13 +151,13 @@ StyledElement parseStyledElement(
continue italics;
case "div":
styledElement.style = Style(
- margin: EdgeInsets.all(0),
+ margin: Margins.all(0),
display: Display.BLOCK,
);
break;
case "dl":
styledElement.style = Style(
- margin: EdgeInsets.symmetric(vertical: 14.0),
+ margin: Margins.symmetric(vertical: 14.0),
display: Display.BLOCK,
);
break;
@@ -172,7 +175,7 @@ StyledElement parseStyledElement(
break;
case "figure":
styledElement.style = Style(
- margin: EdgeInsets.symmetric(vertical: 14.0, horizontal: 40.0),
+ margin: Margins.symmetric(vertical: 14.0, horizontal: 40.0),
display: Display.BLOCK,
);
break;
@@ -183,60 +186,62 @@ StyledElement parseStyledElement(
break;
case "font":
styledElement.style = Style(
- color: element.attributes['color'] != null ?
- element.attributes['color']!.startsWith("#") ?
- ExpressionMapping.stringToColor(element.attributes['color']!) :
- ExpressionMapping.namedColorToColor(element.attributes['color']!) :
- null,
+ color: element.attributes['color'] != null
+ ? element.attributes['color']!.startsWith("#")
+ ? ExpressionMapping.stringToColor(element.attributes['color']!)
+ : ExpressionMapping.namedColorToColor(
+ element.attributes['color']!)
+ : null,
fontFamily: element.attributes['face']?.split(",").first,
- fontSize: element.attributes['size'] != null ? numberToFontSize(element.attributes['size']!) : null,
+ fontSize: element.attributes['size'] != null
+ ? numberToFontSize(element.attributes['size']!)
+ : null,
);
break;
case "h1":
styledElement.style = Style(
- fontSize: FontSize.xxLarge,
+ fontSize: FontSize(2, Unit.em),
fontWeight: FontWeight.bold,
- margin: EdgeInsets.symmetric(vertical: 18.67),
+ margin: Margins.symmetric(vertical: 0.67, unit: Unit.em),
display: Display.BLOCK,
);
break;
case "h2":
styledElement.style = Style(
- fontSize: FontSize.xLarge,
+ fontSize: FontSize(1.5, Unit.em),
fontWeight: FontWeight.bold,
- margin: EdgeInsets.symmetric(vertical: 17.5),
+ margin: Margins.symmetric(vertical: 0.83, unit: Unit.em),
display: Display.BLOCK,
);
break;
case "h3":
styledElement.style = Style(
- fontSize: FontSize(16.38),
+ fontSize: FontSize(1.17, Unit.em),
fontWeight: FontWeight.bold,
- margin: EdgeInsets.symmetric(vertical: 16.5),
+ margin: Margins.symmetric(vertical: 1, unit: Unit.em),
display: Display.BLOCK,
);
break;
case "h4":
styledElement.style = Style(
- fontSize: FontSize.medium,
fontWeight: FontWeight.bold,
- margin: EdgeInsets.symmetric(vertical: 18.5),
+ margin: Margins.symmetric(vertical: 1.33, unit: Unit.em),
display: Display.BLOCK,
);
break;
case "h5":
styledElement.style = Style(
- fontSize: FontSize(11.62),
+ fontSize: FontSize(0.83, Unit.em),
fontWeight: FontWeight.bold,
- margin: EdgeInsets.symmetric(vertical: 19.25),
+ margin: Margins.symmetric(vertical: 1.67, unit: Unit.em),
display: Display.BLOCK,
);
break;
case "h6":
styledElement.style = Style(
- fontSize: FontSize(9.38),
+ fontSize: FontSize(0.67, Unit.em),
fontWeight: FontWeight.bold,
- margin: EdgeInsets.symmetric(vertical: 22),
+ margin: Margins.symmetric(vertical: 2.33, unit: Unit.em),
display: Display.BLOCK,
);
break;
@@ -247,10 +252,13 @@ StyledElement parseStyledElement(
break;
case "hr":
styledElement.style = Style(
- margin: EdgeInsets.symmetric(vertical: 7.0),
- width: double.infinity,
- height: 1,
- backgroundColor: Colors.black,
+ margin: Margins(
+ top: Margin(0.5, Unit.em),
+ bottom: Margin(0.5, Unit.em),
+ left: Margin.auto(),
+ right: Margin.auto(),
+ ),
+ border: Border.all(),
display: Display.BLOCK,
);
break;
@@ -318,14 +326,14 @@ StyledElement parseStyledElement(
break;
case "p":
styledElement.style = Style(
- margin: EdgeInsets.symmetric(vertical: 14.0),
+ margin: Margins.symmetric(vertical: 1, unit: Unit.em),
display: Display.BLOCK,
);
break;
case "pre":
styledElement.style = Style(
fontFamily: 'monospace',
- margin: EdgeInsets.symmetric(vertical: 14.0),
+ margin: Margins.symmetric(vertical: 14.0),
whiteSpace: WhiteSpace.PRE,
display: Display.BLOCK,
);
@@ -409,4 +417,4 @@ FontSize numberToFontSize(String num) {
return numberToFontSize((3 - relativeNum).toString());
}
return FontSize.medium;
-}
\ No newline at end of file
+}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 389cb4fc6b..e79bba1008 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -38,7 +38,8 @@ class MultipleTapGestureDetector extends InheritedWidget {
}) : super(key: key, child: child);
static MultipleTapGestureDetector? of(BuildContext context) {
- return context.dependOnInheritedWidgetOfExactType();
+ return context
+ .dependOnInheritedWidgetOfExactType();
}
@override
@@ -85,4 +86,4 @@ extension TextTransformUtil on String? {
return this;
}
}
-}
\ No newline at end of file
+}
diff --git a/lib/style.dart b/lib/style.dart
index 95bde15088..38d12ca136 100644
--- a/lib/style.dart
+++ b/lib/style.dart
@@ -4,6 +4,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_html/src/css_parser.dart';
+//Export Style value-unit APIs
+export 'package:flutter_html/src/style/margin.dart';
+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';
+
///This class represents all the available CSS attributes
///for this package.
class Style {
@@ -37,14 +44,12 @@ class Style {
/// Default: Theme.of(context).style.textTheme.body1.fontFamily
String? fontFamily;
-
/// The list of font families to fall back on when a glyph cannot be found in default font family.
///
/// Inherited: yes,
/// Default: null
List? fontFamilyFallback;
-
/// CSS attribute "`font-feature-settings`"
///
/// Inherited: yes,
@@ -72,8 +77,8 @@ class Style {
/// CSS attribute "`height`"
///
/// Inherited: no,
- /// Default: Unspecified (null),
- double? height;
+ /// Default: Height.auto(),
+ Height? height;
/// CSS attribute "`letter-spacing`"
///
@@ -103,7 +108,7 @@ class Style {
///
/// Inherited: no,
/// Default: EdgeInsets.zero
- EdgeInsets? margin;
+ Margins? margin;
/// CSS attribute "`text-align`"
///
@@ -159,8 +164,8 @@ class Style {
/// CSS attribute "`width`"
///
/// Inherited: no,
- /// Default: unspecified (null)
- double? width;
+ /// Default: Width.auto()
+ Width? width;
/// CSS attribute "`word-spacing`"
///
@@ -245,16 +250,17 @@ class Style {
}
static Map fromThemeData(ThemeData theme) => {
- 'h1': Style.fromTextStyle(theme.textTheme.headline1!),
- 'h2': Style.fromTextStyle(theme.textTheme.headline2!),
- 'h3': Style.fromTextStyle(theme.textTheme.headline3!),
- 'h4': Style.fromTextStyle(theme.textTheme.headline4!),
- 'h5': Style.fromTextStyle(theme.textTheme.headline5!),
- 'h6': Style.fromTextStyle(theme.textTheme.headline6!),
- 'body': Style.fromTextStyle(theme.textTheme.bodyText2!),
- };
-
- static Map fromCss(String css, OnCssParseError? onCssParseError) {
+ 'h1': Style.fromTextStyle(theme.textTheme.headline1!),
+ 'h2': Style.fromTextStyle(theme.textTheme.headline2!),
+ 'h3': Style.fromTextStyle(theme.textTheme.headline3!),
+ 'h4': Style.fromTextStyle(theme.textTheme.headline4!),
+ 'h5': Style.fromTextStyle(theme.textTheme.headline5!),
+ 'h6': Style.fromTextStyle(theme.textTheme.headline6!),
+ 'body': Style.fromTextStyle(theme.textTheme.bodyText2!),
+ };
+
+ static Map fromCss(
+ String css, OnCssParseError? onCssParseError) {
final declarations = parseExternalCss(css, onCssParseError);
Map styleMap = {};
declarations.forEach((key, value) {
@@ -274,7 +280,7 @@ class Style {
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
fontFeatures: fontFeatureSettings,
- fontSize: fontSize?.size,
+ fontSize: fontSize?.value,
fontStyle: fontStyle,
fontWeight: fontWeight,
letterSpacing: letterSpacing,
@@ -311,7 +317,7 @@ class Style {
padding: other.padding,
//TODO merge EdgeInsets
margin: other.margin,
- //TODO merge EdgeInsets
+ //TODO merge Margins
textAlign: other.textAlign,
textDecoration: other.textDecoration,
textDecorationColor: other.textDecorationColor,
@@ -336,18 +342,20 @@ class Style {
}
Style copyOnlyInherited(Style child) {
- FontSize? finalFontSize = child.fontSize != null ?
- fontSize != null && child.fontSize?.units == "em" ?
- FontSize(child.fontSize!.size! * fontSize!.size!) : child.fontSize
- : fontSize != null && fontSize!.size! < 0 ?
- FontSize.percent(100) : fontSize;
- LineHeight? finalLineHeight = child.lineHeight != null ?
- child.lineHeight?.units == "length" ?
- LineHeight(child.lineHeight!.size! / (finalFontSize == null ? 14 : finalFontSize.size!) * 1.2) : child.lineHeight
- : lineHeight;
+ FontSize? finalFontSize = FontSize.inherit(fontSize, child.fontSize);
+
+ LineHeight? finalLineHeight = child.lineHeight != null
+ ? child.lineHeight?.units == "length"
+ ? LineHeight(child.lineHeight!.size! /
+ (finalFontSize == null ? 14 : finalFontSize.value) *
+ 1.2)
+ : child.lineHeight
+ : lineHeight;
+
return child.copyWith(
- backgroundColor: child.backgroundColor != Colors.transparent ?
- child.backgroundColor : backgroundColor,
+ backgroundColor: child.backgroundColor != Colors.transparent
+ ? child.backgroundColor
+ : backgroundColor,
color: child.color ?? color,
direction: child.direction ?? direction,
display: display == Display.NONE ? display : child.display,
@@ -362,9 +370,10 @@ class Style {
listStyleType: child.listStyleType ?? listStyleType,
listStylePosition: child.listStylePosition ?? listStylePosition,
textAlign: child.textAlign ?? textAlign,
- textDecoration: TextDecoration.combine(
- [child.textDecoration ?? TextDecoration.none,
- textDecoration ?? TextDecoration.none]),
+ textDecoration: TextDecoration.combine([
+ child.textDecoration ?? TextDecoration.none,
+ textDecoration ?? TextDecoration.none,
+ ]),
textShadow: child.textShadow ?? textShadow,
whiteSpace: child.whiteSpace ?? whiteSpace,
wordSpacing: child.wordSpacing ?? wordSpacing,
@@ -385,13 +394,13 @@ class Style {
FontSize? fontSize,
FontStyle? fontStyle,
FontWeight? fontWeight,
- double? height,
+ Height? height,
LineHeight? lineHeight,
double? letterSpacing,
ListStyleType? listStyleType,
ListStylePosition? listStylePosition,
EdgeInsets? padding,
- EdgeInsets? margin,
+ Margins? margin,
TextAlign? textAlign,
TextDecoration? textDecoration,
Color? textDecorationColor,
@@ -400,7 +409,7 @@ class Style {
List? textShadow,
VerticalAlign? verticalAlign,
WhiteSpace? whiteSpace,
- double? width,
+ Width? width,
double? wordSpacing,
String? before,
String? after,
@@ -462,7 +471,8 @@ class Style {
this.fontFamily = textStyle.fontFamily;
this.fontFamilyFallback = textStyle.fontFamilyFallback;
this.fontFeatureSettings = textStyle.fontFeatures;
- this.fontSize = FontSize(textStyle.fontSize);
+ this.fontSize =
+ textStyle.fontSize != null ? FontSize(textStyle.fontSize!) : null;
this.fontStyle = textStyle.fontStyle;
this.fontWeight = textStyle.fontWeight;
this.letterSpacing = textStyle.letterSpacing;
@@ -471,75 +481,71 @@ class Style {
this.lineHeight = LineHeight(textStyle.height ?? 1.2);
this.textTransform = TextTransform.none;
}
-}
-
-enum Display {
- BLOCK,
- INLINE,
- INLINE_BLOCK,
- LIST_ITEM,
- NONE,
-}
-
-class FontSize {
- final double? size;
- final String units;
-
- const FontSize(this.size, {this.units = ""});
- /// A percentage of the parent style's font size.
- factory FontSize.percent(double percent) {
- return FontSize(percent / -100.0, units: "%");
- }
+ /// Sets any dimensions set to rem or em to the computed size
+ void setRelativeValues(double remValue, double emValue) {
+ if (width?.unit == Unit.rem) {
+ width = Width(width!.value * remValue);
+ } else if (width?.unit == Unit.em) {
+ width = Width(width!.value * emValue);
+ }
- factory FontSize.em(double? em) {
- return FontSize(em, units: "em");
- }
+ if (height?.unit == Unit.rem) {
+ height = Height(height!.value * remValue);
+ } else if (height?.unit == Unit.em) {
+ height = Height(height!.value * emValue);
+ }
- factory FontSize.rem(double rem) {
- return FontSize(rem * 16 - 2, units: "rem");
- }
- // These values are calculated based off of the default (`medium`)
- // being 14px.
- //
- // TODO(Sub6Resources): This seems to override Flutter's accessibility text scaling.
- //
- // Negative values are computed during parsing to be a percentage of
- // the parent style's font size.
- static const xxSmall = FontSize(7.875);
- static const xSmall = FontSize(8.75);
- static const small = FontSize(11.375);
- static const medium = FontSize(14.0);
- static const large = FontSize(15.75);
- static const xLarge = FontSize(21.0);
- static const xxLarge = FontSize(28.0);
- static const smaller = FontSize(-0.83);
- static const larger = FontSize(-1.2);
-}
+ if (fontSize?.unit == Unit.rem) {
+ fontSize = FontSize(fontSize!.value * remValue);
+ } else if (fontSize?.unit == Unit.em) {
+ fontSize = FontSize(fontSize!.value * emValue);
+ }
-class LineHeight {
- final double? size;
- final String units;
+ Margin? marginLeft;
+ Margin? marginTop;
+ Margin? marginRight;
+ Margin? marginBottom;
- const LineHeight(this.size, {this.units = ""});
+ if (margin?.left?.unit == Unit.rem) {
+ marginLeft = Margin(margin!.left!.value * remValue);
+ } else if (margin?.left?.unit == Unit.em) {
+ marginLeft = Margin(margin!.left!.value * emValue);
+ }
- factory LineHeight.percent(double percent) {
- return LineHeight(percent / 100.0 * 1.2, units: "%");
- }
+ if (margin?.top?.unit == Unit.rem) {
+ marginTop = Margin(margin!.top!.value * remValue);
+ } else if (margin?.top?.unit == Unit.em) {
+ marginTop = Margin(margin!.top!.value * emValue);
+ }
- factory LineHeight.em(double em) {
- return LineHeight(em * 1.2, units: "em");
- }
+ if (margin?.right?.unit == Unit.rem) {
+ marginRight = Margin(margin!.right!.value * remValue);
+ } else if (margin?.right?.unit == Unit.em) {
+ marginRight = Margin(margin!.right!.value * emValue);
+ }
- factory LineHeight.rem(double rem) {
- return LineHeight(rem * 1.2, units: "rem");
- }
+ if (margin?.bottom?.unit == Unit.rem) {
+ marginBottom = Margin(margin!.bottom!.value * remValue);
+ } else if (margin?.bottom?.unit == Unit.em) {
+ marginBottom = Margin(margin!.bottom!.value * emValue);
+ }
- factory LineHeight.number(double num) {
- return LineHeight(num * 1.2, units: "number");
+ margin = margin?.copyWith(
+ left: marginLeft,
+ top: marginTop,
+ right: marginRight,
+ bottom: marginBottom,
+ );
}
+}
- static const normal = LineHeight(1.2);
+enum Display {
+ BLOCK,
+ INLINE,
+ INLINE_BLOCK,
+ LIST_ITEM,
+ NONE,
}
class ListStyleType {
@@ -549,9 +555,11 @@ class ListStyleType {
const ListStyleType(this.text, {this.type = "marker", this.widget});
- factory ListStyleType.fromImage(String url) => ListStyleType(url, type: "image");
+ factory ListStyleType.fromImage(String url) =>
+ ListStyleType(url, type: "image");
- factory ListStyleType.fromWidget(Widget widget) => ListStyleType("", widget: widget, type: "widget");
+ factory ListStyleType.fromWidget(Widget widget) =>
+ ListStyleType("", widget: widget, type: "widget");
static const LOWER_ALPHA = ListStyleType("LOWER_ALPHA");
static const UPPER_ALPHA = ListStyleType("UPPER_ALPHA");
diff --git a/packages/flutter_html_audio/README.md b/packages/flutter_html_audio/README.md
index 8c63614c36..81ac741006 100644
--- a/packages/flutter_html_audio/README.md
+++ b/packages/flutter_html_audio/README.md
@@ -1,6 +1,6 @@
# flutter_html_audio
-Audio widget for flutter_html.
+Audio extension for flutter_html.
This package renders audio elements using the [`chewie_audio`](https://pub.dev/packages/chewie_audio) and the [`video_player`](https://pub.dev/packages/video_player) plugin.
diff --git a/packages/flutter_html_audio/lib/flutter_html_audio.dart b/packages/flutter_html_audio/lib/flutter_html_audio.dart
index 2484b8e45d..c542f12242 100644
--- a/packages/flutter_html_audio/lib/flutter_html_audio.dart
+++ b/packages/flutter_html_audio/lib/flutter_html_audio.dart
@@ -80,13 +80,14 @@ class _AudioWidgetState extends State {
if (sources.isEmpty || sources.first == null) {
return Container(height: 0, width: 0);
}
- return Container(
+
+ return CssBoxWidget(
key: widget.context.key,
- width: widget.context.style.width ?? 300,
- height: Theme.of(bContext).platform == TargetPlatform.android ? 48 : 75,
+ style: widget.context.style,
child: ChewieAudio(
controller: chewieAudioController!,
),
+ childIsReplaced: true,
);
}
}
diff --git a/packages/flutter_html_iframe/lib/iframe_mobile.dart b/packages/flutter_html_iframe/lib/iframe_mobile.dart
index a1c9bbb713..b35f7c7e3b 100644
--- a/packages/flutter_html_iframe/lib/iframe_mobile.dart
+++ b/packages/flutter_html_iframe/lib/iframe_mobile.dart
@@ -15,9 +15,9 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) =>
return Container(
width: givenWidth ?? (givenHeight ?? 150) * 2,
height: givenHeight ?? (givenWidth ?? 300) / 2,
- child: ContainerSpan(
+ child: CssBoxWidget(
style: context.style,
- newContext: context,
+ childIsReplaced: true,
child: WebView(
initialUrl: context.tree.element?.attributes['src'],
key: key,
diff --git a/packages/flutter_html_iframe/lib/iframe_web.dart b/packages/flutter_html_iframe/lib/iframe_web.dart
index fd776fe980..aaf81c23ff 100644
--- a/packages/flutter_html_iframe/lib/iframe_web.dart
+++ b/packages/flutter_html_iframe/lib/iframe_web.dart
@@ -24,27 +24,28 @@ CustomRender iframeRender({NavigationDelegate? navigationDelegate}) =>
ui.platformViewRegistry
.registerViewFactory(createdViewId, (int viewId) => iframe);
return Container(
- width: double.tryParse(
- context.tree.element?.attributes['width'] ?? "") ??
- (double.tryParse(
- context.tree.element?.attributes['height'] ?? "") ??
- 150) *
- 2,
- height: double.tryParse(
- context.tree.element?.attributes['height'] ?? "") ??
- (double.tryParse(
- context.tree.element?.attributes['width'] ?? "") ??
- 300) /
- 2,
- child: ContainerSpan(
- style: context.style,
- newContext: context,
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: HtmlElementView(
- viewType: createdViewId,
- )),
- ));
+ width:
+ double.tryParse(context.tree.element?.attributes['width'] ?? "") ??
+ (double.tryParse(
+ context.tree.element?.attributes['height'] ?? "") ??
+ 150) *
+ 2,
+ height: double.tryParse(
+ context.tree.element?.attributes['height'] ?? "") ??
+ (double.tryParse(context.tree.element?.attributes['width'] ?? "") ??
+ 300) /
+ 2,
+ child: CssBoxWidget(
+ style: context.style,
+ childIsReplaced: true,
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: HtmlElementView(
+ viewType: createdViewId,
+ ),
+ ),
+ ),
+ );
});
String getRandString(int len) {
diff --git a/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart b/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart
index ed136f0e18..eb31307d7a 100644
--- a/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart
+++ b/packages/flutter_html_svg/test/svg_image_matcher_source_matcher_test.dart
@@ -6,19 +6,20 @@ import 'package:meta/meta.dart';
void main() {
group("custom image data uri matcher", () {
- CustomRenderMatcher matcher = svgDataUriMatcher(encoding: null, mime: 'image/svg+xml');
+ CustomRenderMatcher matcher =
+ svgDataUriMatcher(encoding: null, mime: 'image/svg+xml');
testImgSrcMatcher(
"matches an svg data uri with base64 encoding",
matcher,
imgSrc:
- 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMzAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE1IiBjeT0iMTAiIHI9IjEwIiBmaWxsPSJncmVlbiIvPgo8L3N2Zz4=',
+ 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMzAgMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxjaXJjbGUgY3g9IjE1IiBjeT0iMTAiIHI9IjEwIiBmaWxsPSJncmVlbiIvPgo8L3N2Zz4=',
shouldMatch: true,
);
testImgSrcMatcher(
"matches an svg data uri without specified encoding",
matcher,
imgSrc:
- 'data:image/svg+xml,%3C?xml version="1.0" encoding="UTF-8"?%3E%3Csvg viewBox="0 0 30 20" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="15" cy="10" r="10" fill="green"/%3E%3C/svg%3E',
+ 'data:image/svg+xml,%3C?xml version="1.0" encoding="UTF-8"?%3E%3Csvg viewBox="0 0 30 20" xmlns="http://www.w3.org/2000/svg"%3E%3Ccircle cx="15" cy="10" r="10" fill="green"/%3E%3C/svg%3E',
shouldMatch: true,
);
testImgSrcMatcher(
@@ -31,7 +32,7 @@ void main() {
"doesn't match non-base64 image data uri",
matcher,
imgSrc:
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==',
shouldMatch: false,
);
testImgSrcMatcher(
@@ -69,11 +70,11 @@ String _fakeElement(String? src) {
@isTest
void testImgSrcMatcher(
- String name,
- CustomRenderMatcher matcher, {
- required String? imgSrc,
- required bool shouldMatch,
- }) {
+ String name,
+ CustomRenderMatcher matcher, {
+ required String? imgSrc,
+ required bool shouldMatch,
+}) {
testWidgets(name, (WidgetTester tester) async {
await tester.pumpWidget(
TestApp(
@@ -89,7 +90,8 @@ void testImgSrcMatcher(
),
),
);
- await expectLater(find.text("Success"), shouldMatch ? findsOneWidget : findsNothing);
+ await expectLater(
+ find.text("Success"), shouldMatch ? findsOneWidget : findsNothing);
});
}
@@ -107,4 +109,4 @@ class TestApp extends StatelessWidget {
),
);
}
-}
\ No newline at end of file
+}
diff --git a/packages/flutter_html_table/lib/flutter_html_table.dart b/packages/flutter_html_table/lib/flutter_html_table.dart
index c65387e9c2..8a207abbf3 100644
--- a/packages/flutter_html_table/lib/flutter_html_table.dart
+++ b/packages/flutter_html_table/lib/flutter_html_table.dart
@@ -9,19 +9,12 @@ import 'package:flutter_html/flutter_html.dart';
/// The CustomRender function that will render the HTML tag
CustomRender tableRender() =>
CustomRender.widget(widget: (context, buildChildren) {
- return Container(
+ return CssBoxWidget(
key: context.key,
- margin: context.style.margin?.nonNegative,
- padding: context.style.padding?.nonNegative,
- alignment: context.style.alignment,
- decoration: BoxDecoration(
- color: context.style.backgroundColor,
- border: context.style.border,
- ),
- width: context.style.width,
- height: context.style.height,
+ style: context.style,
child: LayoutBuilder(
- builder: (_, constraints) => _layoutCells(context, constraints)),
+ builder: (_, constraints) => _layoutCells(context, constraints),
+ ),
);
});
@@ -110,24 +103,17 @@ Widget _layoutCells(RenderContext context, BoxConstraints constraints) {
columnColspanOffset[columni].clamp(1, columnMax - columni - 1);
}
cells.add(GridPlacement(
- child: Container(
- width: child.style.width ?? double.infinity,
- height: child.style.height,
- padding: child.style.padding?.nonNegative ??
- row.style.padding?.nonNegative,
- decoration: BoxDecoration(
- color: child.style.backgroundColor ?? row.style.backgroundColor,
- border: child.style.border ?? row.style.border,
- ),
+ child: CssBoxWidget(
+ style: child.style
+ .merge(row.style), //TODO padding/decoration(color/border)
child: SizedBox.expand(
child: Container(
alignment: child.style.alignment ??
context.style.alignment ??
Alignment.centerLeft,
- child: StyledText(
- textSpan: context.parser.parseTree(context, child),
- style: child.style,
- renderContext: context,
+ child: CssBoxWidget.withInlineSpanChildren(
+ children: [context.parser.parseTree(context, child)],
+ style: child.style, //TODO updated this. Does it work?
),
),
),
diff --git a/packages/flutter_html_video/README.md b/packages/flutter_html_video/README.md
index 62b7551e4b..a4d0644a4c 100644
--- a/packages/flutter_html_video/README.md
+++ b/packages/flutter_html_video/README.md
@@ -1,6 +1,6 @@
# flutter_html_video
-Video widget for flutter_html.
+Video extension for flutter_html.
This package renders video elements using the [`chewie`](https://pub.dev/packages/chewie) and the [`video_player`](https://pub.dev/packages/video_player) plugin.
diff --git a/pubspec.yaml b/pubspec.yaml
index f47df5c0d3..6f1baadd9f 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -4,7 +4,7 @@ version: 3.0.0-alpha.5
homepage: https://github.com/Sub6Resources/flutter_html
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: '>=2.17.0 <3.0.0'
flutter: '>=2.2.0'
dependencies:
diff --git a/test/flutter_html_test.dart b/test/flutter_html_test.dart
new file mode 100644
index 0000000000..9a5a752c70
--- /dev/null
+++ b/test/flutter_html_test.dart
@@ -0,0 +1,105 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ testWidgets(
+ "Check that widget does not fail on empty data",
+ (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "",
+ ),
+ ),
+ );
+ expect(find.text('', findRichText: true), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ "Check that selectable widget does not fail on empty data",
+ (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: SelectableHtml(
+ data: '',
+ ),
+ ),
+ );
+ expect(find.text('', findRichText: true), findsOneWidget);
+ },
+ );
+
+ testWidgets(
+ "Check that widget displays given text",
+ (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "Text",
+ ),
+ ),
+ );
+ expect(find.text('Text', findRichText: true), findsOneWidget);
+ },
+ );
+
+ testWidgets('Check that a simple element is displayed', (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "Text
",
+ ),
+ ),
+ );
+ expect(find.text('Text', findRichText: true), findsOneWidget);
+ });
+
+ testWidgets('Check that a simple element is hidden when tagsList does not contain it', (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "Text
",
+ tagsList: ['div'], //Anything but `p`
+ ),
+ ),
+ );
+ expect(find.text('Text', findRichText: true), findsNothing);
+ });
+
+ testWidgets('Check that a simple element is displayed when it is included in tagsList', (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "Text
",
+ tagsList: ['html', 'body', 'p'],
+ ),
+ ),
+ );
+ expect(find.text('Text', findRichText: true), findsOneWidget);
+ });
+
+ testWidgets('Check that a custom element is not displayed', (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "Text",
+ ),
+ ),
+ );
+ expect(find.text('Text', findRichText: true), findsNothing);
+ });
+
+ testWidgets('Check that a custom element is not displayed', (tester) async {
+ await tester.pumpWidget(
+ MaterialApp(
+ home: Html(
+ data: "Text",
+ tagsList: Html.tags..add('custom'),
+ ),
+ ),
+ );
+ expect(find.text('Text', findRichText: true), findsOneWidget);
+ });
+}
diff --git a/test/html_parser_test.dart b/test/html_parser_test.dart
index c351ab2a36..3937c4a296 100644
--- a/test/html_parser_test.dart
+++ b/test/html_parser_test.dart
@@ -50,11 +50,11 @@ void testNewParser(BuildContext context) {
shrinkWrap: false,
selectable: true,
style: {},
- customRenders: defaultRenders,
+ customRenders: generateDefaultRenders(),
tagsList: Html.tags,
selectionControls: null,
scrollPhysics: null,
- )
+ ),
);
print(tree.toString());
@@ -76,11 +76,11 @@ void testNewParser(BuildContext context) {
shrinkWrap: false,
selectable: true,
style: {},
- customRenders: defaultRenders,
+ customRenders: generateDefaultRenders(),
tagsList: Html.tags,
selectionControls: null,
scrollPhysics: null,
- )
+ ),
);
print(tree.toString());
@@ -100,11 +100,11 @@ void testNewParser(BuildContext context) {
shrinkWrap: false,
selectable: true,
style: {},
- customRenders: defaultRenders,
+ customRenders: generateDefaultRenders(),
tagsList: Html.tags,
selectionControls: null,
scrollPhysics: null,
- )
+ ),
);
print(tree.toString());
@@ -126,11 +126,11 @@ void testNewParser(BuildContext context) {
shrinkWrap: false,
selectable: true,
style: {},
- customRenders: defaultRenders,
+ customRenders: generateDefaultRenders(),
tagsList: Html.tags,
selectionControls: null,
scrollPhysics: null,
- )
+ ),
);
print(tree.toString());
diff --git a/test/style/dimension_test.dart b/test/style/dimension_test.dart
new file mode 100644
index 0000000000..cf239249f2
--- /dev/null
+++ b/test/style/dimension_test.dart
@@ -0,0 +1,44 @@
+import 'package:flutter_html/src/style/length.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+const nonZeroNumber = 16.0;
+
+void main() {
+ test("Basic length unspecified units test", () {
+ final length = Length(nonZeroNumber);
+ expect(length.value, equals(nonZeroNumber));
+ expect(length.unit, equals(Unit.px));
+ });
+
+ test("Basic length-percent unspecified units test", () {
+ final lengthPercent = LengthOrPercent(nonZeroNumber);
+ expect(lengthPercent.value, equals(nonZeroNumber));
+ expect(lengthPercent.unit, equals(Unit.px));
+ });
+
+ test("Zero-length unspecified units test", () {
+ final length = Length(0);
+ expect(length.value, equals(0));
+ expect(length.unit, equals(Unit.px));
+ });
+
+ test("Zero-percent-length unspecified units test", () {
+ final lengthPercent = LengthOrPercent(0);
+ expect(lengthPercent.value, equals(0));
+ expect(lengthPercent.unit, equals(Unit.px));
+ });
+
+ test("Pass in invalid unit", () {
+ expect(() => Length(nonZeroNumber, Unit.percent), throwsAssertionError);
+ });
+
+ test("Pass in invalid unit with zero", () {
+ expect(() => Length(0, Unit.percent), throwsAssertionError);
+ });
+
+ test("Pass in a valid unit", () {
+ final lengthPercent = LengthOrPercent(nonZeroNumber, Unit.percent);
+ expect(lengthPercent.value, equals(nonZeroNumber));
+ expect(lengthPercent.unit, equals(Unit.percent));
+ });
+}
\ No newline at end of file
diff --git a/test/style/fontsize_test.dart b/test/style/fontsize_test.dart
new file mode 100644
index 0000000000..e985d73706
--- /dev/null
+++ b/test/style/fontsize_test.dart
@@ -0,0 +1,59 @@
+import 'package:flutter_html/src/style/fontsize.dart';
+import 'package:flutter_html/src/style/length.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ test('Check basic FontSize inheritance', () {
+ final FontSize parent = FontSize(16);
+ final FontSize? child = null;
+
+ final result = FontSize.inherit(parent, child);
+
+ expect(result?.value, equals(16));
+ });
+
+ test('Check double null FontSize inheritance', () {
+ final FontSize? parent = null;
+ final FontSize? child = null;
+
+ final result = FontSize.inherit(parent, child);
+
+ expect(result?.value, equals(null));
+ });
+
+ test('Check basic em inheritance', () {
+ final FontSize? parent = FontSize(16);
+ final FontSize? child = FontSize(1, Unit.em);
+
+ final result = FontSize.inherit(parent, child);
+
+ expect(result?.value, equals(16));
+ });
+
+ test('Check factor em inheritance', () {
+ final FontSize? parent = FontSize(16);
+ final FontSize? child = FontSize(0.5, Unit.em);
+
+ final result = FontSize.inherit(parent, child);
+
+ expect(result?.value, equals(8));
+ });
+
+ test('Check basic % inheritance', () {
+ final FontSize? parent = FontSize(16);
+ final FontSize? child = FontSize(100, Unit.percent);
+
+ final result = FontSize.inherit(parent, child);
+
+ expect(result?.value, equals(16));
+ });
+
+ test('Check scaled % inheritance', () {
+ final FontSize? parent = FontSize(16);
+ final FontSize? child = FontSize(50, Unit.percent);
+
+ final result = FontSize.inherit(parent, child);
+
+ expect(result?.value, equals(8));
+ });
+}
\ No newline at end of file
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000000..c4cec61f93
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,42 @@
+import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_html/src/css_parser.dart';
+import 'package:flutter_html/src/utils.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+/// Tests the file lib/src/utils.dart
+
+void main() {
+ test('Tests that namedColors returns a valid color', () {
+ expect(ExpressionMapping.namedColorToColor('red'), equals(ExpressionMapping.stringToColor(namedColors['Red']!)));
+ expect(namedColors['Red'], equals('#FF0000'));
+ });
+
+ test('CustomBorderSide does not allow negative width', () {
+ expect(() => CustomBorderSide(width: -5), throwsAssertionError);
+ expect(CustomBorderSide(width: 0), TypeMatcher());
+ expect(CustomBorderSide(width: 5), TypeMatcher());
+ });
+
+ const originalString = 'Hello';
+ const uppercaseString = 'HELLO';
+ const lowercaseString = 'hello';
+
+ test('TextTransformUtil returns self if transform is null', () {
+ expect(originalString.transformed(null), equals(originalString));
+ });
+
+ test('TextTransformUtil uppercases correctly', () {
+ expect(originalString.transformed(TextTransform.uppercase), equals(uppercaseString));
+ });
+
+ test('TextTransformUtil lowercases correctly', () {
+ expect(originalString.transformed(TextTransform.lowercase), equals(lowercaseString));
+ });
+
+ const originalLongString = 'Hello, world! pub.dev';
+ const capitalizedLongString = 'Hello, World! Pub.Dev';
+
+ test('TextTransformUtil capitalizs correctly', () {
+ expect(originalLongString.transformed(TextTransform.capitalize), equals(capitalizedLongString));
+ });
+}
\ No newline at end of file