Skip to content

Commit 3ba3a8d

Browse files
authored
Merge pull request #1655 from LucasXu0/feat_1649
feat: #1649 [FR] Convert quill delta to appflowy document
2 parents b25db83 + 50f9ac1 commit 3ba3a8d

File tree

7 files changed

+846
-9
lines changed

7 files changed

+846
-9
lines changed

frontend/app_flowy/packages/appflowy_editor/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
<!--
1+
<!--
22
This README describes the package. If you publish this package to pub.dev,
33
this README's contents appear on the landing page for your package.
44
55
For information about how to write a good package README, see the guide for
6-
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
6+
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
77
88
For general information about developing packages, see the Dart guide for
99
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
1010
and the Flutter guide for
11-
[developing packages and plugins](https://flutter.dev/developing-packages).
11+
[developing packages and plugins](https://flutter.dev/developing-packages).
1212
-->
1313

1414
<h1 align="center"><b>AppFlowy Editor</b></h1>
@@ -51,7 +51,7 @@ flutter pub get
5151

5252
## Creating Your First Editor
5353

54-
Start by creating a new empty AppFlowyEditor object.
54+
Start by creating a new empty AppFlowyEditor object.
5555

5656
```dart
5757
final editorState = EditorState.empty(); // an empty state
@@ -60,7 +60,7 @@ final editor = AppFlowyEditor(
6060
);
6161
```
6262

63-
You can also create an editor from a JSON object in order to configure your initial state.
63+
You can also create an editor from a JSON object in order to configure your initial state. Or you can [create an editor from Markdown or Quill Delta](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/importing.md).
6464

6565
```dart
6666
final json = ...;
@@ -79,7 +79,7 @@ MaterialApp(
7979
);
8080
```
8181

82-
To get a sense for how the AppFlowy Editor works, run our example:
82+
To get a sense of how the AppFlowy Editor works, run our example:
8383

8484
```shell
8585
git clone https://github.com/AppFlowy-IO/AppFlowy.git
@@ -98,7 +98,7 @@ Below are some examples of component customizations:
9898
* [Checkbox Text](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/checkbox_text.dart) demonstrates how to extend new styles based on existing rich text components
9999
* [Image](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/plugin/network_image_node_widget.dart) demonstrates how to extend a new node and render it
100100
* See further examples of [rich-text plugins](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text)
101-
101+
102102
### Customizing Shortcut Events
103103

104104
Please refer to our documentation on customizing AppFlowy for a detailed discussion about [customizing shortcut events](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/documentation/customizing.md#customize-a-shortcut-event).
@@ -113,7 +113,7 @@ Below are some examples of shortcut event customizations:
113113
Please refer to the API documentation.
114114

115115
## Contributing
116-
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
116+
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
117117

118118
Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
119119

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Importing data
2+
3+
For now, we have supported three ways to import data to initialize AppFlowy Editor.
4+
5+
1. From AppFlowy Document JSON
6+
7+
```dart
8+
const document = r'''{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"},"delta":[{"insert":"Hello AppFlowy!"}]}]}}''';
9+
final json = jsonDecode(document);
10+
final editorState = EditorState(
11+
document: Document.fromJson(
12+
Map<String, Object>.from(json),
13+
),
14+
);
15+
```
16+
17+
2. From Markdown
18+
19+
```dart
20+
const markdown = r'''# Hello AppFlowy!''';
21+
final editorState = EditorState(
22+
document: markdownToDocument(markdown),
23+
);
24+
```
25+
26+
3. From Quill Delta
27+
28+
```dart
29+
const delta = r'''[{"insert":"Hello AppFlowy!"},{"attributes":{"header":1},"insert":"\n"}]''';
30+
final json = jsonDecode(delta);
31+
final editorState = EditorState(
32+
document: DeltaDocumentConvert().convertFromJSON(json),
33+
);
34+
```
35+
36+
For more details, please refer to the function `_importFile` through this [link](https://github.com/AppFlowy-IO/AppFlowy/blob/main/frontend/app_flowy/packages/appflowy_editor/example/lib/home_page.dart).

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ enum ExportFileType {
1414
json,
1515
markdown,
1616
html,
17+
delta,
1718
}
1819

1920
extension on ExportFileType {
2021
String get extension {
2122
switch (this) {
2223
case ExportFileType.json:
24+
case ExportFileType.delta:
2325
return 'json';
2426
case ExportFileType.markdown:
2527
return 'md';
@@ -117,6 +119,9 @@ class _HomePageState extends State<HomePage> {
117119
_buildListTile(context, 'Import From Markdown', () {
118120
_importFile(ExportFileType.markdown);
119121
}),
122+
_buildListTile(context, 'Import From Quill Delta', () {
123+
_importFile(ExportFileType.delta);
124+
}),
120125

121126
// Theme Demo
122127
_buildSeparator(context, 'Theme Demo'),
@@ -224,6 +229,7 @@ class _HomePageState extends State<HomePage> {
224229
result = documentToMarkdown(editorState.document);
225230
break;
226231
case ExportFileType.html:
232+
case ExportFileType.delta:
227233
throw UnimplementedError();
228234
}
229235

@@ -280,6 +286,17 @@ class _HomePageState extends State<HomePage> {
280286
case ExportFileType.markdown:
281287
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
282288
break;
289+
case ExportFileType.delta:
290+
jsonString = jsonEncode(
291+
DeltaDocumentConvert()
292+
.convertFromJSON(
293+
jsonDecode(
294+
plainText.replaceAll('\\\\\n', '\\n'),
295+
),
296+
)
297+
.toJson(),
298+
);
299+
break;
283300
case ExportFileType.html:
284301
throw UnimplementedError();
285302
}

frontend/app_flowy/packages/appflowy_editor/example/macos/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ EXTERNAL SOURCES:
3535

3636
SPEC CHECKSUMS:
3737
flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
38-
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
38+
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
3939
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
4040
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
4141
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ export 'src/plugins/markdown/encoder/parser/text_node_parser.dart';
4141
export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
4242
export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
4343
export 'src/plugins/markdown/document_markdown.dart';
44+
export 'src/plugins/quill_delta/delta_document_encoder.dart';
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import 'package:appflowy_editor/src/core/document/attributes.dart';
2+
import 'package:appflowy_editor/src/core/document/document.dart';
3+
import 'package:appflowy_editor/src/core/document/node.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:flutter/material.dart';
7+
8+
class DeltaDocumentConvert {
9+
DeltaDocumentConvert();
10+
11+
var _number = 1;
12+
final Map<int, List<TextNode>> _bulletedList = {};
13+
14+
Document convertFromJSON(List<dynamic> json) {
15+
final delta = Delta.fromJson(json);
16+
return convertFromDelta(delta);
17+
}
18+
19+
Document convertFromDelta(Delta delta) {
20+
final iter = delta.iterator;
21+
22+
final document = Document.empty();
23+
TextNode textNode = TextNode(delta: Delta());
24+
int path = 0;
25+
26+
while (iter.moveNext()) {
27+
final op = iter.current;
28+
if (op is TextInsert) {
29+
if (op.text != '\n') {
30+
// Attributes associated with a newline character describes formatting for that line.
31+
final texts = op.text.split('\n');
32+
if (texts.length > 1) {
33+
textNode.delta.insert(texts[0]);
34+
document.insert([path++], [textNode]);
35+
textNode = TextNode(delta: Delta()..insert(texts[1]));
36+
} else {
37+
_applyStyle(textNode, op.text, op.attributes);
38+
}
39+
} else {
40+
if (!_containNumberListStyle(op.attributes)) {
41+
_number = 1;
42+
}
43+
_applyListStyle(textNode, op.attributes);
44+
_applyHeaderStyle(textNode, op.attributes);
45+
_applyIndent(textNode, op.attributes);
46+
_applyBlockquote(textNode, op.attributes);
47+
// _applyCodeBlock(textNode, op.attributes);
48+
49+
if (_containIndentBulletedListStyle(op.attributes)) {
50+
final level = _indentLevel(op.attributes);
51+
final path = [
52+
..._bulletedList[level - 1]!.last.path,
53+
_bulletedList[level]!.length - 1,
54+
];
55+
document.insert(path, [textNode]);
56+
} else {
57+
document.insert([path++], [textNode]);
58+
}
59+
textNode = TextNode(delta: Delta());
60+
}
61+
} else {
62+
assert(false, 'op must be TextInsert');
63+
}
64+
}
65+
66+
return document;
67+
}
68+
69+
void _applyStyle(TextNode textNode, String text, Map? attributes) {
70+
Attributes attrs = {};
71+
72+
if (_containsStyle(attributes, 'strike')) {
73+
attrs[BuiltInAttributeKey.strikethrough] = true;
74+
}
75+
if (_containsStyle(attributes, 'underline')) {
76+
attrs[BuiltInAttributeKey.underline] = true;
77+
}
78+
if (_containsStyle(attributes, 'bold')) {
79+
attrs[BuiltInAttributeKey.bold] = true;
80+
}
81+
if (_containsStyle(attributes, 'italic')) {
82+
attrs[BuiltInAttributeKey.italic] = true;
83+
}
84+
final link = attributes?['link'] as String?;
85+
if (link != null) {
86+
attrs[BuiltInAttributeKey.href] = link;
87+
}
88+
final color = attributes?['color'] as String?;
89+
final colorHex = _convertColorToHexString(color);
90+
if (colorHex != null) {
91+
attrs[BuiltInAttributeKey.color] = colorHex;
92+
}
93+
final backgroundColor = attributes?['background'] as String?;
94+
final backgroundHex = _convertColorToHexString(backgroundColor);
95+
if (backgroundHex != null) {
96+
attrs[BuiltInAttributeKey.backgroundColor] = backgroundHex;
97+
}
98+
99+
textNode.delta.insert(text, attributes: attrs);
100+
}
101+
102+
bool _containsStyle(Map? attributes, String key) {
103+
final value = attributes?[key] as bool?;
104+
return value == true;
105+
}
106+
107+
String? _convertColorToHexString(String? color) {
108+
if (color == null) {
109+
return null;
110+
}
111+
if (color.startsWith('#')) {
112+
return '0xFF${color.substring(1)}';
113+
} else if (color.startsWith("rgba")) {
114+
List rgbaList = color.substring(5, color.length - 1).split(',');
115+
return Color.fromRGBO(
116+
int.parse(rgbaList[0]),
117+
int.parse(rgbaList[1]),
118+
int.parse(rgbaList[2]),
119+
double.parse(rgbaList[3]),
120+
).toHex();
121+
}
122+
return null;
123+
}
124+
125+
// convert bullet-list, number-list, check-list to appflowy style list.
126+
void _applyListStyle(TextNode textNode, Map? attributes) {
127+
final indent = attributes?['indent'] as int?;
128+
final list = attributes?['list'] as String?;
129+
if (list != null) {
130+
switch (list) {
131+
case 'bullet':
132+
textNode.updateAttributes({
133+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
134+
});
135+
if (indent != null) {
136+
_bulletedList[indent] ??= [];
137+
_bulletedList[indent]?.add(textNode);
138+
} else {
139+
_bulletedList.clear();
140+
_bulletedList[0] ??= [];
141+
_bulletedList[0]?.add(textNode);
142+
}
143+
break;
144+
case 'ordered':
145+
textNode.updateAttributes({
146+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
147+
BuiltInAttributeKey.number: _number++,
148+
});
149+
break;
150+
case 'checked':
151+
textNode.updateAttributes({
152+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
153+
BuiltInAttributeKey.checkbox: true,
154+
});
155+
break;
156+
case 'unchecked':
157+
textNode.updateAttributes({
158+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
159+
BuiltInAttributeKey.checkbox: false,
160+
});
161+
break;
162+
}
163+
}
164+
}
165+
166+
bool _containNumberListStyle(Map? attributes) {
167+
final list = attributes?['list'] as String?;
168+
return list == 'ordered';
169+
}
170+
171+
bool _containIndentBulletedListStyle(Map? attributes) {
172+
final list = attributes?['list'] as String?;
173+
final indent = attributes?['indent'] as int?;
174+
return list == 'bullet' && indent != null;
175+
}
176+
177+
int _indentLevel(Map? attributes) {
178+
final indent = attributes?['indent'] as int?;
179+
return indent ?? 1;
180+
}
181+
182+
// convert header to appflowy style heading
183+
void _applyHeaderStyle(TextNode textNode, Map? attributes) {
184+
final header = attributes?['header'] as int?;
185+
if (header != null) {
186+
textNode.updateAttributes({
187+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
188+
BuiltInAttributeKey.heading: 'h$header',
189+
});
190+
}
191+
}
192+
193+
// convert indent to tab
194+
void _applyIndent(TextNode textNode, Map? attributes) {
195+
final indent = attributes?['indent'] as int?;
196+
final list = attributes?['list'] as String?;
197+
if (indent != null && list == null) {
198+
textNode.delta = textNode.delta.compose(
199+
Delta()
200+
..retain(0)
201+
..insert(' ' * indent),
202+
);
203+
}
204+
}
205+
206+
/*
207+
// convert code-block to appflowy style code
208+
void _applyCodeBlock(TextNode textNode, Map? attributes) {
209+
final codeBlock = attributes?['code-block'] as bool?;
210+
if (codeBlock != null) {
211+
textNode.updateAttributes({
212+
BuiltInAttributeKey.subtype: 'code_block',
213+
});
214+
}
215+
}
216+
*/
217+
218+
void _applyBlockquote(TextNode textNode, Map? attributes) {
219+
final blockquote = attributes?['blockquote'] as bool?;
220+
if (blockquote != null) {
221+
textNode.updateAttributes({
222+
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
223+
});
224+
}
225+
}
226+
}
227+
228+
extension on Color {
229+
String toHex() {
230+
return '0x${value.toRadixString(16)}';
231+
}
232+
}

0 commit comments

Comments
 (0)