@@ -5,11 +5,34 @@ import 'package:flutter_html/flutter_html.dart';
5
5
import 'package:flutter_html/src/tree/image_element.dart' ;
6
6
7
7
class ImageBuiltIn extends Extension {
8
+ final String ? dataEncoding;
9
+ final Set <String >? mimeTypes;
8
10
final Map <String , String >? networkHeaders;
11
+ final Set <String > networkSchemas;
12
+ final Set <String >? networkDomains;
13
+ final Set <String >? fileExtensions;
14
+
15
+ final String assetSchema;
16
+ final AssetBundle ? assetBundle;
17
+ final String ? assetPackage;
18
+
19
+ final bool handleNetworkImages;
20
+ final bool handleAssetImages;
21
+ final bool handleDataImages;
9
22
10
- //TODO how can the end user access this?
11
23
const ImageBuiltIn ({
12
24
this .networkHeaders,
25
+ this .networkDomains,
26
+ this .networkSchemas = const {"http" , "https" },
27
+ this .fileExtensions,
28
+ this .assetSchema = "asset:" ,
29
+ this .assetBundle,
30
+ this .assetPackage,
31
+ this .mimeTypes,
32
+ this .dataEncoding,
33
+ this .handleNetworkImages = true ,
34
+ this .handleAssetImages = true ,
35
+ this .handleDataImages = true ,
13
36
});
14
37
15
38
@override
@@ -23,30 +46,9 @@ class ImageBuiltIn extends Extension {
23
46
return false ;
24
47
}
25
48
26
- if (context.attributes['src' ] == null ) {
27
- return false ;
28
- }
29
-
30
- final src = context.attributes['src' ]! ;
31
-
32
- // Data Image Schema:
33
- final dataUri = dataUriFormat.firstMatch (src);
34
- if (dataUri != null && dataUri.namedGroup ('mime' ) != "image/svg+xml" ) {
35
- return true ;
36
- }
37
-
38
- // Asset Image Schema:
39
- if (src.startsWith ("asset:" ) && ! src.endsWith (".svg" )) {
40
- return true ;
41
- }
42
-
43
- // Network Image Schema:
44
- try {
45
- final srcUri = Uri .parse (src);
46
- return ! srcUri.path.endsWith (".svg" );
47
- } on FormatException {
48
- return false ;
49
- }
49
+ return (_matchesNetworkImage (context) && handleNetworkImages) ||
50
+ (_matchesAssetImage (context) && handleAssetImages) ||
51
+ (_matchesBase64Image (context) && handleDataImages);
50
52
}
51
53
52
54
@override
@@ -72,92 +74,125 @@ class ImageBuiltIn extends Extension {
72
74
Map <StyledElement , InlineSpan > Function () parseChildren) {
73
75
final element = context.styledElement as ImageElement ;
74
76
75
- final dataUri = dataUriFormat.firstMatch (element.src);
76
- if (dataUri != null && dataUri.namedGroup ('mime' ) != "image/svg+xml" ) {
77
- return WidgetSpan (
78
- child: _base64ImageRender (context),
79
- );
80
- }
77
+ final imageStyle = Style (
78
+ width: element.width,
79
+ height: element.height,
80
+ ).merge (context.styledElement! .style);
81
81
82
- if (element.src.startsWith ("asset:" ) && ! element.src.endsWith (".svg" )) {
83
- return WidgetSpan (
84
- child: _assetImageRender (context),
85
- );
82
+ late Widget child;
83
+ if (_matchesBase64Image (context)) {
84
+ child = _base64ImageRender (context, imageStyle);
85
+ } else if (_matchesAssetImage (context)) {
86
+ child = _assetImageRender (context, imageStyle);
87
+ } else if (_matchesNetworkImage (context)) {
88
+ child = _networkImageRender (context, imageStyle);
89
+ } else {
90
+ // Our matcher went a little overboard and matched
91
+ // something we can't render
92
+ return TextSpan (text: element.alt);
86
93
}
87
94
88
- try {
89
- final srcUri = Uri .parse (element.src);
90
- return WidgetSpan (
91
- child: _networkImageRender (context, srcUri),
92
- );
93
- } on FormatException {
94
- return const TextSpan (text: "" );
95
- }
95
+ return WidgetSpan (
96
+ child: CssBoxWidget (
97
+ style: imageStyle,
98
+ childIsReplaced: true ,
99
+ child: child,
100
+ ),
101
+ );
96
102
}
97
103
98
104
static RegExp get dataUriFormat => RegExp (
99
- r"^(?<scheme>data):(?<mime>image\ /[\w+\-.]+);(?<encoding>base64)?,\s*(?<data>.*)" );
105
+ r"^(?<scheme>data):(?<mime>image/[\w+\-.]+);* (?<encoding>base64)?,\s*(?<data>.*)" );
100
106
101
- //TODO remove repeated code between these methods:
107
+ bool _matchesBase64Image (ExtensionContext context) {
108
+ final attributes = context.attributes;
102
109
103
- Widget _base64ImageRender (ExtensionContext context) {
110
+ if (attributes['src' ] == null ) {
111
+ return false ;
112
+ }
113
+
114
+ final dataUri = dataUriFormat.firstMatch (attributes['src' ]! );
115
+
116
+ return context.elementName == "img" &&
117
+ dataUri != null &&
118
+ (mimeTypes == null ||
119
+ mimeTypes! .contains (dataUri.namedGroup ('mime' ))) &&
120
+ dataUri.namedGroup ('mime' ) != 'image/svg+xml' &&
121
+ (dataEncoding == null ||
122
+ dataUri.namedGroup ('encoding' ) == dataEncoding);
123
+ }
124
+
125
+ bool _matchesAssetImage (ExtensionContext context) {
126
+ final attributes = context.attributes;
127
+
128
+ return context.elementName == "img" &&
129
+ attributes['src' ] != null &&
130
+ ! attributes['src' ]! .endsWith (".svg" ) &&
131
+ attributes['src' ]! .startsWith (assetSchema) &&
132
+ (fileExtensions == null ||
133
+ attributes['src' ]! .endsWithAnyFileExtension (fileExtensions! ));
134
+ }
135
+
136
+ bool _matchesNetworkImage (ExtensionContext context) {
137
+ final attributes = context.attributes;
138
+
139
+ if (attributes['src' ] == null ) {
140
+ return false ;
141
+ }
142
+
143
+ final src = Uri .tryParse (attributes['src' ]! );
144
+ if (src == null ) {
145
+ return false ;
146
+ }
147
+
148
+ return context.elementName == "img" &&
149
+ networkSchemas.contains (src.scheme) &&
150
+ ! src.path.endsWith (".svg" ) &&
151
+ (networkDomains == null || networkDomains! .contains (src.host)) &&
152
+ (fileExtensions == null ||
153
+ src.path.endsWithAnyFileExtension (fileExtensions! ));
154
+ }
155
+
156
+ Widget _base64ImageRender (ExtensionContext context, Style imageStyle) {
104
157
final element = context.styledElement as ImageElement ;
105
158
final decodedImage = base64.decode (element.src.split ("base64," )[1 ].trim ());
106
- final imageStyle = Style (
107
- width: element.width,
108
- height: element.height,
109
- ).merge (context.styledElement! .style);
110
159
111
- return CssBoxWidget (
112
- style: imageStyle,
113
- childIsReplaced: true ,
114
- child: Image .memory (
115
- decodedImage,
116
- width: imageStyle.width? .value,
117
- height: imageStyle.height? .value,
118
- fit: BoxFit .fill,
119
- errorBuilder: (ctx, error, stackTrace) {
120
- return Text (
121
- element.alt ?? "" ,
122
- style: context.styledElement! .style.generateTextStyle (),
123
- );
124
- },
125
- ),
160
+ return Image .memory (
161
+ decodedImage,
162
+ width: imageStyle.width? .value,
163
+ height: imageStyle.height? .value,
164
+ fit: BoxFit .fill,
165
+ errorBuilder: (ctx, error, stackTrace) {
166
+ return Text (
167
+ element.alt ?? "" ,
168
+ style: context.styledElement! .style.generateTextStyle (),
169
+ );
170
+ },
126
171
);
127
172
}
128
173
129
- Widget _assetImageRender (ExtensionContext context) {
174
+ Widget _assetImageRender (ExtensionContext context, Style imageStyle ) {
130
175
final element = context.styledElement as ImageElement ;
131
176
final assetPath = element.src.replaceFirst ('asset:' , '' );
132
- final imageStyle = Style (
133
- width: element.width,
134
- height: element.height,
135
- ).merge (context.styledElement! .style);
136
177
137
- return CssBoxWidget (
138
- style: imageStyle,
139
- childIsReplaced: true ,
140
- child: Image .asset (
141
- assetPath,
142
- width: imageStyle.width? .value,
143
- height: imageStyle.height? .value,
144
- fit: BoxFit .fill,
145
- errorBuilder: (ctx, error, stackTrace) {
146
- return Text (
147
- element.alt ?? "" ,
148
- style: context.styledElement! .style.generateTextStyle (),
149
- );
150
- },
151
- ),
178
+ return Image .asset (
179
+ assetPath,
180
+ width: imageStyle.width? .value,
181
+ height: imageStyle.height? .value,
182
+ fit: BoxFit .fill,
183
+ bundle: assetBundle,
184
+ package: assetPackage,
185
+ errorBuilder: (ctx, error, stackTrace) {
186
+ return Text (
187
+ element.alt ?? "" ,
188
+ style: context.styledElement! .style.generateTextStyle (),
189
+ );
190
+ },
152
191
);
153
192
}
154
193
155
- Widget _networkImageRender (ExtensionContext context, Uri srcUri ) {
194
+ Widget _networkImageRender (ExtensionContext context, Style imageStyle ) {
156
195
final element = context.styledElement as ImageElement ;
157
- final imageStyle = Style (
158
- width: element.width,
159
- height: element.height,
160
- ).merge (context.styledElement! .style);
161
196
162
197
return CssBoxWidget (
163
198
style: imageStyle,
@@ -178,3 +213,14 @@ class ImageBuiltIn extends Extension {
178
213
);
179
214
}
180
215
}
216
+
217
+ extension _SetFolding on String {
218
+ bool endsWithAnyFileExtension (Iterable <String > endings) {
219
+ for (final element in endings) {
220
+ if (endsWith (".$element " )) {
221
+ return true ;
222
+ }
223
+ }
224
+ return false ;
225
+ }
226
+ }
0 commit comments