mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1655 from LucasXu0/feat_1649
feat: #1649 [FR] Convert quill delta to appflowy document
This commit is contained in:
commit
3ba3a8dc18
@ -1,14 +1,14 @@
|
|||||||
<!--
|
<!--
|
||||||
This README describes the package. If you publish this package to pub.dev,
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
this README's contents appear on the landing page for your package.
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
For information about how to write a good package README, see the guide for
|
For information about how to write a good package README, see the guide for
|
||||||
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||||
|
|
||||||
For general information about developing packages, see the Dart guide for
|
For general information about developing packages, see the Dart guide for
|
||||||
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||||
and the Flutter guide for
|
and the Flutter guide for
|
||||||
[developing packages and plugins](https://flutter.dev/developing-packages).
|
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<h1 align="center"><b>AppFlowy Editor</b></h1>
|
<h1 align="center"><b>AppFlowy Editor</b></h1>
|
||||||
@ -51,7 +51,7 @@ flutter pub get
|
|||||||
|
|
||||||
## Creating Your First Editor
|
## Creating Your First Editor
|
||||||
|
|
||||||
Start by creating a new empty AppFlowyEditor object.
|
Start by creating a new empty AppFlowyEditor object.
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final editorState = EditorState.empty(); // an empty state
|
final editorState = EditorState.empty(); // an empty state
|
||||||
@ -60,7 +60,7 @@ final editor = AppFlowyEditor(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also create an editor from a JSON object in order to configure your initial state.
|
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).
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
final json = ...;
|
final json = ...;
|
||||||
@ -79,7 +79,7 @@ MaterialApp(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
To get a sense for how the AppFlowy Editor works, run our example:
|
To get a sense of how the AppFlowy Editor works, run our example:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/AppFlowy-IO/AppFlowy.git
|
git clone https://github.com/AppFlowy-IO/AppFlowy.git
|
||||||
@ -98,7 +98,7 @@ Below are some examples of component customizations:
|
|||||||
* [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
|
* [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
|
||||||
* [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
|
* [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
|
||||||
* 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)
|
* 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)
|
||||||
|
|
||||||
### Customizing Shortcut Events
|
### Customizing Shortcut Events
|
||||||
|
|
||||||
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).
|
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:
|
|||||||
Please refer to the API documentation.
|
Please refer to the API documentation.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
|
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.
|
||||||
|
|
||||||
Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
|
Please look at [CONTRIBUTING.md](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
|
||||||
|
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
# Importing data
|
||||||
|
|
||||||
|
For now, we have supported three ways to import data to initialize AppFlowy Editor.
|
||||||
|
|
||||||
|
1. From AppFlowy Document JSON
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const document = r'''{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"},"delta":[{"insert":"Hello AppFlowy!"}]}]}}''';
|
||||||
|
final json = jsonDecode(document);
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: Document.fromJson(
|
||||||
|
Map<String, Object>.from(json),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. From Markdown
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const markdown = r'''# Hello AppFlowy!''';
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: markdownToDocument(markdown),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
3. From Quill Delta
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const delta = r'''[{"insert":"Hello AppFlowy!"},{"attributes":{"header":1},"insert":"\n"}]''';
|
||||||
|
final json = jsonDecode(delta);
|
||||||
|
final editorState = EditorState(
|
||||||
|
document: DeltaDocumentConvert().convertFromJSON(json),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
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).
|
@ -14,12 +14,14 @@ enum ExportFileType {
|
|||||||
json,
|
json,
|
||||||
markdown,
|
markdown,
|
||||||
html,
|
html,
|
||||||
|
delta,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on ExportFileType {
|
extension on ExportFileType {
|
||||||
String get extension {
|
String get extension {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case ExportFileType.json:
|
case ExportFileType.json:
|
||||||
|
case ExportFileType.delta:
|
||||||
return 'json';
|
return 'json';
|
||||||
case ExportFileType.markdown:
|
case ExportFileType.markdown:
|
||||||
return 'md';
|
return 'md';
|
||||||
@ -117,6 +119,9 @@ class _HomePageState extends State<HomePage> {
|
|||||||
_buildListTile(context, 'Import From Markdown', () {
|
_buildListTile(context, 'Import From Markdown', () {
|
||||||
_importFile(ExportFileType.markdown);
|
_importFile(ExportFileType.markdown);
|
||||||
}),
|
}),
|
||||||
|
_buildListTile(context, 'Import From Quill Delta', () {
|
||||||
|
_importFile(ExportFileType.delta);
|
||||||
|
}),
|
||||||
|
|
||||||
// Theme Demo
|
// Theme Demo
|
||||||
_buildSeparator(context, 'Theme Demo'),
|
_buildSeparator(context, 'Theme Demo'),
|
||||||
@ -224,6 +229,7 @@ class _HomePageState extends State<HomePage> {
|
|||||||
result = documentToMarkdown(editorState.document);
|
result = documentToMarkdown(editorState.document);
|
||||||
break;
|
break;
|
||||||
case ExportFileType.html:
|
case ExportFileType.html:
|
||||||
|
case ExportFileType.delta:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,6 +286,17 @@ class _HomePageState extends State<HomePage> {
|
|||||||
case ExportFileType.markdown:
|
case ExportFileType.markdown:
|
||||||
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
|
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
|
||||||
break;
|
break;
|
||||||
|
case ExportFileType.delta:
|
||||||
|
jsonString = jsonEncode(
|
||||||
|
DeltaDocumentConvert()
|
||||||
|
.convertFromJSON(
|
||||||
|
jsonDecode(
|
||||||
|
plainText.replaceAll('\\\\\n', '\\n'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toJson(),
|
||||||
|
);
|
||||||
|
break;
|
||||||
case ExportFileType.html:
|
case ExportFileType.html:
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
|
flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
|
||||||
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
|
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
|
||||||
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
|
||||||
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
|
||||||
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727
|
||||||
|
@ -41,3 +41,4 @@ export 'src/plugins/markdown/encoder/parser/text_node_parser.dart';
|
|||||||
export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
|
export 'src/plugins/markdown/encoder/parser/image_node_parser.dart';
|
||||||
export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
|
export 'src/plugins/markdown/decoder/delta_markdown_decoder.dart';
|
||||||
export 'src/plugins/markdown/document_markdown.dart';
|
export 'src/plugins/markdown/document_markdown.dart';
|
||||||
|
export 'src/plugins/quill_delta/delta_document_encoder.dart';
|
||||||
|
@ -0,0 +1,232 @@
|
|||||||
|
import 'package:appflowy_editor/src/core/document/attributes.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/document/document.dart';
|
||||||
|
import 'package:appflowy_editor/src/core/document/node.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:flutter/material.dart';
|
||||||
|
|
||||||
|
class DeltaDocumentConvert {
|
||||||
|
DeltaDocumentConvert();
|
||||||
|
|
||||||
|
var _number = 1;
|
||||||
|
final Map<int, List<TextNode>> _bulletedList = {};
|
||||||
|
|
||||||
|
Document convertFromJSON(List<dynamic> json) {
|
||||||
|
final delta = Delta.fromJson(json);
|
||||||
|
return convertFromDelta(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
Document convertFromDelta(Delta delta) {
|
||||||
|
final iter = delta.iterator;
|
||||||
|
|
||||||
|
final document = Document.empty();
|
||||||
|
TextNode textNode = TextNode(delta: Delta());
|
||||||
|
int path = 0;
|
||||||
|
|
||||||
|
while (iter.moveNext()) {
|
||||||
|
final op = iter.current;
|
||||||
|
if (op is TextInsert) {
|
||||||
|
if (op.text != '\n') {
|
||||||
|
// Attributes associated with a newline character describes formatting for that line.
|
||||||
|
final texts = op.text.split('\n');
|
||||||
|
if (texts.length > 1) {
|
||||||
|
textNode.delta.insert(texts[0]);
|
||||||
|
document.insert([path++], [textNode]);
|
||||||
|
textNode = TextNode(delta: Delta()..insert(texts[1]));
|
||||||
|
} else {
|
||||||
|
_applyStyle(textNode, op.text, op.attributes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!_containNumberListStyle(op.attributes)) {
|
||||||
|
_number = 1;
|
||||||
|
}
|
||||||
|
_applyListStyle(textNode, op.attributes);
|
||||||
|
_applyHeaderStyle(textNode, op.attributes);
|
||||||
|
_applyIndent(textNode, op.attributes);
|
||||||
|
_applyBlockquote(textNode, op.attributes);
|
||||||
|
// _applyCodeBlock(textNode, op.attributes);
|
||||||
|
|
||||||
|
if (_containIndentBulletedListStyle(op.attributes)) {
|
||||||
|
final level = _indentLevel(op.attributes);
|
||||||
|
final path = [
|
||||||
|
..._bulletedList[level - 1]!.last.path,
|
||||||
|
_bulletedList[level]!.length - 1,
|
||||||
|
];
|
||||||
|
document.insert(path, [textNode]);
|
||||||
|
} else {
|
||||||
|
document.insert([path++], [textNode]);
|
||||||
|
}
|
||||||
|
textNode = TextNode(delta: Delta());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, 'op must be TextInsert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _applyStyle(TextNode textNode, String text, Map? attributes) {
|
||||||
|
Attributes attrs = {};
|
||||||
|
|
||||||
|
if (_containsStyle(attributes, 'strike')) {
|
||||||
|
attrs[BuiltInAttributeKey.strikethrough] = true;
|
||||||
|
}
|
||||||
|
if (_containsStyle(attributes, 'underline')) {
|
||||||
|
attrs[BuiltInAttributeKey.underline] = true;
|
||||||
|
}
|
||||||
|
if (_containsStyle(attributes, 'bold')) {
|
||||||
|
attrs[BuiltInAttributeKey.bold] = true;
|
||||||
|
}
|
||||||
|
if (_containsStyle(attributes, 'italic')) {
|
||||||
|
attrs[BuiltInAttributeKey.italic] = true;
|
||||||
|
}
|
||||||
|
final link = attributes?['link'] as String?;
|
||||||
|
if (link != null) {
|
||||||
|
attrs[BuiltInAttributeKey.href] = link;
|
||||||
|
}
|
||||||
|
final color = attributes?['color'] as String?;
|
||||||
|
final colorHex = _convertColorToHexString(color);
|
||||||
|
if (colorHex != null) {
|
||||||
|
attrs[BuiltInAttributeKey.color] = colorHex;
|
||||||
|
}
|
||||||
|
final backgroundColor = attributes?['background'] as String?;
|
||||||
|
final backgroundHex = _convertColorToHexString(backgroundColor);
|
||||||
|
if (backgroundHex != null) {
|
||||||
|
attrs[BuiltInAttributeKey.backgroundColor] = backgroundHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
textNode.delta.insert(text, attributes: attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _containsStyle(Map? attributes, String key) {
|
||||||
|
final value = attributes?[key] as bool?;
|
||||||
|
return value == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _convertColorToHexString(String? color) {
|
||||||
|
if (color == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (color.startsWith('#')) {
|
||||||
|
return '0xFF${color.substring(1)}';
|
||||||
|
} else if (color.startsWith("rgba")) {
|
||||||
|
List rgbaList = color.substring(5, color.length - 1).split(',');
|
||||||
|
return Color.fromRGBO(
|
||||||
|
int.parse(rgbaList[0]),
|
||||||
|
int.parse(rgbaList[1]),
|
||||||
|
int.parse(rgbaList[2]),
|
||||||
|
double.parse(rgbaList[3]),
|
||||||
|
).toHex();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert bullet-list, number-list, check-list to appflowy style list.
|
||||||
|
void _applyListStyle(TextNode textNode, Map? attributes) {
|
||||||
|
final indent = attributes?['indent'] as int?;
|
||||||
|
final list = attributes?['list'] as String?;
|
||||||
|
if (list != null) {
|
||||||
|
switch (list) {
|
||||||
|
case 'bullet':
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
|
||||||
|
});
|
||||||
|
if (indent != null) {
|
||||||
|
_bulletedList[indent] ??= [];
|
||||||
|
_bulletedList[indent]?.add(textNode);
|
||||||
|
} else {
|
||||||
|
_bulletedList.clear();
|
||||||
|
_bulletedList[0] ??= [];
|
||||||
|
_bulletedList[0]?.add(textNode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ordered':
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
|
||||||
|
BuiltInAttributeKey.number: _number++,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'checked':
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
|
||||||
|
BuiltInAttributeKey.checkbox: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'unchecked':
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
|
||||||
|
BuiltInAttributeKey.checkbox: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _containNumberListStyle(Map? attributes) {
|
||||||
|
final list = attributes?['list'] as String?;
|
||||||
|
return list == 'ordered';
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _containIndentBulletedListStyle(Map? attributes) {
|
||||||
|
final list = attributes?['list'] as String?;
|
||||||
|
final indent = attributes?['indent'] as int?;
|
||||||
|
return list == 'bullet' && indent != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _indentLevel(Map? attributes) {
|
||||||
|
final indent = attributes?['indent'] as int?;
|
||||||
|
return indent ?? 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert header to appflowy style heading
|
||||||
|
void _applyHeaderStyle(TextNode textNode, Map? attributes) {
|
||||||
|
final header = attributes?['header'] as int?;
|
||||||
|
if (header != null) {
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
|
||||||
|
BuiltInAttributeKey.heading: 'h$header',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert indent to tab
|
||||||
|
void _applyIndent(TextNode textNode, Map? attributes) {
|
||||||
|
final indent = attributes?['indent'] as int?;
|
||||||
|
final list = attributes?['list'] as String?;
|
||||||
|
if (indent != null && list == null) {
|
||||||
|
textNode.delta = textNode.delta.compose(
|
||||||
|
Delta()
|
||||||
|
..retain(0)
|
||||||
|
..insert(' ' * indent),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// convert code-block to appflowy style code
|
||||||
|
void _applyCodeBlock(TextNode textNode, Map? attributes) {
|
||||||
|
final codeBlock = attributes?['code-block'] as bool?;
|
||||||
|
if (codeBlock != null) {
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: 'code_block',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
void _applyBlockquote(TextNode textNode, Map? attributes) {
|
||||||
|
final blockquote = attributes?['blockquote'] as bool?;
|
||||||
|
if (blockquote != null) {
|
||||||
|
textNode.updateAttributes({
|
||||||
|
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on Color {
|
||||||
|
String toHex() {
|
||||||
|
return '0x${value.toRadixString(16)}';
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user