feat: #1649 [FR] Convert quill delta to appflowy document

This commit is contained in:
Lucas.Xu 2023-01-05 17:08:43 +08:00
parent 1a2af1cf49
commit 2fb0e8da28
5 changed files with 802 additions and 1 deletions

View File

@ -14,12 +14,14 @@ enum ExportFileType {
json,
markdown,
html,
delta,
}
extension on ExportFileType {
String get extension {
switch (this) {
case ExportFileType.json:
case ExportFileType.delta:
return 'json';
case ExportFileType.markdown:
return 'md';
@ -117,6 +119,9 @@ class _HomePageState extends State<HomePage> {
_buildListTile(context, 'Import From Markdown', () {
_importFile(ExportFileType.markdown);
}),
_buildListTile(context, 'Import From Quill Delta', () {
_importFile(ExportFileType.delta);
}),
// Theme Demo
_buildSeparator(context, 'Theme Demo'),
@ -224,6 +229,7 @@ class _HomePageState extends State<HomePage> {
result = documentToMarkdown(editorState.document);
break;
case ExportFileType.html:
case ExportFileType.delta:
throw UnimplementedError();
}
@ -280,6 +286,17 @@ class _HomePageState extends State<HomePage> {
case ExportFileType.markdown:
jsonString = jsonEncode(markdownToDocument(plainText).toJson());
break;
case ExportFileType.delta:
jsonString = jsonEncode(
DeltaDocumentConvert()
.convertFromJSON(
jsonDecode(
plainText.replaceAll('\\\\\n', '\\n'),
),
)
.toJson(),
);
break;
case ExportFileType.html:
throw UnimplementedError();
}

View File

@ -35,7 +35,7 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
flowy_infra_ui: c34d49d615ed9fe552cd47f90d7850815a74e9e9
FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424
FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811
path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19
rich_clipboard_macos: 43364b66b9dc69d203eb8dd6d758e2d12e02723c
shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727

View File

@ -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/decoder/delta_markdown_decoder.dart';
export 'src/plugins/markdown/document_markdown.dart';
export 'src/plugins/quill_delta/delta_document_encoder.dart';

View File

@ -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