Skip to content

Commit cdf6f1b

Browse files
authored
Merge pull request #1424 from LucasXu0/markdown
Implement appflowy editor document to markdown
2 parents 882d553 + eb73564 commit cdf6f1b

19 files changed

+871
-39
lines changed

frontend/app_flowy/lib/plugins/doc/application/share_bloc.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import 'dart:convert';
22
import 'dart:io';
33
import 'package:app_flowy/plugins/doc/application/share_service.dart';
4-
import 'package:app_flowy/workspace/application/markdown/document_markdown.dart';
54
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
65
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
76
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
87
import 'package:freezed_annotation/freezed_annotation.dart';
98
import 'package:flutter_bloc/flutter_bloc.dart';
109
import 'package:dartz/dartz.dart';
11-
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
10+
import 'package:appflowy_editor/appflowy_editor.dart'
11+
show Document, documentToMarkdown;
1212
part 'share_bloc.freezed.dart';
1313

1414
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {

frontend/app_flowy/packages/appflowy_editor/lib/appflowy_editor.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ export 'src/render/selection_menu/selection_menu_widget.dart';
3333
export 'src/l10n/l10n.dart';
3434
export 'src/render/style/plugin_styles.dart';
3535
export 'src/render/style/editor_style.dart';
36+
export 'src/plugins/markdown/encoder/delta_markdown_encoder.dart';
37+
export 'src/plugins/markdown/encoder/document_markdown_encoder.dart';
38+
export 'src/plugins/markdown/encoder/parser/node_parser.dart';
39+
export 'src/plugins/markdown/encoder/parser/text_node_parser.dart';
40+
export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
41+
export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
42+
export 'src/plugins/markdown/document_markdown.dart';

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

Lines changed: 12 additions & 5 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;
@@ -62,7 +62,7 @@ class TextInsert extends TextOperation {
6262

6363
return other is TextInsert &&
6464
other.text == text &&
65-
mapEquals(_attributes, other._attributes);
65+
_mapEquals(_attributes, other._attributes);
6666
}
6767

6868
@override
@@ -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;
@@ -99,7 +99,7 @@ class TextRetain extends TextOperation {
9999

100100
return other is TextRetain &&
101101
other.length == length &&
102-
mapEquals(_attributes, other._attributes);
102+
_mapEquals(_attributes, other._attributes);
103103
}
104104

105105
@override
@@ -181,7 +181,7 @@ class Delta extends Iterable<TextOperation> {
181181
lastOp.length += textOperation.length;
182182
return;
183183
}
184-
if (mapEquals(lastOp.attributes, textOperation.attributes)) {
184+
if (_mapEquals(lastOp.attributes, textOperation.attributes)) {
185185
if (lastOp is TextInsert && textOperation is TextInsert) {
186186
lastOp.text += textOperation.text;
187187
return;
@@ -539,3 +539,10 @@ class _OpIterator {
539539
}
540540
}
541541
}
542+
543+
bool _mapEquals<T, U>(Map<T, U>? a, Map<T, U>? b) {
544+
if ((a == null || a.isEmpty) && (b == null || b.isEmpty)) {
545+
return true;
546+
}
547+
return mapEquals(a, b);
548+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import 'dart:convert';
2+
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';
6+
import 'package:markdown/markdown.dart' as md;
7+
8+
class DeltaMarkdownDecoder extends Converter<String, Delta>
9+
with md.NodeVisitor {
10+
final _delta = Delta();
11+
final Attributes _attributes = {};
12+
13+
@override
14+
Delta convert(String input) {
15+
final document =
16+
md.Document(extensionSet: md.ExtensionSet.gitHubWeb).parseInline(input);
17+
for (final node in document) {
18+
node.accept(this);
19+
}
20+
return _delta;
21+
}
22+
23+
@override
24+
void visitElementAfter(md.Element element) {
25+
_removeAttributeKey(element);
26+
}
27+
28+
@override
29+
bool visitElementBefore(md.Element element) {
30+
_addAttributeKey(element);
31+
return true;
32+
}
33+
34+
@override
35+
void visitText(md.Text text) {
36+
_delta.add(TextInsert(text.text, attributes: {..._attributes}));
37+
}
38+
39+
void _addAttributeKey(md.Element element) {
40+
if (element.tag == 'strong') {
41+
_attributes[BuiltInAttributeKey.bold] = true;
42+
} else if (element.tag == 'em') {
43+
_attributes[BuiltInAttributeKey.italic] = true;
44+
} else if (element.tag == 'code') {
45+
_attributes[BuiltInAttributeKey.code] = true;
46+
} else if (element.tag == 'del') {
47+
_attributes[BuiltInAttributeKey.strikethrough] = true;
48+
} else if (element.tag == 'a') {
49+
_attributes[BuiltInAttributeKey.href] = element.attributes['href'];
50+
}
51+
}
52+
53+
void _removeAttributeKey(md.Element element) {
54+
if (element.tag == 'strong') {
55+
_attributes.remove(BuiltInAttributeKey.bold);
56+
} else if (element.tag == 'em') {
57+
_attributes.remove(BuiltInAttributeKey.italic);
58+
} else if (element.tag == 'code') {
59+
_attributes.remove(BuiltInAttributeKey.code);
60+
} else if (element.tag == 'del') {
61+
_attributes.remove(BuiltInAttributeKey.strikethrough);
62+
} else if (element.tag == 'a') {
63+
_attributes.remove(BuiltInAttributeKey.href);
64+
}
65+
}
66+
}
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+
}

frontend/app_flowy/lib/workspace/application/markdown/document_markdown.dart renamed to frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/document_markdown.dart

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ library delta_markdown;
22

33
import 'dart:convert';
44

5-
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
6-
import 'package:app_flowy/workspace/application/markdown/src/parser/markdown_encoder.dart';
5+
import 'package:appflowy_editor/src/core/document/document.dart';
6+
import 'package:appflowy_editor/src/plugins/markdown/decoder/document_markdown_decoder.dart';
7+
import 'package:appflowy_editor/src/plugins/markdown/encoder/document_markdown_encoder.dart';
78

89
/// Codec used to convert between Markdown and AppFlowy Editor Document.
910
const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec();
@@ -20,10 +21,8 @@ class AppFlowyEditorMarkdownCodec extends Codec<Document, String> {
2021
const AppFlowyEditorMarkdownCodec();
2122

2223
@override
23-
Converter<String, Document> get decoder => throw UnimplementedError();
24+
Converter<String, Document> get decoder => DocumentMarkdownDecoder();
2425

2526
@override
26-
Converter<Document, String> get encoder {
27-
return AppFlowyEditorMarkdownEncoder();
28-
}
27+
Converter<Document, String> get encoder => DocumentMarkdownEncoder();
2928
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import 'dart:convert';
2+
3+
import 'package:appflowy_editor/appflowy_editor.dart';
4+
5+
/// A [Delta] encoder that encodes a [Delta] to Markdown.
6+
///
7+
/// Only support inline styles, like bold, italic, underline, strike, code.
8+
class DeltaMarkdownEncoder extends Converter<Delta, String> {
9+
@override
10+
String convert(Delta input) {
11+
final buffer = StringBuffer();
12+
final iterator = input.iterator;
13+
while (iterator.moveNext()) {
14+
final op = iterator.current;
15+
if (op is TextInsert) {
16+
final attributes = op.attributes;
17+
if (attributes != null) {
18+
buffer.write(_prefixSyntax(attributes));
19+
buffer.write(op.text);
20+
buffer.write(_suffixSyntax(attributes));
21+
} else {
22+
buffer.write(op.text);
23+
}
24+
}
25+
}
26+
return buffer.toString();
27+
}
28+
29+
String _prefixSyntax(Attributes attributes) {
30+
var syntax = '';
31+
32+
if (attributes[BuiltInAttributeKey.bold] == true &&
33+
attributes[BuiltInAttributeKey.italic] == true) {
34+
syntax += '***';
35+
} else if (attributes[BuiltInAttributeKey.bold] == true) {
36+
syntax += '**';
37+
} else if (attributes[BuiltInAttributeKey.italic] == true) {
38+
syntax += '_';
39+
}
40+
41+
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
42+
syntax += '~~';
43+
}
44+
if (attributes[BuiltInAttributeKey.underline] == true) {
45+
syntax += '<u>';
46+
}
47+
if (attributes[BuiltInAttributeKey.code] == true) {
48+
syntax += '`';
49+
}
50+
51+
if (attributes[BuiltInAttributeKey.href] != null) {
52+
syntax += '[';
53+
}
54+
55+
return syntax;
56+
}
57+
58+
String _suffixSyntax(Attributes attributes) {
59+
var syntax = '';
60+
61+
if (attributes[BuiltInAttributeKey.href] != null) {
62+
syntax += '](${attributes[BuiltInAttributeKey.href]})';
63+
}
64+
65+
if (attributes[BuiltInAttributeKey.code] == true) {
66+
syntax += '`';
67+
}
68+
69+
if (attributes[BuiltInAttributeKey.underline] == true) {
70+
syntax += '</u>';
71+
}
72+
73+
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
74+
syntax += '~~';
75+
}
76+
77+
if (attributes[BuiltInAttributeKey.bold] == true &&
78+
attributes[BuiltInAttributeKey.italic] == true) {
79+
syntax += '***';
80+
} else if (attributes[BuiltInAttributeKey.bold] == true) {
81+
syntax += '**';
82+
} else if (attributes[BuiltInAttributeKey.italic] == true) {
83+
syntax += '_';
84+
}
85+
86+
return syntax;
87+
}
88+
}

frontend/app_flowy/lib/workspace/application/markdown/src/parser/markdown_encoder.dart renamed to frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/encoder/document_markdown_encoder.dart

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

3-
import 'package:app_flowy/workspace/application/markdown/src/parser/image_node_parser.dart';
4-
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
5-
import 'package:app_flowy/workspace/application/markdown/src/parser/text_node_parser.dart';
6-
import 'package:appflowy_editor/appflowy_editor.dart';
3+
import 'package:appflowy_editor/src/core/document/document.dart';
4+
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/image_node_parser.dart';
5+
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/node_parser.dart';
6+
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/text_node_parser.dart';
77

8-
class AppFlowyEditorMarkdownEncoder extends Converter<Document, String> {
9-
AppFlowyEditorMarkdownEncoder({
8+
class DocumentMarkdownEncoder extends Converter<Document, String> {
9+
DocumentMarkdownEncoder({
1010
this.parsers = const [
1111
TextNodeParser(),
1212
ImageNodeParser(),

frontend/app_flowy/lib/workspace/application/markdown/src/parser/image_node_parser.dart renamed to frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/encoder/parser/image_node_parser.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
2-
import 'package:appflowy_editor/appflowy_editor.dart';
1+
import 'package:appflowy_editor/src/core/document/node.dart';
2+
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/node_parser.dart';
33

44
class ImageNodeParser extends NodeParser {
55
const ImageNodeParser();

frontend/app_flowy/lib/workspace/application/markdown/src/parser/node_parser.dart renamed to frontend/app_flowy/packages/appflowy_editor/lib/src/plugins/markdown/encoder/parser/node_parser.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import 'package:appflowy_editor/appflowy_editor.dart';
1+
import 'package:appflowy_editor/src/core/document/node.dart';
22

33
abstract class NodeParser {
44
const NodeParser();

0 commit comments

Comments
 (0)