Browse Source

feat: implement theme customizer

Lucas.Xu 2 years ago
parent
commit
68997a9c93
81 changed files with 2970 additions and 853 deletions
  1. 4 4
      frontend/app_flowy/packages/appflowy_editor/documentation/testing.md
  2. 35 9
      frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart
  3. 2 0
      frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart
  4. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ca.arb
  5. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_de_DE.arb
  6. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb
  7. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_es_VE.arb
  8. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb
  9. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb
  10. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb
  11. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb
  12. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb
  13. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ja_JP.arb
  14. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pl_PL.arb
  15. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_BR.arb
  16. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb
  17. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ru_RU.arb
  18. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_tr_TR.arb
  19. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_zh_CN.arb
  20. 35 0
      frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_zh_TW.arb
  21. 59 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart
  22. 7 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart
  23. 15 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart
  24. 93 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart
  25. 13 12
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart
  26. 73 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_style_extension.dart
  27. 40 36
      frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart
  28. 126 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart
  29. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ca.dart
  30. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_de-DE.dart
  31. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart
  32. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_es-VE.dart
  33. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart
  34. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart
  35. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart
  36. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart
  37. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart
  38. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ja-JP.dart
  39. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pl-PL.dart
  40. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart
  41. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart
  42. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ru-RU.dart
  43. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_tr-TR.dart
  44. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_zh-CN.dart
  45. 42 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_zh-TW.dart
  46. 257 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart
  47. 61 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart
  48. 15 9
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart
  49. 26 42
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart
  50. 66 47
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart
  51. 13 45
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart
  52. 39 28
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart
  53. 14 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart
  54. 12 4
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart
  55. 0 282
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart
  56. 21 13
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart
  57. 239 5
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart
  58. 51 38
      frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart
  59. 40 26
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart
  60. 19 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart
  61. 9 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart
  62. 3 3
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart
  63. 21 20
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart
  64. 16 2
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart
  65. 5 5
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart
  66. 17 14
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart
  67. 19 8
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart
  68. 6 0
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart
  69. 1 1
      frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart
  70. 11 0
      frontend/app_flowy/packages/appflowy_editor/pubspec.yaml
  71. 11 9
      frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart
  72. 3 0
      frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart
  73. 8 7
      frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart
  74. 4 3
      frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart
  75. 98 90
      frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart
  76. 27 25
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart
  77. 10 10
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart
  78. 20 10
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart
  79. 11 10
      frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart
  80. 2 0
      frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart
  81. 19 17
      frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart

+ 4 - 4
frontend/app_flowy/packages/appflowy_editor/documentation/testing.md

@@ -22,8 +22,8 @@ editor.insertTextNode(text);
 
 // Insert the same text, but with the heading style.
 editor.insertTextNode(text, attributes: {
-    StyleKey.subtype: StyleKey.heading,
-    StyleKey.heading: StyleKey.h1,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+    BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
 });
 
 // Insert our text with the bulleted list style and the bold style.
