Skip to content

Commit 18c4b10

Browse files
authored
Feature: support standalone css border properties (#1570)
1 parent 6e035e3 commit 18c4b10

3 files changed

Lines changed: 213 additions & 9 deletions

File tree

packages/core/lib/src/data/css.dart

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,11 @@ class CssBorderSide {
194194
bool get isNoOp => style == null || width?.isPositive != true;
195195

196196
BorderSide? _getValue(InheritedProperties resolved) {
197-
if (identical(this, none)) {
197+
if (isNoOp) {
198198
return null;
199199
}
200200

201-
final scopedColor = color?.getValue(resolved);
201+
final scopedColor = (color ?? CssColor.current()).getValue(resolved);
202202
if (scopedColor == null) {
203203
return null;
204204
}
@@ -230,10 +230,6 @@ class CssBorderSide {
230230
);
231231
}
232232

233-
if (copied?.isNoOp == true) {
234-
return none;
235-
}
236-
237233
return copied;
238234
}
239235
}

packages/core/lib/src/internal/parser/border.dart

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
part of '../core_parser.dart';
22

33
const kCssBorder = 'border';
4+
const kCssBorderColor = 'border-color';
45
const kCssBorderInherit = 'inherit';
56
const kCssBorderNone = 'none';
7+
const kCssBorderStyle = 'border-style';
8+
const kCssBorderWidth = 'border-width';
69

710
const kCssBorderRadius = 'border-radius';
811
const kCssBorderRadiusSuffix = 'radius';
@@ -24,7 +27,11 @@ CssBorder tryParseBorder(BuildTree tree) {
2427
continue;
2528
}
2629

27-
if (key.endsWith(kCssBorderRadiusSuffix)) {
30+
if (key == kCssBorderColor ||
31+
key == kCssBorderStyle ||
32+
key == kCssBorderWidth) {
33+
border = _tryParseBorderComponent(border, style);
34+
} else if (key.endsWith(kCssBorderRadiusSuffix)) {
2835
border = _tryParseBorderRadius(border, style);
2936
} else {
3037
border = _tryParseBorderSide(border, style);
@@ -36,6 +43,50 @@ CssBorder tryParseBorder(BuildTree tree) {
3643
return border;
3744
}
3845

46+
CssBorder _tryParseBorderComponent(CssBorder border, css.Declaration style) {
47+
final expressions = style.values;
48+
if (expressions.isEmpty) {
49+
return border;
50+
}
51+
52+
// CSS box-model shorthand order:
53+
// 1=all
54+
// 2=top/bottom+left/right
55+
// 3=top+left/right+bottom
56+
// 4=top+right+bottom+left
57+
final count = expressions.length;
58+
final topExpr = expressions[0];
59+
final rightExpr = expressions[count >= 2 ? 1 : 0];
60+
final bottomExpr = expressions[count >= 3 ? 2 : 0];
61+
final leftExpr = expressions[count >= 4 ? 3 : (count >= 2 ? 1 : 0)];
62+
63+
CssBorderSide? parseSide(css.Expression expr) {
64+
switch (style.property) {
65+
case kCssBorderStyle:
66+
final val = expr is css.LiteralTerm ? expr.valueAsString : null;
67+
if (val == kCssBorderNone) {
68+
return CssBorderSide.none;
69+
}
70+
final s = tryParseTextDecorationStyle(expr);
71+
return s != null ? CssBorderSide(style: s) : null;
72+
case kCssBorderWidth:
73+
final w = tryParseCssLength(expr);
74+
return w != null ? CssBorderSide(width: w) : null;
75+
case kCssBorderColor:
76+
final c = tryParseColor(expr);
77+
return c != null ? CssBorderSide(color: c) : null;
78+
}
79+
return null;
80+
}
81+
82+
return border.copyWith(
83+
top: parseSide(topExpr),
84+
right: parseSide(rightExpr),
85+
bottom: parseSide(bottomExpr),
86+
left: parseSide(leftExpr),
87+
);
88+
}
89+
3990
CssBorder _tryParseBorderSide(CssBorder border, css.Declaration style) {
4091
final suffix = style.property.substring(kCssBorder.length);
4192
if (suffix.isEmpty && style.term == kCssBorderInherit) {
@@ -44,8 +95,6 @@ CssBorder _tryParseBorderSide(CssBorder border, css.Declaration style) {
4495

4596
TextDecorationStyle? borderStyle;
4697
CssColor? color = CssColor.current();
47-
// TODO: look for official document regarding this default value
48-
// WebKit & Blink seem to follow the same (hidden?) specs
4998
var width = const CssLength(1);
5099
for (final expression in style.values) {
51100
final value =

packages/core/test/style_border_test.dart

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,165 @@ void main() {
12221222
});
12231223
});
12241224

1225+
group('border-style', () {
1226+
testWidgets('solid alone produces no border', (WidgetTester tester) async {
1227+
const html = '<span style="border-style: solid">Foo</span>';
1228+
final explained = await explain(tester, html);
1229+
expect(explained, equals('[RichText:(:Foo)]'));
1230+
});
1231+
1232+
testWidgets('solid + width renders', (WidgetTester tester) async {
1233+
const html =
1234+
'<span style="border-style: solid; border-width: 2px">Foo</span>';
1235+
final explained = await explain(tester, html);
1236+
expect(
1237+
explained,
1238+
equals('[Container:border=$_border2,child=[RichText:(:Foo)]]'),
1239+
);
1240+
});
1241+
1242+
testWidgets('width before style renders', (WidgetTester tester) async {
1243+
const html =
1244+
'<span style="border-width: 2px; border-style: solid">Foo</span>';
1245+
final explained = await explain(tester, html);
1246+
expect(
1247+
explained,
1248+
equals('[Container:border=$_border2,child=[RichText:(:Foo)]]'),
1249+
);
1250+
});
1251+
1252+
testWidgets('2-value style with width renders',
1253+
(WidgetTester tester) async {
1254+
// 2 values: top/bottom and left/right — both render as solid
1255+
const html =
1256+
'<span style="border-style: solid dotted; border-width: 2px">Foo</span>';
1257+
final explained = await explain(tester, html);
1258+
expect(
1259+
explained,
1260+
equals('[Container:border=$_border2,child=[RichText:(:Foo)]]'),
1261+
);
1262+
});
1263+
1264+
testWidgets('none clears existing border', (WidgetTester tester) async {
1265+
const html =
1266+
'<span style="border: 2px solid red; border-style: none">Foo</span>';
1267+
final explained = await explain(tester, html);
1268+
expect(explained, equals('[RichText:(:Foo)]'));
1269+
});
1270+
});
1271+
1272+
group('border-width', () {
1273+
testWidgets('width alone produces no border', (WidgetTester tester) async {
1274+
const html = '<span style="border-width: 2px">Foo</span>';
1275+
final explained = await explain(tester, html);
1276+
expect(explained, equals('[RichText:(:Foo)]'));
1277+
});
1278+
1279+
testWidgets('4-value widths with style', (WidgetTester tester) async {
1280+
const html =
1281+
'<span style="border-width: 1px 2px 3px 4px; border-style: solid">Foo</span>';
1282+
final explained = await explain(tester, html);
1283+
expect(
1284+
explained,
1285+
equals(
1286+
'[Container:'
1287+
'border=(1.0@solid#FF001234,2.0@solid#FF001234,3.0@solid#FF001234,4.0@solid#FF001234),'
1288+
'child=[RichText:(:Foo)]]',
1289+
),
1290+
);
1291+
});
1292+
1293+
testWidgets('2-value widths with style', (WidgetTester tester) async {
1294+
const html =
1295+
'<span style="border-width: 1px 2px; border-style: solid">Foo</span>';
1296+
final explained = await explain(tester, html);
1297+
expect(
1298+
explained,
1299+
equals(
1300+
'[Container:'
1301+
'border=(1.0@solid#FF001234,2.0@solid#FF001234,1.0@solid#FF001234,2.0@solid#FF001234),'
1302+
'child=[RichText:(:Foo)]]',
1303+
),
1304+
);
1305+
});
1306+
1307+
testWidgets('3-value widths with style', (WidgetTester tester) async {
1308+
const html =
1309+
'<span style="border-width: 1px 2px 3px; border-style: solid">Foo</span>';
1310+
final explained = await explain(tester, html);
1311+
expect(
1312+
explained,
1313+
equals(
1314+
'[Container:'
1315+
'border=(1.0@solid#FF001234,2.0@solid#FF001234,3.0@solid#FF001234,2.0@solid#FF001234),'
1316+
'child=[RichText:(:Foo)]]',
1317+
),
1318+
);
1319+
});
1320+
1321+
testWidgets('overrides shorthand width', (WidgetTester tester) async {
1322+
const html =
1323+
'<span style="border: 1px solid red; border-width: 2px">Foo</span>';
1324+
final explained = await explain(tester, html);
1325+
expect(
1326+
explained,
1327+
equals('[Container:border=2.0@solid#FFFF0000,child=[RichText:(:Foo)]]'),
1328+
);
1329+
});
1330+
});
1331+
1332+
group('border-color', () {
1333+
testWidgets('color alone produces no border', (WidgetTester tester) async {
1334+
const html = '<span style="border-color: red">Foo</span>';
1335+
final explained = await explain(tester, html);
1336+
expect(explained, equals('[RichText:(:Foo)]'));
1337+
});
1338+
1339+
testWidgets('color + style + width renders', (WidgetTester tester) async {
1340+
const html =
1341+
'<span style="border-color: red; border-style: solid; border-width: 2px">Foo</span>';
1342+
final explained = await explain(tester, html);
1343+
expect(
1344+
explained,
1345+
equals('[Container:border=2.0@solid#FFFF0000,child=[RichText:(:Foo)]]'),
1346+
);
1347+
});
1348+
1349+
testWidgets('overrides shorthand color', (WidgetTester tester) async {
1350+
const html =
1351+
'<span style="border: 1px solid red; border-color: blue">Foo</span>';
1352+
final explained = await explain(tester, html);
1353+
expect(
1354+
explained,
1355+
equals('[Container:border=1.0@solid#FF0000FF,child=[RichText:(:Foo)]]'),
1356+
);
1357+
});
1358+
});
1359+
1360+
group('standalone property cascade order', () {
1361+
testWidgets('shorthand after standalone overrides',
1362+
(WidgetTester tester) async {
1363+
const html =
1364+
'<span style="border-width: 2px; border: solid red">Foo</span>';
1365+
final explained = await explain(tester, html);
1366+
expect(
1367+
explained,
1368+
equals('[Container:border=1.0@solid#FFFF0000,child=[RichText:(:Foo)]]'),
1369+
);
1370+
});
1371+
1372+
testWidgets('standalone after shorthand updates single component',
1373+
(WidgetTester tester) async {
1374+
const html =
1375+
'<span style="border: 1px solid red; border-width: 2px">Foo</span>';
1376+
final explained = await explain(tester, html);
1377+
expect(
1378+
explained,
1379+
equals('[Container:border=2.0@solid#FFFF0000,child=[RichText:(:Foo)]]'),
1380+
);
1381+
});
1382+
});
1383+
12251384
group('combos', () {
12261385
testWidgets('renders with background & h2', (WidgetTester tester) async {
12271386
const html = '<div style="background: red; border: solid">'

0 commit comments

Comments
 (0)