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

fix: Don't rely on removeDiacritics to highlight text #4636

Merged
merged 9 commits into from
Dec 16, 2023
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
Expand Down Expand Up @@ -146,19 +145,19 @@ class LanguageSelector extends StatelessWidget {
prefixIcon: const Icon(Icons.search),
controller: languageSelectorController,
onChanged: (String? query) {
query = removeDiacritics(query!.trim().toLowerCase());
query = query!.trim().getComparisonSafeString();

setState(
() {
filteredList = leftovers
.where((OpenFoodFactsLanguage item) =>
removeDiacritics(_languages
.getNameInEnglish(item)
.toLowerCase())
_languages
.getNameInEnglish(item)
.getComparisonSafeString()
.contains(query!.toLowerCase()) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.contains(query!.toLowerCase()) ||
.contains(query!) ||

removeDiacritics(_languages
.getNameInLanguage(item)
.toLowerCase())
_languages
.getNameInLanguage(item)
.getComparisonSafeString()
.contains(query.toLowerCase()) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.contains(query.toLowerCase()) ||
.contains(query!) ||

item.code.contains(query))
.toList();
Expand Down
19 changes: 9 additions & 10 deletions packages/smooth_app/lib/pages/onboarding/country_selector.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
Expand Down Expand Up @@ -104,23 +103,23 @@ class _CountrySelectorState extends State<CountrySelector> {
prefixIcon: const Icon(Icons.search),
controller: _countryController,
onChanged: (String? query) {
query = removeDiacritics(query!.trim().toLowerCase());
query = query!.trim()..getComparisonSafeString();

setState(
() {
filteredList = _countryList
.where(
(Country item) =>
removeDiacritics(
item.name.toLowerCase())
item.name
.getComparisonSafeString()
.contains(
query!,
) ||
removeDiacritics(
item.countryCode.toLowerCase())
query!,
) ||
item.countryCode
.getComparisonSafeString()
.contains(
query,
),
query,
),
)
.toList(growable: false);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:smooth_app/data_models/preferences/user_preferences.dart';
import 'package:smooth_app/generic_lib/design_constants.dart';
import 'package:smooth_app/pages/preferences/abstract_user_preferences.dart';
import 'package:smooth_app/pages/preferences/user_preferences_item.dart';
import 'package:smooth_app/pages/preferences/user_preferences_page.dart';
import 'package:smooth_app/widgets/smooth_text.dart';

/// Search page for preferences, with TextField filter.
class UserPreferencesSearchPage extends StatefulWidget {
Expand Down Expand Up @@ -70,7 +70,7 @@ class _UserPreferencesSearchPageState extends State<UserPreferencesSearchPage> {
final String searchString,
final UserPreferences userPreferences,
) {
final String needle = removeDiacritics(searchString.toLowerCase());
final String needle = searchString.getComparisonSafeString();
final List<Widget> result = <Widget>[];
final List<PreferencePageType> types =
PreferencePageType.getPreferencePageTypes(userPreferences);
Expand Down Expand Up @@ -101,7 +101,7 @@ class _UserPreferencesSearchPageState extends State<UserPreferencesSearchPage> {

bool _findLabels(final String needle, final Iterable<String> labels) {
for (final String label in labels) {
if (removeDiacritics(label.toLowerCase()).contains(needle)) {
if (label.getComparisonSafeString().contains(needle)) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:openfoodfacts/openfoodfacts.dart';
Expand Down Expand Up @@ -55,9 +54,14 @@ class NutritionAddNutrientButton extends StatelessWidget {
controller: nutritionTextController,
onChanged: (String? query) => setState(
() => filteredList = leftovers
.where((OrderedNutrient item) =>
removeDiacritics(item.name!).toLowerCase().contains(
removeDiacritics(query!).toLowerCase().trim()))
.where(
(OrderedNutrient item) => item.name!
.trim()
.getComparisonSafeString()
.contains(
query!.trim().getComparisonSafeString(),
),
)
.toList(),
),
),
Expand Down
94 changes: 80 additions & 14 deletions packages/smooth_app/lib/widgets/smooth_text.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import 'package:diacritic/diacritic.dart';
import 'package:diacritic/diacritic.dart' as lib show removeDiacritics;
import 'package:flutter/material.dart';
import 'package:smooth_app/services/smooth_services.dart';

/// An extension on [String]
extension StringExtension on String {
/// Please use this method instead of directly calling the library.
/// It will ease the migration if we decide to remove/change it.
String removeDiacritics() {
return lib.removeDiacritics(this);
}

/// Same as [removeDiacritics] but also lowercases the string.
/// Prefer this method when you want to compare two strings.
String getComparisonSafeString() {
return toLowerCase().removeDiacritics();
}
}

/// An extension on [TextStyle] that allows to have "well spaced" variant
extension TextStyleExtension on TextStyle {
Expand Down Expand Up @@ -60,13 +76,23 @@ class TextHighlighter extends StatelessWidget {

@override
Widget build(BuildContext context) {
final List<(String, TextStyle?)> parts = _getParts(
defaultStyle: TextStyle(fontWeight: selected ? FontWeight.bold : null),
highlightedStyle: TextStyle(
fontWeight: selected ? FontWeight.bold : null,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2),
),
);
List<(String, TextStyle?)> parts;
try {
parts = _getParts(
defaultStyle: TextStyle(fontWeight: selected ? FontWeight.bold : null),
highlightedStyle: TextStyle(
fontWeight: selected ? FontWeight.bold : null,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.2),
),
);
} catch (e, trace) {
parts = <(String, TextStyle?)>[(text, null)];
Logs.e(
'Unable to parse text "$text" with filter "$filter".',
ex: e,
stacktrace: trace,
);
}

final TextStyle defaultTextStyle = DefaultTextStyle.of(context).style;

Expand All @@ -91,9 +117,12 @@ class TextHighlighter extends StatelessWidget {
required TextStyle? defaultStyle,
required TextStyle? highlightedStyle,
}) {
final String filterWithoutDiacritics = filter.getComparisonSafeString();
final String textWithoutDiacritics = text.getComparisonSafeString();

final Iterable<RegExpMatch> highlightedParts =
RegExp(removeDiacritics(filter).toLowerCase().trim()).allMatches(
removeDiacritics(text).toLowerCase(),
RegExp(filterWithoutDiacritics.trim()).allMatches(
textWithoutDiacritics,
);

final List<(String, TextStyle?)> parts = <(String, TextStyle?)>[];
Expand All @@ -103,24 +132,61 @@ class TextHighlighter extends StatelessWidget {
} else {
parts
.add((text.substring(0, highlightedParts.first.start), defaultStyle));
int diff = 0;

for (int i = 0; i != highlightedParts.length; i++) {
final RegExpMatch subPart = highlightedParts.elementAt(i);
final int startPosition = subPart.start - diff;
final int endPosition = _computeEndPosition(
startPosition,
subPart.end - diff,
subPart,
textWithoutDiacritics,
filterWithoutDiacritics,
);
diff = subPart.end - endPosition;

parts.add(
(text.substring(subPart.start, subPart.end), highlightedStyle),
(text.substring(startPosition, endPosition), highlightedStyle),
);

if (i < highlightedParts.length - 1) {
parts.add((
text.substring(
subPart.end, highlightedParts.elementAt(i + 1).start),
endPosition, highlightedParts.elementAt(i + 1).start - diff),
defaultStyle
));
} else if (subPart.end < text.length) {
parts.add((text.substring(subPart.end, text.length), defaultStyle));
} else if (endPosition < text.length) {
parts.add((text.substring(endPosition, text.length), defaultStyle));
}
}
}
return parts;
}

int _computeEndPosition(
int startPosition,
int endPosition,
RegExpMatch subPart,
String textWithoutDiacritics,
String filterWithoutDiacritics,
) {
final String subText = text.substring(startPosition);
if (subText.startsWith(filterWithoutDiacritics)) {
return endPosition;
}

int diff = 0;
for (int pos = 0; pos < endPosition; pos++) {
if (pos == subText.length - 1) {
diff = pos - subText.length;
break;
}

final int charLength = subText[pos].removeDiacritics().length;
diff -= charLength > 1 ? charLength - 1 : 0;
}

return endPosition + diff;
}
}
21 changes: 21 additions & 0 deletions packages/smooth_app/test/utils/smooth_text_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:smooth_app/widgets/smooth_text.dart' show StringExtension;

void main() {
group('Smooth text', () {
group('String extension', () {
test('Remove diacritics (oeuf)', () {
expect(
'œuf'.removeDiacritics(),
equals('oeuf'),
);
});
test('Comparison Safe String', () {
expect(
'œuF'.getComparisonSafeString(),
equals('oeuf'),
);
});
});
});
}