@@ -31,10 +31,10 @@ editor.insertTextNode(text, attributes: {
 editor.insertTextNode(
     '',
     attributes: {
-        StyleKey.subtype: StyleKey.bulletedList,
+        BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
     },
     delta: Delta([
-        TextInsert(text, {StyleKey.bold: true}),
+        TextInsert(text, {BuiltInAttributeKey.bold: true}),
     ]),
 );
 ```

+ 35 - 9
frontend/app_flowy/packages/appflowy_editor/example/lib/main.dart

@@ -21,6 +21,10 @@ class MyApp extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return MaterialApp(
+      localizationsDelegates: const [
+        AppFlowyEditorLocalizations.delegate,
+      ],
+      supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
       debugShowCheckedModeBanner: false,
       theme: ThemeData(
         primarySwatch: Colors.blue,
@@ -40,7 +44,9 @@ class MyHomePage extends StatefulWidget {
 
 class _MyHomePageState extends State<MyHomePage> {
   int _pageIndex = 0;
-  late EditorState _editorState;
+  EditorState? _editorState;
+  bool darkMode = false;
+  EditorStyle _editorStyle = EditorStyle.defaultStyle();
   Future<String>? _jsonString;
 
   @override
@@ -78,24 +84,29 @@ class _MyHomePageState extends State<MyHomePage> {
     return FutureBuilder<String>(
       future: jsonString,
       builder: (_, snapshot) {
-        if (snapshot.hasData) {
-          _editorState = EditorState(
+        if (snapshot.hasData &&
+            snapshot.connectionState == ConnectionState.done) {
+          _editorState ??= EditorState(
             document: StateTree.fromJson(
               Map<String, Object>.from(
                 json.decode(snapshot.data!),
               ),
             ),
           );
-          _editorState.logConfiguration
+          _editorState!.logConfiguration
             ..level = LogLevel.all
             ..handler = (message) {
               debugPrint(message);
             };
-          return SizedBox(
+          _editorState!.operationStream.listen((event) {
+            debugPrint('Operation: ${event.toJson()}');
+          });
+          return Container(
+            color: darkMode ? Colors.black : Colors.white,
             width: MediaQuery.of(context).size.width,
             child: AppFlowyEditor(
-              editorState: _editorState,
-              editorStyle: const EditorStyle.defaultStyle(),
+              editorState: _editorState!,
+              editorStyle: _editorStyle,
               shortcutEvents: [
                 underscoreToItalicEvent,
               ],
@@ -127,12 +138,26 @@ class _MyHomePageState extends State<MyHomePage> {
           onPressed: () => _switchToPage(2),
         ),
         ActionButton(
-            icon: const Icon(Icons.print),
-            onPressed: () => {_exportDocument(_editorState)}),
+          icon: const Icon(Icons.print),
+          onPressed: () => _exportDocument(_editorState!),
+        ),
         ActionButton(
           icon: const Icon(Icons.import_export),
           onPressed: () => _importDocument(),
         ),
+        ActionButton(
+          icon: const Icon(Icons.color_lens),
+          onPressed: () {
+            setState(() {
+              _editorStyle = _editorStyle.copyWith(
+                textStyle: darkMode
+                    ? BuiltInTextStyle.builtIn()
+                    : BuiltInTextStyle.builtInDarkMode(),
+              );
+              darkMode = !darkMode;
+            });
+          },
+        ),
       ],
     );
   }
@@ -166,6 +191,7 @@ class _MyHomePageState extends State<MyHomePage> {
   void _switchToPage(int pageIndex) {
     if (pageIndex != _pageIndex) {
       setState(() {
+        _editorState = null;
         _pageIndex = pageIndex;
       });
     }

+ 2 - 0
frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

@@ -26,3 +26,5 @@ export 'src/service/input_service.dart';
 export 'src/service/shortcut_event/keybinding.dart';
 export 'src/service/shortcut_event/shortcut_event.dart';
 export 'src/service/shortcut_event/shortcut_event_handler.dart';
+export 'src/extensions/attributes_extension.dart';
+export 'src/l10n/l10n.dart';

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ca.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "ca",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_de_DE.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "de-DE",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_en.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "en",
+  "bold": "Bold",
+  "@bold": {},
+  "bulletedList": "Bulleted List",
+  "@bulletedList": {},
+  "checkbox": "Checkbox",
+  "@checkbox": {},
+  "embedCode": "Embed Code",
+  "@embedCode": {},
+  "heading1": "H1",
+  "@heading1": {},
+  "heading2": "H2",
+  "@heading2": {},
+  "heading3": "H3",
+  "@heading3": {},
+  "highlight": "Highlight",
+  "@highlight": {},
+  "image": "Image",
+  "@image": {},
+  "italic": "Italic",
+  "@italic": {},
+  "link": "Link",
+  "@link": {},
+  "numberedList": "Numbered List",
+  "@numberedList": {},
+  "quote": "Quote",
+  "@quote": {},
+  "strikethrough": "Strikethrough",
+  "@strikethrough": {},
+  "text": "Text",
+  "@text": {},
+  "underline": "Underline",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_es_VE.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "es-VE",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_CA.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "fr-CA",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_fr_FR.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "fr-FR",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_hu_HU.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "hu-HU",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_id_ID.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "id-ID",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_it_IT.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "it-IT",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ja_JP.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "ja-JP",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pl_PL.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "pl-PL",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_BR.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "pt-BR",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_pt_PT.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "pt-PT",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_ru_RU.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "ru-RU",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_tr_TR.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "tr-TR",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_zh_CN.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "zh-CN",
+  "bold": "加粗",
+  "@bold": {},
+  "bulletedList": "无序列表",
+  "@bulletedList": {},
+  "checkbox": "复选框",
+  "@checkbox": {},
+  "embedCode": "内嵌代码",
+  "@embedCode": {},
+  "heading1": "标题 1",
+  "@heading1": {},
+  "heading2": "标题 2",
+  "@heading2": {},
+  "heading3": "标题 3",
+  "@heading3": {},
+  "highlight": "高亮",
+  "@highlight": {},
+  "image": "图片",
+  "@image": {},
+  "italic": "斜体",
+  "@italic": {},
+  "link": "链接",
+  "@link": {},
+  "numberedList": "有序列表",
+  "@numberedList": {},
+  "quote": "引用",
+  "@quote": {},
+  "strikethrough": "删除线",
+  "@strikethrough": {},
+  "text": "文字",
+  "@text": {},
+  "underline": "下划线",
+  "@underline": {}
+}

+ 35 - 0
frontend/app_flowy/packages/appflowy_editor/lib/l10n/intl_zh_TW.arb

@@ -0,0 +1,35 @@
+{
+  "@@locale": "zh-TW",
+  "bold": "",
+  "@bold": {},
+  "bulletedList": "",
+  "@bulletedList": {},
+  "checkbox": "",
+  "@checkbox": {},
+  "embedCode": "",
+  "@embedCode": {},
+  "heading1": "",
+  "@heading1": {},
+  "heading2": "",
+  "@heading2": {},
+  "heading3": "",
+  "@heading3": {},
+  "highlight": "",
+  "@highlight": {},
+  "image": "",
+  "@image": {},
+  "italic": "",
+  "@italic": {},
+  "link": "",
+  "@link": {},
+  "numberedList": "",
+  "@numberedList": {},
+  "quote": "",
+  "@quote": {},
+  "strikethrough": "",
+  "@strikethrough": {},
+  "text": "",
+  "@text": {},
+  "underline": "",
+  "@underline": {}
+}

+ 59 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/document/built_in_attribute_keys.dart

@@ -0,0 +1,59 @@
+///
+/// Supported partial rendering types:
+///   bold, italic,
+///   underline, strikethrough,
+///   color, font,
+///   href
+///
+/// Supported global rendering types:
+///   heading: h1, h2, h3, h4, h5, h6, ...
+///   block quote,
+///   list: ordered list, bulleted list,
+///   code block
+///
+class BuiltInAttributeKey {
+  static String bold = 'bold';
+  static String italic = 'italic';
+  static String underline = 'underline';
+  static String strikethrough = 'strikethrough';
+  static String color = 'color';
+  static String backgroundColor = 'backgroundColor';
+  static String font = 'font';
+  static String href = 'href';
+
+  static String subtype = 'subtype';
+  static String heading = 'heading';
+  static String h1 = 'h1';
+  static String h2 = 'h2';
+  static String h3 = 'h3';
+  static String h4 = 'h4';
+  static String h5 = 'h5';
+  static String h6 = 'h6';
+
+  static String bulletedList = 'bulleted-list';
+  static String numberList = 'number-list';
+
+  static String quote = 'quote';
+  static String checkbox = 'checkbox';
+  static String code = 'code';
+  static String number = 'number';
+
+  static List<String> partialStyleKeys = [
+    BuiltInAttributeKey.bold,
+    BuiltInAttributeKey.italic,
+    BuiltInAttributeKey.underline,
+    BuiltInAttributeKey.strikethrough,
+    BuiltInAttributeKey.backgroundColor,
+    BuiltInAttributeKey.href,
+    BuiltInAttributeKey.code,
+  ];
+
+  static List<String> globalStyleKeys = [
+    BuiltInAttributeKey.subtype,
+    BuiltInAttributeKey.heading,
+    BuiltInAttributeKey.checkbox,
+    BuiltInAttributeKey.bulletedList,
+    BuiltInAttributeKey.numberList,
+    BuiltInAttributeKey.quote,
+  ];
+}

+ 7 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/document/node.dart

@@ -24,6 +24,13 @@ class Node extends ChangeNotifier with LinkedListEntry<Node> {
     return null;
   }
 
+  String get id {
+    if (subtype != null) {
+      return '$type/$subtype';
+    }
+    return type;
+  }
+
   Path get path => _path();
 
   Attributes get attributes => _attributes;

+ 15 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/editor_state.dart

@@ -60,7 +60,11 @@ class EditorState {
   List<SelectionMenuItem> selectionMenuItems = [];
 
   /// Stores the editor style.
-  EditorStyle editorStyle = const EditorStyle.defaultStyle();
+  EditorStyle editorStyle = EditorStyle.defaultStyle();
+
+  /// Operation stream.
+  Stream<Operation> get operationStream => _observer.stream;
+  final StreamController<Operation> _observer = StreamController.broadcast();
 
   final UndoManager undoManager = UndoManager();
   Selection? _cursorSelection;
@@ -72,6 +76,15 @@ class EditorState {
     return _cursorSelection;
   }
 
+  RenderBox? get renderBox {
+    final renderObject =
+        service.scrollServiceKey.currentContext?.findRenderObject();
+    if (renderObject != null && renderObject is RenderBox) {
+      return renderObject;
+    }
+    return null;
+  }
+
   updateCursorSelection(Selection? cursorSelection,
       [CursorUpdateReason reason = CursorUpdateReason.others]) {
     // broadcast to other users here
@@ -151,5 +164,6 @@ class EditorState {
     } else if (op is TextEditOperation) {
       document.textEdit(op.path, op.delta);
     }
+    _observer.add(op);
   }
 }

+ 93 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/attributes_extension.dart

@@ -0,0 +1,93 @@
+import 'package:appflowy_editor/src/document/attributes.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
+import 'package:flutter/material.dart';
+
+extension NodeAttributesExtensions on Attributes {
+  String? get heading {
+    if (containsKey(BuiltInAttributeKey.subtype) &&
+        containsKey(BuiltInAttributeKey.heading) &&
+        this[BuiltInAttributeKey.subtype] == BuiltInAttributeKey.heading &&
+        this[BuiltInAttributeKey.heading] is String) {
+      return this[BuiltInAttributeKey.heading];
+    }
+    return null;
+  }
+
+  bool get quote {
+    return containsKey(BuiltInAttributeKey.quote);
+  }
+
+  int? get number {
+    if (containsKey(BuiltInAttributeKey.number) &&
+        this[BuiltInAttributeKey.number] is int) {
+      return this[BuiltInAttributeKey.number];
+    }
+    return null;
+  }
+
+  bool get code {
+    if (containsKey(BuiltInAttributeKey.code) &&
+        this[BuiltInAttributeKey.code] == true) {
+      return this[BuiltInAttributeKey.code];
+    }
+    return false;
+  }
+
+  bool get check {
+    if (containsKey(BuiltInAttributeKey.checkbox) &&
+        this[BuiltInAttributeKey.checkbox] is bool) {
+      return this[BuiltInAttributeKey.checkbox];
+    }
+    return false;
+  }
+}
+
+extension DeltaAttributesExtensions on Attributes {
+  bool get bold {
+    return (containsKey(BuiltInAttributeKey.bold) &&
+        this[BuiltInAttributeKey.bold] == true);
+  }
+
+  bool get italic {
+    return (containsKey(BuiltInAttributeKey.italic) &&
+        this[BuiltInAttributeKey.italic] == true);
+  }
+
+  bool get underline {
+    return (containsKey(BuiltInAttributeKey.underline) &&
+        this[BuiltInAttributeKey.underline] == true);
+  }
+
+  bool get strikethrough {
+    return (containsKey(BuiltInAttributeKey.strikethrough) &&
+        this[BuiltInAttributeKey.strikethrough] == true);
+  }
+
+  Color? get color {
+    if (containsKey(BuiltInAttributeKey.color) &&
+        this[BuiltInAttributeKey.color] is String) {
+      return Color(
+        int.parse(this[BuiltInAttributeKey.color]),
+      );
+    }
+    return null;
+  }
+
+  Color? get backgroundColor {
+    if (containsKey(BuiltInAttributeKey.backgroundColor) &&
+        this[BuiltInAttributeKey.backgroundColor] is String) {
+      return Color(
+        int.parse(this[BuiltInAttributeKey.backgroundColor]),
+      );
+    }
+    return null;
+  }
+
+  String? get href {
+    if (containsKey(BuiltInAttributeKey.href) &&
+        this[BuiltInAttributeKey.href] is String) {
+      return this[BuiltInAttributeKey.href];
+    }
+    return null;
+  }
+}

+ 13 - 12
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_node_extensions.dart

@@ -3,7 +3,7 @@ import 'package:appflowy_editor/src/document/path.dart';
 import 'package:appflowy_editor/src/document/position.dart';
 import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:appflowy_editor/src/document/text_delta.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 extension TextNodeExtension on TextNode {
   dynamic getAttributeInSelection(Selection selection, String styleKey) {
@@ -29,27 +29,28 @@ extension TextNodeExtension on TextNode {
   }
 
   bool allSatisfyLinkInSelection(Selection selection) =>
-      allSatisfyInSelection(selection, StyleKey.href, (value) {
+      allSatisfyInSelection(selection, BuiltInAttributeKey.href, (value) {
         return value != null;
       });
 
   bool allSatisfyBoldInSelection(Selection selection) =>
-      allSatisfyInSelection(selection, StyleKey.bold, (value) {
+      allSatisfyInSelection(selection, BuiltInAttributeKey.bold, (value) {
         return value == true;
       });
 
   bool allSatisfyItalicInSelection(Selection selection) =>
-      allSatisfyInSelection(selection, StyleKey.italic, (value) {
+      allSatisfyInSelection(selection, BuiltInAttributeKey.italic, (value) {
         return value == true;
       });
 
   bool allSatisfyUnderlineInSelection(Selection selection) =>
-      allSatisfyInSelection(selection, StyleKey.underline, (value) {
+      allSatisfyInSelection(selection, BuiltInAttributeKey.underline, (value) {
         return value == true;
       });
 
   bool allSatisfyStrikethroughInSelection(Selection selection) =>
-      allSatisfyInSelection(selection, StyleKey.strikethrough, (value) {
+      allSatisfyInSelection(selection, BuiltInAttributeKey.strikethrough,
+          (value) {
         return value == true;
       });
 
@@ -58,11 +59,11 @@ extension TextNodeExtension on TextNode {
     String styleKey,
     bool Function(dynamic value) test,
   ) {
-    if (StyleKey.globalStyleKeys.contains(styleKey)) {
+    if (BuiltInAttributeKey.globalStyleKeys.contains(styleKey)) {
       if (attributes.containsKey(styleKey)) {
         return test(attributes[styleKey]);
       }
-    } else if (StyleKey.partialStyleKeys.contains(styleKey)) {
+    } else if (BuiltInAttributeKey.partialStyleKeys.contains(styleKey)) {
       final ops = delta.whereType<TextInsert>();
       final startOffset =
           selection.isBackward ? selection.start.offset : selection.end.offset;
@@ -120,28 +121,28 @@ extension TextNodeExtension on TextNode {
 extension TextNodesExtension on List<TextNode> {
   bool allSatisfyBoldInSelection(Selection selection) => allSatisfyInSelection(
         selection,
-        StyleKey.bold,
+        BuiltInAttributeKey.bold,
         (value) => value == true,
       );
 
   bool allSatisfyItalicInSelection(Selection selection) =>
       allSatisfyInSelection(
         selection,
-        StyleKey.italic,
+        BuiltInAttributeKey.italic,
         (value) => value == true,
       );
 
   bool allSatisfyUnderlineInSelection(Selection selection) =>
       allSatisfyInSelection(
         selection,
-        StyleKey.underline,
+        BuiltInAttributeKey.underline,
         (value) => value == true,
       );
 
   bool allSatisfyStrikethroughInSelection(Selection selection) =>
       allSatisfyInSelection(
         selection,
-        StyleKey.strikethrough,
+        BuiltInAttributeKey.strikethrough,
         (value) => value == true,
       );
 

+ 73 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/extensions/text_style_extension.dart

@@ -0,0 +1,73 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+
+extension TextSpanExtensions on TextSpan {
+  TextSpan copyWith({
+    String? text,
+    TextStyle? style,
+    List<InlineSpan>? children,
+    GestureRecognizer? recognizer,
+    String? semanticsLabel,
+  }) {
+    return TextSpan(
+      text: text ?? this.text,
+      style: style ?? this.style,
+      children: children ?? this.children,
+      recognizer: recognizer ?? this.recognizer,
+      semanticsLabel: semanticsLabel ?? this.semanticsLabel,
+    );
+  }
+
+  TextSpan updateTextStyle(TextStyle? other) {
+    if (other == null) {
+      return this;
+    }
+    return copyWith(
+      style: style?.combine(other),
+      children: children?.map((child) {
+        if (child is TextSpan) {
+          return child.updateTextStyle(other);
+        }
+        return child;
+      }).toList(growable: false),
+    );
+  }
+}
+
+extension TextStyleExtensions on TextStyle {
+  TextStyle combine(TextStyle? other) {
+    if (other == null) {
+      return this;
+    }
+    if (!other.inherit) {
+      return other;
+    }
+
+    return copyWith(
+      color: other.color,
+      backgroundColor: other.backgroundColor,
+      fontSize: other.fontSize,
+      fontWeight: other.fontWeight,
+      fontStyle: other.fontStyle,
+      letterSpacing: other.letterSpacing,
+      wordSpacing: other.wordSpacing,
+      textBaseline: other.textBaseline,
+      height: other.height,
+      leadingDistribution: other.leadingDistribution,
+      locale: other.locale,
+      foreground: other.foreground,
+      background: other.background,
+      shadows: other.shadows,
+      fontFeatures: other.fontFeatures,
+      decoration: TextDecoration.combine([
+        if (decoration != null) decoration!,
+        if (other.decoration != null) other.decoration!,
+      ]),
+      decorationColor: other.decorationColor,
+      decorationStyle: other.decorationStyle,
+      decorationThickness: other.decorationThickness,
+      fontFamilyFallback: other.fontFamilyFallback,
+      overflow: other.overflow,
+    );
+  }
+}

+ 40 - 36
frontend/app_flowy/packages/appflowy_editor/lib/src/infra/html_converter.dart

@@ -4,11 +4,11 @@ import 'dart:ui';
 import 'package:appflowy_editor/src/document/attributes.dart';
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/document/text_delta.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/extensions/color_extension.dart';
 import 'package:flutter/material.dart';
 import 'package:html/parser.dart' show parse;
 import 'package:html/dom.dart' as html;
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 class HTMLTag {
   static const h1 = "h1";
@@ -99,7 +99,8 @@ class HTMLToNodesConverter {
 
     for (final child in element.nodes.toList()) {
       if (child is html.Element) {
-        result.addAll(_handleElement(child, {"subtype": StyleKey.quote}));
+        result.addAll(
+            _handleElement(child, {"subtype": BuiltInAttributeKey.quote}));
       }
     }
 
@@ -174,11 +175,11 @@ class HTMLToNodesConverter {
     final fontWeightStr = cssMap["font-weight"];
     if (fontWeightStr != null) {
       if (fontWeightStr == "bold") {
-        attrs[StyleKey.bold] = true;
+        attrs[BuiltInAttributeKey.bold] = true;
       } else {
         int? weight = int.tryParse(fontWeightStr);
         if (weight != null && weight > 500) {
-          attrs[StyleKey.bold] = true;
+          attrs[BuiltInAttributeKey.bold] = true;
         }
       }
     }
@@ -193,12 +194,12 @@ class HTMLToNodesConverter {
         ? null
         : ColorExtension.tryFromRgbaString(backgroundColorStr);
     if (backgroundColor != null) {
-      attrs[StyleKey.backgroundColor] =
+      attrs[BuiltInAttributeKey.backgroundColor] =
           '0x${backgroundColor.value.toRadixString(16)}';
     }
 
     if (cssMap["font-style"] == "italic") {
-      attrs[StyleKey.italic] = true;
+      attrs[BuiltInAttributeKey.italic] = true;
     }
 
     return attrs.isEmpty ? null : attrs;
@@ -208,9 +209,9 @@ class HTMLToNodesConverter {
     final decorations = decorationStr.split(" ");
     for (final d in decorations) {
       if (d == "line-through") {
-        attrs[StyleKey.strikethrough] = true;
+        attrs[BuiltInAttributeKey.strikethrough] = true;
       } else if (d == "underline") {
-        attrs[StyleKey.underline] = true;
+        attrs[BuiltInAttributeKey.underline] = true;
       }
     }
   }
@@ -228,13 +229,13 @@ class HTMLToNodesConverter {
       delta.insert(element.text, attributes);
     } else if (element.localName == HTMLTag.strong ||
         element.localName == HTMLTag.bold) {
-      delta.insert(element.text, {StyleKey.bold: true});
+      delta.insert(element.text, {BuiltInAttributeKey.bold: true});
     } else if (element.localName == HTMLTag.underline) {
-      delta.insert(element.text, {StyleKey.underline: true});
+      delta.insert(element.text, {BuiltInAttributeKey.underline: true});
     } else if (element.localName == HTMLTag.italic) {
-      delta.insert(element.text, {StyleKey.italic: true});
+      delta.insert(element.text, {BuiltInAttributeKey.italic: true});
     } else if (element.localName == HTMLTag.del) {
-      delta.insert(element.text, {StyleKey.strikethrough: true});
+      delta.insert(element.text, {BuiltInAttributeKey.strikethrough: true});
     } else {
       delta.insert(element.text);
     }
@@ -273,7 +274,7 @@ class HTMLToNodesConverter {
     final textNode =
         TextNode(type: "text", delta: delta, attributes: attributes);
     if (isCheckbox) {
-      textNode.attributes["subtype"] = StyleKey.checkbox;
+      textNode.attributes["subtype"] = BuiltInAttributeKey.checkbox;
       textNode.attributes["checkbox"] = checked;
     }
     return textNode;
@@ -291,8 +292,8 @@ class HTMLToNodesConverter {
   List<Node> _handleUnorderedList(html.Element element) {
     final result = <Node>[];
     for (var child in element.children) {
-      result.addAll(
-          _handleListElement(child, {"subtype": StyleKey.bulletedList}));
+      result.addAll(_handleListElement(
+          child, {"subtype": BuiltInAttributeKey.bulletedList}));
     }
     return result;
   }
@@ -302,7 +303,7 @@ class HTMLToNodesConverter {
     for (var i = 0; i < element.children.length; i++) {
       final child = element.children[i];
       result.addAll(_handleListElement(
-          child, {"subtype": StyleKey.numberList, "number": i + 1}));
+          child, {"subtype": BuiltInAttributeKey.numberList, "number": i + 1}));
     }
     return result;
   }
@@ -401,7 +402,8 @@ class NodesToHTMLConverter {
 
   _addElement(TextNode textNode, html.Element element) {
     if (element.localName == HTMLTag.list) {
-      final isNumbered = textNode.attributes["subtype"] == StyleKey.numberList;
+      final isNumbered =
+          textNode.attributes["subtype"] == BuiltInAttributeKey.numberList;
       _stashListContainer ??= html.Element.tag(
           isNumbered ? HTMLTag.orderedList : HTMLTag.unorderedList);
       _stashListContainer?.append(element);
@@ -433,10 +435,10 @@ class NodesToHTMLConverter {
 
   String _textDecorationsFromAttributes(Attributes attributes) {
     var textDecoration = <String>[];
-    if (attributes[StyleKey.strikethrough] == true) {
+    if (attributes[BuiltInAttributeKey.strikethrough] == true) {
       textDecoration.add("line-through");
     }
-    if (attributes[StyleKey.underline] == true) {
+    if (attributes[BuiltInAttributeKey.underline] == true) {
       textDecoration.add("underline");
     }
 
@@ -445,19 +447,19 @@ class NodesToHTMLConverter {
 
   String _attributesToCssStyle(Map<String, dynamic> attributes) {
     final cssMap = <String, String>{};
-    if (attributes[StyleKey.backgroundColor] != null) {
+    if (attributes[BuiltInAttributeKey.backgroundColor] != null) {
       final color = Color(
-        int.parse(attributes[StyleKey.backgroundColor]),
+        int.parse(attributes[BuiltInAttributeKey.backgroundColor]),
       );
       cssMap["background-color"] = color.toRgbaString();
     }
-    if (attributes[StyleKey.color] != null) {
+    if (attributes[BuiltInAttributeKey.color] != null) {
       final color = Color(
-        int.parse(attributes[StyleKey.color]),
+        int.parse(attributes[BuiltInAttributeKey.color]),
       );
       cssMap["color"] = color.toRgbaString();
     }
-    if (attributes[StyleKey.bold] == true) {
+    if (attributes[BuiltInAttributeKey.bold] == true) {
       cssMap["font-weight"] = "bold";
     }
 
@@ -466,7 +468,7 @@ class NodesToHTMLConverter {
       cssMap["text-decoration"] = textDecoration;
     }
 
-    if (attributes[StyleKey.italic] == true) {
+    if (attributes[BuiltInAttributeKey.italic] == true) {
       cssMap["font-style"] = "italic";
     }
     return _cssMapToCssStyle(cssMap);
@@ -507,23 +509,24 @@ class NodesToHTMLConverter {
     final childNodes = <html.Node>[];
     String tagName = HTMLTag.paragraph;
 
-    if (subType == StyleKey.bulletedList || subType == StyleKey.numberList) {
+    if (subType == BuiltInAttributeKey.bulletedList ||
+        subType == BuiltInAttributeKey.numberList) {
       tagName = HTMLTag.list;
-    } else if (subType == StyleKey.checkbox) {
+    } else if (subType == BuiltInAttributeKey.checkbox) {
       final node = html.Element.html('<input type="checkbox" />');
       if (checked != null && checked) {
         node.attributes["checked"] = "true";
       }
       childNodes.add(node);
-    } else if (subType == StyleKey.heading) {
-      if (heading == StyleKey.h1) {
+    } else if (subType == BuiltInAttributeKey.heading) {
+      if (heading == BuiltInAttributeKey.h1) {
         tagName = HTMLTag.h1;
-      } else if (heading == StyleKey.h2) {
+      } else if (heading == BuiltInAttributeKey.h2) {
         tagName = HTMLTag.h2;
-      } else if (heading == StyleKey.h3) {
+      } else if (heading == BuiltInAttributeKey.h3) {
         tagName = HTMLTag.h3;
       }
-    } else if (subType == StyleKey.quote) {
+    } else if (subType == BuiltInAttributeKey.quote) {
       tagName = HTMLTag.blockQuote;
     }
 
@@ -531,22 +534,23 @@ class NodesToHTMLConverter {
       if (op is TextInsert) {
         final attributes = op.attributes;
         if (attributes != null) {
-          if (attributes.length == 1 && attributes[StyleKey.bold] == true) {
+          if (attributes.length == 1 &&
+              attributes[BuiltInAttributeKey.bold] == true) {
             final strong = html.Element.tag(HTMLTag.strong);
             strong.append(html.Text(op.content));
             childNodes.add(strong);
           } else if (attributes.length == 1 &&
-              attributes[StyleKey.underline] == true) {
+              attributes[BuiltInAttributeKey.underline] == true) {
             final strong = html.Element.tag(HTMLTag.underline);
             strong.append(html.Text(op.content));
             childNodes.add(strong);
           } else if (attributes.length == 1 &&
-              attributes[StyleKey.italic] == true) {
+              attributes[BuiltInAttributeKey.italic] == true) {
             final strong = html.Element.tag(HTMLTag.italic);
             strong.append(html.Text(op.content));
             childNodes.add(strong);
           } else if (attributes.length == 1 &&
-              attributes[StyleKey.strikethrough] == true) {
+              attributes[BuiltInAttributeKey.strikethrough] == true) {
             final strong = html.Element.tag(HTMLTag.del);
             strong.append(html.Text(op.content));
             childNodes.add(strong);

+ 126 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_all.dart

@@ -0,0 +1,126 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that looks up messages for specific locales by
+// delegating to the appropriate library.
+
+// Ignore issues from commonly used lints in this file.
+// ignore_for_file:implementation_imports, file_names, unnecessary_new
+// ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
+// ignore_for_file:argument_type_not_assignable, invalid_assignment
+// ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
+// ignore_for_file:comment_references
+
+import 'dart:async';
+
+import 'package:intl/intl.dart';
+import 'package:intl/message_lookup_by_library.dart';
+import 'package:intl/src/intl_helpers.dart';
+
+import 'messages_ca.dart' as messages_ca;
+import 'messages_de-DE.dart' as messages_de_de;
+import 'messages_en.dart' as messages_en;
+import 'messages_es-VE.dart' as messages_es_ve;
+import 'messages_fr-CA.dart' as messages_fr_ca;
+import 'messages_fr-FR.dart' as messages_fr_fr;
+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_pl-PL.dart' as messages_pl_pl;
+import 'messages_pt-BR.dart' as messages_pt_br;
+import 'messages_pt-PT.dart' as messages_pt_pt;
+import 'messages_ru-RU.dart' as messages_ru_ru;
+import 'messages_tr-TR.dart' as messages_tr_tr;
+import 'messages_zh-CN.dart' as messages_zh_cn;
+import 'messages_zh-TW.dart' as messages_zh_tw;
+
+typedef Future<dynamic> LibraryLoader();
+Map<String, LibraryLoader> _deferredLibraries = {
+  'ca': () => new Future.value(null),
+  'de_DE': () => new Future.value(null),
+  'en': () => new Future.value(null),
+  'es_VE': () => new Future.value(null),
+  'fr_CA': () => new Future.value(null),
+  'fr_FR': () => new Future.value(null),
+  'hu_HU': () => new Future.value(null),
+  'id_ID': () => new Future.value(null),
+  'it_IT': () => new Future.value(null),
+  'ja_JP': () => new Future.value(null),
+  'pl_PL': () => new Future.value(null),
+  'pt_BR': () => new Future.value(null),
+  'pt_PT': () => new Future.value(null),
+  'ru_RU': () => new Future.value(null),
+  'tr_TR': () => new Future.value(null),
+  'zh_CN': () => new Future.value(null),
+  'zh_TW': () => new Future.value(null),
+};
+
+MessageLookupByLibrary? _findExact(String localeName) {
+  switch (localeName) {
+    case 'ca':
+      return messages_ca.messages;
+    case 'de_DE':
+      return messages_de_de.messages;
+    case 'en':
+      return messages_en.messages;
+    case 'es_VE':
+      return messages_es_ve.messages;
+    case 'fr_CA':
+      return messages_fr_ca.messages;
+    case 'fr_FR':
+      return messages_fr_fr.messages;
+    case 'hu_HU':
+      return messages_hu_hu.messages;
+    case 'id_ID':
+      return messages_id_id.messages;
+    case 'it_IT':
+      return messages_it_it.messages;
+    case 'ja_JP':
+      return messages_ja_jp.messages;
+    case 'pl_PL':
+      return messages_pl_pl.messages;
+    case 'pt_BR':
+      return messages_pt_br.messages;
+    case 'pt_PT':
+      return messages_pt_pt.messages;
+    case 'ru_RU':
+      return messages_ru_ru.messages;
+    case 'tr_TR':
+      return messages_tr_tr.messages;
+    case 'zh_CN':
+      return messages_zh_cn.messages;
+    case 'zh_TW':
+      return messages_zh_tw.messages;
+    default:
+      return null;
+  }
+}
+
+/// User programs should call this before using [localeName] for messages.
+Future<bool> initializeMessages(String localeName) async {
+  var availableLocale = Intl.verifiedLocale(
+      localeName, (locale) => _deferredLibraries[locale] != null,
+      onFailure: (_) => null);
+  if (availableLocale == null) {
+    return new Future.value(false);
+  }
+  var lib = _deferredLibraries[availableLocale];
+  await (lib == null ? new Future.value(false) : lib());
+  initializeInternalMessageLookup(() => new CompositeMessageLookup());
+  messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
+  return new Future.value(true);
+}
+
+bool _messagesExistFor(String locale) {
+  try {
+    return _findExact(locale) != null;
+  } catch (e) {
+    return false;
+  }
+}
+
+MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
+  var actualLocale =
+      Intl.verifiedLocale(locale, _messagesExistFor, onFailure: (_) => null);
+  if (actualLocale == null) return null;
+  return _findExact(actualLocale);
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ca.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a ca 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'ca';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_de-DE.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a de_DE 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'de_DE';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_en.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a en 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'en';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage("Bold"),
+        "bulletedList": MessageLookupByLibrary.simpleMessage("Bulleted List"),
+        "checkbox": MessageLookupByLibrary.simpleMessage("Checkbox"),
+        "embedCode": MessageLookupByLibrary.simpleMessage("Embed Code"),
+        "heading1": MessageLookupByLibrary.simpleMessage("H1"),
+        "heading2": MessageLookupByLibrary.simpleMessage("H2"),
+        "heading3": MessageLookupByLibrary.simpleMessage("H3"),
+        "highlight": MessageLookupByLibrary.simpleMessage("Highlight"),
+        "image": MessageLookupByLibrary.simpleMessage("Image"),
+        "italic": MessageLookupByLibrary.simpleMessage("Italic"),
+        "link": MessageLookupByLibrary.simpleMessage("Link"),
+        "numberedList": MessageLookupByLibrary.simpleMessage("Numbered List"),
+        "quote": MessageLookupByLibrary.simpleMessage("Quote"),
+        "strikethrough": MessageLookupByLibrary.simpleMessage("Strikethrough"),
+        "text": MessageLookupByLibrary.simpleMessage("Text"),
+        "underline": MessageLookupByLibrary.simpleMessage("Underline")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_es-VE.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a es_VE 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'es_VE';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-CA.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a fr_CA 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'fr_CA';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_fr-FR.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a fr_FR 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'fr_FR';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_hu-HU.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a hu_HU 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'hu_HU';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_id-ID.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a id_ID 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'id_ID';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_it-IT.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a it_IT 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'it_IT';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ja-JP.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a ja_JP 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'ja_JP';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pl-PL.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a pl_PL 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'pl_PL';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-BR.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a pt_BR 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'pt_BR';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_pt-PT.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a pt_PT 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'pt_PT';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_ru-RU.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a ru_RU 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'ru_RU';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_tr-TR.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a tr_TR 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'tr_TR';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_zh-CN.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a zh_CN 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'zh_CN';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "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("下划线")
+      };
+}

+ 42 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/intl/messages_zh-TW.dart

@@ -0,0 +1,42 @@
+// DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
+// This is a library that provides messages for a zh_TW 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<dynamic> args);
+
+class MessageLookup extends MessageLookupByLibrary {
+  String get localeName => 'zh_TW';
+
+  final messages = _notInlinedMessages(_notInlinedMessages);
+  static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
+        "bold": MessageLookupByLibrary.simpleMessage(""),
+        "bulletedList": MessageLookupByLibrary.simpleMessage(""),
+        "checkbox": MessageLookupByLibrary.simpleMessage(""),
+        "embedCode": MessageLookupByLibrary.simpleMessage(""),
+        "heading1": MessageLookupByLibrary.simpleMessage(""),
+        "heading2": MessageLookupByLibrary.simpleMessage(""),
+        "heading3": MessageLookupByLibrary.simpleMessage(""),
+        "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("")
+      };
+}

+ 257 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/l10n/l10n.dart

@@ -0,0 +1,257 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'intl/messages_all.dart';
+
+// **************************************************************************
+// Generator: Flutter Intl IDE plugin
+// Made by Localizely
+// **************************************************************************
+
+// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
+// ignore_for_file: join_return_with_assignment, prefer_final_in_for_each
+// ignore_for_file: avoid_redundant_argument_values, avoid_escaping_inner_quotes
+
+class AppFlowyEditorLocalizations {
+  AppFlowyEditorLocalizations();
+
+  static AppFlowyEditorLocalizations? _current;
+
+  static AppFlowyEditorLocalizations get current {
+    assert(_current != null,
+        'No instance of AppFlowyEditorLocalizations was loaded. Try to initialize the AppFlowyEditorLocalizations delegate before accessing AppFlowyEditorLocalizations.current.');
+    return _current!;
+  }
+
+  static const AppLocalizationDelegate delegate = AppLocalizationDelegate();
+
+  static Future<AppFlowyEditorLocalizations> load(Locale locale) {
+    final name = (locale.countryCode?.isEmpty ?? false)
+        ? locale.languageCode
+        : locale.toString();
+    final localeName = Intl.canonicalizedLocale(name);
+    return initializeMessages(localeName).then((_) {
+      Intl.defaultLocale = localeName;
+      final instance = AppFlowyEditorLocalizations();
+      AppFlowyEditorLocalizations._current = instance;
+
+      return instance;
+    });
+  }
+
+  static AppFlowyEditorLocalizations of(BuildContext context) {
+    final instance = AppFlowyEditorLocalizations.maybeOf(context);
+    assert(instance != null,
+        'No instance of AppFlowyEditorLocalizations present in the widget tree. Did you add AppFlowyEditorLocalizations.delegate in localizationsDelegates?');
+    return instance!;
+  }
+
+  static AppFlowyEditorLocalizations? maybeOf(BuildContext context) {
+    return Localizations.of<AppFlowyEditorLocalizations>(
+        context, AppFlowyEditorLocalizations);
+  }
+
+  /// `Bold`
+  String get bold {
+    return Intl.message(
+      'Bold',
+      name: 'bold',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Bulleted List`
+  String get bulletedList {
+    return Intl.message(
+      'Bulleted List',
+      name: 'bulletedList',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Checkbox`
+  String get checkbox {
+    return Intl.message(
+      'Checkbox',
+      name: 'checkbox',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Embed Code`
+  String get embedCode {
+    return Intl.message(
+      'Embed Code',
+      name: 'embedCode',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `H1`
+  String get heading1 {
+    return Intl.message(
+      'H1',
+      name: 'heading1',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `H2`
+  String get heading2 {
+    return Intl.message(
+      'H2',
+      name: 'heading2',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `H3`
+  String get heading3 {
+    return Intl.message(
+      'H3',
+      name: 'heading3',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Highlight`
+  String get highlight {
+    return Intl.message(
+      'Highlight',
+      name: 'highlight',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Image`
+  String get image {
+    return Intl.message(
+      'Image',
+      name: 'image',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Italic`
+  String get italic {
+    return Intl.message(
+      'Italic',
+      name: 'italic',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Link`
+  String get link {
+    return Intl.message(
+      'Link',
+      name: 'link',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Numbered List`
+  String get numberedList {
+    return Intl.message(
+      'Numbered List',
+      name: 'numberedList',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Quote`
+  String get quote {
+    return Intl.message(
+      'Quote',
+      name: 'quote',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Strikethrough`
+  String get strikethrough {
+    return Intl.message(
+      'Strikethrough',
+      name: 'strikethrough',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Text`
+  String get text {
+    return Intl.message(
+      'Text',
+      name: 'text',
+      desc: '',
+      args: [],
+    );
+  }
+
+  /// `Underline`
+  String get underline {
+    return Intl.message(
+      'Underline',
+      name: 'underline',
+      desc: '',
+      args: [],
+    );
+  }
+}
+
+class AppLocalizationDelegate
+    extends LocalizationsDelegate<AppFlowyEditorLocalizations> {
+  const AppLocalizationDelegate();
+
+  List<Locale> get supportedLocales {
+    return const <Locale>[
+      Locale.fromSubtags(languageCode: 'en'),
+      Locale.fromSubtags(languageCode: 'ca'),
+      Locale.fromSubtags(languageCode: 'de', countryCode: 'DE'),
+      Locale.fromSubtags(languageCode: 'es', countryCode: 'VE'),
+      Locale.fromSubtags(languageCode: 'fr', countryCode: 'CA'),
+      Locale.fromSubtags(languageCode: 'fr', countryCode: 'FR'),
+      Locale.fromSubtags(languageCode: 'hu', countryCode: 'HU'),
+      Locale.fromSubtags(languageCode: 'id', countryCode: 'ID'),
+      Locale.fromSubtags(languageCode: 'it', countryCode: 'IT'),
+      Locale.fromSubtags(languageCode: 'ja', countryCode: 'JP'),
+      Locale.fromSubtags(languageCode: 'pl', countryCode: 'PL'),
+      Locale.fromSubtags(languageCode: 'pt', countryCode: 'BR'),
+      Locale.fromSubtags(languageCode: 'pt', countryCode: 'PT'),
+      Locale.fromSubtags(languageCode: 'ru', countryCode: 'RU'),
+      Locale.fromSubtags(languageCode: 'tr', countryCode: 'TR'),
+      Locale.fromSubtags(languageCode: 'zh', countryCode: 'CN'),
+      Locale.fromSubtags(languageCode: 'zh', countryCode: 'TW'),
+    ];
+  }
+
+  @override
+  bool isSupported(Locale locale) => _isSupported(locale);
+  @override
+  Future<AppFlowyEditorLocalizations> load(Locale locale) =>
+      AppFlowyEditorLocalizations.load(locale);
+  @override
+  bool shouldReload(AppLocalizationDelegate old) => false;
+
+  bool _isSupported(Locale locale) {
+    for (var supportedLocale in supportedLocales) {
+      if (supportedLocale.languageCode == locale.languageCode) {
+        return true;
+      }
+    }
+    return false;
+  }
+}

+ 61 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/built_in_text_widget.dart

@@ -0,0 +1,61 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+
+abstract class BuiltInTextWidget extends StatefulWidget {
+  const BuiltInTextWidget({
+    Key? key,
+  }) : super(key: key);
+
+  EditorState get editorState;
+  TextNode get textNode;
+}
+
+mixin BuiltInStyleMixin<T extends BuiltInTextWidget> on State<T> {
+  EdgeInsets get padding {
+    final padding = widget.editorState.editorStyle.style(
+      widget.editorState,
+      widget.textNode,
+      'padding',
+    );
+    if (padding is EdgeInsets) {
+      return padding;
+    }
+    return const EdgeInsets.all(0);
+  }
+
+  TextStyle get textStyle {
+    final textStyle = widget.editorState.editorStyle.style(
+      widget.editorState,
+      widget.textNode,
+      'textStyle',
+    );
+    if (textStyle is TextStyle) {
+      return textStyle;
+    }
+    return const TextStyle();
+  }
+
+  Size? get iconSize {
+    final iconSize = widget.editorState.editorStyle.style(
+      widget.editorState,
+      widget.textNode,
+      'iconSize',
+    );
+    if (iconSize is Size) {
+      return iconSize;
+    }
+    return const Size.square(18.0);
+  }
+
+  EdgeInsets? get iconPadding {
+    final iconPadding = widget.editorState.editorStyle.style(
+      widget.editorState,
+      widget.textNode,
+      'iconPadding',
+    );
+    if (iconPadding is EdgeInsets) {
+      return iconPadding;
+    }
+    return const EdgeInsets.all(0);
+  }
+}

+ 15 - 9
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/bulleted_list_text.dart

@@ -1,9 +1,9 @@
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
@@ -24,14 +24,16 @@ class BulletedListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
       });
 }
 
-class BulletedListTextNodeWidget extends StatefulWidget {
+class BulletedListTextNodeWidget extends BuiltInTextWidget {
   const BulletedListTextNodeWidget({
     Key? key,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
 
+  @override
   final TextNode textNode;
+  @override
   final EditorState editorState;
 
   @override
@@ -42,36 +44,40 @@ class BulletedListTextNodeWidget extends StatefulWidget {
 // customize
 
 class _BulletedListTextNodeWidgetState extends State<BulletedListTextNodeWidget>
-    with SelectableMixin, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
   @override
   final iconKey = GlobalKey();
 
   final _richTextKey = GlobalKey(debugLabel: 'bulleted_list_text');
-  final _iconWidth = 20.0;
-  final _iconRightPadding = 5.0;
 
   @override
   SelectableMixin<StatefulWidget> get forward =>
       _richTextKey.currentState as SelectableMixin;
 
+  @override
+  Offset get baseOffset {
+    return super.baseOffset.translate(0, padding.top);
+  }
+
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: EdgeInsets.only(bottom: defaultLinePadding),
+      padding: padding,
       child: Row(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           FlowySvg(
             key: iconKey,
-            width: _iconWidth,
-            height: _iconWidth,
-            padding: EdgeInsets.only(right: _iconRightPadding),
+            width: iconSize?.width,
+            height: iconSize?.height,
+            padding: iconPadding,
             name: 'point',
           ),
           Flexible(
             child: FlowyRichText(
               key: _richTextKey,
               placeholderText: 'List',
+              lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
               textNode: widget.textNode,
               editorState: widget.editorState,
             ),

+ 26 - 42
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart

@@ -1,12 +1,16 @@
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
-import 'package:appflowy_editor/src/operation/transaction_builder.dart';
+import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
+
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
+import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 import 'package:flutter/material.dart';
 
 class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
@@ -21,18 +25,20 @@ class CheckboxNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
 
   @override
   NodeValidator<Node> get nodeValidator => ((node) {
-        return node.attributes.containsKey(StyleKey.checkbox);
+        return node.attributes.containsKey(BuiltInAttributeKey.checkbox);
       });
 }
 
-class CheckboxNodeWidget extends StatefulWidget {
+class CheckboxNodeWidget extends BuiltInTextWidget {
   const CheckboxNodeWidget({
     Key? key,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
 
+  @override
   final TextNode textNode;
+  @override
   final EditorState editorState;
 
   @override
@@ -40,18 +46,21 @@ class CheckboxNodeWidget extends StatefulWidget {
 }
 
 class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
-    with SelectableMixin, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
   @override
   final iconKey = GlobalKey();
 
   final _richTextKey = GlobalKey(debugLabel: 'checkbox_text');
-  final _iconWidth = 20.0;
-  final _iconRightPadding = 5.0;
 
   @override
   SelectableMixin<StatefulWidget> get forward =>
       _richTextKey.currentState as SelectableMixin;
 
+  @override
+  Offset get baseOffset {
+    return super.baseOffset.translate(0, padding.top);
+  }
+
   @override
   Widget build(BuildContext context) {
     if (widget.textNode.children.isEmpty) {
@@ -64,33 +73,32 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
   Widget _buildWithSingle(BuildContext context) {
     final check = widget.textNode.attributes.check;
     return Padding(
-      padding: EdgeInsets.only(bottom: defaultLinePadding),
+      padding: padding,
       child: Row(
         crossAxisAlignment: CrossAxisAlignment.start,
         children: [
           GestureDetector(
             key: iconKey,
             child: FlowySvg(
-              width: _iconWidth,
-              height: _iconWidth,
-              padding: EdgeInsets.only(right: _iconRightPadding),
+              width: iconSize?.width,
+              height: iconSize?.height,
+              padding: iconPadding,
               name: check ? 'check' : 'uncheck',
             ),
             onTap: () {
-              TransactionBuilder(widget.editorState)
-                ..updateNode(widget.textNode, {
-                  StyleKey.checkbox: !check,
-                })
-                ..commit();
+              formatCheckbox(widget.editorState, !check);
             },
           ),
           Flexible(
             child: FlowyRichText(
               key: _richTextKey,
               placeholderText: 'To-do',
+              lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
               textNode: widget.textNode,
-              textSpanDecorator: _textSpanDecorator,
-              placeholderTextSpanDecorator: _textSpanDecorator,
+              textSpanDecorator: (textSpan) =>
+                  textSpan.updateTextStyle(textStyle),
+              placeholderTextSpanDecorator: (textSpan) =>
+                  textSpan.updateTextStyle(textStyle),
               editorState: widget.editorState,
             ),
           ),
@@ -134,28 +142,4 @@ class _CheckboxNodeWidgetState extends State<CheckboxNodeWidget>
       ],
     );
   }
-
-  TextSpan _textSpanDecorator(TextSpan textSpan) {
-    return TextSpan(
-      children: textSpan.children
-          ?.whereType<TextSpan>()
-          .map(
-            (span) => TextSpan(
-              text: span.text,
-              style: widget.textNode.attributes.check
-                  ? span.style?.copyWith(
-                      color: Colors.grey,
-                      decoration: TextDecoration.combine([
-                        TextDecoration.lineThrough,
-                        if (span.style?.decoration != null)
-                          span.style!.decoration!
-                      ]),
-                    )
-                  : span.style,
-              recognizer: span.recognizer,
-            ),
-          )
-          .toList(),
-    );
-  }
 }

+ 66 - 47
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart

@@ -1,8 +1,6 @@
 import 'dart:async';
 import 'dart:ui';
 
-import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
-import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
@@ -13,8 +11,12 @@ import 'package:appflowy_editor/src/document/position.dart';
 import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:appflowy_editor/src/document/text_delta.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
+import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
+
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
+import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
 
 typedef FlowyTextSpanDecorator = TextSpan Function(TextSpan textSpan);
 
@@ -23,6 +25,7 @@ class FlowyRichText extends StatefulWidget {
     Key? key,
     this.cursorHeight,
     this.cursorWidth = 1.0,
+    this.lineHeight = 1.0,
     this.textSpanDecorator,
     this.placeholderText = ' ',
     this.placeholderTextSpanDecorator,
@@ -34,6 +37,7 @@ class FlowyRichText extends StatefulWidget {
   final EditorState editorState;
   final double? cursorHeight;
   final double cursorWidth;
+  final double lineHeight;
   final FlowyTextSpanDecorator? textSpanDecorator;
   final String placeholderText;
   final FlowyTextSpanDecorator? placeholderTextSpanDecorator;
@@ -46,8 +50,6 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
   var _textKey = GlobalKey();
   final _placeholderTextKey = GlobalKey();
 
-  final _lineHeight = 1.5;
-
   RenderParagraph get _renderParagraph =>
       _textKey.currentContext?.findRenderObject() as RenderParagraph;
 
@@ -90,20 +92,6 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
       cursorOffset = _placeholderRenderParagraph.getOffsetForCaret(
           textPosition, Rect.zero);
     }
-    if (cursorHeight != null) {
-      // workaround: Calling the `getFullHeightForCaret` function will return
-      // the full height of rich text component instead of the plain text
-      // if we set the line height.
-      // So need to divide by the line height to get the expected value.
-      //
-      // And the default height of plain text is too short. Add a magic height
-      // to expand it.
-      const magicHeight = 3.0;
-      cursorOffset = cursorOffset.translate(
-          0, (cursorHeight - cursorHeight / _lineHeight) / 2.0);
-      cursorHeight /= _lineHeight;
-      cursorHeight += magicHeight;
-    }
     final rect = Rect.fromLTWH(
       cursorOffset.dx - (widget.cursorWidth / 2),
       cursorOffset.dy,
@@ -190,8 +178,8 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
       key: _placeholderTextKey,
       textHeightBehavior: const TextHeightBehavior(
           applyHeightToFirstAscent: false, applyHeightToLastDescent: false),
-      text: widget.textSpanDecorator != null
-          ? widget.textSpanDecorator!(textSpan)
+      text: widget.placeholderTextSpanDecorator != null
+          ? widget.placeholderTextSpanDecorator!(textSpan)
           : textSpan,
     );
   }
@@ -210,43 +198,74 @@ class _FlowyRichTextState extends State<FlowyRichText> with SelectableMixin {
     );
   }
 
+  TextSpan get _placeholderTextSpan {
+    final style = widget.editorState.editorStyle.textStyle;
+    return TextSpan(
+      children: [
+        TextSpan(
+          text: widget.placeholderText,
+          style: style.defaultPlaceholderTextStyle,
+        ),
+      ],
+    );
+  }
+
   TextSpan get _textSpan {
     var offset = 0;
-    return TextSpan(
-      children: widget.textNode.delta.whereType<TextInsert>().map((insert) {
-        GestureRecognizer? gestureRecognizer;
-        if (insert.attributes?[StyleKey.href] != null) {
-          gestureRecognizer = _buildTapHrefGestureRecognizer(
-            insert.attributes![StyleKey.href],
+    List<TextSpan> textSpans = [];
+    final style = widget.editorState.editorStyle.textStyle;
+    final textInserts = widget.textNode.delta.whereType<TextInsert>();
+    for (final textInsert in textInserts) {
+      var textStyle = style.defaultTextStyle;
+      GestureRecognizer? recognizer;
+      final attributes = textInsert.attributes;
+      if (attributes != null) {
+        if (attributes.bold == true) {
+          textStyle = textStyle.combine(style.bold);
+        }
+        if (attributes.italic == true) {
+          textStyle = textStyle.combine(style.italic);
+        }
+        if (attributes.underline == true) {
+          textStyle = textStyle.combine(style.underline);
+        }
+        if (attributes.strikethrough == true) {
+          textStyle = textStyle.combine(style.strikethrough);
+        }
+        if (attributes.href != null) {
+          textStyle = textStyle.combine(style.href);
+          recognizer = _buildTapHrefGestureRecognizer(
+            attributes.href!,
             Selection.single(
               path: widget.textNode.path,
               startOffset: offset,
-              endOffset: offset + insert.length,
+              endOffset: offset + textInsert.length,
             ),
           );
         }
-        offset += insert.length;
-        final textSpan = RichTextStyle(
-          attributes: insert.attributes ?? {},
-          text: insert.content,
-          height: _lineHeight,
-          gestureRecognizer: gestureRecognizer,
-        ).toTextSpan();
-        return textSpan;
-      }).toList(growable: false),
+        if (attributes.code == true) {
+          textStyle = textStyle.combine(style.code);
+        }
+        if (attributes.backgroundColor != null) {
+          textStyle = textStyle.combine(
+            TextStyle(backgroundColor: attributes.backgroundColor),
+          );
+        }
+      }
+      offset += textInsert.length;
+      textSpans.add(
+        TextSpan(
+          text: textInsert.content,
+          style: textStyle,
+          recognizer: recognizer,
+        ),
+      );
+    }
+    return TextSpan(
+      children: textSpans,
     );
   }
 
-  TextSpan get _placeholderTextSpan => TextSpan(children: [
-        RichTextStyle(
-          text: widget.placeholderText,
-          attributes: {
-            StyleKey.color: '0xFF707070',
-          },
-          height: _lineHeight,
-        ).toTextSpan()
-      ]);
-
   GestureRecognizer _buildTapHrefGestureRecognizer(
       String href, Selection selection) {
     Timer? timer;

+ 13 - 45
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/heading_text.dart

@@ -1,11 +1,13 @@
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
+import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
+import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 
 class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
@@ -23,14 +25,16 @@ class HeadingTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
       });
 }
 
-class HeadingTextNodeWidget extends StatefulWidget {
+class HeadingTextNodeWidget extends BuiltInTextWidget {
   const HeadingTextNodeWidget({
     Key? key,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
 
+  @override
   final TextNode textNode;
+  @override
   final EditorState editorState;
 
   @override
@@ -39,12 +43,11 @@ class HeadingTextNodeWidget extends StatefulWidget {
 
 // customize
 class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
-    with SelectableMixin, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
   @override
   GlobalKey? get iconKey => null;
 
   final _richTextKey = GlobalKey(debugLabel: 'heading_text');
-  final _topPadding = 5.0;
 
   @override
   SelectableMixin<StatefulWidget> get forward =>
@@ -52,58 +55,23 @@ class _HeadingTextNodeWidgetState extends State<HeadingTextNodeWidget>
 
   @override
   Offset get baseOffset {
-    return Offset(0, _topPadding);
+    return padding.topLeft;
   }
 
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: EdgeInsets.only(
-        top: _topPadding,
-        bottom: defaultLinePadding,
-      ),
+      padding: padding,
       child: FlowyRichText(
         key: _richTextKey,
         placeholderText: 'Heading',
-        placeholderTextSpanDecorator: _placeholderTextSpanDecorator,
-        textSpanDecorator: _textSpanDecorator,
+        placeholderTextSpanDecorator: (textSpan) =>
+            textSpan.updateTextStyle(textStyle),
+        textSpanDecorator: (textSpan) => textSpan.updateTextStyle(textStyle),
+        lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
         textNode: widget.textNode,
         editorState: widget.editorState,
       ),
     );
   }
-
-  TextSpan _textSpanDecorator(TextSpan textSpan) {
-    return TextSpan(
-      children: textSpan.children
-          ?.whereType<TextSpan>()
-          .map(
-            (span) => TextSpan(
-              text: span.text,
-              style: span.style?.copyWith(
-                fontSize: widget.textNode.attributes.fontSize,
-              ),
-              recognizer: span.recognizer,
-            ),
-          )
-          .toList(),
-    );
-  }
-
-  TextSpan _placeholderTextSpanDecorator(TextSpan textSpan) {
-    return TextSpan(
-      children: textSpan.children
-          ?.whereType<TextSpan>()
-          .map(
-            (span) => TextSpan(
-              text: span.text,
-              style: span.style?.copyWith(
-                fontSize: widget.textNode.attributes.fontSize,
-              ),
-              recognizer: span.recognizer,
-            ),
-          )
-          .toList(),
-    );
-  }
 }

+ 39 - 28
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/number_list_text.dart

@@ -1,12 +1,13 @@
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
+import 'package:appflowy_editor/src/extensions/text_style_extension.dart';
 
 class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
   @override
@@ -24,14 +25,16 @@ class NumberListTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
       });
 }
 
-class NumberListTextNodeWidget extends StatefulWidget {
+class NumberListTextNodeWidget extends BuiltInTextWidget {
   const NumberListTextNodeWidget({
     Key? key,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
 
+  @override
   final TextNode textNode;
+  @override
   final EditorState editorState;
 
   @override
@@ -39,11 +42,8 @@ class NumberListTextNodeWidget extends StatefulWidget {
       _NumberListTextNodeWidgetState();
 }
 
-// customize
-const double _numberHorizontalPadding = 8;
-
 class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
-    with SelectableMixin, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
   @override
   final iconKey = GlobalKey();
 
@@ -53,31 +53,42 @@ class _NumberListTextNodeWidgetState extends State<NumberListTextNodeWidget>
   SelectableMixin<StatefulWidget> get forward =>
       _richTextKey.currentState as SelectableMixin;
 
+  @override
+  Offset get baseOffset {
+    return super.baseOffset.translate(0, padding.top);
+  }
+
   @override
   Widget build(BuildContext context) {
     return Padding(
-        padding: EdgeInsets.only(bottom: defaultLinePadding),
-        child: Row(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Padding(
-              key: iconKey,
-              padding: const EdgeInsets.symmetric(
-                  horizontal: _numberHorizontalPadding, vertical: 0),
-              child: Text(
-                '${widget.textNode.attributes.number.toString()}.',
-                style: const TextStyle(fontSize: 16),
-              ),
+      padding: padding,
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Container(
+            key: iconKey,
+            padding: iconPadding,
+            child: Text(
+              '${widget.textNode.attributes.number.toString()}.',
+              // FIXME: customize
+              style: const TextStyle(fontSize: 16.0, color: Colors.black),
             ),
-            Flexible(
-              child: FlowyRichText(
-                key: _richTextKey,
-                placeholderText: 'List',
-                textNode: widget.textNode,
-                editorState: widget.editorState,
-              ),
+          ),
+          Flexible(
+            child: FlowyRichText(
+              key: _richTextKey,
+              placeholderText: 'List',
+              textNode: widget.textNode,
+              editorState: widget.editorState,
+              lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
+              placeholderTextSpanDecorator: (textSpan) =>
+                  textSpan.updateTextStyle(textStyle),
+              textSpanDecorator: (textSpan) =>
+                  textSpan.updateTextStyle(textStyle),
             ),
-          ],
-        ));
+          ),
+        ],
+      ),
+    );
   }
 }

+ 14 - 8
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/quoted_text.dart

@@ -1,9 +1,9 @@
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
@@ -24,14 +24,16 @@ class QuotedTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
       });
 }
 
-class QuotedTextNodeWidget extends StatefulWidget {
+class QuotedTextNodeWidget extends BuiltInTextWidget {
   const QuotedTextNodeWidget({
     Key? key,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
 
+  @override
   final TextNode textNode;
+  @override
   final EditorState editorState;
 
   @override
@@ -41,30 +43,33 @@ class QuotedTextNodeWidget extends StatefulWidget {
 // customize
 
 class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
-    with SelectableMixin, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
   @override
   final iconKey = GlobalKey();
 
   final _richTextKey = GlobalKey(debugLabel: 'quoted_text');
-  final _iconWidth = 20.0;
-  final _iconRightPadding = 5.0;
 
   @override
   SelectableMixin<StatefulWidget> get forward =>
       _richTextKey.currentState as SelectableMixin;
 
+  @override
+  Offset get baseOffset {
+    return super.baseOffset.translate(0, padding.top);
+  }
+
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: EdgeInsets.only(bottom: defaultLinePadding),
+      padding: padding,
       child: IntrinsicHeight(
         child: Row(
           crossAxisAlignment: CrossAxisAlignment.stretch,
           children: [
             FlowySvg(
               key: iconKey,
-              width: _iconWidth,
-              padding: EdgeInsets.only(right: _iconRightPadding),
+              width: iconSize?.width,
+              padding: iconPadding,
               name: 'quote',
             ),
             Flexible(
@@ -72,6 +77,7 @@ class _QuotedTextNodeWidgetState extends State<QuotedTextNodeWidget>
                 key: _richTextKey,
                 placeholderText: 'Quote',
                 textNode: widget.textNode,
+                lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
                 editorState: widget.editorState,
               ),
             ),

+ 12 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text.dart

@@ -1,8 +1,8 @@
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
+import 'package:appflowy_editor/src/render/rich_text/built_in_text_widget.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
 import 'package:appflowy_editor/src/render/rich_text/flowy_rich_text.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection/selectable.dart';
 import 'package:appflowy_editor/src/service/render_plugin_service.dart';
 import 'package:flutter/material.dart';
@@ -23,14 +23,16 @@ class RichTextNodeWidgetBuilder extends NodeWidgetBuilder<TextNode> {
       });
 }
 
-class RichTextNodeWidget extends StatefulWidget {
+class RichTextNodeWidget extends BuiltInTextWidget {
   const RichTextNodeWidget({
     Key? key,
     required this.textNode,
     required this.editorState,
   }) : super(key: key);
 
+  @override
   final TextNode textNode;
+  @override
   final EditorState editorState;
 
   @override
@@ -40,7 +42,7 @@ class RichTextNodeWidget extends StatefulWidget {
 // customize
 
 class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
-    with SelectableMixin, DefaultSelectable {
+    with SelectableMixin, DefaultSelectable, BuiltInStyleMixin {
   @override
   GlobalKey? get iconKey => null;
 
@@ -50,13 +52,19 @@ class _RichTextNodeWidgetState extends State<RichTextNodeWidget>
   SelectableMixin<StatefulWidget> get forward =>
       _richTextKey.currentState as SelectableMixin;
 
+  @override
+  Offset get baseOffset {
+    return padding.topLeft;
+  }
+
   @override
   Widget build(BuildContext context) {
     return Padding(
-      padding: EdgeInsets.only(bottom: defaultLinePadding),
+      padding: padding,
       child: FlowyRichText(
         key: _richTextKey,
         textNode: widget.textNode,
+        lineHeight: widget.editorState.editorStyle.textStyle.lineHeight,
         editorState: widget.editorState,
       ),
     );

+ 0 - 282
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/rich_text_style.dart

@@ -1,282 +0,0 @@
-import 'package:appflowy_editor/src/document/attributes.dart';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart';
-
-///
-/// Supported partial rendering types:
-///   bold, italic,
-///   underline, strikethrough,
-///   color, font,
-///   href
-///
-/// Supported global rendering types:
-///   heading: h1, h2, h3, h4, h5, h6, ...
-///   block quote,
-///   list: ordered list, bulleted list,
-///   code block
-///
-class StyleKey {
-  static String bold = 'bold';
-  static String italic = 'italic';
-  static String underline = 'underline';
-  static String strikethrough = 'strikethrough';
-  static String color = 'color';
-  static String backgroundColor = 'backgroundColor';
-  static String font = 'font';
-  static String href = 'href';
-
-  static String subtype = 'subtype';
-  static String heading = 'heading';
-  static String h1 = 'h1';
-  static String h2 = 'h2';
-  static String h3 = 'h3';
-  static String h4 = 'h4';
-  static String h5 = 'h5';
-  static String h6 = 'h6';
-
-  static String bulletedList = 'bulleted-list';
-  static String numberList = 'number-list';
-
-  static String quote = 'quote';
-  static String checkbox = 'checkbox';
-  static String code = 'code';
-  static String number = 'number';
-
-  static List<String> partialStyleKeys = [
-    StyleKey.bold,
-    StyleKey.italic,
-    StyleKey.underline,
-    StyleKey.strikethrough,
-    StyleKey.backgroundColor,
-    StyleKey.href,
-    StyleKey.code,
-  ];
-
-  static List<String> globalStyleKeys = [
-    StyleKey.subtype,
-    StyleKey.heading,
-    StyleKey.checkbox,
-    StyleKey.bulletedList,
-    StyleKey.numberList,
-    StyleKey.quote,
-  ];
-}
-
-// TODO: customize
-double defaultLinePadding = 8.0;
-double baseFontSize = 16.0;
-String defaultHighlightColor = '0x6000BCF0';
-String defaultBackgroundColor = '0x00000000';
-// TODO: customize.
-Map<String, double> headingToFontSize = {
-  StyleKey.h1: baseFontSize + 15,
-  StyleKey.h2: baseFontSize + 12,
-  StyleKey.h3: baseFontSize + 9,
-  StyleKey.h4: baseFontSize + 6,
-  StyleKey.h5: baseFontSize + 3,
-  StyleKey.h6: baseFontSize,
-};
-
-extension NodeAttributesExtensions on Attributes {
-  String? get heading {
-    if (containsKey(StyleKey.subtype) &&
-        containsKey(StyleKey.heading) &&
-        this[StyleKey.subtype] == StyleKey.heading &&
-        this[StyleKey.heading] is String) {
-      return this[StyleKey.heading];
-    }
-    return null;
-  }
-
-  double get fontSize {
-    if (heading != null) {
-      return headingToFontSize[heading]!;
-    }
-    return baseFontSize;
-  }
-
-  bool get quote {
-    return containsKey(StyleKey.quote);
-  }
-
-  Color? get quoteColor {
-    if (quote) {
-      return Colors.grey;
-    }
-    return null;
-  }
-
-  int? get number {
-    if (containsKey(StyleKey.number) && this[StyleKey.number] is int) {
-      return this[StyleKey.number];
-    }
-    return null;
-  }
-
-  bool get code {
-    if (containsKey(StyleKey.code) && this[StyleKey.code] == true) {
-      return this[StyleKey.code];
-    }
-    return false;
-  }
-
-  bool get check {
-    if (containsKey(StyleKey.checkbox) && this[StyleKey.checkbox] is bool) {
-      return this[StyleKey.checkbox];
-    }
-    return false;
-  }
-}
-
-extension DeltaAttributesExtensions on Attributes {
-  bool get bold {
-    return (containsKey(StyleKey.bold) && this[StyleKey.bold] == true);
-  }
-
-  bool get italic {
-    return (containsKey(StyleKey.italic) && this[StyleKey.italic] == true);
-  }
-
-  bool get underline {
-    return (containsKey(StyleKey.underline) &&
-        this[StyleKey.underline] == true);
-  }
-
-  bool get strikethrough {
-    return (containsKey(StyleKey.strikethrough) &&
-        this[StyleKey.strikethrough] == true);
-  }
-
-  Color? get color {
-    if (containsKey(StyleKey.color) && this[StyleKey.color] is String) {
-      return Color(
-        int.parse(this[StyleKey.color]),
-      );
-    }
-    return null;
-  }
-
-  Color? get backgroundColor {
-    if (containsKey(StyleKey.backgroundColor) &&
-        this[StyleKey.backgroundColor] is String) {
-      return Color(
-        int.parse(this[StyleKey.backgroundColor]),
-      );
-    }
-    return null;
-  }
-
-  String? get font {
-    // TODO: unspport now.
-    return null;
-  }
-
-  String? get href {
-    if (containsKey(StyleKey.href) && this[StyleKey.href] is String) {
-      return this[StyleKey.href];
-    }
-    return null;
-  }
-}
-
-class RichTextStyle {
-  // TODO: customize
-  RichTextStyle({
-    required this.attributes,
-    required this.text,
-    this.gestureRecognizer,
-    this.height = 1.5,
-  });
-
-  final Attributes attributes;
-  final String text;
-  final GestureRecognizer? gestureRecognizer;
-  final double height;
-
-  TextSpan toTextSpan() => _toTextSpan(height);
-
-  double get topPadding {
-    return 0;
-  }
-
-  TextSpan _toTextSpan(double? height) {
-    return TextSpan(
-      text: text,
-      recognizer: _recognizer,
-      style: TextStyle(
-        fontWeight: _fontWeight,
-        fontStyle: _fontStyle,
-        fontSize: _fontSize,
-        color: _textColor,
-        decoration: _textDecoration,
-        background: _background,
-        height: height,
-      ),
-    );
-  }
-
-  Paint? get _background {
-    if (_backgroundColor != null) {
-      return Paint()
-        ..color = _backgroundColor!
-        ..strokeWidth = 24.0
-        ..style = PaintingStyle.fill
-        ..strokeJoin = StrokeJoin.round;
-    }
-    return null;
-  }
-
-  // bold
-  FontWeight get _fontWeight {
-    if (attributes.bold) {
-      return FontWeight.bold;
-    }
-    return FontWeight.normal;
-  }
-
-  // underline or strikethrough
-  TextDecoration get _textDecoration {
-    var decorations = [TextDecoration.none];
-    if (attributes.underline || attributes.href != null) {
-      decorations.add(TextDecoration.underline);
-    }
-    if (attributes.strikethrough) {
-      decorations.add(TextDecoration.lineThrough);
-    }
-    return TextDecoration.combine(decorations);
-  }
-
-  // font
-  FontStyle get _fontStyle =>
-      attributes.italic ? FontStyle.italic : FontStyle.normal;
-
-  // text color
-  Color get _textColor {
-    if (attributes.href != null) {
-      return Colors.lightBlue;
-    }
-    if (attributes.code) {
-      return Colors.lightBlue.withOpacity(0.8);
-    }
-    return attributes.color ?? Colors.black;
-  }
-
-  Color? get _backgroundColor {
-    if (attributes.backgroundColor != null) {
-      return attributes.backgroundColor!;
-    } else if (attributes.code) {
-      return Colors.blue.shade300.withOpacity(0.3);
-    }
-    return null;
-  }
-
-  // font size
-  double get _fontSize {
-    return baseFontSize;
-  }
-
-  // recognizer
-  GestureRecognizer? get _recognizer {
-    return gestureRecognizer;
-  }
-}

+ 21 - 13
frontend/app_flowy/packages/appflowy_editor/lib/src/render/selection_menu/selection_menu_service.dart

@@ -1,10 +1,12 @@
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
+import 'package:appflowy_editor/src/l10n/l10n.dart';
 import 'package:appflowy_editor/src/render/image/image_upload_widget.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
 import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
+
 import 'package:flutter/material.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 abstract class SelectionMenuService {
   Offset get topLeft;
@@ -54,7 +56,13 @@ class SelectionMenu implements SelectionMenuService {
     if (selectionRects.isEmpty) {
       return;
     }
-    final offset = selectionRects.first.bottomRight + const Offset(10, 10);
+    // Workaround: We can customize the padding through the [EditorStyle],
+    //  but the coordinates of overlay are not properly converted currently.
+    //  Just subtract the padding here as a result.
+    final baseOffset =
+        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
+    final offset =
+        selectionRects.first.bottomRight + const Offset(10, 10) - baseOffset;
     _topLeft = offset;
 
     _selectionMenuEntry = OverlayEntry(builder: (context) {
@@ -116,7 +124,7 @@ List<SelectionMenuItem> get defaultSelectionMenuItems =>
     _defaultSelectionMenuItems;
 final List<SelectionMenuItem> _defaultSelectionMenuItems = [
   SelectionMenuItem(
-    name: 'Text',
+    name: AppFlowyEditorLocalizations.current.text,
     icon: _selectionMenuIcon('text'),
     keywords: ['text'],
     handler: (editorState, _, __) {
@@ -124,37 +132,37 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
     },
   ),
   SelectionMenuItem(
-    name: 'Heading 1',
+    name: AppFlowyEditorLocalizations.current.heading1,
     icon: _selectionMenuIcon('h1'),
     keywords: ['heading 1, h1'],
     handler: (editorState, _, __) {
-      insertHeadingAfterSelection(editorState, StyleKey.h1);
+      insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h1);
     },
   ),
   SelectionMenuItem(
-    name: 'Heading 2',
+    name: AppFlowyEditorLocalizations.current.heading2,
     icon: _selectionMenuIcon('h2'),
     keywords: ['heading 2, h2'],
     handler: (editorState, _, __) {
-      insertHeadingAfterSelection(editorState, StyleKey.h2);
+      insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h2);
     },
   ),
   SelectionMenuItem(
-    name: 'Heading 3',
+    name: AppFlowyEditorLocalizations.current.heading3,
     icon: _selectionMenuIcon('h3'),
     keywords: ['heading 3, h3'],
     handler: (editorState, _, __) {
-      insertHeadingAfterSelection(editorState, StyleKey.h3);
+      insertHeadingAfterSelection(editorState, BuiltInAttributeKey.h3);
     },
   ),
   SelectionMenuItem(
-    name: 'Image',
+    name: AppFlowyEditorLocalizations.current.image,
     icon: _selectionMenuIcon('image'),
     keywords: ['image'],
     handler: showImageUploadMenu,
   ),
   SelectionMenuItem(
-    name: 'Bulleted list',
+    name: AppFlowyEditorLocalizations.current.bulletedList,
     icon: _selectionMenuIcon('bulleted_list'),
     keywords: ['bulleted list', 'list', 'unordered list'],
     handler: (editorState, _, __) {
@@ -162,7 +170,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
     },
   ),
   SelectionMenuItem(
-    name: 'Checkbox',
+    name: AppFlowyEditorLocalizations.current.checkbox,
     icon: _selectionMenuIcon('checkbox'),
     keywords: ['todo list', 'list', 'checkbox list'],
     handler: (editorState, _, __) {
@@ -170,7 +178,7 @@ final List<SelectionMenuItem> _defaultSelectionMenuItems = [
     },
   ),
   SelectionMenuItem(
-    name: 'Quote',
+    name: AppFlowyEditorLocalizations.current.quote,
     icon: _selectionMenuIcon('quote'),
     keywords: ['quote', 'refer'],
     handler: (editorState, _, __) {

+ 239 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/render/style/editor_style.dart

@@ -1,20 +1,254 @@
 import 'package:flutter/material.dart';
 
+import 'package:appflowy_editor/src/document/node.dart';
+import 'package:appflowy_editor/src/editor_state.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
+
+typedef PluginStyler = Object Function(EditorState editorState, Node node);
+typedef PluginStyle = Map<String, PluginStyler>;
+
 /// Editor style configuration
 class EditorStyle {
-  const EditorStyle({
+  EditorStyle({
     required this.padding,
-  });
+    required this.textStyle,
+    required this.cursorColor,
+    required this.selectionColor,
+    Map<String, PluginStyle> pluginStyles = const {},
+  }) {
+    _pluginStyles.addAll(pluginStyles);
+  }
 
-  const EditorStyle.defaultStyle()
-      : padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0);
+  EditorStyle.defaultStyle()
+      : padding = const EdgeInsets.fromLTRB(200.0, 0.0, 200.0, 0.0),
+        textStyle = BuiltInTextStyle.builtIn(),
+        cursorColor = const Color(0xFF00BCF0),
+        selectionColor = const Color.fromARGB(53, 111, 201, 231);
 
   /// The margin of the document context from the editor.
   final EdgeInsets padding;
+  final BuiltInTextStyle textStyle;
+  final Color cursorColor;
+  final Color selectionColor;
+
+  final Map<String, PluginStyle> _pluginStyles = Map.from(builtInTextStylers);
+
+  Object? style(EditorState editorState, Node node, String key) {
+    final styler = _pluginStyles[node.id]?[key];
+    if (styler != null) {
+      return styler(editorState, node);
+    }
+    return null;
+  }
 
-  EditorStyle copyWith({EdgeInsets? padding}) {
+  EditorStyle copyWith({
+    EdgeInsets? padding,
+    BuiltInTextStyle? textStyle,
+    Color? cursorColor,
+    Color? selectionColor,
+    Map<String, PluginStyle>? pluginStyles,
+  }) {
     return EditorStyle(
       padding: padding ?? this.padding,
+      textStyle: textStyle ?? this.textStyle,
+      cursorColor: cursorColor ?? this.cursorColor,
+      selectionColor: selectionColor ?? this.selectionColor,
+      pluginStyles: pluginStyles ?? {},
+    );
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is EditorStyle &&
+        other.padding == padding &&
+        other.textStyle == textStyle &&
+        other.cursorColor == cursorColor &&
+        other.selectionColor == selectionColor;
+  }
+
+  @override
+  int get hashCode {
+    return padding.hashCode ^
+        textStyle.hashCode ^
+        cursorColor.hashCode ^
+        selectionColor.hashCode;
+  }
+}
+
+PluginStyle get builtInPluginStyle => Map.from({
+      'padding': (_, __) => const EdgeInsets.symmetric(vertical: 8.0),
+      'textStyle': (_, __) => const TextStyle(),
+      'iconSize': (_, __) => const Size.square(20.0),
+      'iconPadding': (_, __) => const EdgeInsets.only(right: 5.0),
+    });
+
+Map<String, PluginStyle> builtInTextStylers = {
+  'text': builtInPluginStyle,
+  'text/checkbox': builtInPluginStyle
+    ..update(
+      'textStyle',
+      (_) => (EditorState editorState, Node node) {
+        if (node is TextNode && node.attributes.check == true) {
+          return const TextStyle(
+            color: Colors.grey,
+            decoration: TextDecoration.lineThrough,
+          );
+        }
+        return const TextStyle();
+      },
+    ),
+  'text/heading': builtInPluginStyle
+    ..update(
+      'textStyle',
+      (_) => (EditorState editorState, Node node) {
+        final headingToFontSize = {
+          'h1': 32.0,
+          'h2': 28.0,
+          'h3': 24.0,
+          'h4': 18.0,
+          'h5': 18.0,
+          'h6': 18.0,
+        };
+        final fontSize = headingToFontSize[node.attributes.heading] ?? 18.0;
+        return TextStyle(fontSize: fontSize, fontWeight: FontWeight.bold);
+      },
+    ),
+  'text/bulleted-list': builtInPluginStyle,
+  'text/number-list': builtInPluginStyle
+    ..update(
+      'iconPadding',
+      (_) => (EditorState editorState, Node node) {
+        return const EdgeInsets.only(left: 5.0, right: 5.0);
+      },
+    ),
+  'text/quote': builtInPluginStyle,
+  'image': builtInPluginStyle,
+};
+
+class BuiltInTextStyle {
+  const BuiltInTextStyle({
+    required this.defaultTextStyle,
+    required this.defaultPlaceholderTextStyle,
+    required this.bold,
+    required this.italic,
+    required this.underline,
+    required this.strikethrough,
+    required this.href,
+    required this.code,
+    this.highlightColorHex = '0x6000BCF0',
+    this.lineHeight = 1.5,
+  });
+
+  final TextStyle defaultTextStyle;
+  final TextStyle defaultPlaceholderTextStyle;
+  final TextStyle bold;
+  final TextStyle italic;
+  final TextStyle underline;
+  final TextStyle strikethrough;
+  final TextStyle href;
+  final TextStyle code;
+  final String highlightColorHex;
+  final double lineHeight;
+
+  BuiltInTextStyle.builtIn()
+      : defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.black),
+        defaultPlaceholderTextStyle =
+            const TextStyle(fontSize: 16.0, color: Colors.grey),
+        bold = const TextStyle(fontWeight: FontWeight.bold),
+        italic = const TextStyle(fontStyle: FontStyle.italic),
+        underline = const TextStyle(decoration: TextDecoration.underline),
+        strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
+        href = const TextStyle(
+          color: Colors.blue,
+          decoration: TextDecoration.underline,
+        ),
+        code = const TextStyle(
+          fontFamily: 'monospace',
+          color: Color(0xFF00BCF0),
+          backgroundColor: Color(0xFFE0F8FF),
+        ),
+        highlightColorHex = '0x6000BCF0',
+        lineHeight = 1.5;
+
+  BuiltInTextStyle.builtInDarkMode()
+      : defaultTextStyle = const TextStyle(fontSize: 16.0, color: Colors.white),
+        defaultPlaceholderTextStyle = TextStyle(
+          fontSize: 16.0,
+          color: Colors.white.withOpacity(0.3),
+        ),
+        bold = const TextStyle(fontWeight: FontWeight.bold),
+        italic = const TextStyle(fontStyle: FontStyle.italic),
+        underline = const TextStyle(decoration: TextDecoration.underline),
+        strikethrough = const TextStyle(decoration: TextDecoration.lineThrough),
+        href = const TextStyle(
+          color: Colors.blue,
+          decoration: TextDecoration.underline,
+        ),
+        code = const TextStyle(
+          fontFamily: 'monospace',
+          color: Color(0xFF00BCF0),
+          backgroundColor: Color(0xFFE0F8FF),
+        ),
+        highlightColorHex = '0x6000BCF0',
+        lineHeight = 1.5;
+
+  BuiltInTextStyle copyWith({
+    TextStyle? defaultTextStyle,
+    TextStyle? defaultPlaceholderTextStyle,
+    TextStyle? bold,
+    TextStyle? italic,
+    TextStyle? underline,
+    TextStyle? strikethrough,
+    TextStyle? href,
+    TextStyle? code,
+    String? highlightColorHex,
+    double? lineHeight,
+  }) {
+    return BuiltInTextStyle(
+      defaultTextStyle: defaultTextStyle ?? this.defaultTextStyle,
+      defaultPlaceholderTextStyle:
+          defaultPlaceholderTextStyle ?? this.defaultPlaceholderTextStyle,
+      bold: bold ?? this.bold,
+      italic: italic ?? this.italic,
+      underline: underline ?? this.underline,
+      strikethrough: strikethrough ?? this.strikethrough,
+      href: href ?? this.href,
+      code: code ?? this.code,
+      highlightColorHex: highlightColorHex ?? this.highlightColorHex,
+      lineHeight: lineHeight ?? this.lineHeight,
     );
   }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is BuiltInTextStyle &&
+        other.defaultTextStyle == defaultTextStyle &&
+        other.defaultPlaceholderTextStyle == defaultPlaceholderTextStyle &&
+        other.bold == bold &&
+        other.italic == italic &&
+        other.underline == underline &&
+        other.strikethrough == strikethrough &&
+        other.href == href &&
+        other.code == code &&
+        other.highlightColorHex == highlightColorHex &&
+        other.lineHeight == lineHeight;
+  }
+
+  @override
+  int get hashCode {
+    return defaultTextStyle.hashCode ^
+        defaultPlaceholderTextStyle.hashCode ^
+        bold.hashCode ^
+        italic.hashCode ^
+        underline.hashCode ^
+        strikethrough.hashCode ^
+        href.hashCode ^
+        code.hashCode ^
+        highlightColorHex.hashCode ^
+        lineHeight.hashCode;
+  }
 }

+ 51 - 38
frontend/app_flowy/packages/appflowy_editor/lib/src/render/toolbar/toolbar_item.dart

@@ -2,12 +2,13 @@ import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/extensions/url_launcher_extension.dart';
 import 'package:appflowy_editor/src/infra/flowy_svg.dart';
 import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/extensions/editor_state_extensions.dart';
 import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
+
 import 'package:flutter/material.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 typedef ToolbarItemEventHandler = void Function(
     EditorState editorState, BuildContext context);
@@ -63,7 +64,7 @@ List<ToolbarItem> defaultToolbarItems = [
   ToolbarItem(
     id: 'appflowy.toolbar.h1',
     type: 1,
-    tooltipsMessage: 'Heading 1',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.heading1,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/h1',
       color: isHighlight ? Colors.lightBlue : null,
@@ -71,15 +72,16 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _onlyShowInSingleTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.heading,
-      (value) => value == StyleKey.h1,
+      BuiltInAttributeKey.heading,
+      (value) => value == BuiltInAttributeKey.h1,
     ),
-    handler: (editorState, context) => formatHeading(editorState, StyleKey.h1),
+    handler: (editorState, context) =>
+        formatHeading(editorState, BuiltInAttributeKey.h1),
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.h2',
     type: 1,
-    tooltipsMessage: 'Heading 2',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.heading2,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/h2',
       color: isHighlight ? Colors.lightBlue : null,
@@ -87,15 +89,16 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _onlyShowInSingleTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.heading,
-      (value) => value == StyleKey.h2,
+      BuiltInAttributeKey.heading,
+      (value) => value == BuiltInAttributeKey.h2,
     ),
-    handler: (editorState, context) => formatHeading(editorState, StyleKey.h2),
+    handler: (editorState, context) =>
+        formatHeading(editorState, BuiltInAttributeKey.h2),
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.h3',
     type: 1,
-    tooltipsMessage: 'Heading 3',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.heading3,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/h3',
       color: isHighlight ? Colors.lightBlue : null,
@@ -103,15 +106,16 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _onlyShowInSingleTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.heading,
-      (value) => value == StyleKey.h3,
+      BuiltInAttributeKey.heading,
+      (value) => value == BuiltInAttributeKey.h3,
     ),
-    handler: (editorState, context) => formatHeading(editorState, StyleKey.h3),
+    handler: (editorState, context) =>
+        formatHeading(editorState, BuiltInAttributeKey.h3),
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.bold',
     type: 2,
-    tooltipsMessage: 'Bold',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.bold,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/bold',
       color: isHighlight ? Colors.lightBlue : null,
@@ -119,7 +123,7 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _showInTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.bold,
+      BuiltInAttributeKey.bold,
       (value) => value == true,
     ),
     handler: (editorState, context) => formatBold(editorState),
@@ -127,7 +131,7 @@ List<ToolbarItem> defaultToolbarItems = [
   ToolbarItem(
     id: 'appflowy.toolbar.italic',
     type: 2,
-    tooltipsMessage: 'Italic',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.italic,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/italic',
       color: isHighlight ? Colors.lightBlue : null,
@@ -135,7 +139,7 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _showInTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.italic,
+      BuiltInAttributeKey.italic,
       (value) => value == true,
     ),
     handler: (editorState, context) => formatItalic(editorState),
@@ -143,7 +147,7 @@ List<ToolbarItem> defaultToolbarItems = [
   ToolbarItem(
     id: 'appflowy.toolbar.underline',
     type: 2,
-    tooltipsMessage: 'Underline',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.underline,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/underline',
       color: isHighlight ? Colors.lightBlue : null,
@@ -151,7 +155,7 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _showInTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.underline,
+      BuiltInAttributeKey.underline,
       (value) => value == true,
     ),
     handler: (editorState, context) => formatUnderline(editorState),
@@ -159,7 +163,7 @@ List<ToolbarItem> defaultToolbarItems = [
   ToolbarItem(
     id: 'appflowy.toolbar.strikethrough',
     type: 2,
-    tooltipsMessage: 'Strikethrough',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.strikethrough,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/strikethrough',
       color: isHighlight ? Colors.lightBlue : null,
@@ -167,7 +171,7 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _showInTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.strikethrough,
+      BuiltInAttributeKey.strikethrough,
       (value) => value == true,
     ),
     handler: (editorState, context) => formatStrikethrough(editorState),
@@ -175,7 +179,7 @@ List<ToolbarItem> defaultToolbarItems = [
   ToolbarItem(
     id: 'appflowy.toolbar.code',
     type: 2,
-    tooltipsMessage: 'Embed Code',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.embedCode,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/code',
       color: isHighlight ? Colors.lightBlue : null,
@@ -183,15 +187,15 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _showInTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.code,
-      (value) => value == StyleKey.code,
+      BuiltInAttributeKey.code,
+      (value) => value == true,
     ),
     handler: (editorState, context) => formatEmbedCode(editorState),
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.quote',
     type: 3,
-    tooltipsMessage: 'Quote',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.quote,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/quote',
       color: isHighlight ? Colors.lightBlue : null,
@@ -199,15 +203,15 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _onlyShowInSingleTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.subtype,
-      (value) => value == StyleKey.quote,
+      BuiltInAttributeKey.subtype,
+      (value) => value == BuiltInAttributeKey.quote,
     ),
     handler: (editorState, context) => formatQuote(editorState),
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.bulleted_list',
     type: 3,
-    tooltipsMessage: 'Bulleted list',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.bulletedList,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/bulleted_list',
       color: isHighlight ? Colors.lightBlue : null,
@@ -215,15 +219,15 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _onlyShowInSingleTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.subtype,
-      (value) => value == StyleKey.bulletedList,
+      BuiltInAttributeKey.subtype,
+      (value) => value == BuiltInAttributeKey.bulletedList,
     ),
     handler: (editorState, context) => formatBulletedList(editorState),
   ),
   ToolbarItem(
     id: 'appflowy.toolbar.link',
     type: 4,
-    tooltipsMessage: 'Link',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.link,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/link',
       color: isHighlight ? Colors.lightBlue : null,
@@ -231,7 +235,7 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _onlyShowInSingleTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.href,
+      BuiltInAttributeKey.href,
       (value) => value != null,
     ),
     handler: (editorState, context) => showLinkMenu(context, editorState),
@@ -239,7 +243,7 @@ List<ToolbarItem> defaultToolbarItems = [
   ToolbarItem(
     id: 'appflowy.toolbar.highlight',
     type: 4,
-    tooltipsMessage: 'Highlight',
+    tooltipsMessage: AppFlowyEditorLocalizations.current.highlight,
     iconBuilder: (isHighlight) => FlowySvg(
       name: 'toolbar/highlight',
       color: isHighlight ? Colors.lightBlue : null,
@@ -247,10 +251,13 @@ List<ToolbarItem> defaultToolbarItems = [
     validator: _showInTextSelection,
     highlightCallback: (editorState) => _allSatisfy(
       editorState,
-      StyleKey.backgroundColor,
+      BuiltInAttributeKey.backgroundColor,
       (value) => value != null,
     ),
-    handler: (editorState, context) => formatHighlight(editorState),
+    handler: (editorState, context) => formatHighlight(
+      editorState,
+      editorState.editorStyle.textStyle.highlightColorHex,
+    ),
   ),
 ];
 
@@ -296,6 +303,9 @@ void showLinkMenu(
       matchRect = rect;
     }
   }
+  final baseOffset =
+      editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
+  matchRect = matchRect.shift(-baseOffset);
 
   _dismissLinkMenu();
   _editorState = editorState;
@@ -314,7 +324,8 @@ void showLinkMenu(
   final textNode = node.first as TextNode;
   String? linkText;
   if (textNode.allSatisfyLinkInSelection(selection)) {
-    linkText = textNode.getAttributeInSelection(selection, StyleKey.href);
+    linkText =
+        textNode.getAttributeInSelection(selection, BuiltInAttributeKey.href);
   }
   _linkMenuOverlay = OverlayEntry(builder: (context) {
     return Positioned(
@@ -328,7 +339,8 @@ void showLinkMenu(
           },
           onSubmitted: (text) {
             TransactionBuilder(editorState)
-              ..formatText(textNode, index, length, {StyleKey.href: text})
+              ..formatText(
+                  textNode, index, length, {BuiltInAttributeKey.href: text})
               ..commit();
             _dismissLinkMenu();
           },
@@ -338,7 +350,8 @@ void showLinkMenu(
           },
           onRemoveLink: () {
             TransactionBuilder(editorState)
-              ..formatText(textNode, index, length, {StyleKey.href: null})
+              ..formatText(
+                  textNode, index, length, {BuiltInAttributeKey.href: null})
               ..commit();
             _dismissLinkMenu();
           },

+ 40 - 26
frontend/app_flowy/packages/appflowy_editor/lib/src/service/default_text_operations/format_rich_text_style.dart

@@ -6,31 +6,31 @@ import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/extensions/path_extensions.dart';
 import 'package:appflowy_editor/src/operation/transaction_builder.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void insertHeadingAfterSelection(EditorState editorState, String heading) {
   insertTextNodeAfterSelection(editorState, {
-    StyleKey.subtype: StyleKey.heading,
-    StyleKey.heading: heading,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+    BuiltInAttributeKey.heading: heading,
   });
 }
 
 void insertQuoteAfterSelection(EditorState editorState) {
   insertTextNodeAfterSelection(editorState, {
-    StyleKey.subtype: StyleKey.quote,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
   });
 }
 
 void insertCheckboxAfterSelection(EditorState editorState) {
   insertTextNodeAfterSelection(editorState, {
-    StyleKey.subtype: StyleKey.checkbox,
-    StyleKey.checkbox: false,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+    BuiltInAttributeKey.checkbox: false,
   });
 }
 
 void insertBulletedListAfterSelection(EditorState editorState) {
   insertTextNodeAfterSelection(editorState, {
-    StyleKey.subtype: StyleKey.bulletedList,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
   });
 }
 
@@ -68,27 +68,27 @@ void formatText(EditorState editorState) {
 
 void formatHeading(EditorState editorState, String heading) {
   formatTextNodes(editorState, {
-    StyleKey.subtype: StyleKey.heading,
-    StyleKey.heading: heading,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+    BuiltInAttributeKey.heading: heading,
   });
 }
 
 void formatQuote(EditorState editorState) {
   formatTextNodes(editorState, {
-    StyleKey.subtype: StyleKey.quote,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
   });
 }
 
-void formatCheckbox(EditorState editorState) {
+void formatCheckbox(EditorState editorState, bool check) {
   formatTextNodes(editorState, {
-    StyleKey.subtype: StyleKey.checkbox,
-    StyleKey.checkbox: false,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+    BuiltInAttributeKey.checkbox: check,
   });
 }
 
 void formatBulletedList(EditorState editorState) {
   formatTextNodes(editorState, {
-    StyleKey.subtype: StyleKey.bulletedList,
+    BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
   });
 }
 
@@ -107,7 +107,7 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
       ..updateNode(
         textNode,
         Attributes.fromIterable(
-          StyleKey.globalStyleKeys,
+          BuiltInAttributeKey.globalStyleKeys,
           value: (_) => null,
         )..addAll(attributes),
       )
@@ -124,44 +124,58 @@ bool formatTextNodes(EditorState editorState, Attributes attributes) {
 }
 
 bool formatBold(EditorState editorState) {
-  return formatRichTextPartialStyle(editorState, StyleKey.bold);
+  return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.bold);
 }
 
 bool formatItalic(EditorState editorState) {
-  return formatRichTextPartialStyle(editorState, StyleKey.italic);
+  return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.italic);
 }
 
 bool formatUnderline(EditorState editorState) {
-  return formatRichTextPartialStyle(editorState, StyleKey.underline);
+  return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.underline);
 }
 
 bool formatStrikethrough(EditorState editorState) {
-  return formatRichTextPartialStyle(editorState, StyleKey.strikethrough);
+  return formatRichTextPartialStyle(
+      editorState, BuiltInAttributeKey.strikethrough);
 }
 
 bool formatEmbedCode(EditorState editorState) {
-  return formatRichTextPartialStyle(editorState, StyleKey.code);
+  return formatRichTextPartialStyle(editorState, BuiltInAttributeKey.code);
 }
 
-bool formatHighlight(EditorState editorState) {
+bool formatHighlight(EditorState editorState, String colorHex) {
   bool value = _allSatisfyInSelection(
-      editorState, StyleKey.backgroundColor, defaultHighlightColor);
-  return formatRichTextPartialStyle(editorState, StyleKey.backgroundColor,
-      customValue: value ? defaultBackgroundColor : defaultHighlightColor);
+    editorState,
+    BuiltInAttributeKey.backgroundColor,
+    colorHex,
+  );
+  return formatRichTextPartialStyle(
+    editorState,
+    BuiltInAttributeKey.backgroundColor,
+    customValue: value ? '0x00000000' : colorHex,
+  );
 }
 
 bool formatRichTextPartialStyle(EditorState editorState, String styleKey,
     {Object? customValue}) {
   Attributes attributes = {
     styleKey: customValue ??
-        !_allSatisfyInSelection(editorState, styleKey, customValue ?? true),
+        !_allSatisfyInSelection(
+          editorState,
+          styleKey,
+          customValue ?? true,
+        ),
   };
 
   return formatRichTextStyle(editorState, attributes);
 }
 
 bool _allSatisfyInSelection(
-    EditorState editorState, String styleKey, dynamic matchValue) {
+  EditorState editorState,
+  String styleKey,
+  dynamic matchValue,
+) {
   final selection = editorState.service.selectionService.currentSelection.value;
   final nodes = editorState.service.selectionService.currentSelectedNodes;
   final textNodes = nodes.whereType<TextNode>().toList(growable: false);

+ 19 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/service/editor_service.dart

@@ -38,7 +38,7 @@ class AppFlowyEditor extends StatefulWidget {
     this.customBuilders = const {},
     this.shortcutEvents = const [],
     this.selectionMenuItems = const [],
-    this.editorStyle = const EditorStyle.defaultStyle(),
+    required this.editorStyle,
   }) : super(key: key);
 
   final EditorState editorState;
@@ -58,6 +58,8 @@ class AppFlowyEditor extends StatefulWidget {
 }
 
 class _AppFlowyEditorState extends State<AppFlowyEditor> {
+  Widget? services;
+
   EditorState get editorState => widget.editorState;
 
   @override
@@ -75,19 +77,34 @@ class _AppFlowyEditorState extends State<AppFlowyEditor> {
 
     if (editorState.service != oldWidget.editorState.service) {
       editorState.selectionMenuItems = widget.selectionMenuItems;
-      editorState.editorStyle = widget.editorStyle;
       editorState.service.renderPluginService = _createRenderPlugin();
     }
+
+    editorState.editorStyle = widget.editorStyle;
+    services = null;
   }
 
   @override
   Widget build(BuildContext context) {
+    services ??= _buildServices(context);
+    return Overlay(
+      initialEntries: [
+        OverlayEntry(
+          builder: (context) => services!,
+        ),
+      ],
+    );
+  }
+
+  AppFlowyScroll _buildServices(BuildContext context) {
     return AppFlowyScroll(
       key: editorState.service.scrollServiceKey,
       child: Padding(
         padding: widget.editorStyle.padding,
         child: AppFlowySelection(
           key: editorState.service.selectionServiceKey,
+          cursorColor: widget.editorStyle.cursorColor,
+          selectionColor: widget.editorStyle.selectionColor,
           editorState: editorState,
           child: AppFlowyInput(
             key: editorState.service.inputServiceKey,

+ 9 - 8
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/backspace_handler.dart

@@ -1,8 +1,7 @@
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/appflowy_editor.dart';
 
 // Handle delete text.
@@ -42,12 +41,12 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
     if (index < 0 && selection.isCollapsed) {
       // 1. style
       if (textNode.subtype != null) {
-        if (textNode.subtype == StyleKey.numberList) {
+        if (textNode.subtype == BuiltInAttributeKey.numberList) {
           cancelNumberListPath = textNode.path;
         }
         transactionBuilder
           ..updateNode(textNode, {
-            StyleKey.subtype: null,
+            BuiltInAttributeKey.subtype: null,
             textNode.subtype!: null,
           })
           ..afterSelection = Selection.collapsed(
@@ -91,7 +90,8 @@ KeyEventResult _handleBackspace(EditorState editorState, RawKeyEvent event) {
     _deleteTextNodes(transactionBuilder, textNodes, selection);
     transactionBuilder.commit();
 
-    if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) {
+    if (nodeAtStart is TextNode &&
+        nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
       makeFollowingNodesIncremental(
         editorState,
         startPosition.path,
@@ -130,7 +130,7 @@ KeyEventResult _backDeleteToPreviousTextNode(
   bool prevIsNumberList = false;
   while (previous != null) {
     if (previous is TextNode) {
-      if (previous.subtype == StyleKey.numberList) {
+      if (previous.subtype == BuiltInAttributeKey.numberList) {
         prevIsNumberList = true;
       }
 
@@ -212,7 +212,8 @@ KeyEventResult _handleDelete(EditorState editorState, RawKeyEvent event) {
     _deleteTextNodes(transactionBuilder, textNodes, selection);
     transactionBuilder.commit();
 
-    if (nodeAtStart is TextNode && nodeAtStart.subtype == StyleKey.numberList) {
+    if (nodeAtStart is TextNode &&
+        nodeAtStart.subtype == BuiltInAttributeKey.numberList) {
       makeFollowingNodesIncremental(
           editorState, startPosition.path, transactionBuilder.afterSelection!);
     }
@@ -236,7 +237,7 @@ KeyEventResult _mergeNextLineIntoThisLine(
   transactionBuilder.deleteNode(nextNode);
   transactionBuilder.commit();
 
-  if (textNode.subtype == StyleKey.numberList) {
+  if (textNode.subtype == BuiltInAttributeKey.numberList) {
     makeFollowingNodesIncremental(editorState, textNode.path, selection);
   }
 

+ 3 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -1,7 +1,7 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/infra/html_converter.dart';
 import 'package:appflowy_editor/src/document/node_iterator.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/number_list_helper.dart';
 import 'package:flutter/material.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
@@ -108,8 +108,8 @@ void _pasteMultipleLinesInText(
 
   if (nodeAtPath.type == "text" && firstNode.type == "text") {
     int? startNumber;
-    if (nodeAtPath.subtype == StyleKey.numberList) {
-      startNumber = nodeAtPath.attributes[StyleKey.number] as int;
+    if (nodeAtPath.subtype == BuiltInAttributeKey.numberList) {
+      startNumber = nodeAtPath.attributes[BuiltInAttributeKey.number] as int;
     }
 
     // split and merge

+ 21 - 20
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
 import 'package:appflowy_editor/src/extensions/path_extensions.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import './number_list_helper.dart';
 
 /// Handle some cases where enter is pressed and shift is not pressed.
@@ -59,7 +59,8 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
       ..afterSelection = afterSelection
       ..commit();
 
-    if (startNode is TextNode && startNode.subtype == StyleKey.numberList) {
+    if (startNode is TextNode &&
+        startNode.subtype == BuiltInAttributeKey.numberList) {
       makeFollowingNodesIncremental(
           editorState, selection.start.path, afterSelection);
     }
@@ -82,17 +83,15 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
         Position(path: textNode.path, offset: 0),
       );
       TransactionBuilder(editorState)
-        ..updateNode(
-            textNode,
-            Attributes.fromIterable(
-              StyleKey.globalStyleKeys,
-              value: (_) => null,
-            ))
+        ..updateNode(textNode, {
+          BuiltInAttributeKey.subtype: null,
+        })
         ..afterSelection = afterSelection
         ..commit();
 
       final nextNode = textNode.next;
-      if (nextNode is TextNode && nextNode.subtype == StyleKey.numberList) {
+      if (nextNode is TextNode &&
+          nextNode.subtype == BuiltInAttributeKey.numberList) {
         makeFollowingNodesIncremental(
             editorState, textNode.path, afterSelection,
             beginNum: 0);
@@ -103,11 +102,13 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
         Position(path: textNode.path.next, offset: 0),
       );
 
-      if (subtype == StyleKey.numberList) {
-        final prevNumber = textNode.attributes[StyleKey.number] as int;
+      if (subtype == BuiltInAttributeKey.numberList) {
+        final prevNumber =
+            textNode.attributes[BuiltInAttributeKey.number] as int;
         final newNode = TextNode.empty();
-        newNode.attributes[StyleKey.subtype] = StyleKey.numberList;
-        newNode.attributes[StyleKey.number] = prevNumber;
+        newNode.attributes[BuiltInAttributeKey.subtype] =
+            BuiltInAttributeKey.numberList;
+        newNode.attributes[BuiltInAttributeKey.number] = prevNumber;
         final insertPath = textNode.path;
         TransactionBuilder(editorState)
           ..insertNode(
@@ -159,7 +160,7 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
 
   // If the new type of a text node is number list,
   // the numbers of the following nodes should be incremental.
-  if (textNode.subtype == StyleKey.numberList) {
+  if (textNode.subtype == BuiltInAttributeKey.numberList) {
     makeFollowingNodesIncremental(editorState, nextPath, afterSelection);
   }
 
@@ -169,17 +170,17 @@ ShortcutEventHandler enterWithoutShiftInTextNodesHandler =
 Attributes _attributesFromPreviousLine(TextNode textNode) {
   final prevAttributes = textNode.attributes;
   final subType = textNode.subtype;
-  if (subType == null || subType == StyleKey.heading) {
+  if (subType == null || subType == BuiltInAttributeKey.heading) {
     return {};
   }
 
   final copy = Attributes.from(prevAttributes);
-  if (subType == StyleKey.numberList) {
+  if (subType == BuiltInAttributeKey.numberList) {
     return _nextNumberAttributesFromPreviousLine(copy, textNode);
   }
 
-  if (subType == StyleKey.checkbox) {
-    copy[StyleKey.checkbox] = false;
+  if (subType == BuiltInAttributeKey.checkbox) {
+    copy[BuiltInAttributeKey.checkbox] = false;
     return copy;
   }
 
@@ -188,7 +189,7 @@ Attributes _attributesFromPreviousLine(TextNode textNode) {
 
 Attributes _nextNumberAttributesFromPreviousLine(
     Attributes copy, TextNode textNode) {
-  final prevNum = textNode.attributes[StyleKey.number] as int?;
-  copy[StyleKey.number] = prevNum == null ? 1 : prevNum + 1;
+  final prevNum = textNode.attributes[BuiltInAttributeKey.number] as int?;
+  copy[BuiltInAttributeKey.number] = prevNum == null ? 1 : prevNum + 1;
   return copy;
 }

+ 16 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/format_style_handler.dart

@@ -1,8 +1,8 @@
+import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
 import 'package:flutter/material.dart';
 
 import 'package:appflowy_editor/src/document/node.dart';
-import 'package:appflowy_editor/src/service/default_text_operations/format_rich_text_style.dart';
 
 ShortcutEventHandler formatBoldEventHandler = (editorState, event) {
   final selection = editorState.service.selectionService.currentSelection.value;
@@ -55,7 +55,10 @@ ShortcutEventHandler formatHighlightEventHandler = (editorState, event) {
   if (selection == null || textNodes.isEmpty) {
     return KeyEventResult.ignored;
   }
-  formatHighlight(editorState);
+  formatHighlight(
+    editorState,
+    editorState.editorStyle.textStyle.highlightColorHex,
+  );
   return KeyEventResult.handled;
 };
 
@@ -73,3 +76,14 @@ ShortcutEventHandler formatLinkEventHandler = (editorState, event) {
   }
   return KeyEventResult.ignored;
 };
+
+ShortcutEventHandler formatEmbedCodeEventHandler = (editorState, event) {
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final nodes = editorState.service.selectionService.currentSelectedNodes;
+  final textNodes = nodes.whereType<TextNode>().toList(growable: false);
+  if (selection == null || textNodes.isEmpty) {
+    return KeyEventResult.ignored;
+  }
+  formatEmbedCode(editorState);
+  return KeyEventResult.ignored;
+};

+ 5 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/number_list_helper.dart

@@ -1,6 +1,6 @@
 import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/operation/transaction_builder.dart';
 import 'package:appflowy_editor/src/document/attributes.dart';
 
@@ -11,7 +11,7 @@ void makeFollowingNodesIncremental(
   if (insertNode == null) {
     return;
   }
-  beginNum ??= insertNode.attributes[StyleKey.number] as int;
+  beginNum ??= insertNode.attributes[BuiltInAttributeKey.number] as int;
 
   int numPtr = beginNum + 1;
   var ptr = insertNode.next;
@@ -19,13 +19,13 @@ void makeFollowingNodesIncremental(
   final builder = TransactionBuilder(editorState);
 
   while (ptr != null) {
-    if (ptr.subtype != StyleKey.numberList) {
+    if (ptr.subtype != BuiltInAttributeKey.numberList) {
       break;
     }
-    final currentNum = ptr.attributes[StyleKey.number] as int;
+    final currentNum = ptr.attributes[BuiltInAttributeKey.number] as int;
     if (currentNum != numPtr) {
       Attributes updateAttributes = {};
-      updateAttributes[StyleKey.number] = numPtr;
+      updateAttributes[BuiltInAttributeKey.number] = numPtr;
       builder.updateNode(ptr, updateAttributes);
     }
 

+ 17 - 14
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/whitespace_handler.dart

@@ -1,14 +1,14 @@
 import 'package:appflowy_editor/src/service/shortcut_event/shortcut_event_handler.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/document/node.dart';
 import 'package:appflowy_editor/src/document/position.dart';
 import 'package:appflowy_editor/src/document/selection.dart';
 import 'package:appflowy_editor/src/editor_state.dart';
 import 'package:appflowy_editor/src/operation/transaction_builder.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import './number_list_helper.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 
 @visibleForTesting
 List<String> get checkboxListSymbols => _checkboxListSymbols;
@@ -68,7 +68,7 @@ ShortcutEventHandler whiteSpaceHandler = (editorState, event) {
 
 KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
     String matchText, String numText) {
-  if (textNode.subtype == StyleKey.bulletedList) {
+  if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
     return KeyEventResult.ignored;
   }
 
@@ -86,8 +86,9 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
   final prevNode = textNode.previous;
   if (prevNode != null &&
       prevNode is TextNode &&
-      prevNode.attributes[StyleKey.subtype] == StyleKey.numberList) {
-    final prevNumber = prevNode.attributes[StyleKey.number] as int;
+      prevNode.attributes[BuiltInAttributeKey.subtype] ==
+          BuiltInAttributeKey.numberList) {
+    final prevNumber = prevNode.attributes[BuiltInAttributeKey.number] as int;
     if (numValue != prevNumber + 1) {
       return KeyEventResult.ignored;
     }
@@ -102,8 +103,10 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
 
   TransactionBuilder(editorState)
     ..deleteText(textNode, 0, matchText.length)
-    ..updateNode(textNode,
-        {StyleKey.subtype: StyleKey.numberList, StyleKey.number: numValue})
+    ..updateNode(textNode, {
+      BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
+      BuiltInAttributeKey.number: numValue
+    })
     ..afterSelection = afterSelection
     ..commit();
 
@@ -113,13 +116,13 @@ KeyEventResult _toNumberList(EditorState editorState, TextNode textNode,
 }
 
 KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
-  if (textNode.subtype == StyleKey.bulletedList) {
+  if (textNode.subtype == BuiltInAttributeKey.bulletedList) {
     return KeyEventResult.ignored;
   }
   TransactionBuilder(editorState)
     ..deleteText(textNode, 0, 1)
     ..updateNode(textNode, {
-      StyleKey.subtype: StyleKey.bulletedList,
+      BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
     })
     ..afterSelection = Selection.collapsed(
       Position(
@@ -132,7 +135,7 @@ KeyEventResult _toBulletedList(EditorState editorState, TextNode textNode) {
 }
 
 KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
-  if (textNode.subtype == StyleKey.checkbox) {
+  if (textNode.subtype == BuiltInAttributeKey.checkbox) {
     return KeyEventResult.ignored;
   }
   final String symbol;
@@ -152,8 +155,8 @@ KeyEventResult _toCheckboxList(EditorState editorState, TextNode textNode) {
   TransactionBuilder(editorState)
     ..deleteText(textNode, 0, symbol.length)
     ..updateNode(textNode, {
-      StyleKey.subtype: StyleKey.checkbox,
-      StyleKey.checkbox: check,
+      BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+      BuiltInAttributeKey.checkbox: check,
     })
     ..afterSelection = Selection.collapsed(
       Position(
@@ -178,8 +181,8 @@ KeyEventResult _toHeadingStyle(
   TransactionBuilder(editorState)
     ..deleteText(textNode, 0, x)
     ..updateNode(textNode, {
-      StyleKey.subtype: StyleKey.heading,
-      StyleKey.heading: hX,
+      BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+      BuiltInAttributeKey.heading: hX,
     })
     ..afterSelection = Selection.collapsed(
       Position(

+ 19 - 8
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -82,7 +82,7 @@ abstract class AppFlowySelectionService {
 class AppFlowySelection extends StatefulWidget {
   const AppFlowySelection({
     Key? key,
-    this.cursorColor = Colors.black,
+    this.cursorColor = const Color(0xFF00BCF0),
     this.selectionColor = const Color.fromARGB(53, 111, 201, 231),
     required this.editorState,
     required this.child,
@@ -343,8 +343,10 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
     currentSelectedNodes = nodes;
 
     // TODO: need to be refactored.
-    Rect? topmostRect;
+    Offset? toolbarOffset;
     LayerLink? layerLink;
+    final editorOffset =
+        editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero;
 
     final backwardNodes =
         selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
@@ -381,14 +383,21 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
         }
       }
 
+      const baseToolbarOffset = Offset(0, 35.0);
       final rects = selectable.getRectsInSelection(newSelection);
       for (final rect in rects) {
+        final selectionRect = _transformRectToGlobal(selectable, rect);
+        selectionRects.add(selectionRect);
+
         // TODO: Need to compute more precise location.
-        topmostRect ??= rect;
+        if ((selectionRect.topLeft.dy - editorOffset.dy) <=
+            baseToolbarOffset.dy) {
+          toolbarOffset ??= rect.bottomLeft;
+        } else {
+          toolbarOffset ??= rect.topLeft - baseToolbarOffset;
+        }
         layerLink ??= node.layerLink;
 
-        selectionRects.add(_transformRectToGlobal(selectable, rect));
-
         final overlay = OverlayEntry(
           builder: (context) => SelectionWidget(
             color: widget.selectionColor,
@@ -402,9 +411,11 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
     Overlay.of(context)?.insertAll(_selectionAreas);
 
-    if (topmostRect != null && layerLink != null) {
-      editorState.service.toolbarService
-          ?.showInOffset(topmostRect.topLeft, layerLink);
+    if (toolbarOffset != null && layerLink != null) {
+      editorState.service.toolbarService?.showInOffset(
+        toolbarOffset,
+        layerLink,
+      );
     }
   }
 

+ 6 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/shortcut_event/built_in_shortcut_events.dart

@@ -144,6 +144,12 @@ List<ShortcutEvent> builtInShortcutEvents = [
     windowsCommand: 'ctrl+shift+h',
     handler: formatHighlightEventHandler,
   ),
+  ShortcutEvent(
+    key: 'Format embed code',
+    command: 'meta+e',
+    windowsCommand: 'ctrl+e',
+    handler: formatEmbedCodeEventHandler,
+  ),
   ShortcutEvent(
     key: 'Format link',
     command: 'meta+k',

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/toolbar_service.dart

@@ -44,7 +44,7 @@ class _FlowyToolbarState extends State<FlowyToolbar>
         key: _toolbarWidgetKey,
         editorState: widget.editorState,
         layerLink: layerLink,
-        offset: offset.translate(0, -37.0),
+        offset: offset,
         items: _filterItems(defaultToolbarItems),
       ),
     );

+ 11 - 0
frontend/app_flowy/packages/appflowy_editor/pubspec.yaml

@@ -22,6 +22,7 @@ dependencies:
   provider: ^6.0.3
   url_launcher: ^6.1.5
   logging: ^1.0.2
+  intl_utils: ^2.7.0
 
 dev_dependencies:
   flutter_test:
@@ -66,3 +67,13 @@ flutter:
   #
   # For details regarding fonts in packages, see
   # https://flutter.dev/custom-fonts/#from-packages
+
+flutter_intl:
+  enabled: true
+  class_name: AppFlowyEditorLocalizations
+  main_locale: en
+  arb_dir: lib/l10n
+  output_dir: lib/src/l10n
+  use_deferred_loading: false
+  localizely:
+    project_id: b7199c7d-eca0-4025-894d-230cdcafa9aa

+ 11 - 9
frontend/app_flowy/packages/appflowy_editor/test/infra/test_editor.dart

@@ -23,18 +23,20 @@ class EditorWidgetTester {
       _editorState.service.selectionService.currentSelection.value;
 
   Future<EditorWidgetTester> startTesting() async {
-    await tester.pumpWidget(
-      MaterialApp(
-        home: Scaffold(
-          body: AppFlowyEditor(
-            editorState: _editorState,
-            editorStyle: const EditorStyle(
-              padding: EdgeInsets.symmetric(vertical: 30),
-            ),
-          ),
+    final app = MaterialApp(
+      localizationsDelegates: const [
+        AppFlowyEditorLocalizations.delegate,
+      ],
+      supportedLocales: AppFlowyEditorLocalizations.delegate.supportedLocales,
+      home: Scaffold(
+        body: AppFlowyEditor(
+          editorState: _editorState,
+          editorStyle: EditorStyle.defaultStyle(),
         ),
       ),
     );
+    await tester.pumpWidget(app);
+    await tester.pump();
     return this;
   }
 

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart

@@ -118,6 +118,9 @@ extension on LogicalKeyboardKey {
     if (this == LogicalKeyboardKey.keyC) {
       return PhysicalKeyboardKey.keyC;
     }
+    if (this == LogicalKeyboardKey.keyE) {
+      return PhysicalKeyboardKey.keyE;
+    }
     if (this == LogicalKeyboardKey.keyI) {
       return PhysicalKeyboardKey.keyI;
     }

+ 8 - 7
frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/checkbox_text_test.dart

@@ -1,9 +1,10 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/render/rich_text/default_selectable.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 
 void main() async {
   setUpAll(() {
@@ -25,15 +26,15 @@ void main() async {
         ..insertTextNode(
           '',
           attributes: {
-            StyleKey.subtype: StyleKey.checkbox,
-            StyleKey.checkbox: false,
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: false,
           },
           delta: Delta([
             TextInsert(text, {
-              StyleKey.bold: true,
-              StyleKey.italic: true,
-              StyleKey.underline: true,
-              StyleKey.strikethrough: true,
+              BuiltInAttributeKey.bold: true,
+              BuiltInAttributeKey.italic: true,
+              BuiltInAttributeKey.underline: true,
+              BuiltInAttributeKey.strikethrough: true,
             }),
           ]),
         );

+ 4 - 3
frontend/app_flowy/packages/appflowy_editor/test/render/rich_text/toolbar_rich_text_test.dart

@@ -1,10 +1,11 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 
 void main() async {
   setUpAll(() {
@@ -229,7 +230,7 @@ void main() async {
       expect(
         node.allSatisfyInSelection(
           code,
-          StyleKey.code,
+          BuiltInAttributeKey.code,
           (value) {
             return value == true;
           },
@@ -319,7 +320,7 @@ void main() async {
       expect(
         node.allSatisfyInSelection(
           selection,
-          StyleKey.backgroundColor,
+          BuiltInAttributeKey.backgroundColor,
           (value) {
             return value == blue;
           },

+ 98 - 90
frontend/app_flowy/packages/appflowy_editor/test/render/selection_menu/selection_menu_widget_test.dart

@@ -1,5 +1,5 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_item_widget.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_service.dart';
 import 'package:appflowy_editor/src/render/selection_menu/selection_menu_widget.dart';
@@ -13,91 +13,99 @@ void main() async {
   });
 
   group('selection_menu_widget.dart', () {
-    for (var i = 0; i < defaultSelectionMenuItems.length; i++) {
-      testWidgets('Selects number.$i item in selection menu', (tester) async {
-        final editor = await _prepare(tester);
-        for (var j = 0; j < i; j++) {
-          await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
-        }
+    // const i = defaultSelectionMenuItems.length;
+    //
+    // Because the `defaultSelectionMenuItems` uses localization,
+    // and the MaterialApp has not been initialized at the time of getting the value,
+    // it will crash.
+    //
+    // Use const value temporarily instead.
+    const i = 7;
+    testWidgets('Selects number.$i item in selection menu', (tester) async {
+      final editor = await _prepare(tester);
+      for (var j = 0; j < i; j++) {
+        await editor.pressLogicKey(LogicalKeyboardKey.arrowDown);
+      }
 
-        await editor.pressLogicKey(LogicalKeyboardKey.enter);
-        expect(
-          find.byType(SelectionMenuWidget, skipOffstage: false),
-          findsNothing,
-        );
-        if (defaultSelectionMenuItems[i].name != 'Image') {
-          await _testDefaultSelectionMenuItems(i, editor);
-        }
-      });
-    }
-  });
+      await editor.pressLogicKey(LogicalKeyboardKey.enter);
+      expect(
+        find.byType(SelectionMenuWidget, skipOffstage: false),
+        findsNothing,
+      );
+      if (defaultSelectionMenuItems[i].name != 'Image') {
+        await _testDefaultSelectionMenuItems(i, editor);
+      }
+    });
 
-  testWidgets('Search item in selection menu util no results', (tester) async {
-    final editor = await _prepare(tester);
-    await editor.pressLogicKey(LogicalKeyboardKey.keyT);
-    await editor.pressLogicKey(LogicalKeyboardKey.keyE);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(3),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(4),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.keyE);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(3),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.keyX);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(1),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.keyT);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(1),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.keyT);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNothing,
-    );
-  });
+    testWidgets('Search item in selection menu util no results',
+        (tester) async {
+      final editor = await _prepare(tester);
+      await editor.pressLogicKey(LogicalKeyboardKey.keyT);
+      await editor.pressLogicKey(LogicalKeyboardKey.keyE);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(3),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(4),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.keyE);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(3),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.keyX);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(1),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.keyT);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(1),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.keyT);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNothing,
+      );
+    });
 
-  testWidgets('Search item in selection menu and presses esc', (tester) async {
-    final editor = await _prepare(tester);
-    await editor.pressLogicKey(LogicalKeyboardKey.keyT);
-    await editor.pressLogicKey(LogicalKeyboardKey.keyE);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(3),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.escape);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNothing,
-    );
-  });
+    testWidgets('Search item in selection menu and presses esc',
+        (tester) async {
+      final editor = await _prepare(tester);
+      await editor.pressLogicKey(LogicalKeyboardKey.keyT);
+      await editor.pressLogicKey(LogicalKeyboardKey.keyE);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(3),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.escape);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNothing,
+      );
+    });
 
-  testWidgets('Search item in selection menu and presses backspace',
-      (tester) async {
-    final editor = await _prepare(tester);
-    await editor.pressLogicKey(LogicalKeyboardKey.keyT);
-    await editor.pressLogicKey(LogicalKeyboardKey.keyE);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNWidgets(3),
-    );
-    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
-    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
-    await editor.pressLogicKey(LogicalKeyboardKey.backspace);
-    expect(
-      find.byType(SelectionMenuItemWidget, skipOffstage: false),
-      findsNothing,
-    );
+    testWidgets('Search item in selection menu and presses backspace',
+        (tester) async {
+      final editor = await _prepare(tester);
+      await editor.pressLogicKey(LogicalKeyboardKey.keyT);
+      await editor.pressLogicKey(LogicalKeyboardKey.keyE);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNWidgets(3),
+      );
+      await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+      await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+      await editor.pressLogicKey(LogicalKeyboardKey.backspace);
+      expect(
+        find.byType(SelectionMenuItemWidget, skipOffstage: false),
+        findsNothing,
+      );
+    });
   });
 }
 
@@ -135,18 +143,18 @@ Future<void> _testDefaultSelectionMenuItems(
   if (item.name == 'Text') {
     expect(node?.subtype == null, true);
   } else if (item.name == 'Heading 1') {
-    expect(node?.subtype, StyleKey.heading);
-    expect(node?.attributes.heading, StyleKey.h1);
+    expect(node?.subtype, BuiltInAttributeKey.heading);
+    expect(node?.attributes.heading, BuiltInAttributeKey.h1);
   } else if (item.name == 'Heading 2') {
-    expect(node?.subtype, StyleKey.heading);
-    expect(node?.attributes.heading, StyleKey.h2);
+    expect(node?.subtype, BuiltInAttributeKey.heading);
+    expect(node?.attributes.heading, BuiltInAttributeKey.h2);
   } else if (item.name == 'Heading 3') {
-    expect(node?.subtype, StyleKey.heading);
-    expect(node?.attributes.heading, StyleKey.h3);
+    expect(node?.subtype, BuiltInAttributeKey.heading);
+    expect(node?.attributes.heading, BuiltInAttributeKey.h3);
   } else if (item.name == 'Bulleted list') {
-    expect(node?.subtype, StyleKey.bulletedList);
+    expect(node?.subtype, BuiltInAttributeKey.bulletedList);
   } else if (item.name == 'Checkbox') {
-    expect(node?.subtype, StyleKey.checkbox);
+    expect(node?.subtype, BuiltInAttributeKey.checkbox);
     expect(node?.attributes.check, false);
   }
 }

+ 27 - 25
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/backspace_handler_test.dart

@@ -1,10 +1,11 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/image/image_node_widget.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:network_image_mock/network_image_mock.dart';
 import '../../infra/test_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 
 void main() async {
   setUpAll(() {
@@ -132,17 +133,18 @@ void main() async {
   //
   testWidgets('Presses backspace key in styled text (checkbox)',
       (tester) async {
-    await _deleteStyledTextByBackspace(tester, StyleKey.checkbox);
+    await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.checkbox);
   });
   testWidgets('Presses backspace key in styled text (bulletedList)',
       (tester) async {
-    await _deleteStyledTextByBackspace(tester, StyleKey.bulletedList);
+    await _deleteStyledTextByBackspace(
+        tester, BuiltInAttributeKey.bulletedList);
   });
   testWidgets('Presses backspace key in styled text (heading)', (tester) async {
-    await _deleteStyledTextByBackspace(tester, StyleKey.heading);
+    await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.heading);
   });
   testWidgets('Presses backspace key in styled text (quote)', (tester) async {
-    await _deleteStyledTextByBackspace(tester, StyleKey.quote);
+    await _deleteStyledTextByBackspace(tester, BuiltInAttributeKey.quote);
   });
 
   // Before
@@ -157,17 +159,17 @@ void main() async {
   // [Style] Welcome to Appflowy 😁
   //
   testWidgets('Presses delete key in styled text (checkbox)', (tester) async {
-    await _deleteStyledTextByDelete(tester, StyleKey.checkbox);
+    await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.checkbox);
   });
   testWidgets('Presses delete key in styled text (bulletedList)',
       (tester) async {
-    await _deleteStyledTextByDelete(tester, StyleKey.bulletedList);
+    await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.bulletedList);
   });
   testWidgets('Presses delete key in styled text (heading)', (tester) async {
-    await _deleteStyledTextByDelete(tester, StyleKey.heading);
+    await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.heading);
   });
   testWidgets('Presses delete key in styled text (quote)', (tester) async {
-    await _deleteStyledTextByDelete(tester, StyleKey.quote);
+    await _deleteStyledTextByDelete(tester, BuiltInAttributeKey.quote);
   });
 
   // Before
@@ -250,7 +252,7 @@ void main() async {
     await editor.pressLogicKey(LogicalKeyboardKey.space);
     expect(
       (editor.nodeAtPath([0]) as TextNode).attributes.heading,
-      StyleKey.h1,
+      BuiltInAttributeKey.h1,
     );
 
     await editor.pressLogicKey(LogicalKeyboardKey.backspace);
@@ -263,7 +265,7 @@ void main() async {
     await editor.pressLogicKey(LogicalKeyboardKey.space);
     expect(
       (editor.nodeAtPath([0]) as TextNode).attributes.heading,
-      StyleKey.h1,
+      BuiltInAttributeKey.h1,
     );
   });
 }
@@ -330,14 +332,14 @@ Future<void> _deleteStyledTextByBackspace(
     WidgetTester tester, String style) async {
   const text = 'Welcome to Appflowy 😁';
   Attributes attributes = {
-    StyleKey.subtype: style,
+    BuiltInAttributeKey.subtype: style,
   };
-  if (style == StyleKey.checkbox) {
-    attributes[StyleKey.checkbox] = true;
-  } else if (style == StyleKey.numberList) {
-    attributes[StyleKey.number] = 1;
-  } else if (style == StyleKey.heading) {
-    attributes[StyleKey.heading] = StyleKey.h1;
+  if (style == BuiltInAttributeKey.checkbox) {
+    attributes[BuiltInAttributeKey.checkbox] = true;
+  } else if (style == BuiltInAttributeKey.numberList) {
+    attributes[BuiltInAttributeKey.number] = 1;
+  } else if (style == BuiltInAttributeKey.heading) {
+    attributes[BuiltInAttributeKey.heading] = BuiltInAttributeKey.h1;
   }
   final editor = tester.editor
     ..insertTextNode(text)
@@ -377,14 +379,14 @@ Future<void> _deleteStyledTextByDelete(
     WidgetTester tester, String style) async {
   const text = 'Welcome to Appflowy 😁';
   Attributes attributes = {
-    StyleKey.subtype: style,
+    BuiltInAttributeKey.subtype: style,
   };
-  if (style == StyleKey.checkbox) {
-    attributes[StyleKey.checkbox] = true;
-  } else if (style == StyleKey.numberList) {
-    attributes[StyleKey.number] = 1;
-  } else if (style == StyleKey.heading) {
-    attributes[StyleKey.heading] = StyleKey.h1;
+  if (style == BuiltInAttributeKey.checkbox) {
+    attributes[BuiltInAttributeKey.checkbox] = true;
+  } else if (style == BuiltInAttributeKey.numberList) {
+    attributes[BuiltInAttributeKey.number] = 1;
+  } else if (style == BuiltInAttributeKey.heading) {
+    attributes[BuiltInAttributeKey.heading] = BuiltInAttributeKey.h1;
   }
   final editor = tester.editor
     ..insertTextNode(text)

+ 10 - 10
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler_test.dart

@@ -1,8 +1,8 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {
@@ -95,16 +95,16 @@ void main() async {
     // [Style] Welcome to Appflowy 😁
     // [Style]
     testWidgets('Presses enter key in bulleted list', (tester) async {
-      await _testStyleNeedToBeCopy(tester, StyleKey.bulletedList);
+      await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.bulletedList);
     });
     testWidgets('Presses enter key in numbered list', (tester) async {
-      await _testStyleNeedToBeCopy(tester, StyleKey.numberList);
+      await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.numberList);
     });
     testWidgets('Presses enter key in checkbox styled text', (tester) async {
-      await _testStyleNeedToBeCopy(tester, StyleKey.checkbox);
+      await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.checkbox);
     });
     testWidgets('Presses enter key in quoted text', (tester) async {
-      await _testStyleNeedToBeCopy(tester, StyleKey.quote);
+      await _testStyleNeedToBeCopy(tester, BuiltInAttributeKey.quote);
     });
 
     testWidgets('Presses enter key in multiple selection from top to bottom',
@@ -143,12 +143,12 @@ void main() async {
 Future<void> _testStyleNeedToBeCopy(WidgetTester tester, String style) async {
   const text = 'Welcome to Appflowy 😁';
   Attributes attributes = {
-    StyleKey.subtype: style,
+    BuiltInAttributeKey.subtype: style,
   };
-  if (style == StyleKey.checkbox) {
-    attributes[StyleKey.checkbox] = true;
-  } else if (style == StyleKey.numberList) {
-    attributes[StyleKey.number] = 1;
+  if (style == BuiltInAttributeKey.checkbox) {
+    attributes[BuiltInAttributeKey.checkbox] = true;
+  } else if (style == BuiltInAttributeKey.numberList) {
+    attributes[BuiltInAttributeKey.number] = 1;
   }
   final editor = tester.editor
     ..insertTextNode(text)

+ 20 - 10
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/update_text_style_by_command_x_handler_test.dart → frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/format_style_handler_test.dart

@@ -2,24 +2,24 @@ import 'dart:io';
 
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/render/link_menu/link_menu.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/extensions/text_node_extensions.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {
     TestWidgetsFlutterBinding.ensureInitialized();
   });
 
-  group('update_text_style_by_command_x_handler.dart', () {
+  group('format_style_handler.dart', () {
     testWidgets('Presses Command + B to update text style', (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
-        StyleKey.bold,
+        BuiltInAttributeKey.bold,
         true,
         LogicalKeyboardKey.keyB,
       );
@@ -27,7 +27,7 @@ void main() async {
     testWidgets('Presses Command + I to update text style', (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
-        StyleKey.italic,
+        BuiltInAttributeKey.italic,
         true,
         LogicalKeyboardKey.keyI,
       );
@@ -35,7 +35,7 @@ void main() async {
     testWidgets('Presses Command + U to update text style', (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
-        StyleKey.underline,
+        BuiltInAttributeKey.underline,
         true,
         LogicalKeyboardKey.keyU,
       );
@@ -44,7 +44,7 @@ void main() async {
         (tester) async {
       await _testUpdateTextStyleByCommandX(
         tester,
-        StyleKey.strikethrough,
+        BuiltInAttributeKey.strikethrough,
         true,
         LogicalKeyboardKey.keyS,
       );
@@ -52,10 +52,11 @@ void main() async {
 
     testWidgets('Presses Command + Shift + H to update text style',
         (tester) async {
+      // FIXME: customize the highlight color instead of using magic number.
       await _testUpdateTextStyleByCommandX(
         tester,
-        StyleKey.backgroundColor,
-        defaultHighlightColor,
+        BuiltInAttributeKey.backgroundColor,
+        '0x6000BCF0',
         LogicalKeyboardKey.keyH,
       );
     });
@@ -63,6 +64,15 @@ void main() async {
     testWidgets('Presses Command + K to trigger link menu', (tester) async {
       await _testLinkMenuInSingleTextSelection(tester);
     });
+
+    testWidgets('Presses Command + E to update text style', (tester) async {
+      await _testUpdateTextStyleByCommandX(
+        tester,
+        BuiltInAttributeKey.code,
+        true,
+        LogicalKeyboardKey.keyE,
+      );
+    });
   });
 }
 
@@ -256,7 +266,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
   expect(
       node.allSatisfyInSelection(
         selection,
-        StyleKey.href,
+        BuiltInAttributeKey.href,
         (value) => value == link,
       ),
       true);
@@ -293,7 +303,7 @@ Future<void> _testLinkMenuInSingleTextSelection(WidgetTester tester) async {
   expect(
       node.allSatisfyInSelection(
         selection,
-        StyleKey.href,
+        BuiltInAttributeKey.href,
         (value) => value == link,
       ),
       false);

+ 11 - 10
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/white_space_handler_test.dart

@@ -1,9 +1,10 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/whitespace_handler.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../../infra/test_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
+import 'package:appflowy_editor/src/extensions/attributes_extension.dart';
 
 void main() async {
   setUpAll(() {
@@ -45,8 +46,8 @@ void main() async {
 
         final textNode = (editor.nodeAtPath([i - 1]) as TextNode);
 
-        expect(textNode.subtype, StyleKey.heading);
-        // StyleKey.h1 ~ StyleKey.h6
+        expect(textNode.subtype, BuiltInAttributeKey.heading);
+        // BuiltInAttributeKey.h1 ~ BuiltInAttributeKey.h6
         expect(textNode.attributes.heading, 'h$i');
       }
     });
@@ -85,8 +86,8 @@ void main() async {
 
         final textNode = (editor.nodeAtPath([i - 1]) as TextNode);
 
-        expect(textNode.subtype, StyleKey.heading);
-        // StyleKey.h1 ~ StyleKey.h6
+        expect(textNode.subtype, BuiltInAttributeKey.heading);
+        // BuiltInAttributeKey.h1 ~ BuiltInAttributeKey.h6
         expect(textNode.attributes.heading, 'h$i');
         expect(textNode.toRawString().startsWith('##'), true);
       }
@@ -117,8 +118,8 @@ void main() async {
         await editor.insertText(textNode, '#' * i, 0);
         await editor.pressLogicKey(LogicalKeyboardKey.space);
 
-        expect(textNode.subtype, StyleKey.heading);
-        // StyleKey.h2 ~ StyleKey.h6
+        expect(textNode.subtype, BuiltInAttributeKey.heading);
+        // BuiltInAttributeKey.h2 ~ BuiltInAttributeKey.h6
         expect(textNode.attributes.heading, 'h$i');
       }
     });
@@ -136,7 +137,7 @@ void main() async {
         );
         await editor.insertText(textNode, symbol, 0);
         await editor.pressLogicKey(LogicalKeyboardKey.space);
-        expect(textNode.subtype, StyleKey.checkbox);
+        expect(textNode.subtype, BuiltInAttributeKey.checkbox);
         expect(textNode.attributes.check, false);
       }
     });
@@ -154,7 +155,7 @@ void main() async {
         );
         await editor.insertText(textNode, symbol, 0);
         await editor.pressLogicKey(LogicalKeyboardKey.space);
-        expect(textNode.subtype, StyleKey.checkbox);
+        expect(textNode.subtype, BuiltInAttributeKey.checkbox);
         expect(textNode.attributes.check, true);
       }
     });
@@ -171,7 +172,7 @@ void main() async {
         );
         await editor.insertText(textNode, symbol, 0);
         await editor.pressLogicKey(LogicalKeyboardKey.space);
-        expect(textNode.subtype, StyleKey.bulletedList);
+        expect(textNode.subtype, BuiltInAttributeKey.bulletedList);
       }
     });
   });

+ 2 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/shortcut_event/shortcut_event_test.dart

@@ -81,6 +81,8 @@ void main() async {
         editor.documentSelection,
         Selection.single(path: [1], startOffset: 0),
       );
+
+      tester.pumpAndSettle();
     });
   });
 }

+ 19 - 17
frontend/app_flowy/packages/appflowy_editor/test/service/toolbar_service_test.dart

@@ -1,10 +1,10 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/rich_text/rich_text_style.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_item_widget.dart';
 import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
 import 'package:flutter_test/flutter_test.dart';
 import '../infra/test_editor.dart';
+import 'package:appflowy_editor/src/document/built_in_attribute_keys.dart';
 
 void main() async {
   setUpAll(() {
@@ -45,13 +45,13 @@ void main() async {
     });
 
     testWidgets(
-        'Test toolbar service in single text selection with StyleKey.partialStyleKeys',
+        'Test toolbar service in single text selection with BuiltInAttributeKey.partialStyleKeys',
         (tester) async {
-      final attributes = StyleKey.partialStyleKeys.fold<Attributes>({},
-          (previousValue, element) {
-        if (element == StyleKey.backgroundColor) {
+      final attributes = BuiltInAttributeKey.partialStyleKeys
+          .fold<Attributes>({}, (previousValue, element) {
+        if (element == BuiltInAttributeKey.backgroundColor) {
           previousValue[element] = '0x6000BCF0';
-        } else if (element == StyleKey.href) {
+        } else if (element == BuiltInAttributeKey.href) {
           previousValue[element] = 'appflowy.io';
         } else {
           previousValue[element] = true;
@@ -77,11 +77,11 @@ void main() async {
       expect(find.byType(ToolbarWidget), findsOneWidget);
 
       void testHighlight(bool expectedValue) {
-        for (final styleKey in StyleKey.partialStyleKeys) {
+        for (final styleKey in BuiltInAttributeKey.partialStyleKeys) {
           var key = styleKey;
-          if (styleKey == StyleKey.backgroundColor) {
+          if (styleKey == BuiltInAttributeKey.backgroundColor) {
             key = 'highlight';
-          } else if (styleKey == StyleKey.href) {
+          } else if (styleKey == BuiltInAttributeKey.href) {
             key = 'link';
           } else {
             continue;
@@ -116,22 +116,24 @@ void main() async {
     });
 
     testWidgets(
-        'Test toolbar service in single text selection with StyleKey.globalStyleKeys',
+        'Test toolbar service in single text selection with BuiltInAttributeKey.globalStyleKeys',
         (tester) async {
       const text = 'Welcome to Appflowy 😁';
 
       final editor = tester.editor
         ..insertTextNode(text, attributes: {
-          StyleKey.subtype: StyleKey.heading,
-          StyleKey.heading: StyleKey.h1,
+          BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+          BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
         })
         ..insertTextNode(
           text,
-          attributes: {StyleKey.subtype: StyleKey.quote},
+          attributes: {BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote},
         )
         ..insertTextNode(
           text,
-          attributes: {StyleKey.subtype: StyleKey.bulletedList},
+          attributes: {
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList
+          },
         );
       await editor.startTesting();
 
@@ -167,12 +169,12 @@ void main() async {
         ..insertTextNode(
           null,
           attributes: {
-            StyleKey.subtype: StyleKey.heading,
-            StyleKey.heading: StyleKey.h1,
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+            BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
           },
           delta: Delta([
             TextInsert(text, {
-              StyleKey.bold: true,
+              BuiltInAttributeKey.bold: true,
             })
           ]),
         )