Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding tests for SVG and fixing height/width bug #1202

Merged
merged 1 commit into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/flutter_html_svg/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
.pub-cache/
.pub/
build/
coverage/

# Android related
**/android/**/gradle-wrapper.jar
Expand Down
133 changes: 81 additions & 52 deletions packages/flutter_html_svg/lib/flutter_html_svg.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
library flutter_html_svg;

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
// ignore: implementation_imports
Expand All @@ -11,16 +10,18 @@ import 'package:flutter_svg/flutter_svg.dart';
/// The CustomRender function that renders the <svg> HTML tag.
CustomRender svgTagRender() =>
CustomRender.widget(widget: (context, buildChildren) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

return Builder(
key: context.key,
builder: (buildContext) {
return GestureDetector(
child: SvgPicture.string(
context.tree.element?.outerHtml ?? "",
width: double.tryParse(
context.tree.element?.attributes['width'] ?? ""),
height: double.tryParse(
context.tree.element?.attributes['width'] ?? ""),
width: _width(attributes),
height: _height(attributes),
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
Expand All @@ -29,7 +30,7 @@ CustomRender svgTagRender() =>
context.parser.onImageTap?.call(
context.tree.element?.outerHtml ?? "",
context,
context.tree.element!.attributes.cast(),
attributes,
context.tree.element);
},
);
Expand All @@ -39,32 +40,39 @@ CustomRender svgTagRender() =>
/// The CustomRender function that renders an <img> tag with hardcoded svg data.
CustomRender svgDataImageRender() =>
CustomRender.widget(widget: (context, buildChildren) {
final dataUri = _dataUriFormat.firstMatch(
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!);
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};
final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);
final data = dataUri?.namedGroup('data');
if (data == null) return const SizedBox(height: 0, width: 0);

if (data == null || data.isEmpty) {
return const SizedBox(height: 0, width: 0);
}
return Builder(
key: context.key,
builder: (buildContext) {
final width = _width(attributes);
final height = _height(attributes);

return GestureDetector(
child: dataUri?.namedGroup('encoding') == ';base64'
? SvgPicture.memory(
base64.decode(data.trim()),
width: _width(context.tree.element?.attributes.cast() ??
<String, String>{}),
height: _height(context.tree.element?.attributes.cast() ??
<String, String>{}),
width: width,
height: height,
)
: SvgPicture.string(Uri.decodeFull(data)),
: SvgPicture.string(
Uri.decodeFull(data),
width: width,
height: height,
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
context.parser.onImageTap?.call(
Uri.decodeFull(data),
context,
context.tree.element!.attributes.cast(),
context.tree.element);
context.parser.onImageTap?.call(Uri.decodeFull(data), context,
attributes, context.tree.element);
},
);
});
Expand All @@ -73,55 +81,62 @@ CustomRender svgDataImageRender() =>
/// The CustomRender function that renders an <img> tag with a network svg image.
CustomRender svgNetworkImageRender() =>
CustomRender.widget(widget: (context, buildChildren) {
if (context.tree.element?.attributes["src"] == null) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (attributes["src"] == null) {
return const SizedBox(height: 0, width: 0);
}
return Builder(
key: context.key,
builder: (buildContext) {
return GestureDetector(
child: SvgPicture.network(
context.tree.element!.attributes["src"]!,
width: _width(context.tree.element!.attributes.cast()),
height: _height(context.tree.element!.attributes.cast()),
attributes["src"]!,
width: _width(attributes),
height: _height(attributes),
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
context.parser.onImageTap?.call(
context.tree.element!.attributes["src"]!,
context,
context.tree.element!.attributes.cast(),
context.tree.element);
context.parser.onImageTap?.call(attributes["src"]!, context,
attributes, context.tree.element);
},
);
});
});

/// The CustomRender function that renders an <img> tag with an svg asset in your app
CustomRender svgAssetImageRender() =>
CustomRender svgAssetImageRender({AssetBundle? bundle}) =>
CustomRender.widget(widget: (context, buildChildren) {
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
null) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (_src(attributes) == null) {
return const SizedBox(height: 0, width: 0);
}

final assetPath = _src(context.tree.element!.attributes.cast())!
.replaceFirst('asset:', '');
return Builder(
key: context.key,
builder: (buildContext) {
return GestureDetector(
child: SvgPicture.asset(assetPath),
child: SvgPicture.asset(
assetPath,
bundle: bundle,
width: _width(attributes),
height: _height(attributes),
),
onTap: () {
if (MultipleTapGestureDetector.of(buildContext) != null) {
MultipleTapGestureDetector.of(buildContext)!.onTap?.call();
}
context.parser.onImageTap?.call(
assetPath,
context,
context.tree.element!.attributes.cast(),
context.tree.element);
assetPath, context, attributes, context.tree.element);
},
);
});
Expand All @@ -136,10 +151,16 @@ CustomRenderMatcher svgTagMatcher() => (context) {
CustomRenderMatcher svgDataUriMatcher(
{String? encoding = 'base64', String? mime = 'image/svg+xml'}) =>
(context) {
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
null) return false;
final dataUri = _dataUriFormat.firstMatch(
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!);
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (_src(attributes) == null) {
return false;
}

final dataUri = _dataUriFormat.firstMatch(_src(attributes)!);

return context.tree.element?.localName == "img" &&
dataUri != null &&
(mime == null || dataUri.namedGroup('mime') == mime) &&
Expand All @@ -153,11 +174,17 @@ CustomRenderMatcher svgNetworkSourceMatcher({
String? extension = "svg",
}) =>
(context) {
if (_src(context.tree.element?.attributes.cast() ?? <String, String>{}) ==
null) return false;
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

if (_src(attributes) == null) {
return false;
}

try {
final src = Uri.parse(_src(
context.tree.element?.attributes.cast() ?? <String, String>{})!);
final src = Uri.parse(_src(attributes)!);

return context.tree.element?.localName == "img" &&
schemas.contains(src.scheme) &&
(domains == null || domains.contains(src.host)) &&
Expand All @@ -168,14 +195,16 @@ CustomRenderMatcher svgNetworkSourceMatcher({
};

/// A CustomRenderMatcher for an <img> tag with an in-app svg asset
CustomRenderMatcher svgAssetUriMatcher() => (context) =>
context.tree.element?.localName == "img" &&
_src(context.tree.element?.attributes.cast() ?? <String, String>{}) !=
null &&
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!
.startsWith("asset:") &&
_src(context.tree.element?.attributes.cast() ?? <String, String>{})!
.endsWith(".svg");
CustomRenderMatcher svgAssetUriMatcher() => (context) {
final attributes =
context.tree.element?.attributes.cast<String, String>() ??
<String, String>{};

return context.tree.element?.localName == "img" &&
_src(attributes) != null &&
_src(attributes)!.startsWith("asset:") &&
_src(attributes)!.endsWith(".svg");
};

final _dataUriFormat = RegExp(
"^(?<scheme>data):(?<mime>image\\/[\\w\\+\\-\\.]+)(?<encoding>;base64)?\\,(?<data>.*)");
Expand Down
38 changes: 38 additions & 0 deletions packages/flutter_html_svg/test/svg_asset_image_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'package:flutter_html_svg/flutter_html_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import './test_utils.dart';

void main() {
group("custom image asset tests:", () {
const String svgString = svgRawString;
String makeImgTag({
String? src,
int? width,
int? height,
}) {
String srcAttr = src != null ? 'src="$src"' : '';
String widthAttr = width != null ? 'width=$width' : '';
String heightAttr = height != null ? 'height=$height' : '';

return """
<img $widthAttr $heightAttr $srcAttr />
""";
}

// Happy path (taken from SvgPicture examples)
testMatchAndRender(
"matches and renders img with asset",
makeImgTag(src: "asset:fake.svg", width: 100, height: 100),
svgAssetUriMatcher(),
svgAssetImageRender(bundle: FakeAssetBundle()),
TestResult.matchAndRenderSvgPicture);

// Failure paths
testMatchAndRender(
"does not match",
makeImgTag(src: "fake.svg"),
svgAssetUriMatcher(),
svgAssetImageRender(bundle: FakeAssetBundle()),
TestResult.noMatch);
});
}
60 changes: 60 additions & 0 deletions packages/flutter_html_svg/test/svg_data_image_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter_html_svg/flutter_html_svg.dart';
import 'package:flutter_test/flutter_test.dart';
import './test_utils.dart';

void main() {
group("custom image data uri tests:", () {
String makeImgTag({
String? src,
int? width,
int? height,
}) {
String srcAttr = src != null ? 'src="$src"' : '';
String widthAttr = width != null ? 'width=$width' : '';
String heightAttr = height != null ? 'height=$height' : '';

return """
<img alt='dummy' $widthAttr $heightAttr $srcAttr />
""";
}

// Happy path (taken from SvgPicture examples)
testMatchAndRender(
"matches and renders image/svg+xml with text encoding",
makeImgTag(
src: 'data:image/svg+xml,$svgEncoded', width: 100, height: 100),
svgDataUriMatcher(encoding: null),
svgDataImageRender(),
TestResult.matchAndRenderSvgPicture);
testMatchAndRender(
"matches and renders image/svg+xml and base64 encoding",
makeImgTag(src: 'data:image/svg+xml;base64,$svgBase64'),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.matchAndRenderSvgPicture);

// Failure paths
testMatchAndRender("image tag with no attributes", makeImgTag(),
svgDataUriMatcher(), svgDataImageRender(), TestResult.noMatch);
testMatchAndRender(
"does not match base64 image data uri",
makeImgTag(
src:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.noMatch);
testMatchAndRender(
"does not match non-svg mime data",
makeImgTag(src: 'data:text/plain;base64,'),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.noMatch);
testMatchAndRender(
"does not match non-data schema",
makeImgTag(src: 'http:'),
svgDataUriMatcher(),
svgDataImageRender(),
TestResult.noMatch);
});
}
Loading