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 9969681e73..9aa66d962a 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 @@ -50,7 +50,7 @@ class TextInsert extends TextOperation { final result = { 'insert': text, }; - if (_attributes != null) { + if (_attributes != null && _attributes!.isNotEmpty) { result['attributes'] = attributes; } return result; @@ -87,7 +87,7 @@ class TextRetain extends TextOperation { final result = { 'retain': length, }; - if (_attributes != null) { + if (_attributes != null && _attributes!.isNotEmpty) { result['attributes'] = attributes; } return result; 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 index ccd49f22fd..e015546989 100644 --- 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 @@ -1,6 +1,8 @@ import 'dart:convert'; -import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/core/document/attributes.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:markdown/markdown.dart' as md; class DeltaMarkdownDecoder extends Converter diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart new file mode 100644 index 0000000000..257cfabb28 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/document_markdown_decoder.dart @@ -0,0 +1,91 @@ +import 'dart:convert'; + +import 'package:appflowy_editor/appflowy_editor.dart'; + +class DocumentMarkdownDecoder extends Converter { + @override + Document convert(String input) { + final lines = input.split('\n'); + final document = Document.empty(); + + var i = 0; + for (final line in lines) { + document.insert([i++], [_convertLineToNode(line)]); + } + + return document; + } + + Node _convertLineToNode(String text) { + final decoder = DeltaMarkdownDecoder(); + // Heading Style + if (text.startsWith('### ')) { + return TextNode( + delta: decoder.convert(text.substring(4)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, + BuiltInAttributeKey.heading: BuiltInAttributeKey.h3, + }, + ); + } else if (text.startsWith('## ')) { + return TextNode( + delta: decoder.convert(text.substring(3)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, + BuiltInAttributeKey.heading: BuiltInAttributeKey.h2, + }, + ); + } else if (text.startsWith('# ')) { + return TextNode( + delta: decoder.convert(text.substring(2)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading, + BuiltInAttributeKey.heading: BuiltInAttributeKey.h1, + }, + ); + } else if (text.startsWith('- [ ] ')) { + return TextNode( + delta: decoder.convert(text.substring(6)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, + BuiltInAttributeKey.checkbox: false, + }, + ); + } else if (text.startsWith('- [x] ')) { + return TextNode( + delta: decoder.convert(text.substring(6)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox, + BuiltInAttributeKey.checkbox: true, + }, + ); + } else if (text.startsWith('> ')) { + return TextNode( + delta: decoder.convert(text.substring(2)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote, + }, + ); + } else if (text.startsWith('- ') || text.startsWith('* ')) { + return TextNode( + delta: decoder.convert(text.substring(2)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList, + }, + ); + } else if (text.startsWith('> ')) { + return TextNode( + delta: decoder.convert(text.substring(2)), + attributes: { + BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote, + }, + ); + } + + if (text.isNotEmpty) { + return TextNode(delta: decoder.convert(text)); + } + + return TextNode(delta: Delta()); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/document_markdown.dart b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/document_markdown.dart new file mode 100644 index 0000000000..48714a6a85 --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/document_markdown.dart @@ -0,0 +1,29 @@ +library delta_markdown; + +import 'dart:convert'; + +import 'package:appflowy_editor/src/core/document/document.dart'; +import 'package:appflowy_editor/src/plugins/markdown/encoder/document_markdown_encoder.dart'; + +/// Codec used to convert between Markdown and AppFlowy Editor Document. +const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec(); + +Document markdownToDocument(String markdown) { + return _kCodec.decode(markdown); +} + +String documentToMarkdown(Document document) { + return _kCodec.encode(document); +} + +class AppFlowyEditorMarkdownCodec extends Codec { + const AppFlowyEditorMarkdownCodec(); + + @override + Converter get decoder => throw UnimplementedError(); + + @override + Converter get encoder { + return DocumentMarkdownEncoder(); + } +} diff --git a/frontend/app_flowy/packages/appflowy_editor/test/plugins/markdown/decoder/document_markdown_decoder_test.dart b/frontend/app_flowy/packages/appflowy_editor/test/plugins/markdown/decoder/document_markdown_decoder_test.dart new file mode 100644 index 0000000000..a95e7b877e --- /dev/null +++ b/frontend/app_flowy/packages/appflowy_editor/test/plugins/markdown/decoder/document_markdown_decoder_test.dart @@ -0,0 +1,126 @@ +import 'dart:convert'; + +import 'package:appflowy_editor/src/plugins/markdown/decoder/document_markdown_decoder.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + group('document_markdown_decoder.dart', () { + const example = ''' +{ + "document": { + "type": "editor", + "children": [ + { + "type": "text", + "attributes": {"subtype": "heading", "heading": "h2"}, + "delta": [ + {"insert": "👋 "}, + {"insert": "Welcome to", "attributes": {"bold": true}}, + {"insert": " "}, + { + "insert": "AppFlowy Editor", + "attributes": {"italic": true, "bold": true, "href": "appflowy.io"} + } + ] + }, + {"type": "text", "delta": []}, + { + "type": "text", + "delta": [ + {"insert": "AppFlowy Editor is a "}, + {"insert": "highly customizable", "attributes": {"bold": true}}, + {"insert": " "}, + {"insert": "rich-text editor", "attributes": {"italic": true}} + ] + }, + { + "type": "text", + "attributes": {"subtype": "checkbox", "checkbox": true}, + "delta": [{"insert": "Customizable"}] + }, + { + "type": "text", + "attributes": {"subtype": "checkbox", "checkbox": true}, + "delta": [{"insert": "Test-covered"}] + }, + { + "type": "text", + "attributes": {"subtype": "checkbox", "checkbox": false}, + "delta": [{"insert": "more to come!"}] + }, + {"type": "text", "delta": []}, + { + "type": "text", + "attributes": {"subtype": "quote"}, + "delta": [{"insert": "Here is an example you can give a try"}] + }, + {"type": "text", "delta": []}, + { + "type": "text", + "delta": [ + {"insert": "You can also use "}, + { + "insert": "AppFlowy Editor", + "attributes": {"italic": true, "bold": true} + }, + {"insert": " as a component to build your own app."} + ] + }, + {"type": "text", "delta": []}, + { + "type": "text", + "attributes": {"subtype": "bulleted-list"}, + "delta": [{"insert": "Use / to insert blocks"}] + }, + { + "type": "text", + "attributes": {"subtype": "bulleted-list"}, + "delta": [ + { + "insert": "Select text to trigger to the toolbar to format your notes." + } + ] + }, + {"type": "text", "delta": []}, + { + "type": "text", + "delta": [ + { + "insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!" + } + ] + }, + {"type": "text", "delta": []}, + {"type": "text", "delta": [{"insert": ""}]} + ] + } +} +'''; + setUpAll(() { + TestWidgetsFlutterBinding.ensureInitialized(); + }); + + test('parser document', () async { + const markdown = ''' +## 👋 **Welcome to** ***[AppFlowy Editor](appflowy.io)*** + +AppFlowy Editor is a **highly customizable** _rich-text editor_ +- [x] Customizable +- [x] Test-covered +- [ ] more to come! + +> Here is an example you can give a try + +You can also use ***AppFlowy Editor*** as a component to build your own app. + +* Use / to insert blocks +* Select text to trigger to the toolbar to format your notes. + +If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders! +'''; + final result = DocumentMarkdownDecoder().convert(markdown); + final data = Map.from(json.decode(example)); + expect(result.toJson(), data); + }); + }); +}