Skip to content

Commit 4622a41

Browse files
committed
feat: markdown to document
1 parent fc35f74 commit 4622a41

File tree

5 files changed

+251
-3
lines changed

5 files changed

+251
-3
lines changed

frontend/app_flowy/packages/appflowy_editor/lib/src/core/document/text_delta.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class TextInsert extends TextOperation {
5050
final result = <String, dynamic>{
5151
'insert': text,
5252
};
53-
if (_attributes != null) {
53+
if (_attributes != null && _attributes!.isNotEmpty) {
5454
result['attributes'] = attributes;
5555
}
5656
return result;
@@ -87,7 +87,7 @@ class TextRetain extends TextOperation {
8787
final result = <String, dynamic>{
8888
'retain': length,
8989
};
90-
if (_attributes != null) {
90+
if (_attributes != null && _attributes!.isNotEmpty) {
9191
result['attributes'] = attributes;
9292
}
9393
return result;

frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/decoder/delta_markdown_decoder.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import 'dart:convert';
22

3-
import 'package:appflowy_editor/appflowy_editor.dart';
3+
import 'package:appflowy_editor/src/core/document/attributes.dart';
4+
import 'package:appflowy_editor/src/core/document/text_delta.dart';
5+
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
46
import 'package:markdown/markdown.dart' as md;
57

68
class DeltaMarkdownDecoder extends Converter<String, Delta>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'dart:convert';
2+
3+
import 'package:appflowy_editor/appflowy_editor.dart';
4+
5+
class DocumentMarkdownDecoder extends Converter<String, Document> {
6+
@override
7+
Document convert(String input) {
8+
final lines = input.split('\n');
9+
final document = Document.empty();
10+
11+
var i = 0;
12+
for (final line in lines) {
13+
document.insert([i++], [_convertLineToNode(line)]);
14+
}
15+
16+
return document;
17+
}
18+
19+
Node _convertLineToNode(String text) {
20+
final decoder = DeltaMarkdownDecoder();
21+
// Heading Style
22+
if (text.startsWith('### ')) {
23+
return TextNode(
24+
delta: decoder.convert(text.substring(4)),
25+
attributes: {
26+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
27+
BuiltInAttributeKey.heading: BuiltInAttributeKey.h3,
28+
},
29+
);
30+
} else if (text.startsWith('## ')) {
31+
return TextNode(
32+
delta: decoder.convert(text.substring(3)),
33+
attributes: {
34+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
35+
BuiltInAttributeKey.heading: BuiltInAttributeKey.h2,
36+
},
37+
);
38+
} else if (text.startsWith('# ')) {
39+
return TextNode(
40+
delta: decoder.convert(text.substring(2)),
41+
attributes: {
42+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
43+
BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
44+
},
45+
);
46+
} else if (text.startsWith('- [ ] ')) {
47+
return TextNode(
48+
delta: decoder.convert(text.substring(6)),
49+
attributes: {
50+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
51+
BuiltInAttributeKey.checkbox: false,
52+
},
53+
);
54+
} else if (text.startsWith('- [x] ')) {
55+
return TextNode(
56+
delta: decoder.convert(text.substring(6)),
57+
attributes: {
58+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
59+
BuiltInAttributeKey.checkbox: true,
60+
},
61+
);
62+
} else if (text.startsWith('> ')) {
63+
return TextNode(
64+
delta: decoder.convert(text.substring(2)),
65+
attributes: {
66+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
67+
},
68+
);
69+
} else if (text.startsWith('- ') || text.startsWith('* ')) {
70+
return TextNode(
71+
delta: decoder.convert(text.substring(2)),
72+
attributes: {
73+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
74+
},
75+
);
76+
} else if (text.startsWith('> ')) {
77+
return TextNode(
78+
delta: decoder.convert(text.substring(2)),
79+
attributes: {
80+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
81+
},
82+
);
83+
}
84+
85+
if (text.isNotEmpty) {
86+
return TextNode(delta: decoder.convert(text));
87+
}
88+
89+
return TextNode(delta: Delta());
90+
}
91+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
library delta_markdown;
2+
3+
import 'dart:convert';
4+
5+
import 'package:appflowy_editor/src/core/document/document.dart';
6+
import 'package:appflowy_editor/src/plugins/markdown/encoder/document_markdown_encoder.dart';
7+
8+
/// Codec used to convert between Markdown and AppFlowy Editor Document.
9+
const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec();
10+
11+
Document markdownToDocument(String markdown) {
12+
return _kCodec.decode(markdown);
13+
}
14+
15+
String documentToMarkdown(Document document) {
16+
return _kCodec.encode(document);
17+
}
18+
19+
class AppFlowyEditorMarkdownCodec extends Codec<Document, String> {
20+
const AppFlowyEditorMarkdownCodec();
21+
22+
@override
23+
Converter<String, Document> get decoder => throw UnimplementedError();
24+
25+
@override
26+
Converter<Document, String> get encoder {
27+
return DocumentMarkdownEncoder();
28+
}
29+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import 'dart:convert';
2+
3+
import 'package:appflowy_editor/src/plugins/markdown/decoder/document_markdown_decoder.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
void main() async {
7+
group('document_markdown_decoder.dart', () {
8+
const example = '''
9+
{
10+
"document": {
11+
"type": "editor",
12+
"children": [
13+
{
14+
"type": "text",
15+
"attributes": {"subtype": "heading", "heading": "h2"},
16+
"delta": [
17+
{"insert": "👋 "},
18+
{"insert": "Welcome to", "attributes": {"bold": true}},
19+
{"insert": " "},
20+
{
21+
"insert": "AppFlowy Editor",
22+
"attributes": {"italic": true, "bold": true, "href": "appflowy.io"}
23+
}
24+
]
25+
},
26+
{"type": "text", "delta": []},
27+
{
28+
"type": "text",
29+
"delta": [
30+
{"insert": "AppFlowy Editor is a "},
31+
{"insert": "highly customizable", "attributes": {"bold": true}},
32+
{"insert": " "},
33+
{"insert": "rich-text editor", "attributes": {"italic": true}}
34+
]
35+
},
36+
{
37+
"type": "text",
38+
"attributes": {"subtype": "checkbox", "checkbox": true},
39+
"delta": [{"insert": "Customizable"}]
40+
},
41+
{
42+
"type": "text",
43+
"attributes": {"subtype": "checkbox", "checkbox": true},
44+
"delta": [{"insert": "Test-covered"}]
45+
},
46+
{
47+
"type": "text",
48+
"attributes": {"subtype": "checkbox", "checkbox": false},
49+
"delta": [{"insert": "more to come!"}]
50+
},
51+
{"type": "text", "delta": []},
52+
{
53+
"type": "text",
54+
"attributes": {"subtype": "quote"},
55+
"delta": [{"insert": "Here is an example you can give a try"}]
56+
},
57+
{"type": "text", "delta": []},
58+
{
59+
"type": "text",
60+
"delta": [
61+
{"insert": "You can also use "},
62+
{
63+
"insert": "AppFlowy Editor",
64+
"attributes": {"italic": true, "bold": true}
65+
},
66+
{"insert": " as a component to build your own app."}
67+
]
68+
},
69+
{"type": "text", "delta": []},
70+
{
71+
"type": "text",
72+
"attributes": {"subtype": "bulleted-list"},
73+
"delta": [{"insert": "Use / to insert blocks"}]
74+
},
75+
{
76+
"type": "text",
77+
"attributes": {"subtype": "bulleted-list"},
78+
"delta": [
79+
{
80+
"insert": "Select text to trigger to the toolbar to format your notes."
81+
}
82+
]
83+
},
84+
{"type": "text", "delta": []},
85+
{
86+
"type": "text",
87+
"delta": [
88+
{
89+
"insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!"
90+
}
91+
]
92+
},
93+
{"type": "text", "delta": []},
94+
{"type": "text", "delta": [{"insert": ""}]}
95+
]
96+
}
97+
}
98+
''';
99+
setUpAll(() {
100+
TestWidgetsFlutterBinding.ensureInitialized();
101+
});
102+
103+
test('parser document', () async {
104+
const markdown = '''
105+
## 👋 **Welcome to** ***[AppFlowy Editor](appflowy.io)***
106+
107+
AppFlowy Editor is a **highly customizable** _rich-text editor_
108+
- [x] Customizable
109+
- [x] Test-covered
110+
- [ ] more to come!
111+
112+
> Here is an example you can give a try
113+
114+
You can also use ***AppFlowy Editor*** as a component to build your own app.
115+
116+
* Use / to insert blocks
117+
* Select text to trigger to the toolbar to format your notes.
118+
119+
If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!
120+
''';
121+
final result = DocumentMarkdownDecoder().convert(markdown);
122+
final data = Map<String, Object>.from(json.decode(example));
123+
expect(result.toJson(), data);
124+
});
125+
});
126+
}

0 commit comments

Comments
 (0)