Skip to content

Commit

Permalink
Merge pull request nubank#7 from williamhjcho/escape
Browse files Browse the repository at this point in the history
feat: adds escaping variables by default
  • Loading branch information
williamhjcho authored Aug 17, 2022
2 parents 8def5a1 + 7812349 commit 17604a0
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 4 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# [Next version]

* Adds variable (XML) escaping by default
* To override this behavior, set `I18NextOptions.escape`
* Or to disable it, set `I18NextOptions.escapeValue = false`

# [0.6.0-dev+1]

* Fix `missingInterpolationHandler` to also be called for interpolations that do not result into String (and are not null)
Expand Down
24 changes: 23 additions & 1 deletion lib/interpolator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ String interpolate(
final pattern = interpolationPattern(options);
final formatSeparator = options.formatSeparator ?? ',';
final keySeparator = options.keySeparator ?? '.';
final escapeValue = options.escapeValue ?? true;

return string.splitMapJoin(pattern, onMatch: (match) {
var variable = match[1]!.trim();
Expand All @@ -74,9 +75,13 @@ String interpolate(

final path = variable.split(keySeparator);
final value = evaluate(path, variables);
return formatter.format(value, formats, locale, options) ??
var result = formatter.format(value, formats, locale, options) ??
(throw InterpolationException(
'Could not evaluate or format variable', match));
if (escapeValue) {
result = (options.escape ?? escape).call(result);
}
return result;
});
}

Expand Down Expand Up @@ -138,3 +143,20 @@ RegExp nestingPattern(I18NextOptions options) {
'$suffix',
);
}

String escape(String input) {
const _entityMap = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
'/': '&#x2F;',
};

final pattern = RegExp('[&<>"\'\\/]');
return input.replaceAllMapped(pattern, (match) {
final char = match[0]!;
return _entityMap[char] ?? char;
});
}
33 changes: 31 additions & 2 deletions lib/src/options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ typedef TranslationFailedHandler = String Function(
Object error,
);

typedef EscapeHandler = String Function(String input);

/// Contains all options for [I18Next] to work properly.
class I18NextOptions with Diagnosticable {
const I18NextOptions({
Expand All @@ -56,6 +58,8 @@ class I18NextOptions with Diagnosticable {
this.missingKeyHandler,
this.missingInterpolationHandler,
this.translationFailedHandler,
this.escape,
this.escapeValue,
}) : super();

static const I18NextOptions base = I18NextOptions(
Expand All @@ -81,6 +85,8 @@ class I18NextOptions with Diagnosticable {
missingKeyHandler: null,
missingInterpolationHandler: null,
translationFailedHandler: null,
escape: null,
escapeValue: true,
);

/// The namespaces used to fallback to when no key matches were found on the
Expand Down Expand Up @@ -200,6 +206,17 @@ class I18NextOptions with Diagnosticable {
/// If the key was missing, then it will call [missingKeyHandler] instead.
final TranslationFailedHandler? translationFailedHandler;

/// The escape handler that is called after interpolating and formatting a
/// variable only if [escapeValue] is enabled.
///
/// By default will escape XML tags.
final EscapeHandler? escape;

/// Whether to call [escape] after interpolating and formatting a variable.
///
/// Default is true.
final bool? escapeValue;

/// Creates a new instance of [I18NextOptions] overriding any properties
/// where [other] isn't null.
///
Expand Down Expand Up @@ -232,6 +249,8 @@ class I18NextOptions with Diagnosticable {
other.missingInterpolationHandler ?? missingInterpolationHandler,
translationFailedHandler:
other.translationFailedHandler ?? translationFailedHandler,
escape: other.escape ?? escape,
escapeValue: other.escapeValue ?? escapeValue,
);
}

Expand All @@ -257,6 +276,8 @@ class I18NextOptions with Diagnosticable {
MissingKeyHandler? missingKeyHandler,
ValueFormatter? missingInterpolationHandler,
TranslationFailedHandler? translationFailedHandler,
EscapeHandler? escape,
bool? escapeValue,
}) {
return I18NextOptions(
fallbackNamespaces: fallbackNamespaces ?? this.fallbackNamespaces,
Expand All @@ -280,6 +301,8 @@ class I18NextOptions with Diagnosticable {
missingInterpolationHandler ?? this.missingInterpolationHandler,
translationFailedHandler:
translationFailedHandler ?? this.translationFailedHandler,
escape: escape ?? this.escape,
escapeValue: escapeValue ?? this.escapeValue,
);
}

Expand All @@ -303,6 +326,8 @@ class I18NextOptions with Diagnosticable {
missingKeyHandler,
missingInterpolationHandler,
translationFailedHandler,
escape,
escapeValue,
);

@override
Expand All @@ -328,7 +353,9 @@ class I18NextOptions with Diagnosticable {
other.pluralSuffix == pluralSuffix &&
other.missingKeyHandler == missingKeyHandler &&
other.missingInterpolationHandler == missingInterpolationHandler &&
other.translationFailedHandler == translationFailedHandler;
other.translationFailedHandler == translationFailedHandler &&
other.escape == escape &&
other.escapeValue == escapeValue;
}

@override
Expand All @@ -355,6 +382,8 @@ class I18NextOptions with Diagnosticable {
..add(StringProperty('missingInterpolationHandler',
missingInterpolationHandler?.toString()))
..add(StringProperty(
'translationFailedHandler', translationFailedHandler?.toString()));
'translationFailedHandler', translationFailedHandler?.toString()))
..add(StringProperty('escape', escape?.toString()))
..add(StringProperty('escapeValue', escapeValue?.toString()));
}
}
26 changes: 26 additions & 0 deletions test/i18next_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,32 @@ void main() {
);
});

test('escape', () {
mockKey('key', 'interpolated {{myVar}}');
mockKey('keyTagged', '<tag attr="value">and then {{myVar}}</tag>');

final vars = {'myVar': '<img />'};
expect(
i18next.t('$namespace:key', variables: vars),
'interpolated &lt;img &#x2F;&gt;',
);
expect(
i18next.t('$namespace:keyTagged', variables: vars),
'<tag attr="value">and then &lt;img &#x2F;&gt;</tag>',
);

// don't escape
const opts = I18NextOptions(escapeValue: false);
expect(
i18next.t('$namespace:key', variables: vars, options: opts),
'interpolated <img />',
);
expect(
i18next.t('$namespace:keyTagged', variables: vars, options: opts),
'<tag attr="value">and then <img /></tag>',
);
});

group('.of', () {
BuildContext? capturedContext;

Expand Down
33 changes: 32 additions & 1 deletion test/interpolator_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:ui';

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_test/flutter_test.dart' hide escape;
import 'package:i18next/i18next.dart';
import 'package:i18next/interpolator.dart';

Expand All @@ -15,10 +15,12 @@ void main() {
Locale locale = defaultLocale,
Map<String, ValueFormatter>? formats,
TranslationFailedHandler? translationFailedHandler,
EscapeHandler? escape,
}) {
final options = baseOptions.copyWith(
formats: formats,
translationFailedHandler: translationFailedHandler,
escape: escape,
);
return interpolate(locale, string, variables, options);
}
Expand Down Expand Up @@ -196,6 +198,20 @@ void main() {
'This is a my variable string',
);
});

test('given escape', () {
expect(
interpol(
'This is a {{variable}} string',
variables: {'variable': '<tag>my variable</tag>'},
escape: expectAsync1((input) {
expect(input, '<tag>my variable</tag>');
return 'ESCAPED VAR';
}),
),
'This is a ESCAPED VAR string',
);
});
});
});

Expand Down Expand Up @@ -508,6 +524,21 @@ void main() {
]);
});
});

test('escape', () {
expect(escape(''), '');
expect(escape('&'), '&amp;');
expect(escape('<'), '&lt;');
expect(escape('>'), '&gt;');
expect(escape('"'), '&quot;');
expect(escape('\''), '&#39;');
expect(escape('/'), '&#x2F;');

expect(
escape('<tag attr="value">Some text</tag>'),
'&lt;tag attr=&quot;value&quot;&gt;Some text&lt;&#x2F;tag&gt;',
);
});
}

String? _defaultTranslate(
Expand Down
1 change: 1 addition & 0 deletions test/options_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ void main() {
nestingSuffix: 'Some nestingSuffix',
nestingSeparator: 'Some nestingSeparator',
pluralSuffix: 'Some pluralSuffix',
escapeValue: true,
);

test('given no values', () {
Expand Down

0 comments on commit 17604a0

Please sign in to comment.