Skip to content

Commit a8467db

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 3da05e5 + 68033b6 commit a8467db

File tree

9 files changed

+197
-49
lines changed

9 files changed

+197
-49
lines changed

lib/flutter_html.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ class SelectableHtml extends StatelessWidget {
221221
this.shrinkWrap = false,
222222
this.style = const {},
223223
this.tagsList = const [],
224-
this.selectionControls
224+
this.selectionControls,
225+
this.scrollPhysics,
225226
}) : document = null,
226227
assert(data != null),
227228
_anchorKey = anchorKey ?? GlobalKey(),
@@ -237,7 +238,8 @@ class SelectableHtml extends StatelessWidget {
237238
this.shrinkWrap = false,
238239
this.style = const {},
239240
this.tagsList = const [],
240-
this.selectionControls
241+
this.selectionControls,
242+
this.scrollPhysics,
241243
}) : data = null,
242244
assert(document != null),
243245
_anchorKey = anchorKey ?? GlobalKey(),
@@ -276,6 +278,9 @@ class SelectableHtml extends StatelessWidget {
276278
/// options
277279
final TextSelectionControls? selectionControls;
278280

281+
/// Allows you to override the default scrollPhysics for [SelectableText.rich]
282+
final ScrollPhysics? scrollPhysics;
283+
279284
static List<String> get tags => new List<String>.from(SELECTABLE_ELEMENTS);
280285

281286
@override
@@ -302,6 +307,7 @@ class SelectableHtml extends StatelessWidget {
302307
tagsList: tagsList.isEmpty ? SelectableHtml.tags : tagsList,
303308
navigationDelegateForIframe: null,
304309
selectionControls: selectionControls,
310+
scrollPhysics: scrollPhysics,
305311
),
306312
);
307313
}

