diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart index c80753b217..b8970d8225 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart @@ -17,9 +17,9 @@ extension NodeAttributesExtensions on Attributes { return containsKey(BuiltInAttributeKey.quote); } - int? get number { + num? get number { if (containsKey(BuiltInAttributeKey.number) && - this[BuiltInAttributeKey.number] is int) { + this[BuiltInAttributeKey.number] is num) { return this[BuiltInAttributeKey.number]; } return null; @@ -27,7 +27,7 @@ extension NodeAttributesExtensions on Attributes { bool get code { if (containsKey(BuiltInAttributeKey.code) && - this[BuiltInAttributeKey.code] == true) { + this[BuiltInAttributeKey.code] is bool) { return this[BuiltInAttributeKey.code]; } return false; @@ -63,11 +63,14 @@ extension DeltaAttributesExtensions on Attributes { this[BuiltInAttributeKey.strikethrough] == true); } + static const whiteInt = 0XFFFFFFFF; + Color? get color { if (containsKey(BuiltInAttributeKey.color) && this[BuiltInAttributeKey.color] is String) { return Color( - int.parse(this[BuiltInAttributeKey.color]), + // If the parse fails returns white by default + int.tryParse(this[BuiltInAttributeKey.color]) ?? whiteInt, ); } return null; @@ -77,8 +80,7 @@ extension DeltaAttributesExtensions on Attributes { if (containsKey(BuiltInAttributeKey.backgroundColor) && this[BuiltInAttributeKey.backgroundColor] is String) { return Color( - int.parse(this[BuiltInAttributeKey.backgroundColor]), - ); + int.tryParse(this[BuiltInAttributeKey.backgroundColor]) ?? whiteInt); } return null; } diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart index d4d7857286..bcc8722dfa 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart @@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/document/text_delta.dart'; import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart'; extension TextNodeExtension on TextNode { - dynamic getAttributeInSelection(Selection selection, String styleKey) { + T? getAttributeInSelection(Selection selection, String styleKey) { final ops = delta.whereType(); final startOffset = selection.isBackward ? selection.start.offset : selection.end.offset; @@ -19,8 +19,9 @@ extension TextNodeExtension on TextNode { } final length = op.length; if (start < endOffset && start + length > startOffset) { - if (op.attributes?.containsKey(styleKey) == true) { - return op.attributes![styleKey]; + final attributes = op.attributes; + if (attributes != null && attributes[styleKey] is T?) { + return attributes[styleKey]; } } start += length; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart index 02e3d46041..b25b964b13 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart @@ -26,6 +26,7 @@ import 'messages_hu-HU.dart' as messages_hu_hu; import 'messages_id-ID.dart' as messages_id_id; import 'messages_it-IT.dart' as messages_it_it; import 'messages_ja-JP.dart' as messages_ja_jp; +import 'messages_ml_IN.dart' as messages_ml_in; import 'messages_nl-NL.dart' as messages_nl_nl; import 'messages_pl-PL.dart' as messages_pl_pl; import 'messages_pt-BR.dart' as messages_pt_br; @@ -48,6 +49,7 @@ Map _deferredLibraries = { 'id_ID': () => new Future.value(null), 'it_IT': () => new Future.value(null), 'ja_JP': () => new Future.value(null), + 'ml_IN': () => new Future.value(null), 'nl_NL': () => new Future.value(null), 'pl_PL': () => new Future.value(null), 'pt_BR': () => new Future.value(null), @@ -82,6 +84,8 @@ MessageLookupByLibrary? _findExact(String localeName) { return messages_it_it.messages; case 'ja_JP': return messages_ja_jp.messages; + case 'ml_IN': + return messages_ml_in.messages; case 'nl_NL': return messages_nl_nl.messages; case 'pl_PL': diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ml_IN.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ml_IN.dart new file mode 100644 index 0000000000..e7378a907e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ml_IN.dart @@ -0,0 +1,45 @@ +// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart +// This is a library that provides messages for a ml_IN locale. All the +// messages from the main program should be duplicated here with the same +// function name. + +// Ignore issues from commonly used lints in this file. +// ignore_for_file:unnecessary_brace_in_string_interps, unnecessary_new +// ignore_for_file:prefer_single_quotes,comment_references, directives_ordering +// ignore_for_file:annotate_overrides,prefer_generic_function_type_aliases +// ignore_for_file:unused_import, file_names, avoid_escaping_inner_quotes +// ignore_for_file:unnecessary_string_interpolations, unnecessary_string_escapes + +import 'package:intl/intl.dart'; +import 'package:intl/message_lookup_by_library.dart'; + +final messages = new MessageLookup(); + +typedef String MessageIfAbsent(String messageStr, List args); + +class MessageLookup extends MessageLookupByLibrary { + String get localeName => 'ml_IN'; + + final messages = _notInlinedMessages(_notInlinedMessages); + static Map _notInlinedMessages(_) => { + "bold": MessageLookupByLibrary.simpleMessage("ബോൾഡ്"), + "bulletedList": + MessageLookupByLibrary.simpleMessage("ബുള്ളറ്റഡ് പട്ടിക"), + "checkbox": MessageLookupByLibrary.simpleMessage("ചെക്ക്ബോക്സ്"), + "embedCode": MessageLookupByLibrary.simpleMessage("എംബെഡഡ് കോഡ്"), + "heading1": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 1"), + "heading2": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 2"), + "heading3": MessageLookupByLibrary.simpleMessage("തലക്കെട്ട് 3"), + "highlight": + MessageLookupByLibrary.simpleMessage("പ്രമുഖമാക്കിക്കാട്ടുക"), + "image": MessageLookupByLibrary.simpleMessage("ചിത്രം"), + "italic": MessageLookupByLibrary.simpleMessage("ഇറ്റാലിക്"), + "link": MessageLookupByLibrary.simpleMessage("ലിങ്ക്"), + "numberedList": + MessageLookupByLibrary.simpleMessage("അക്കമിട്ട പട്ടിക"), + "quote": MessageLookupByLibrary.simpleMessage("ഉദ്ധരണി"), + "strikethrough": MessageLookupByLibrary.simpleMessage("സ്ട്രൈക്ക്ത്രൂ"), + "text": MessageLookupByLibrary.simpleMessage("വചനം"), + "underline": MessageLookupByLibrary.simpleMessage("അടിവരയിടുക") + }; +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart index 9ef7ddf4c9..38590a144c 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart @@ -229,6 +229,7 @@ class AppLocalizationDelegate Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'), Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'), Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'), + Locale.fromSubtags(languageCode: 'ml', countryCode: 'IN'), Locale.fromSubtags(languageCode: 'nl', countryCode: 'NL'), Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'), Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'), diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart index 6407f81cb3..b3ed2e2471 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart @@ -333,8 +333,10 @@ void showLinkMenu( final textNode = node.first as TextNode; String? linkText; if (textNode.allSatisfyLinkInSelection(selection)) { - linkText = - textNode.getAttributeInSelection(selection, BuiltInAttributeKey.href); + linkText = textNode.getAttributeInSelection( + selection, + BuiltInAttributeKey.href, + ); } _linkMenuOverlay = OverlayEntry(builder: (context) { return Positioned( diff --git a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml index e6a4b6c71d..a778fa8e91 100644 --- a/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml +++ b/frontend/app_flowy/packages/appflowy_editor/pubspec.yaml @@ -33,6 +33,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^2.0.1 network_image_mock: ^2.1.1 + mockito: ^5.3.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/attributes_extension_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/attributes_extension_test.dart new file mode 100644 index 0000000000..427e53f32a --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/attributes_extension_test.dart @@ -0,0 +1,201 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('NodeAttributesExtensions::', () { + test('heading', () { + final Attributes attribute = { + 'subtype': 'heading', + 'heading': 'AppFlowy', + }; + expect(attribute.heading, 'AppFlowy'); + }); + + test('heading - text is not String return null', () { + final Attributes attribute = { + 'subtype': 'heading', + 'heading': 123, + }; + expect(attribute.heading, null); + }); + + test('heading - subtype is not "heading" return null', () { + final Attributes attribute = { + 'subtype': 'code', + 'heading': 'Hello World!', + }; + expect(attribute.heading, null); + }); + + test('quote', () { + final Attributes attribute = { + 'quote': 'quote text', + }; + expect(attribute.quote, true); + }); + + test('number - int', () { + final Attributes attribute = { + 'number': 99, + }; + expect(attribute.number, 99); + }); + + test('number - double', () { + final Attributes attribute = { + 'number': 12.34, + }; + expect(attribute.number, 12.34); + }); + + test('number - return null', () { + final Attributes attribute = { + 'code': 12.34, + }; + expect(attribute.number, null); + }); + + test('code', () { + final Attributes attribute = { + 'code': true, + }; + expect(attribute.code, true); + }); + + test('code - return false', () { + final Attributes attribute = { + 'quote': true, + }; + expect(attribute.code, false); + }); + + test('check', () { + final Attributes attribute = { + 'checkbox': true, + }; + expect(attribute.check, true); + }); + + test('check - return false', () { + final Attributes attribute = { + 'quote': true, + }; + expect(attribute.check, false); + }); + }); + + group('DeltaAttributesExtensions::', () { + test('bold', () { + final Attributes attribute = { + 'bold': true, + }; + expect(attribute.bold, true); + }); + + test('bold - return false', () { + final Attributes attribute = { + 'bold': 123, + }; + expect(attribute.bold, false); + }); + + test('italic', () { + final Attributes attribute = { + 'italic': true, + }; + expect(attribute.italic, true); + }); + + test('italic - return false', () { + final Attributes attribute = { + 'italic': 123, + }; + expect(attribute.italic, false); + }); + + test('underline', () { + final Attributes attribute = { + 'underline': true, + }; + expect(attribute.underline, true); + }); + + test('underline - return false', () { + final Attributes attribute = { + 'underline': 123, + }; + expect(attribute.underline, false); + }); + + test('strikethrough', () { + final Attributes attribute = { + 'strikethrough': true, + }; + expect(attribute.strikethrough, true); + }); + + test('strikethrough - return false', () { + final Attributes attribute = { + 'strikethrough': 123, + }; + expect(attribute.strikethrough, false); + }); + + test('color', () { + final Attributes attribute = { + 'color': '0xff212fff', + }; + expect(attribute.color, const Color(0XFF212FFF)); + }); + + test('color - return null', () { + final Attributes attribute = { + 'color': 123, + }; + expect(attribute.color, null); + }); + + test('color - parse failure return white', () { + final Attributes attribute = { + 'color': 'hello123', + }; + expect(attribute.color, const Color(0XFFFFFFFF)); + }); + + test('backgroundColor', () { + final Attributes attribute = { + 'backgroundColor': '0xff678fff', + }; + expect(attribute.backgroundColor, const Color(0XFF678FFF)); + }); + + test('backgroundColor - return null', () { + final Attributes attribute = { + 'backgroundColor': 123, + }; + expect(attribute.backgroundColor, null); + }); + + test('backgroundColor - parse failure return white', () { + final Attributes attribute = { + 'backgroundColor': 'hello123', + }; + expect(attribute.backgroundColor, const Color(0XFFFFFFFF)); + }); + + test('href', () { + final Attributes attribute = { + 'href': '/app/flowy', + }; + expect(attribute.href, '/app/flowy'); + }); + + test('href - return null', () { + final Attributes attribute = { + 'href': 123, + }; + expect(attribute.href, null); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/color_extension_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/color_extension_test.dart new file mode 100644 index 0000000000..929a5c0378 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/color_extension_test.dart @@ -0,0 +1,40 @@ +import 'package:appflowy_editor/src/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('ColorExtension::', () { + const white = Color(0XFFFFFFFF); + const black = Color(0XFF000000); + const blue = Color(0XFF000FFF); + const blueRgba = 'rgba(0, 15, 255, 255)'; + test('ToRgbaString', () { + expect(blue.toRgbaString(), 'rgba(0, 15, 255, 255)'); + expect(white.toRgbaString(), 'rgba(255, 255, 255, 255)'); + expect(black.toRgbaString(), 'rgba(0, 0, 0, 255)'); + }); + + test('tryFromRgbaString', () { + final color = ColorExtension.tryFromRgbaString(blueRgba); + expect(color, const Color.fromARGB(255, 0, 15, 255)); + }); + + test('tryFromRgbaString - wrong rgba format return null', () { + const wrongRgba = 'abc(1,2,3,4)'; + final color = ColorExtension.tryFromRgbaString(wrongRgba); + expect(color, null); + }); + + test('tryFromRgbaString - wrong length return null', () { + const wrongRgba = 'rgba(0, 15, 255)'; + final color = ColorExtension.tryFromRgbaString(wrongRgba); + expect(color, null); + }); + + test('tryFromRgbaString - wrong values return null', () { + const wrongRgba = 'rgba(-12, 999, 1234, 619)'; + final color = ColorExtension.tryFromRgbaString(wrongRgba); + expect(color, null); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/node_extension_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/node_extension_test.dart new file mode 100644 index 0000000000..0e31aa5a96 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/node_extension_test.dart @@ -0,0 +1,57 @@ +import 'dart:collection'; +import 'dart:ui'; + +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:appflowy_editor/src/extensions/node_extensions.dart'; + +class MockNode extends Mock implements Node {} + +void main() { + final mockNode = MockNode(); + + group('NodeExtensions::', () { + final selection = Selection( + start: Position(path: [0]), + end: Position(path: [1]), + ); + + test('rect - renderBox is null', () { + when(mockNode.renderBox).thenReturn(null); + final result = mockNode.rect; + expect(result, Rect.zero); + }); + + test('inSelection', () { + // I use an empty implementation instead of mock, because the mocked + // version throws error trying to access the path. + + final subLinkedList = LinkedList() + ..addAll([ + Node(type: 'type', children: LinkedList(), attributes: {}), + Node(type: 'type', children: LinkedList(), attributes: {}), + Node(type: 'type', children: LinkedList(), attributes: {}), + Node(type: 'type', children: LinkedList(), attributes: {}), + Node(type: 'type', children: LinkedList(), attributes: {}), + ]); + + final linkedList = LinkedList() + ..addAll([ + Node( + type: 'type', + children: subLinkedList, + attributes: {}, + ), + ]); + + final node = Node( + type: 'type', + children: linkedList, + attributes: {}, + ); + final result = node.inSelection(selection); + expect(result, false); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/object_extension_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/object_extension_test.dart new file mode 100644 index 0000000000..151df9cc31 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/object_extension_test.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/extensions/object_extensions.dart'; + +void main() { + group('FlowyObjectExtensions::', () { + test('unwrapOrNull', () { + final result = const TextSpan().unwrapOrNull(); + assert(result is TextSpan); + }); + + test('unwrapOrNull - return null', () { + final result = const TextSpan().unwrapOrNull(); + expect(result, null); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/text_node_extensions_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/text_node_extensions_test.dart new file mode 100644 index 0000000000..0d7f1ba125 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/text_node_extensions_test.dart @@ -0,0 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('TextNodeExtension::', () { + test('description', () {}); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/text_style_extension_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/text_style_extension_test.dart new file mode 100644 index 0000000000..572178d0f3 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/text_style_extension_test.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy_editor/src/extensions/text_style_extension.dart'; + +void main() { + group('TextStyleExtensions::', () { + const style = TextStyle( + color: Colors.blue, + backgroundColor: Colors.white, + fontSize: 14, + height: 100, + wordSpacing: 2, + fontWeight: FontWeight.w700, + ); + + const otherStyle = TextStyle( + color: Colors.red, + backgroundColor: Colors.black, + fontSize: 12, + height: 10, + wordSpacing: 1, + ); + test('combine', () { + final result = style.combine(otherStyle); + expect(result.color, Colors.red); + expect(result.backgroundColor, Colors.black); + expect(result.fontSize, 12); + expect(result.height, 10); + expect(result.wordSpacing, 1); + }); + + test('combine - return this', () { + final result = style.combine(null); + expect(result, style); + }); + + test('combine - return null with inherit', () { + final styleCopy = otherStyle.copyWith(inherit: false); + final result = style.combine(styleCopy); + expect(result, styleCopy); + }); + }); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/extensions/url_launcher_extension_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/extensions/url_launcher_extension_test.dart new file mode 100644 index 0000000000..81b5b51e37 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/extensions/url_launcher_extension_test.dart @@ -0,0 +1,10 @@ +import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('safeLaunchUrl without scheme', () async { + const href = null; + final result = await safeLaunchUrl(href); + expect(result, false); + }); +}