Browse Source

feat: markdown to delta

Lucas.Xu 2 years ago
parent
commit
fc35f74751

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

@@ -38,3 +38,4 @@ export 'src/plugins/markdown/encoder/document_markdown_encoder.dart';
 export 'src/plugins/markdown/encoder/parser/node_parser.dart';
 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';

+ 10 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart

@@ -62,7 +62,7 @@ class TextInsert extends TextOperation {
 
     return other is TextInsert &&
         other.text == text &&
-        mapEquals(_attributes, other._attributes);
+        _mapEquals(_attributes, other._attributes);
   }
 
   @override
@@ -99,7 +99,7 @@ class TextRetain extends TextOperation {
 
     return other is TextRetain &&
         other.length == length &&
-        mapEquals(_attributes, other._attributes);
+        _mapEquals(_attributes, other._attributes);
   }
 
   @override
@@ -181,7 +181,7 @@ class Delta extends Iterable<TextOperation> {
         lastOp.length += textOperation.length;
         return;
       }
-      if (mapEquals(lastOp.attributes, textOperation.attributes)) {
+      if (_mapEquals(lastOp.attributes, textOperation.attributes)) {
         if (lastOp is TextInsert && textOperation is TextInsert) {
           lastOp.text += textOperation.text;
           return;
@@ -539,3 +539,10 @@ class _OpIterator {
     }
   }
 }
+
+bool _mapEquals<T, U>(Map<T, U>? a, Map<T, U>? b) {
+  if ((a == null || a.isEmpty) && (b == null || b.isEmpty)) {
+    return true;
+  }
+  return mapEquals(a, b);
+}

+ 64 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/delta_markdown_decoder.dart

@@ -0,0 +1,64 @@
+import 'dart:convert';
+
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:markdown/markdown.dart' as md;
+
+class DeltaMarkdownDecoder extends Converter<String, Delta>
+    with md.NodeVisitor {
+  final _delta = Delta();
+  final Attributes _attributes = {};
+
+  @override
+  Delta convert(String input) {
+    final document =
+        md.Document(extensionSet: md.ExtensionSet.gitHubWeb).parseInline(input);
+    for (final node in document) {
+      node.accept(this);
+    }
+    return _delta;
+  }
+
+  @override
+  void visitElementAfter(md.Element element) {
+    _removeAttributeKey(element);
+  }
+
+  @override
+  bool visitElementBefore(md.Element element) {
+    _addAttributeKey(element);
+    return true;
+  }
+
+  @override
+  void visitText(md.Text text) {
+    _delta.add(TextInsert(text.text, attributes: {..._attributes}));
+  }
+
+  void _addAttributeKey(md.Element element) {
+    if (element.tag == 'strong') {
+      _attributes[BuiltInAttributeKey.bold] = true;
+    } else if (element.tag == 'em') {
+      _attributes[BuiltInAttributeKey.italic] = true;
+    } else if (element.tag == 'code') {
+      _attributes[BuiltInAttributeKey.code] = true;
+    } else if (element.tag == 'del') {
+      _attributes[BuiltInAttributeKey.strikethrough] = true;
+    } else if (element.tag == 'a') {
+      _attributes[BuiltInAttributeKey.href] = element.attributes['href'];
+    }
+  }
+
+  void _removeAttributeKey(md.Element element) {
+    if (element.tag == 'strong') {
+      _attributes.remove(BuiltInAttributeKey.bold);
+    } else if (element.tag == 'em') {
+      _attributes.remove(BuiltInAttributeKey.italic);
+    } else if (element.tag == 'code') {
+      _attributes.remove(BuiltInAttributeKey.code);
+    } else if (element.tag == 'del') {
+      _attributes.remove(BuiltInAttributeKey.strikethrough);
+    } else if (element.tag == 'a') {
+      _attributes.remove(BuiltInAttributeKey.href);
+    }
+  }
+}

+ 96 - 0
frontend/app_flowy/packages/appflowy_editor/test/plugins/markdown/decoder/delta_markdown_decoder_test.dart

@@ -0,0 +1,96 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() async {
+  group('delta_markdown_decoder.dart', () {
+    test('bold', () {
+      final delta = Delta(operations: [
+        TextInsert('Welcome to '),
+        TextInsert('AppFlowy', attributes: {
+          BuiltInAttributeKey.bold: true,
+        }),
+      ]);
+      final result = DeltaMarkdownDecoder().convert('Welcome to **AppFlowy**');
+      expect(result, delta);
+    });
+
+    test('italic', () {
+      final delta = Delta(operations: [
+        TextInsert('Welcome to '),
+        TextInsert('AppFlowy', attributes: {
+          BuiltInAttributeKey.italic: true,
+        }),
+      ]);
+      final result = DeltaMarkdownDecoder().convert('Welcome to _AppFlowy_');
+      expect(result, delta);
+    });
+
+    test('strikethrough', () {
+      final delta = Delta(operations: [
+        TextInsert('Welcome to '),
+        TextInsert('AppFlowy', attributes: {
+          BuiltInAttributeKey.strikethrough: true,
+        }),
+      ]);
+      final result = DeltaMarkdownDecoder().convert('Welcome to ~~AppFlowy~~');
+      expect(result, delta);
+    });
+
+    test('href', () {
+      final delta = Delta(operations: [
+        TextInsert('Welcome to '),
+        TextInsert('AppFlowy', attributes: {
+          BuiltInAttributeKey.href: 'https://appflowy.io',
+        }),
+      ]);
+      final result = DeltaMarkdownDecoder()
+          .convert('Welcome to [AppFlowy](https://appflowy.io)');
+      expect(result, delta);
+    });
+
+    test('code', () {
+      final delta = Delta(operations: [
+        TextInsert('Welcome to '),
+        TextInsert('AppFlowy', attributes: {
+          BuiltInAttributeKey.code: true,
+        }),
+      ]);
+      final result = DeltaMarkdownDecoder().convert('Welcome to `AppFlowy`');
+      expect(result, delta);
+    });
+
+    test('bold', () {
+      const markdown =
+          '***<u>`Welcome`</u>*** ***~~to~~*** ***[AppFlowy](https://appflowy.io)***';
+      final delta = Delta(operations: [
+        TextInsert('<u>', attributes: {
+          BuiltInAttributeKey.italic: true,
+          BuiltInAttributeKey.bold: true,
+        }),
+        TextInsert('Welcome', attributes: {
+          BuiltInAttributeKey.code: true,
+          BuiltInAttributeKey.italic: true,
+          BuiltInAttributeKey.bold: true,
+        }),
+        TextInsert('</u>', attributes: {
+          BuiltInAttributeKey.italic: true,
+          BuiltInAttributeKey.bold: true,
+        }),
+        TextInsert(' '),
+        TextInsert('to', attributes: {
+          BuiltInAttributeKey.italic: true,
+          BuiltInAttributeKey.bold: true,
+          BuiltInAttributeKey.strikethrough: true,
+        }),
+        TextInsert(' '),
+        TextInsert('AppFlowy', attributes: {
+          BuiltInAttributeKey.href: 'https://appflowy.io',
+          BuiltInAttributeKey.bold: true,
+          BuiltInAttributeKey.italic: true,
+        }),
+      ]);
+      final result = DeltaMarkdownDecoder().convert(markdown);
+      expect(result, delta);
+    });
+  });
+}