lib/html_parser.dart

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class HtmlParser extends StatelessWidget {
5959
final NavigationDelegate? navigationDelegateForIframe;
6060
final OnTap? _onAnchorTap;
6161
final TextSelectionControls? selectionControls;
62+
final ScrollPhysics? scrollPhysics;
6263

6364
HtmlParser({
6465
required this.key,
@@ -76,7 +77,8 @@ class HtmlParser extends StatelessWidget {
7677
required this.imageRenders,
7778
required this.tagsList,
7879
required this.navigationDelegateForIframe,
79-
this.selectionControls
80+
this.selectionControls,
81+
this.scrollPhysics,
8082
}) : this._onAnchorTap = onAnchorTap != null
8183
? onAnchorTap
8284
: key != null
@@ -128,6 +130,7 @@ class HtmlParser extends StatelessWidget {
128130
style: cleanedTree.style,
129131
),
130132
selectionControls: selectionControls,
133+
scrollPhysics: scrollPhysics,
131134
);
132135
}
133136
return StyledText(
@@ -420,11 +423,7 @@ class HtmlParser extends StatelessWidget {
420423
tree.style.listStylePosition == ListStylePosition.OUTSIDE ?
421424
Padding(
422425
padding: tree.style.padding ?? EdgeInsets.only(left: tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style.direction == TextDirection.rtl ? 10.0 : 0.0),
423-
child: Text(
424-
"${newContext.style.markerContent}",
425-
textAlign: TextAlign.right,
426-
style: newContext.style.generateTextStyle()
427-
),
426+
child: newContext.style.markerContent
428427
) : Container(height: 0, width: 0),
429428
Text("\t", textAlign: TextAlign.right),
430429
Expanded(
@@ -433,11 +432,10 @@ class HtmlParser extends StatelessWidget {
433432
EdgeInsets.only(left: tree.style.direction != TextDirection.rtl ? 10.0 : 0.0, right: tree.style.direction == TextDirection.rtl ? 10.0 : 0.0) : EdgeInsets.zero,
434433
child: StyledText(
435434
textSpan: TextSpan(
436-
text: (tree.style.listStylePosition ==
437-
ListStylePosition.INSIDE)
438-
? '${newContext.style.markerContent}'
439-
: null,
440-
children: getChildren(tree),
435+
children: getChildren(tree)..insertAll(0, tree.style.listStylePosition == ListStylePosition.INSIDE ?
436+
[
437+
WidgetSpan(alignment: PlaceholderAlignment.middle, child: newContext.style.markerContent ?? Container(height: 0, width: 0))
438+
] : []),
441439
style: newContext.style.generateTextStyle(),
442440
),
443441
style: newContext.style,
@@ -451,12 +449,12 @@ class HtmlParser extends StatelessWidget {
451449
);
452450
} else if (tree is ReplacedElement) {
453451
if (tree is TextContentElement) {
454-
return TextSpan(text: tree.text);
452+
return TextSpan(text: tree.text?.transformed(tree.style.textTransform));
455453
} else {
456454
return WidgetSpan(
457455
alignment: tree.alignment,
458456
baseline: TextBaseline.alphabetic,
459-
child: tree.toWidget(context)!,
457+
child: tree.toWidget(newContext)!,
460458
);
461459
}
462460
} else if (tree is InteractableElement) {
@@ -708,7 +706,10 @@ class HtmlParser extends StatelessWidget {
708706
/// bullet all list items according to the [ListStyleType] they have been given.
709707
static StyledElement _processListCharactersRecursive(
710708
StyledElement tree, ListQueue<Context> olStack) {
711-
if (tree.name == 'ol' && tree.style.listStyleType != null) {
709+
if (tree.style.listStylePosition == null) {
710+
tree.style.listStylePosition = ListStylePosition.OUTSIDE;
711+
}
712+
if (tree.name == 'ol' && tree.style.listStyleType != null && tree.style.listStyleType!.type == "marker") {
712713
switch (tree.style.listStyleType!) {
713714
case ListStyleType.LOWER_LATIN:
714715
case ListStyleType.LOWER_ALPHA:
@@ -728,26 +729,31 @@ class HtmlParser extends StatelessWidget {
728729
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
729730
break;
730731
}
732+
} else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "widget") {
733+
tree.style.markerContent = tree.style.listStyleType!.widget!;
734+
} else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null && tree.style.listStyleType!.type == "image") {
735+
tree.style.markerContent = Image.network(tree.style.listStyleType!.text);
731736
} else if (tree.style.display == Display.LIST_ITEM && tree.style.listStyleType != null) {
737+
String marker = "";
732738
switch (tree.style.listStyleType!) {
733739
case ListStyleType.NONE:
734740
tree.style.markerContent = '';
735741
break;
736742
case ListStyleType.CIRCLE:
737-
tree.style.markerContent = '○';
743+
marker = '○';
738744
break;
739745
case ListStyleType.SQUARE:
740-
tree.style.markerContent = '■';
746+
marker = '■';
741747
break;
742748
case ListStyleType.DISC:
743-
tree.style.markerContent = '•';
749+
marker = '•';
744750
break;
745751
case ListStyleType.DECIMAL:
746752
if (olStack.isEmpty) {
747753
olStack.add(Context<int>((tree.attributes['start'] != null ? int.tryParse(tree.attributes['start'] ?? "") ?? 1 : 1) - 1));
748754
}
749755
olStack.last.data += 1;
750-
tree.style.markerContent = '${olStack.last.data}.';
756+
marker = '${olStack.last.data}.';
751757
break;
752758
case ListStyleType.LOWER_LATIN:
753759
case ListStyleType.LOWER_ALPHA:
@@ -762,7 +768,7 @@ class HtmlParser extends StatelessWidget {
762768
}
763769
}
764770
}
765-
tree.style.markerContent = olStack.last.data.toString() + ".";
771+
marker = olStack.last.data.toString() + ".";
766772
olStack.last.data = olStack.last.data.toString().nextLetter();
767773
break;
768774
case ListStyleType.UPPER_LATIN:
@@ -778,7 +784,7 @@ class HtmlParser extends StatelessWidget {
778784
}
779785
}
780786
}
781-
tree.style.markerContent = olStack.last.data.toString().toUpperCase() + ".";
787+
marker = olStack.last.data.toString().toUpperCase() + ".";
782788
olStack.last.data = olStack.last.data.toString().nextLetter();
783789
break;
784790
case ListStyleType.LOWER_ROMAN:
@@ -787,9 +793,9 @@ class HtmlParser extends StatelessWidget {
787793
}
788794
olStack.last.data += 1;
789795
if (olStack.last.data <= 0) {
790-
tree.style.markerContent = '${olStack.last.data}.';
796+
marker = '${olStack.last.data}.';
791797
} else {
792-
tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
798+
marker = (olStack.last.data as int).toRomanNumeralString()!.toLowerCase() + ".";
793799
}
794800
break;
795801
case ListStyleType.UPPER_ROMAN:
@@ -798,12 +804,16 @@ class HtmlParser extends StatelessWidget {
798804
}
799805
olStack.last.data += 1;
800806
if (olStack.last.data <= 0) {
801-
tree.style.markerContent = '${olStack.last.data}.';
807+
marker = '${olStack.last.data}.';
802808
} else {
803-
tree.style.markerContent = (olStack.last.data as int).toRomanNumeralString()! + ".";
809+
marker = (olStack.last.data as int).toRomanNumeralString()! + ".";
804810
}
805811
break;
806812
}
813+
tree.style.markerContent = Text(
814+
marker,
815+
textAlign: TextAlign.right,
816+
);
807817
}
808818

809819
tree.children.forEach((e) => _processListCharactersRecursive(e, olStack));
@@ -1067,6 +1077,7 @@ class StyledText extends StatelessWidget {
10671077
final AnchorKey? key;
10681078
final bool _selectable;
10691079
final TextSelectionControls? selectionControls;
1080+
final ScrollPhysics? scrollPhysics;
10701081

10711082
const StyledText({
10721083
required this.textSpan,
@@ -1075,6 +1086,7 @@ class StyledText extends StatelessWidget {
10751086
required this.renderContext,
10761087
this.key,
10771088
this.selectionControls,
1089+
this.scrollPhysics,
10781090
}) : _selectable = false,
10791091
super(key: key);
10801092

@@ -1084,7 +1096,8 @@ class StyledText extends StatelessWidget {
10841096
this.textScaleFactor = 1.0,
10851097
required this.renderContext,
10861098
this.key,
1087-
this.selectionControls
1099+
this.selectionControls,
1100+
this.scrollPhysics,
10881101
}) : textSpan = textSpan,
10891102
_selectable = true,
10901103
super(key: key);
@@ -1100,6 +1113,7 @@ class StyledText extends StatelessWidget {
11001113
textScaleFactor: textScaleFactor,
11011114
maxLines: style.maxLines,
11021115
selectionControls: selectionControls,
1116+
scrollPhysics: scrollPhysics,
11031117
);
11041118
}
11051119
return SizedBox(

lib/src/css_parser.dart

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
150150
List<String> possibleBorderValues = ["dotted", "dashed", "solid", "double", "groove", "ridge", "inset", "outset", "none", "hidden"];
151151
/// List<css.LiteralTerm> might include other values than the ones we want for [BorderSide.style], so make sure to remove those before passing it to [ExpressionMapping]
152152
potentialStyles.removeWhere((element) => element == null || !possibleBorderValues.contains(element.text));
153-
css.LiteralTerm borderStyle = potentialStyles.first!;
153+
css.LiteralTerm? borderStyle = potentialStyles.firstOrNull;
154154
Border newBorder = Border(
155155
left: style.border?.left ?? BorderSide.none,
156156
right: style.border?.right ?? BorderSide.none,
@@ -194,6 +194,43 @@ Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
194194
case 'font-weight':
195195
style.fontWeight = ExpressionMapping.expressionToFontWeight(value.first);
196196
break;
197+
case 'list-style':
198+
css.LiteralTerm? position = value.firstWhereOrNull((e) => e is css.LiteralTerm && (e.text == "outside" || e.text == "inside")) as css.LiteralTerm?;
199+
css.UriTerm? image = value.firstWhereOrNull((e) => e is css.UriTerm) as css.UriTerm?;
200+
css.LiteralTerm? type = value.firstWhereOrNull((e) => e is css.LiteralTerm && e.text != "outside" && e.text != "inside") as css.LiteralTerm?;
201+
if (position != null) {
202+
switch (position.text) {
203+
case 'outside':
204+
style.listStylePosition = ListStylePosition.OUTSIDE;
205+
break;
206+
case 'inside':
207+
style.listStylePosition = ListStylePosition.INSIDE;
208+
break;
209+
}
210+
}
211+
if (image != null) {
212+
style.listStyleType = ExpressionMapping.expressionToListStyleType(image) ?? style.listStyleType;
213+
} else if (type != null) {
214+
style.listStyleType = ExpressionMapping.expressionToListStyleType(type) ?? style.listStyleType;
215+
}
216+
break;
217+
case 'list-style-image':
218+
if (value.first is css.UriTerm) {
219+
style.listStyleType = ExpressionMapping.expressionToListStyleType(value.first as css.UriTerm) ?? style.listStyleType;
220+
}
221+
break;
222+
case 'list-style-position':
223+
if (value.first is css.LiteralTerm) {
224+
switch ((value.first as css.LiteralTerm).text) {
225+
case 'outside':
226+
style.listStylePosition = ListStylePosition.OUTSIDE;
227+
break;
228+
case 'inside':
229+
style.listStylePosition = ListStylePosition.INSIDE;
230+
break;
231+
}
232+
}
233+
break;
197234
case 'height':
198235
style.height = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.height;
199236
break;
@@ -301,6 +338,18 @@ Style declarationsToStyle(Map<String, List<css.Expression>> declarations) {
301338
case 'text-shadow':
302339
style.textShadow = ExpressionMapping.expressionToTextShadow(value);
303340
break;
341+
case 'text-transform':
342+
final val = (value.first as css.LiteralTerm).text;
343+
if (val == 'uppercase') {
344+
style.textTransform = TextTransform.uppercase;
345+
} else if (val == 'lowercase') {
346+
style.textTransform = TextTransform.lowercase;
347+
} else if (val == 'capitalize') {
348+
style.textTransform = TextTransform.capitalize;
349+
} else {
350+
style.textTransform = TextTransform.none;
351+
}
352+
break;
304353
case 'width':
305354
style.width = ExpressionMapping.expressionToPaddingLength(value.first) ?? style.width;
306355
break;
@@ -671,6 +720,9 @@ class ExpressionMapping {
671720
}
672721

673722
static ListStyleType? expressionToListStyleType(css.LiteralTerm value) {
723+
if (value is css.UriTerm) {
724+
return ListStyleType.fromImage(value.text);
725+
}
674726
switch (value.text) {
675727
case 'disc':
676728
return ListStyleType.DISC;

lib/src/styled_element.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ StyledElement parseStyledElement(
188188
ExpressionMapping.namedColorToColor(element.attributes['color']!) :
189189
null,
190190
fontFamily: element.attributes['face']?.split(",").first,
191-
fontSize: numberToFontSize(element.attributes['size'] ?? ''),
191+
fontSize: element.attributes['size'] != null ? numberToFontSize(element.attributes['size']!) : null,
192192
);
193193
break;
194194
case "h1":

lib/src/utils.dart

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import 'dart:convert';
22
import 'dart:math';
33

4-
import 'package:flutter/gestures.dart';
54
import 'package:flutter/material.dart';
5+
import 'package:flutter_html/style.dart';
66

77
Map<String, String> namedColors = {
88
"White": "#FFFFFF",
@@ -81,4 +81,34 @@ String getRandString(int len) {
8181
var random = Random.secure();
8282
var values = List<int>.generate(len, (i) => random.nextInt(255));
8383
return base64UrlEncode(values);
84+
}
85+
86+
extension TextTransformUtil on String? {
87+
String? transformed(TextTransform? transform) {
88+
if (this == null) return null;
89+
if (transform == TextTransform.uppercase) {
90+
return this!.toUpperCase();
91+
} else if (transform == TextTransform.lowercase) {
92+
return this!.toLowerCase();
93+
} else if (transform == TextTransform.capitalize) {
94+
final stringBuffer = StringBuffer();
95+
96+
var capitalizeNext = true;
97+
for (final letter in this!.toLowerCase().codeUnits) {
98+
// UTF-16: A-Z => 65-90, a-z => 97-122.
99+
if (capitalizeNext && letter >= 97 && letter <= 122) {
100+
stringBuffer.writeCharCode(letter - 32);
101+
capitalizeNext = false;
102+
} else {
103+
// UTF-16: 32 == space, 46 == period
104+
if (letter == 32 || letter == 46) capitalizeNext = true;
105+
stringBuffer.writeCharCode(letter);
106+
}
107+
}
108+
109+
return stringBuffer.toString();
110+
} else {
111+
return this;
112+
}
113+
}
84114
}

0 commit comments

Comments
 (0)