mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #1424 from LucasXu0/markdown
Implement appflowy editor document to markdown
This commit is contained in:
commit
cdf6f1b38a
@ -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> {
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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(),
|
@ -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();
|
@ -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();
|
@ -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';
|
||||
}
|
@ -27,6 +27,7 @@ dependencies:
|
||||
intl:
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
markdown: ^6.0.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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)***',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
@ -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!''');
|
||||
});
|
||||
});
|
||||
}
|
@ -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, '');
|
||||
});
|
||||
});
|
||||
}
|
@ -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```');
|
||||
});
|
||||
});
|
||||
}
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user