Browse Source

Merge pull request #1655 from LucasXu0/feat_1649

feat: #1649 [FR] Convert quill delta to appflowy document
Lucas.Xu 2 years ago
parent
commit
3ba3a8dc18

+ 8 - 8
frontend/app_flowy/packages/appflowy_editor/README.md

@@ -1,14 +1,14 @@
-<!-- 
+<!--
 This README describes the package. If you publish this package to pub.dev,
 This README describes the package. If you publish this package to pub.dev,
 this README's contents appear on the landing page for your package.
 this README's contents appear on the landing page for your package.
 
 
 For information about how to write a good package README, see the guide for
 For information about how to write a good package README, see the guide for
-[writing package pages](https://dart.dev/guides/libraries/writing-package-pages). 
+[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
 
 
 For general information about developing packages, see the Dart guide for
 For general information about developing packages, see the Dart guide for
 [creating packages](https://dart.dev/guides/libraries/create-library-packages)
 [creating packages](https://dart.dev/guides/libraries/create-library-packages)
 and the Flutter guide for
 and the Flutter guide for
-[developing packages and plugins](https://flutter.dev/developing-packages). 
+[developing packages and plugins](https://flutter.dev/developing-packages).
 -->
 -->
 
 
 <h1 align="center"><b>AppFlowy Editor</b></h1>
 <h1 align="center"><b>AppFlowy Editor</b></h1>
@@ -51,7 +51,7 @@ flutter pub get
 
 
 ## Creating Your First Editor
 ## Creating Your First Editor
 
 
-Start by creating a new empty AppFlowyEditor object. 
+Start by creating a new empty AppFlowyEditor object.
 
 
 ```dart
 ```dart
 final editorState = EditorState.empty(); // an empty state
 final editorState = EditorState.empty(); // an empty state
@@ -60,7 +60,7 @@ final editor = AppFlowyEditor(
 );
 );
 ```
 ```
 
 
-You can also create an editor from a JSON object in order to configure your initial state.
+You can also create an editor from a JSON object in order to configure your initial state. Or you can [create an editor from Markdown or Quill Delta](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/importing.md).
 
 
 ```dart
 ```dart
 final json = ...;
 final json = ...;
@@ -79,7 +79,7 @@ MaterialApp(
 );
 );
 ```
 ```
 
 
-To get a sense for how the AppFlowy Editor works, run our example:
+To get a sense of how the AppFlowy Editor works, run our example:
 
 
 ```shell
 ```shell
 git clone https://github.com/AppFlowy-IO/AppFlowy.git
 git clone https://github.com/AppFlowy-IO/AppFlowy.git
@@ -98,7 +98,7 @@ Below are some examples of component customizations:
  * [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components
  * [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components
  * [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it
  * [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it
  * See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
  * See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
-    
+
 ### Customizing Shortcut Events
 ### Customizing Shortcut Events
 
 
 Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing shortcut events](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event).
 Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing shortcut events](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event).
@@ -113,7 +113,7 @@ Below are some examples of shortcut event customizations:
 Please refer to the API documentation.
 Please refer to the API documentation.
 
 
 ## Contributing
 ## Contributing
-Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated. 
+Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
 
 
 Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
 Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
 
 

+ 36 - 0
frontend/app_flowy/packages/appflowy_editor/documentation/importing.md

@@ -0,0 +1,36 @@
+# Importing data
+
+For now, we have supported three ways to import data to initialize AppFlowy Editor.
+
+1. From AppFlowy Document JSON
+
+```dart
+const document = r'''{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"},"delta":[{"insert":"Hello AppFlowy!"}]}]}}''';
+final json = jsonDecode(document);
+final editorState = EditorState(
+    document: Document.fromJson(
+        Map<String, Object>.from(json),
+    ),
+);
+```
+
+2. From Markdown
+
+```dart
+const markdown = r'''# Hello AppFlowy!''';
+final editorState = EditorState(
+    document: markdownToDocument(markdown),
+);
+```
+
+3. From Quill Delta
+
+```dart
+const delta = r'''[{"insert":"Hello AppFlowy!"},{"attributes":{"header":1},"insert":"\n"}]''';
+final json = jsonDecode(delta);
+final editorState = EditorState(
+    document: DeltaDocumentConvert().convertFromJSON(json),
+);
+```
+
+For more details, please refer to the function `_importFile` through this [link](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart).

+ 17 - 0
frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart

@@ -14,12 +14,14 @@ enum ExportFileType {
   json,
   json,
   markdown,
   markdown,
   html,
   html,
+  delta,
 }
 }
 
 
 extension on ExportFileType {
 extension on ExportFileType {
   String get extension {
   String get extension {
     switch (this) {
     switch (this) {
       case ExportFileType.json:
       case ExportFileType.json:
+      case ExportFileType.delta:
         return 'json';
         return 'json';
       case ExportFileType.markdown:
       case ExportFileType.markdown:
         return 'md';
         return 'md';
@@ -117,6 +119,9 @@ class _HomePageState extends State<HomePage> {
           _buildListTile(context, 'Import From Markdown', () {
           _buildListTile(context, 'Import From Markdown', () {
             _importFile(ExportFileType.markdown);
             _importFile(ExportFileType.markdown);
           }),
           }),
+          _buildListTile(context, 'Import From Quill Delta', () {
+            _importFile(ExportFileType.delta);
+          }),
 
 
           // Theme Demo
           // Theme Demo
           _buildSeparator(context, 'Theme Demo'),
           _buildSeparator(context, 'Theme Demo'),
@@ -224,6 +229,7 @@ class _HomePageState extends State<HomePage> {
         result = documentToMarkdown(editorState.document);
         result = documentToMarkdown(editorState.document);
         break;
         break;
       case ExportFileType.html:
       case ExportFileType.html:
+      case ExportFileType.delta:
         throw UnimplementedError();
         throw UnimplementedError();
     }
     }
 
 
@@ -280,6 +286,17 @@ class _HomePageState extends State<HomePage> {
       case ExportFileType.markdown:
       case ExportFileType.markdown:
         jsonString = jsonEncode(markdownToDocument(plainText).toJson());
         jsonString = jsonEncode(markdownToDocument(plainText).toJson());
         break;
         break;
+      case ExportFileType.delta:
+        jsonString = jsonEncode(
+          DeltaDocumentConvert()
+              .convertFromJSON(
+                jsonDecode(
+                  plainText.replaceAll('\\\\\n', '\\n'),
+                ),
+              )
+              .toJson(),
+        );
+        break;
       case ExportFileType.html:
       case ExportFileType.html:
         throw UnimplementedError();
         throw UnimplementedError();
     }
     }

+ 1 - 1
frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock

@@ -35,7 +35,7 @@ EXTERNAL SOURCES:
 
 
 SPEC CHECKSUMS:
 SPEC CHECKSUMS:
   flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
   flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
-  FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+  FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
   path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
   path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
   rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
   rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
   shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
   shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727

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

@@ -41,3 +41,4 @@ export 'src/plugins/markdown/encoder/parser/text_node_parser.dart';
 export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
 export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
 export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
 export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
 export 'src/plugins/markdown/document_markdown.dart';
 export 'src/plugins/markdown/document_markdown.dart';
+export 'src/plugins/quill_delta/delta_document_encoder.dart';

+ 232 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/quill_delta/delta_document_encoder.dart

@@ -0,0 +1,232 @@
+import 'package:appflowy_editor/src/core/document/attributes.dart';
+import 'package:appflowy_editor/src/core/document/document.dart';
+import 'package:appflowy_editor/src/core/document/node.dart';
+import 'package:appflowy_editor/src/core/document/text_delta.dart';
+import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
+import 'package:flutter/material.dart';
+
+class DeltaDocumentConvert {
+  DeltaDocumentConvert();
+
+  var _number = 1;
+  final Map<int, List<TextNode>> _bulletedList = {};
+
+  Document convertFromJSON(List<dynamic> json) {
+    final delta = Delta.fromJson(json);
+    return convertFromDelta(delta);
+  }
+
+  Document convertFromDelta(Delta delta) {
+    final iter = delta.iterator;
+
+    final document = Document.empty();
+    TextNode textNode = TextNode(delta: Delta());
+    int path = 0;
+
+    while (iter.moveNext()) {
+      final op = iter.current;
+      if (op is TextInsert) {
+        if (op.text != '\n') {
+          // Attributes associated with a newline character describes formatting for that line.
+          final texts = op.text.split('\n');
+          if (texts.length > 1) {
+            textNode.delta.insert(texts[0]);
+            document.insert([path++], [textNode]);
+            textNode = TextNode(delta: Delta()..insert(texts[1]));
+          } else {
+            _applyStyle(textNode, op.text, op.attributes);
+          }
+        } else {
+          if (!_containNumberListStyle(op.attributes)) {
+            _number = 1;
+          }
+          _applyListStyle(textNode, op.attributes);
+          _applyHeaderStyle(textNode, op.attributes);
+          _applyIndent(textNode, op.attributes);
+          _applyBlockquote(textNode, op.attributes);
+          // _applyCodeBlock(textNode, op.attributes);
+
+          if (_containIndentBulletedListStyle(op.attributes)) {
+            final level = _indentLevel(op.attributes);
+            final path = [
+              ..._bulletedList[level - 1]!.last.path,
+              _bulletedList[level]!.length - 1,
+            ];
+            document.insert(path, [textNode]);
+          } else {
+            document.insert([path++], [textNode]);
+          }
+          textNode = TextNode(delta: Delta());
+        }
+      } else {
+        assert(false, 'op must be TextInsert');
+      }
+    }
+
+    return document;
+  }
+
+  void _applyStyle(TextNode textNode, String text, Map? attributes) {
+    Attributes attrs = {};
+
+    if (_containsStyle(attributes, 'strike')) {
+      attrs[BuiltInAttributeKey.strikethrough] = true;
+    }
+    if (_containsStyle(attributes, 'underline')) {
+      attrs[BuiltInAttributeKey.underline] = true;
+    }
+    if (_containsStyle(attributes, 'bold')) {
+      attrs[BuiltInAttributeKey.bold] = true;
+    }
+    if (_containsStyle(attributes, 'italic')) {
+      attrs[BuiltInAttributeKey.italic] = true;
+    }
+    final link = attributes?['link'] as String?;
+    if (link != null) {
+      attrs[BuiltInAttributeKey.href] = link;
+    }
+    final color = attributes?['color'] as String?;
+    final colorHex = _convertColorToHexString(color);
+    if (colorHex != null) {
+      attrs[BuiltInAttributeKey.color] = colorHex;
+    }
+    final backgroundColor = attributes?['background'] as String?;
+    final backgroundHex = _convertColorToHexString(backgroundColor);
+    if (backgroundHex != null) {
+      attrs[BuiltInAttributeKey.backgroundColor] = backgroundHex;
+    }
+
+    textNode.delta.insert(text, attributes: attrs);
+  }
+
+  bool _containsStyle(Map? attributes, String key) {
+    final value = attributes?[key] as bool?;
+    return value == true;
+  }
+
+  String? _convertColorToHexString(String? color) {
+    if (color == null) {
+      return null;
+    }
+    if (color.startsWith('#')) {
+      return '0xFF${color.substring(1)}';
+    } else if (color.startsWith("rgba")) {
+      List rgbaList = color.substring(5, color.length - 1).split(',');
+      return Color.fromRGBO(
+        int.parse(rgbaList[0]),
+        int.parse(rgbaList[1]),
+        int.parse(rgbaList[2]),
+        double.parse(rgbaList[3]),
+      ).toHex();
+    }
+    return null;
+  }
+
+  // convert bullet-list, number-list, check-list to appflowy style list.
+  void _applyListStyle(TextNode textNode, Map? attributes) {
+    final indent = attributes?['indent'] as int?;
+    final list = attributes?['list'] as String?;
+    if (list != null) {
+      switch (list) {
+        case 'bullet':
+          textNode.updateAttributes({
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
+          });
+          if (indent != null) {
+            _bulletedList[indent] ??= [];
+            _bulletedList[indent]?.add(textNode);
+          } else {
+            _bulletedList.clear();
+            _bulletedList[0] ??= [];
+            _bulletedList[0]?.add(textNode);
+          }
+          break;
+        case 'ordered':
+          textNode.updateAttributes({
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
+            BuiltInAttributeKey.number: _number++,
+          });
+          break;
+        case 'checked':
+          textNode.updateAttributes({
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: true,
+          });
+          break;
+        case 'unchecked':
+          textNode.updateAttributes({
+            BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
+            BuiltInAttributeKey.checkbox: false,
+          });
+          break;
+      }
+    }
+  }
+
+  bool _containNumberListStyle(Map? attributes) {
+    final list = attributes?['list'] as String?;
+    return list == 'ordered';
+  }
+
+  bool _containIndentBulletedListStyle(Map? attributes) {
+    final list = attributes?['list'] as String?;
+    final indent = attributes?['indent'] as int?;
+    return list == 'bullet' && indent != null;
+  }
+
+  int _indentLevel(Map? attributes) {
+    final indent = attributes?['indent'] as int?;
+    return indent ?? 1;
+  }
+
+  // convert header to appflowy style heading
+  void _applyHeaderStyle(TextNode textNode, Map? attributes) {
+    final header = attributes?['header'] as int?;
+    if (header != null) {
+      textNode.updateAttributes({
+        BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
+        BuiltInAttributeKey.heading: 'h$header',
+      });
+    }
+  }
+
+  // convert indent to tab
+  void _applyIndent(TextNode textNode, Map? attributes) {
+    final indent = attributes?['indent'] as int?;
+    final list = attributes?['list'] as String?;
+    if (indent != null && list == null) {
+      textNode.delta = textNode.delta.compose(
+        Delta()
+          ..retain(0)
+          ..insert('  ' * indent),
+      );
+    }
+  }
+
+  /*
+  // convert code-block to appflowy style code
+  void _applyCodeBlock(TextNode textNode, Map? attributes) {
+    final codeBlock = attributes?['code-block'] as bool?;
+    if (codeBlock != null) {
+      textNode.updateAttributes({
+        BuiltInAttributeKey.subtype: 'code_block',
+      });
+    }
+  }
+  */
+
+  void _applyBlockquote(TextNode textNode, Map? attributes) {
+    final blockquote = attributes?['blockquote'] as bool?;
+    if (blockquote != null) {
+      textNode.updateAttributes({
+        BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
+      });
+    }
+  }
+}
+
+extension on Color {
+  String toHex() {
+    return '0x${value.toRadixString(16)}';
+  }
+}

File diff suppressed because it is too large
+ 16 - 0
frontend/app_flowy/packages/appflowy_editor/test/plugins/quill_delta/delta_document_encoder_test.dart


Some files were not shown because too many files changed in this diff