Forráskód Böngészése

Merge pull request #1655 from LucasXu0/feat_1649

feat: #1649 [FR] Convert quill delta to appflowy document
Lucas.Xu 2 éve
szülő
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's contents appear on the landing page for your package.
 
 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
 [creating packages](https://dart.dev/guides/libraries/create-library-packages)
 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>
@@ -51,7 +51,7 @@ flutter pub get
 
 ## Creating Your First Editor
 
-Start by creating a new empty AppFlowyEditor object. 
+Start by creating a new empty AppFlowyEditor object.
 
 ```dart
 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
 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
 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
  * [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)
-    
+
 ### 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).
@@ -113,7 +113,7 @@ Below are some examples of shortcut event customizations:
 Please refer to the API documentation.
 
 ## 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.
 

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

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

@@ -35,7 +35,7 @@ EXTERNAL SOURCES:
 
 SPEC CHECKSUMS:
   flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
-  FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
+  FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
   path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
   rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
   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/decoder/delta_markdown_decoder.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)}';
+  }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 16 - 0
frontend/app_flowy/packages/appflowy_editor/test/plugins/quill_delta/delta_document_encoder_test.dart


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott