Merge pull request #1424 from LucasXu0/markdown

Implement appflowy editor document to markdown
This commit is contained in:
Lucas.Xu 2022-11-09 09:50:15 +08:00 committed by GitHub
commit cdf6f1b38a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 871 additions and 39 deletions

View File

@ -1,14 +1,14 @@
import 'dart:convert';
import 'dart:io';
import 'package:app_flowy/plugins/doc/application/share_service.dart';
import 'package:app_flowy/workspace/application/markdown/document_markdown.dart';
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
import 'package:appflowy_editor/appflowy_editor.dart'
show Document, documentToMarkdown;
part 'share_bloc.freezed.dart';
class DocShareBloc extends Bloc<DocShareEvent, DocShareState> {

View File

@ -33,3 +33,10 @@ export 'src/render/selection_menu/selection_menu_widget.dart';
export 'src/l10n/l10n.dart';
export 'src/render/style/plugin_styles.dart';
export 'src/render/style/editor_style.dart';
export 'src/plugins/markdown/encoder/delta_markdown_encoder.dart';
export 'src/plugins/markdown/encoder/document_markdown_encoder.dart';
export 'src/plugins/markdown/encoder/parser/node_parser.dart';
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';

View File

@ -50,7 +50,7 @@ class TextInsert extends TextOperation {
final result = <String, dynamic>{
'insert': text,
};
if (_attributes != null) {
if (_attributes != null && _attributes!.isNotEmpty) {
result['attributes'] = attributes;
}
return result;
@ -62,7 +62,7 @@ class TextInsert extends TextOperation {
return other is TextInsert &&
other.text == text &&
mapEquals(_attributes, other._attributes);
_mapEquals(_attributes, other._attributes);
}
@override
@ -87,7 +87,7 @@ class TextRetain extends TextOperation {
final result = <String, dynamic>{
'retain': length,
};
if (_attributes != null) {
if (_attributes != null && _attributes!.isNotEmpty) {
result['attributes'] = attributes;
}
return result;
@ -99,7 +99,7 @@ class TextRetain extends TextOperation {
return other is TextRetain &&
other.length == length &&
mapEquals(_attributes, other._attributes);
_mapEquals(_attributes, other._attributes);
}
@override
@ -181,7 +181,7 @@ class Delta extends Iterable<TextOperation> {
lastOp.length += textOperation.length;
return;
}
if (mapEquals(lastOp.attributes, textOperation.attributes)) {
if (_mapEquals(lastOp.attributes, textOperation.attributes)) {
if (lastOp is TextInsert && textOperation is TextInsert) {
lastOp.text += textOperation.text;
return;
@ -539,3 +539,10 @@ class _OpIterator {
}
}
}
bool _mapEquals<T, U>(Map<T, U>? a, Map<T, U>? b) {
if ((a == null || a.isEmpty) && (b == null || b.isEmpty)) {
return true;
}
return mapEquals(a, b);
}

View File

@ -0,0 +1,66 @@
import 'dart:convert';
import 'package:appflowy_editor/src/core/document/attributes.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:markdown/markdown.dart' as md;
class DeltaMarkdownDecoder extends Converter<String, Delta>
with md.NodeVisitor {
final _delta = Delta();
final Attributes _attributes = {};
@override
Delta convert(String input) {
final document =
md.Document(extensionSet: md.ExtensionSet.gitHubWeb).parseInline(input);
for (final node in document) {
node.accept(this);
}
return _delta;
}
@override
void visitElementAfter(md.Element element) {
_removeAttributeKey(element);
}
@override
bool visitElementBefore(md.Element element) {
_addAttributeKey(element);
return true;
}
@override
void visitText(md.Text text) {
_delta.add(TextInsert(text.text, attributes: {..._attributes}));
}
void _addAttributeKey(md.Element element) {
if (element.tag == 'strong') {
_attributes[BuiltInAttributeKey.bold] = true;
} else if (element.tag == 'em') {
_attributes[BuiltInAttributeKey.italic] = true;
} else if (element.tag == 'code') {
_attributes[BuiltInAttributeKey.code] = true;
} else if (element.tag == 'del') {
_attributes[BuiltInAttributeKey.strikethrough] = true;
} else if (element.tag == 'a') {
_attributes[BuiltInAttributeKey.href] = element.attributes['href'];
}
}
void _removeAttributeKey(md.Element element) {
if (element.tag == 'strong') {
_attributes.remove(BuiltInAttributeKey.bold);
} else if (element.tag == 'em') {
_attributes.remove(BuiltInAttributeKey.italic);
} else if (element.tag == 'code') {
_attributes.remove(BuiltInAttributeKey.code);
} else if (element.tag == 'del') {
_attributes.remove(BuiltInAttributeKey.strikethrough);
} else if (element.tag == 'a') {
_attributes.remove(BuiltInAttributeKey.href);
}
}
}

View File

@ -0,0 +1,91 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
class DocumentMarkdownDecoder extends Converter<String, Document> {
@override
Document convert(String input) {
final lines = input.split('\n');
final document = Document.empty();
var i = 0;
for (final line in lines) {
document.insert([i++], [_convertLineToNode(line)]);
}
return document;
}
Node _convertLineToNode(String text) {
final decoder = DeltaMarkdownDecoder();
// Heading Style
if (text.startsWith('### ')) {
return TextNode(
delta: decoder.convert(text.substring(4)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h3,
},
);
} else if (text.startsWith('## ')) {
return TextNode(
delta: decoder.convert(text.substring(3)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h2,
},
);
} else if (text.startsWith('# ')) {
return TextNode(
delta: decoder.convert(text.substring(2)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
},
);
} else if (text.startsWith('- [ ] ')) {
return TextNode(
delta: decoder.convert(text.substring(6)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: false,
},
);
} else if (text.startsWith('- [x] ')) {
return TextNode(
delta: decoder.convert(text.substring(6)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: true,
},
);
} else if (text.startsWith('> ')) {
return TextNode(
delta: decoder.convert(text.substring(2)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
},
);
} else if (text.startsWith('- ') || text.startsWith('* ')) {
return TextNode(
delta: decoder.convert(text.substring(2)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
},
);
} else if (text.startsWith('> ')) {
return TextNode(
delta: decoder.convert(text.substring(2)),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
},
);
}
if (text.isNotEmpty) {
return TextNode(delta: decoder.convert(text));
}
return TextNode(delta: Delta());
}
}

View File

@ -2,8 +2,9 @@ library delta_markdown;
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart' show Document;
import 'package:app_flowy/workspace/application/markdown/src/parser/markdown_encoder.dart';
import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/plugins/markdown/decoder/document_markdown_decoder.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/document_markdown_encoder.dart';
/// Codec used to convert between Markdown and AppFlowy Editor Document.
const AppFlowyEditorMarkdownCodec _kCodec = AppFlowyEditorMarkdownCodec();
@ -20,10 +21,8 @@ class AppFlowyEditorMarkdownCodec extends Codec<Document, String> {
const AppFlowyEditorMarkdownCodec();
@override
Converter<String, Document> get decoder => throw UnimplementedError();
Converter<String, Document> get decoder => DocumentMarkdownDecoder();
@override
Converter<Document, String> get encoder {
return AppFlowyEditorMarkdownEncoder();
}
Converter<Document, String> get encoder => DocumentMarkdownEncoder();
}

View File

@ -0,0 +1,88 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
/// A [Delta] encoder that encodes a [Delta] to Markdown.
///
/// Only support inline styles, like bold, italic, underline, strike, code.
class DeltaMarkdownEncoder extends Converter<Delta, String> {
@override
String convert(Delta input) {
final buffer = StringBuffer();
final iterator = input.iterator;
while (iterator.moveNext()) {
final op = iterator.current;
if (op is TextInsert) {
final attributes = op.attributes;
if (attributes != null) {
buffer.write(_prefixSyntax(attributes));
buffer.write(op.text);
buffer.write(_suffixSyntax(attributes));
} else {
buffer.write(op.text);
}
}
}
return buffer.toString();
}
String _prefixSyntax(Attributes attributes) {
var syntax = '';
if (attributes[BuiltInAttributeKey.bold] == true &&
attributes[BuiltInAttributeKey.italic] == true) {
syntax += '***';
} else if (attributes[BuiltInAttributeKey.bold] == true) {
syntax += '**';
} else if (attributes[BuiltInAttributeKey.italic] == true) {
syntax += '_';
}
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
syntax += '~~';
}
if (attributes[BuiltInAttributeKey.underline] == true) {
syntax += '<u>';
}
if (attributes[BuiltInAttributeKey.code] == true) {
syntax += '`';
}
if (attributes[BuiltInAttributeKey.href] != null) {
syntax += '[';
}
return syntax;
}
String _suffixSyntax(Attributes attributes) {
var syntax = '';
if (attributes[BuiltInAttributeKey.href] != null) {
syntax += '](${attributes[BuiltInAttributeKey.href]})';
}
if (attributes[BuiltInAttributeKey.code] == true) {
syntax += '`';
}
if (attributes[BuiltInAttributeKey.underline] == true) {
syntax += '</u>';
}
if (attributes[BuiltInAttributeKey.strikethrough] == true) {
syntax += '~~';
}
if (attributes[BuiltInAttributeKey.bold] == true &&
attributes[BuiltInAttributeKey.italic] == true) {
syntax += '***';
} else if (attributes[BuiltInAttributeKey.bold] == true) {
syntax += '**';
} else if (attributes[BuiltInAttributeKey.italic] == true) {
syntax += '_';
}
return syntax;
}
}

View File

@ -1,12 +1,12 @@
import 'dart:convert';
import 'package:app_flowy/workspace/application/markdown/src/parser/image_node_parser.dart';
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
import 'package:app_flowy/workspace/application/markdown/src/parser/text_node_parser.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/core/document/document.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/image_node_parser.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/node_parser.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/text_node_parser.dart';
class AppFlowyEditorMarkdownEncoder extends Converter<Document, String> {
AppFlowyEditorMarkdownEncoder({
class DocumentMarkdownEncoder extends Converter<Document, String> {
DocumentMarkdownEncoder({
this.parsers = const [
TextNodeParser(),
ImageNodeParser(),

View File

@ -1,5 +1,5 @@
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/node_parser.dart';
class ImageNodeParser extends NodeParser {
const ImageNodeParser();

View File

@ -1,4 +1,4 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/core/document/node.dart';
abstract class NodeParser {
const NodeParser();

View File

@ -1,8 +1,7 @@
import 'dart:convert';
import 'package:app_flowy/workspace/application/markdown/delta_markdown.dart';
import 'package:app_flowy/workspace/application/markdown/src/parser/node_parser.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/legacy/built_in_attribute_keys.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/delta_markdown_encoder.dart';
import 'package:appflowy_editor/src/plugins/markdown/encoder/parser/node_parser.dart';
class TextNodeParser extends NodeParser {
const TextNodeParser();
@ -14,20 +13,15 @@ class TextNodeParser extends NodeParser {
String transform(Node node) {
assert(node is TextNode);
final textNode = node as TextNode;
final delta = jsonEncode(
textNode.delta
..add(TextInsert('\n'))
..toJson(),
);
final markdown = deltaToMarkdown(delta);
final markdown = DeltaMarkdownEncoder().convert(textNode.delta);
final attributes = textNode.attributes;
var result = markdown;
var suffix = '';
var suffix = '\n';
if (attributes.isNotEmpty &&
attributes.containsKey(BuiltInAttributeKey.subtype)) {
final subtype = attributes[BuiltInAttributeKey.subtype];
if (node.next?.subtype != subtype) {
suffix = '\n';
if (node.next == null) {
suffix = '';
}
if (subtype == 'heading') {
final heading = attributes[BuiltInAttributeKey.heading];
@ -46,12 +40,10 @@ class TextNodeParser extends NodeParser {
}
} else if (subtype == 'quote') {
result = '> $markdown';
} else if (subtype == 'code') {
result = '`$markdown`';
} else if (subtype == 'code-block') {
result = '```\n$markdown\n```';
} else if (subtype == 'bulleted-list') {
result = '- $markdown';
result = '* $markdown';
} else if (subtype == 'number-list') {
final number = attributes['number'];
result = '$number. $markdown';
@ -62,6 +54,10 @@ class TextNodeParser extends NodeParser {
result = '- [ ] $markdown';
}
}
} else {
if (node.next == null) {
suffix = '';
}
}
return '$result$suffix';
}

View File

@ -27,6 +27,7 @@ dependencies:
intl:
flutter_localizations:
sdk: flutter
markdown: ^6.0.1
dev_dependencies:
flutter_test:

View File

@ -0,0 +1,96 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('delta_markdown_decoder.dart', () {
test('bold', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.bold: true,
}),
]);
final result = DeltaMarkdownDecoder().convert('Welcome to **AppFlowy**');
expect(result, delta);
});
test('italic', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.italic: true,
}),
]);
final result = DeltaMarkdownDecoder().convert('Welcome to _AppFlowy_');
expect(result, delta);
});
test('strikethrough', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.strikethrough: true,
}),
]);
final result = DeltaMarkdownDecoder().convert('Welcome to ~~AppFlowy~~');
expect(result, delta);
});
test('href', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.href: 'https://appflowy.io',
}),
]);
final result = DeltaMarkdownDecoder()
.convert('Welcome to [AppFlowy](https://appflowy.io)');
expect(result, delta);
});
test('code', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.code: true,
}),
]);
final result = DeltaMarkdownDecoder().convert('Welcome to `AppFlowy`');
expect(result, delta);
});
test('bold', () {
const markdown =
'***<u>`Welcome`</u>*** ***~~to~~*** ***[AppFlowy](https://appflowy.io)***';
final delta = Delta(operations: [
TextInsert('<u>', attributes: {
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.bold: true,
}),
TextInsert('Welcome', attributes: {
BuiltInAttributeKey.code: true,
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.bold: true,
}),
TextInsert('</u>', attributes: {
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.bold: true,
}),
TextInsert(' '),
TextInsert('to', attributes: {
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.strikethrough: true,
}),
TextInsert(' '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.href: 'https://appflowy.io',
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.italic: true,
}),
]);
final result = DeltaMarkdownDecoder().convert(markdown);
expect(result, delta);
});
});
}

View File

@ -0,0 +1,126 @@
import 'dart:convert';
import 'package:appflowy_editor/src/plugins/markdown/decoder/document_markdown_decoder.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('document_markdown_decoder.dart', () {
const example = '''
{
"document": {
"type": "editor",
"children": [
{
"type": "text",
"attributes": {"subtype": "heading", "heading": "h2"},
"delta": [
{"insert": "👋 "},
{"insert": "Welcome to", "attributes": {"bold": true}},
{"insert": " "},
{
"insert": "AppFlowy Editor",
"attributes": {"italic": true, "bold": true, "href": "appflowy.io"}
}
]
},
{"type": "text", "delta": []},
{
"type": "text",
"delta": [
{"insert": "AppFlowy Editor is a "},
{"insert": "highly customizable", "attributes": {"bold": true}},
{"insert": " "},
{"insert": "rich-text editor", "attributes": {"italic": true}}
]
},
{
"type": "text",
"attributes": {"subtype": "checkbox", "checkbox": true},
"delta": [{"insert": "Customizable"}]
},
{
"type": "text",
"attributes": {"subtype": "checkbox", "checkbox": true},
"delta": [{"insert": "Test-covered"}]
},
{
"type": "text",
"attributes": {"subtype": "checkbox", "checkbox": false},
"delta": [{"insert": "more to come!"}]
},
{"type": "text", "delta": []},
{
"type": "text",
"attributes": {"subtype": "quote"},
"delta": [{"insert": "Here is an example you can give a try"}]
},
{"type": "text", "delta": []},
{
"type": "text",
"delta": [
{"insert": "You can also use "},
{
"insert": "AppFlowy Editor",
"attributes": {"italic": true, "bold": true}
},
{"insert": " as a component to build your own app."}
]
},
{"type": "text", "delta": []},
{
"type": "text",
"attributes": {"subtype": "bulleted-list"},
"delta": [{"insert": "Use / to insert blocks"}]
},
{
"type": "text",
"attributes": {"subtype": "bulleted-list"},
"delta": [
{
"insert": "Select text to trigger to the toolbar to format your notes."
}
]
},
{"type": "text", "delta": []},
{
"type": "text",
"delta": [
{
"insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!"
}
]
},
{"type": "text", "delta": []},
{"type": "text", "delta": [{"insert": ""}]}
]
}
}
''';
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
const markdown = '''
## 👋 **Welcome to** ***[AppFlowy Editor](appflowy.io)***
AppFlowy Editor is a **highly customizable** _rich-text editor_
- [x] Customizable
- [x] Test-covered
- [ ] more to come!
> Here is an example you can give a try
You can also use ***AppFlowy Editor*** as a component to build your own app.
* Use / to insert blocks
* Select text to trigger to the toolbar to format your notes.
If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!
''';
final result = DocumentMarkdownDecoder().convert(markdown);
final data = Map<String, Object>.from(json.decode(example));
expect(result.toJson(), data);
});
});
}

View File

@ -0,0 +1,100 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('delta_markdown_encoder.dart', () {
test('bold', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.bold: true,
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(result, 'Welcome to **AppFlowy**');
});
test('italic', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.italic: true,
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(result, 'Welcome to _AppFlowy_');
});
test('underline', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.underline: true,
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(result, 'Welcome to <u>AppFlowy</u>');
});
test('strikethrough', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.strikethrough: true,
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(result, 'Welcome to ~~AppFlowy~~');
});
test('href', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.href: 'https://appflowy.io',
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(result, 'Welcome to [AppFlowy](https://appflowy.io)');
});
test('code', () {
final delta = Delta(operations: [
TextInsert('Welcome to '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.code: true,
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(result, 'Welcome to `AppFlowy`');
});
test('composition', () {
final delta = Delta(operations: [
TextInsert('Welcome', attributes: {
BuiltInAttributeKey.code: true,
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.underline: true,
}),
TextInsert(' '),
TextInsert('to', attributes: {
BuiltInAttributeKey.italic: true,
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.strikethrough: true,
}),
TextInsert(' '),
TextInsert('AppFlowy', attributes: {
BuiltInAttributeKey.href: 'https://appflowy.io',
BuiltInAttributeKey.bold: true,
BuiltInAttributeKey.italic: true,
}),
]);
final result = DeltaMarkdownEncoder().convert(delta);
expect(
result,
'***<u>`Welcome`</u>*** ***~~to~~*** ***[AppFlowy](https://appflowy.io)***',
);
});
});
}

View File

@ -0,0 +1,136 @@
import 'dart:convert';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('document_markdown_encoder.dart', () {
const example = '''
{
"document": {
"type": "editor",
"children": [
{
"type": "text",
"attributes": {
"subtype": "heading",
"heading": "h2"
},
"delta": [
{ "insert": "👋 " },
{ "insert": "Welcome to", "attributes": { "bold": true } },
{ "insert": " " },
{
"insert": "AppFlowy Editor",
"attributes": {
"href": "appflowy.io",
"italic": true,
"bold": true
}
}
]
},
{ "type": "text", "delta": [] },
{
"type": "text",
"delta": [
{ "insert": "AppFlowy Editor is a " },
{ "insert": "highly customizable", "attributes": { "bold": true } },
{ "insert": " " },
{ "insert": "rich-text editor", "attributes": { "italic": true } },
{ "insert": " for " },
{ "insert": "Flutter", "attributes": { "underline": true } }
]
},
{
"type": "text",
"attributes": { "checkbox": true, "subtype": "checkbox" },
"delta": [{ "insert": "Customizable" }]
},
{
"type": "text",
"attributes": { "checkbox": true, "subtype": "checkbox" },
"delta": [{ "insert": "Test-covered" }]
},
{
"type": "text",
"attributes": { "checkbox": false, "subtype": "checkbox" },
"delta": [{ "insert": "more to come!" }]
},
{ "type": "text", "delta": [] },
{
"type": "text",
"attributes": { "subtype": "quote" },
"delta": [{ "insert": "Here is an example you can give a try" }]
},
{ "type": "text", "delta": [] },
{
"type": "text",
"delta": [
{ "insert": "You can also use " },
{
"insert": "AppFlowy Editor",
"attributes": {
"italic": true,
"bold": true,
"backgroundColor": "0x6000BCF0"
}
},
{ "insert": " as a component to build your own app." }
]
},
{ "type": "text", "delta": [] },
{
"type": "text",
"attributes": { "subtype": "bulleted-list" },
"delta": [{ "insert": "Use / to insert blocks" }]
},
{
"type": "text",
"attributes": { "subtype": "bulleted-list" },
"delta": [
{
"insert": "Select text to trigger to the toolbar to format your notes."
}
]
},
{ "type": "text", "delta": [] },
{
"type": "text",
"delta": [
{
"insert": "If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!"
}
]
}
]
}
}
''';
setUpAll(() {
TestWidgetsFlutterBinding.ensureInitialized();
});
test('parser document', () async {
final data = Map<String, Object>.from(json.decode(example));
final document = Document.fromJson(data);
final result = DocumentMarkdownEncoder().convert(document);
expect(result, '''
## 👋 **Welcome to** ***[AppFlowy Editor](appflowy.io)***
AppFlowy Editor is a **highly customizable** _rich-text editor_ for <u>Flutter</u>
- [x] Customizable
- [x] Test-covered
- [ ] more to come!
> Here is an example you can give a try
You can also use ***AppFlowy Editor*** as a component to build your own app.
* Use / to insert blocks
* Select text to trigger to the toolbar to format your notes.
If you have questions or feedback, please submit an issue on Github or join the community along with 1000+ builders!''');
});
});
}

View File

@ -0,0 +1,17 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('image_node_parser.dart', () {
test('parser image node', () {
final node = Node(
type: 'image',
attributes: {
'image_src': 'https://appflowy.io',
},
);
final result = const ImageNodeParser().transform(node);
expect(result, '![](https://appflowy.io)');
});
});
}

View File

@ -0,0 +1,95 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('text_node_parser.dart', () {
const text = 'Welcome to AppFlowy';
test('heading style', () {
final h1 = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h1,
},
);
final h2 = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h2,
},
);
final h3 = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.heading,
BuiltInAttributeKey.heading: BuiltInAttributeKey.h3,
},
);
expect(const TextNodeParser().transform(h1), '# $text');
expect(const TextNodeParser().transform(h2), '## $text');
expect(const TextNodeParser().transform(h3), '### $text');
});
test('bulleted list style', () {
final node = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.bulletedList,
},
);
expect(const TextNodeParser().transform(node), '* $text');
});
test('number list style', () {
final node = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.numberList,
BuiltInAttributeKey.number: 1,
},
);
expect(const TextNodeParser().transform(node), '1. $text');
});
test('checkbox style', () {
final checkbox = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: true,
},
);
final unCheckbox = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.checkbox,
BuiltInAttributeKey.checkbox: false,
},
);
expect(const TextNodeParser().transform(checkbox), '- [x] $text');
expect(const TextNodeParser().transform(unCheckbox), '- [ ] $text');
});
test('quote style', () {
final node = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: BuiltInAttributeKey.quote,
},
);
expect(const TextNodeParser().transform(node), '> $text');
});
test('code block style', () {
final node = TextNode(
delta: Delta(operations: [TextInsert(text)]),
attributes: {
BuiltInAttributeKey.subtype: 'code-block',
},
);
expect(const TextNodeParser().transform(node), '```\n$text\n```');
});
});
}

View File

@ -773,6 +773,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
markdown:
dependency: transitive
description:
name: markdown
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
matcher:
dependency: transitive
description: