diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart b/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart index cc8e9556d7..65cfb7c2f0 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart +++ b/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'; diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart index 5bf1832f73..9969681e73 100644 --- a/frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart +++ b/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 { 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(Map? a, Map? b) { + if ((a == null || a.isEmpty) && (b == null || b.isEmpty)) { + return true; + } + return mapEquals(a, b); +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/delta_markdown_decoder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/delta_markdown_decoder.dart new file mode 100644 index 0000000000..ccd49f22fd --- /dev/null +++ b/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 + 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); + } + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/plugins/markdown/decoder/delta_markdown_decoder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/plugins/markdown/decoder/delta_markdown_decoder_test.dart new file mode 100644 index 0000000000..206aa0ea0b --- /dev/null +++ b/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 = + '***`Welcome`*** ***~~to~~*** ***[AppFlowy](https://appflowy.io)***'; + final delta = Delta(operations: [ + TextInsert('', attributes: { + BuiltInAttributeKey.italic: true, + BuiltInAttributeKey.bold: true, + }), + TextInsert('Welcome', attributes: { + BuiltInAttributeKey.code: true, + BuiltInAttributeKey.italic: true, + BuiltInAttributeKey.bold: true, + }), + TextInsert('', 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); + }); + }); +}