Skip to content

Commit e11e846

Browse files
feat: allow abitray font weights (e.g. 350)
1 parent 3b69b19 commit e11e846

9 files changed

Lines changed: 319 additions & 170 deletions

File tree

integration_test/json_path_integration_test.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,16 @@ void main() {
7777
expect(typographyCode, contains('class Label'));
7878
expect(typographyCode, contains('final TextStyle display1;'));
7979
expect(typographyCode, contains('final TextStyle body1;'));
80+
expect(
81+
typographyCode,
82+
contains('fontWeight: const FontWeight(463),'),
83+
);
8084
},
8185
);
8286

8387
test(
84-
'does not collapse branches with different schemas into one nullable class',
88+
'does not collapse branches with different schemas '
89+
'into one nullable class',
8590
() async {
8691
final tempDir = await Directory.systemTemp.createTemp(
8792
'figmage_json_branching_',

lib/src/data/generators/theme_extension_generators/text_style_theme_extension_generator.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class TextStyleThemeExtensionGenerator
6161
return <String, Expression>{
6262
if (includeFamily) 'fontFamily': literalString(typography.fontFamily),
6363
'fontSize': literal(typography.fontSize),
64-
'fontWeight': refer('FontWeight').property('w${typography.fontWeight}'),
64+
'fontWeight': _getFontWeightExpression(typography.fontWeight),
6565
'fontStyle': refer('FontStyle').property(
6666
switch (typography.fontStyle) {
6767
FontStyle.italic => 'italic',
@@ -99,4 +99,11 @@ class TextStyleThemeExtensionGenerator
9999
//'overflow':
100100
};
101101
}
102+
103+
Expression _getFontWeightExpression(int fontWeight) {
104+
if (fontWeight >= 100 && fontWeight <= 900 && fontWeight % 100 == 0) {
105+
return refer('FontWeight').property('w$fontWeight');
106+
}
107+
return refer('FontWeight').constInstance([literal(fontWeight)]);
108+
}
102109
}

lib/src/data/repositories/figma_styles_repository.dart

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,12 @@ class FigmaStylesRepository implements StylesRepository {
120120
);
121121
final stylesFromNodes = [
122122
for (final node in styleNodes)
123-
if (_transformNode(node) case final style?) style,
123+
if (_transformNode(
124+
node,
125+
onDiagnostic: onProgress,
126+
)
127+
case final style?)
128+
style,
124129
];
125130
final droppedNodes = styleNodes.length - stylesFromNodes.length;
126131
onProgress?.call(
@@ -225,14 +230,24 @@ class FigmaStylesRepository implements StylesRepository {
225230
}
226231
}
227232

228-
DesignStyle<dynamic>? _transformNode(Node node) {
233+
DesignStyle<dynamic>? _transformNode(
234+
Node node, {
235+
void Function(String message)? onDiagnostic,
236+
}) {
229237
return switch (node) {
230238
TextNode(
231239
:final id,
232240
:final name,
233241
:final style,
234242
) =>
235-
TextDesignStyle(id: id, fullName: name, value: style.toDomain()),
243+
TextDesignStyle(
244+
id: id,
245+
fullName: name,
246+
value: style.toDomain(
247+
onDiagnostic: onDiagnostic,
248+
diagnosticContext: 'Figma style "$name" ($id)',
249+
),
250+
),
236251
RectangleNode(
237252
:final id,
238253
:final name,

lib/src/data/repositories/json_tokens_repository.dart

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22
import 'dart:io';
33

44
import 'package:figmage/src/data/util/converters/hex_color_conversion_x.dart';
5+
import 'package:figmage/src/data/util/converters/type_style_conversion_x.dart';
56
import 'package:figmage/src/domain/models/design_token.dart';
67
import 'package:figmage/src/domain/models/json_token.dart';
78
import 'package:figmage/src/domain/models/typography/typography.dart';
@@ -160,7 +161,8 @@ class FileJsonTokensRepository implements JsonTokensRepository {
160161
resolveTypography: (token) => _resolveValue<Typography>(
161162
token: token,
162163
expectedType: 'typography',
163-
parseLiteral: _parseTypography,
164+
parseLiteral: (value, token) =>
165+
_parseTypography(value, token, diagnostics),
164166
tokensByKey: tokensByKey,
165167
tokensByCanonicalPath: tokensByCanonicalPath,
166168
documentsByPath: documentsByPath,
@@ -414,7 +416,8 @@ void _collectTokens({
414416
({
415417
Map<String, Map<String, Map<String, ({String modeName})>>>
416418
modeGroupsByCollectionAndType,
417-
Map<String, Map<String, Set<String>>> branchPrefixedBranchesByCollectionAndType,
419+
Map<String, Map<String, Set<String>>>
420+
branchPrefixedBranchesByCollectionAndType,
418421
})
419422
_detectModeGroups(
420423
List<_RawJsonToken> tokens,
@@ -453,9 +456,7 @@ _detectModeGroups(
453456
continue;
454457
}
455458

456-
final signatures = branches.values
457-
.map(_signatureForNameSet)
458-
.toSet();
459+
final signatures = branches.values.map(_signatureForNameSet).toSet();
459460
final sortedBranches = branches.keys.toList()..sort();
460461
if (signatures.length == 1) {
461462
final modeEntries = <String, ({String modeName})>{};
@@ -489,10 +490,7 @@ String _signatureForNameSet(Set<String> names) {
489490

490491
List<DesignToken<dynamic>> _buildTokens({
491492
required List<_RawJsonToken> tokens,
492-
required Map<
493-
String,
494-
Map<String, Map<String, ({String modeName})>>
495-
>
493+
required Map<String, Map<String, Map<String, ({String modeName})>>>
496494
modeGroupsByCollectionAndType,
497495
required Map<String, Map<String, Set<String>>>
498496
branchPrefixedBranchesByCollectionAndType,
@@ -521,7 +519,8 @@ List<DesignToken<dynamic>> _buildTokens({
521519
pathSegments,
522520
modeGroupsByCollectionAndType[token.modeGroupScope]?[token.type] ??
523521
const <String, ({String modeName})>{},
524-
branchPrefixedBranchesByCollectionAndType[token.modeGroupScope]?[token.type] ??
522+
branchPrefixedBranchesByCollectionAndType[token.modeGroupScope]?[token
523+
.type] ??
525524
const <String>{},
526525
);
527526

@@ -707,7 +706,9 @@ AliasOr<T> _resolveValue<T>({
707706
}
708707
} else {
709708
try {
710-
result = AliasOr<T>.data(data: parseLiteral(token.value, token));
709+
result = AliasOr<T>.data(
710+
data: parseLiteral(token.value, token),
711+
);
711712
} on FormatException {
712713
visitStack.remove(tokenKey);
713714
rethrow;
@@ -1026,13 +1027,14 @@ bool _isTokenObject(Object? node) {
10261027
}
10271028

10281029
List<
1029-
({
1030-
String collectionName,
1031-
String mode,
1032-
String name,
1033-
String fullName,
1034-
bool preserveCollectionScope,
1035-
})>
1030+
({
1031+
String collectionName,
1032+
String mode,
1033+
String name,
1034+
String fullName,
1035+
bool preserveCollectionScope,
1036+
})
1037+
>
10361038
_mapPathsForToken(
10371039
List<String> pathSegments,
10381040
Map<String, ({String modeName})> modeEntries,
@@ -1119,26 +1121,22 @@ _mapPathsForToken(
11191121
];
11201122
}
11211123

1122-
(
1123-
{
1124+
({
1125+
String collectionName,
1126+
String collectionId,
1127+
String mode,
1128+
String name,
1129+
String fullName,
1130+
})
1131+
_mapToSetScopedCollection({
1132+
required _RawJsonToken token,
1133+
required ({
11241134
String collectionName,
1125-
String collectionId,
11261135
String mode,
11271136
String name,
11281137
String fullName,
1129-
}
1130-
)
1131-
_mapToSetScopedCollection({
1132-
required _RawJsonToken token,
1133-
required (
1134-
{
1135-
String collectionName,
1136-
String mode,
1137-
String name,
1138-
String fullName,
1139-
bool preserveCollectionScope,
1140-
}
1141-
)
1138+
bool preserveCollectionScope,
1139+
})
11421140
mapped,
11431141
}) {
11441142
final useScopedCollection = mapped.preserveCollectionScope;
@@ -1161,15 +1159,13 @@ _mapToSetScopedCollection({
11611159

11621160
void _addToken<T>(
11631161
Map<String, List<_TokenAccumulator<T>>> target,
1164-
(
1165-
{
1166-
String collectionName,
1167-
String collectionId,
1168-
String mode,
1169-
String name,
1170-
String fullName,
1171-
}
1172-
)
1162+
({
1163+
String collectionName,
1164+
String collectionId,
1165+
String mode,
1166+
String name,
1167+
String fullName,
1168+
})
11731169
mapped,
11741170
AliasOr<T> value,
11751171
) {
@@ -1296,7 +1292,11 @@ String _parseString(Object? value, _RawJsonToken token) {
12961292
);
12971293
}
12981294

1299-
Typography _parseTypography(Object? value, _RawJsonToken token) {
1295+
Typography _parseTypography(
1296+
Object? value,
1297+
_RawJsonToken token,
1298+
List<String> diagnostics,
1299+
) {
13001300
if (value is! Map) {
13011301
throw FormatException(
13021302
'Typography tokens require an object at '
@@ -1330,14 +1330,21 @@ Typography _parseTypography(Object? value, _RawJsonToken token) {
13301330
);
13311331

13321332
final fontWeightValue = map['fontWeight'];
1333-
final fontWeight = fontWeightValue == null
1333+
final rawFontWeight = fontWeightValue == null
13341334
? 400
13351335
: _parseDouble(
13361336
fontWeightValue,
13371337
token,
13381338
fieldName: 'fontWeight',
13391339
allowValueWrapper: false,
1340-
).round();
1340+
);
1341+
final fontWeight = TypeStyleConversionX.convertFontWeight(
1342+
rawFontWeight,
1343+
onDiagnostic: diagnostics.add,
1344+
diagnosticContext:
1345+
'${_formatPath(token.pathSegments)} in '
1346+
'${token.filePath}',
1347+
);
13411348

13421349
final decoration = _parseDecoration(
13431350
map['textDecoration'] ?? map['decoration'],

lib/src/data/util/converters/type_style_conversion_x.dart

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,43 @@ import 'package:meta/meta.dart';
55
/// An extension providing the method to for converting a [TypeStyle] to a
66
/// [Typography].
77
extension TypeStyleConversionX on TypeStyle {
8+
static const int _minFontWeight = 1;
9+
static const int _maxFontWeight = 1000;
10+
811
/// Converts a [TypeStyle] to a [Typography].
9-
Typography toDomain() => Typography(
10-
fontFamily: fontFamily!,
11-
fontFamilyPostScriptName: fontPostScriptName,
12-
fontSize: fontSize!.toDouble(),
13-
fontWeight: convertFontWeight(fontWeight ?? 400),
14-
fontStyle: italic ? FontStyle.italic : FontStyle.normal,
15-
letterSpacing: letterSpacing?.toDouble() ?? 0,
16-
height: convertLineHeight(lineHeightPx, fontSize),
17-
);
12+
Typography toDomain({
13+
void Function(String message)? onDiagnostic,
14+
String? diagnosticContext,
15+
}) => Typography(
16+
fontFamily: fontFamily!,
17+
fontFamilyPostScriptName: fontPostScriptName,
18+
fontSize: fontSize!.toDouble(),
19+
fontWeight: convertFontWeight(
20+
fontWeight ?? 400,
21+
onDiagnostic: onDiagnostic,
22+
diagnosticContext: diagnosticContext,
23+
),
24+
fontStyle: italic ? FontStyle.italic : FontStyle.normal,
25+
letterSpacing: letterSpacing?.toDouble() ?? 0,
26+
height: convertLineHeight(lineHeightPx, fontSize),
27+
);
1828

19-
/// Makes sure that the font weight is between 100 and 900 in increments of
20-
/// 100.
21-
@visibleForTesting
22-
static int convertFontWeight(num weight) {
23-
final incremented = (weight / 100).round() * 100;
24-
return switch (incremented) {
25-
> 900 => 900,
26-
< 100 => 100,
27-
_ => incremented,
28-
};
29+
/// Makes sure that the font weight is between 1 and 1000.
30+
static int convertFontWeight(
31+
num weight, {
32+
void Function(String message)? onDiagnostic,
33+
String? diagnosticContext,
34+
}) {
35+
final rounded = weight.round();
36+
final clamped = rounded.clamp(_minFontWeight, _maxFontWeight);
37+
if (clamped != rounded) {
38+
final msg = 'Clamped fontWeight from $weight to $clamped. '
39+
'Valid range is $_minFontWeight..$_maxFontWeight';
40+
onDiagnostic?.call(
41+
diagnosticContext != null ? '$msg at $diagnosticContext.' : '$msg.',
42+
);
43+
}
44+
return clamped;
2945
}
3046

3147
/// Converts the line height to the way Flutter expects it, relative to the

test/fixtures/json/sample_system/Body.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"$type": "typography",
44
"$value": {
55
"fontFamily": "Synthetic Sans",
6-
"fontWeight": 400,
6+
"fontWeight": 463,
77
"lineHeight": 1.33,
88
"fontSize": 40,
99
"letterSpacing": 0,

0 commit comments

Comments
 (0)